비정형데이터 - HTTP

4 minute read

1. HTTP

  • Hyper Text Transfer Protocol
  • HTTP로 가져오면 기본적으로 document가 HTML이다.
  • Request를 보내면 반드시 Response를 받아야 한다.

Request

  • Header와 Body(Optional)로 이루어져 있다.
    • Header: Get, Host, Accept, Accept-Language, Accept-Charset, User-agent, Connection 등으로 구성됨
  • Header 예시

    Get               /index.html   HTTP/1.1
    Host              www.example.com
    Accept            text/html, */*
    Accept-Language   en-us
    Accept-Charset    ISO-8859-1,utf-8
    User-agent        Mozilla/5.0
    Connection        Keep-alive
    

HTTP Method

  • GET
  • HEAD
  • PUT
  • POST
  • PATCH
  • TRACE
  • OPTIONS
  • DELETE

REST

  • REST : Representational State Transfer
  • CRUD : Create, Read, Update, Delete
  • Restful

Response

  • status가 200이면 정삭적으로 되었다는 뜻

2. 법적 문제

Opt-in vs. Opt-out

  • Opt-in: 정보수집을 명시적으로 동의할 때에만 정보수집 가능
  • Opt-out : 정보수집을 명시적으로 거부할 때에만 정보수집 중단
    • 웹에서는 이 방식을 사용하므로 Opt-out으로 명시된 사항들이 뭐가 있는지를 확인해야 함

합법 or 불법?

합법

  • 검색엔진(bot, spider, crawler etc.), 가격비교 등은 합법
    • Opt-out
    • 공공의 목적이 있기 때문에

불법

  • 봇이 개인정보, 지적재산권이 포함된 DB에서 정보를 가져오게 되면 불법
  • 과도한 트래픽을 유발하면 다른 이용자들이 이용하기 힘들어지기 때문에 불법
    • 텀을 길게 주는 등 과도한 트래픽을 발생시키지 않는 범위 내에서 크롤링되어야 한다.

robots.txt

  • Crawler와 같은 bot 접근을 제어하기 위한 규약
  • 대상 봇, 수집 여부, 수집 범위 등을 기술
  • 권고안
  • 사이트이름/robots.txt로 찾을 수 있다.

모든 User-agent에 대해 허용

'''
User-agent:*
Allow:/
'''

모든 User-agent에 대해 불허

'''
User-agent:*
Disallow:/
'''
Name User-Agent
Google Googlebot
Google image Googlebot-image
Msn MSNBot
Naver Yeti
Daum Daumoa

유의하면 좋을 것들

  • Robots.txt : 접근 제약 규칙 준수
  • Crawl delay : 사이트에 최대한 부담 지양
  • Term of use : 사이트 이용방침(약관) 준수
  • Public Content : 지저재산권 침해 여부
  • Authentication-based site : 민감한 정보 수집 주의

3. URL Handling Modules (urllib 패키지)

  • urllib.request : Opening and reading URLs
  • urllib.error : Contaning the exceptions raised by urllib.request
  • urllib.parse : Parsing URLs
  • urllib.robotparser : Parsing robots.txt files
  • urllib.response : Used internally by the urllib request module

(1) RobotParser

from urllib import robotparser

robot = robotparser.RobotFileParser()
robot.set_url('https://news.naver.com/robots.txt')
robot.read()
robot.can_fetch('Yeti', '/main/imagemontage')
# True: Yeti라는 user-agent는 접근가능

(2) Request

urlopen은 url을 string이나 Request 객체의 형태로 받아 열어준다.

from urllib.request import urlopen, Request

resp = urlopen('http://www.google.com/robots.txt')

type(resp) # http.client.HTTPResponse

resp.getheaders() # ...

resp.code, resp.reason # (200, 'OK')

resp.info() # <http.client.HTTPMessage at 0x196bb36ce08>

read는 한 번 실행하면 사라지기 때문에 일단 담아두는 것이 좋다.

body = resp.read()

# 정리해서 보기
[_.split(':') for _ in body.decode('UTF-8').split('\n')]

구글의 경우, Request 객체가 필요하다.

req = Request(url='https://www.google.com/search?q=%EC%9C%A0%ED%8A%9C%EB%B8%8C&oq=%EC%9C%A0%ED%8A%9C%EB%B8%8C&aqs=chrome..69i57j69i59l3j0j69i61l3.1961j0j7&sourceid=chrome&ie=UTF-8')

# 아직 header는 없는 상태.
req.headers() # {}

header를 추가해보자. 필요한 user-agent를 찾아오기 위해서는 Chrome 개발자도구 -> Network -> 새로고침 -> 제일 위의 search?q=... 선택 -> Headers -> Request Headers -> user-agent 확인의 단계를 거치면 된다.

req.add_header('user-agent','Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36')

Request 객체를 만들 때 바로 header를 추가할 수도 있다.

  • ex. req = Request(url, headers={'user-agent': 'Mozilla~~})
resp = urlopen(req)

resp.getheaders()

resp.read().decode('utf-8')

Request 객체를 사용할 필요가 없는 경우, 다음과 같이 할 수 있다.

resp = urlopen('https://search.naver.com/search.naver?sm=top_hty&fbm=1&ie=utf8&query=%EC%9B%A8%EC%9D%BC')

resp.code

resp.read().decode('utf8')

(3) URL Parse

from urllib.parse import urlparse, urljoin, urlencode, quote, quote_plus, unquote, unquote_plus

urlparse: URL을 6개의 구성요소로 parsing 한다.

urlparse(url)
# ParseResult(scheme='https', netloc='search.naver.com', path='/search.naver', params='', query='sm=top_sly.hst&fbm=1&ie=utf8&query=%ED%8C%8C%EC%9D%B4%EC%8D%AC', fragment='')

urlparse('http://domain.com/service/service/asd/asdasda/asdasdasdfasf/?')
# ParseResult(scheme='http', netloc='domain.com', path='/service/service/asd/asdasda/asdasdasdfasf/', params='', query='', fragment='')

urljoin: 기본 URL을 다른 URL과 결합해 돌려준다.

url = 'https://search.naver.com/search.naver?sm=top_sly.hst&fbm=1&ie=utf8&query=%ED%8C%8C%EC%9D%B4%EC%8D%AC'
urljoin(url, '/get?asf/asdfasdfasdfasdfsaf')
# 'https://search.naver.com/get?asf/asdfasdfasdfasdfsaf'

urlencode: str이나 bytes 객체를 포함할 수 있는 매핑 객체나 두 요소 튜플의 시퀀스를 퍼센트 인코딩된 ASCII 텍스트 문자열로 변환한다.

urlencode({'key': '파이썬'})
# 'key=%ED%8C%8C%EC%9D%B4%EC%8D%AC'

quote, unquote, quote_plus, unquote_plus: 비 ASCII 텍스트를 인코딩하여 URL 구성요소로 안전하게 사용할 수 있게 해주거나 반대로 디코딩한다. quote와 quote_plus의 차이는 띄어쓰기를 %20으로 하느냐 +로 하느냐의 차이이다.

quote('파 이 썬')
# '%ED%8C%8C%20%EC%9D%B4%20%EC%8D%AC'
quote_plus('파 이 썬')
# '%ED%8C%8C+%EC%9D%B4+%EC%8D%AC'
unquote('%ED%8C%8C%20%EC%9D%B4%20%EC%8D%AC')
# '파 이 썬'
unquote_plus('%ED%8C%8C+%EC%9D%B4+%EC%8D%AC')
# '파 이 썬'

(4) Error

from urllib.error import HTTPError

try:
    urlopen(url + '?' + urlencode(params))
except HTTPError as e:
    # HTTP Status Code
    print('code:',e.code)
    # 에러의 이유
    print('\nreason:',e.reason)
    # HTTPError를 일으킨 HTTP request에 대한 header
    print('\nheader:', e.headers)
# 403 Error 

HTTP Status Codes

Code Meaning 설명
2xx Success 성공적으로 동작함
4xx Client error 정상적인 사용자와 동일한 권한이 아님
5xx Server error 서버 내부에서 코드가 꼬이거나 일시적으로 사용자가 폭주해서 정상적으로 응답을 못함

4. URL Handling Modules (requests 패키지)

get 방식 사용해보기

import requests

resp = requests.get('http://httpbin.org/get?key=한글')

resp.headers

print(resp.text) # 위에서 body에 해당했던 내용

resp.request.headers

post 방식 사용해보기

resp = requests.post('http://httpbin.org/post?ie=utf8', data={'key':'value'})

print(resp.text)

5. urllib, request 사용하여 함수 만들기

from urllib.robotparser import RobotFileParser
from requests.compat import urlparse, urljoin
from requests.exceptions import HTTPError
import time
import requests

# 접근 가능한지 확인하는 함수
def canfetch(url, agent='*', path='/'):
    robot = RobotFileParser(urljoin(url, '/robots.txt'))
    robot.read()
    return robot.can_fetch(agent, urlparse(url)[2])

# download 함수
def download(url, params={}, headers={}, method='GET', limit=3):
    if canfetch(url) == False:
        print('[Error] ' + url)

    try:
        resp = requests.request(method, url,
                        params=params if method=='GET' else {},
                        data=params if method=='POST' else {},
                        headers=headers)
        resp.raise_for_status()
    except HTTPError as e:
        if limit > 0 and e.response.status_code >= 500:
            print(limit)
            time.sleep(1)
            resp = download(url, params, headers, method, limit-1)
        else:
            print('[{}]'.format(e.response.status_code) + url)
            print(e.response.status_code)
            print(e.response.reason)
            print(e.response.headers)
    return resp

함수 적용해보기

download('http://httpbin.org/status/500', {'k':'v'}, method='GET')
url = 'https://www.google.com/search'
params = {
    'q':'',
    'oq':'',
    'aqs':'chrome.0.69i59j35i39j0l3j69i61j69i60j69i61.1283j0j7',
    'sourceid':'chrome',
    'ie':'UTF-8'
}
params['q'] = params['oq'] = '파 이 썬'
headers = {
    'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36'
}
resp = download(url, params, headers, 'GET')
resp.text

Leave a comment