[Python] requests 기초와 beautiful soup를 활용한 크롤링

반응형
 [Python] requests 기초와 beautiful soup를 활용한 크롤링

본게시글은 HTTP 헤더 이해와 Requests의 설치가 되어있어야 합니다.

또한 requests의 라이브러리를 자세하게 살펴보는 게시글 입니다.

Requests: HTTP for Humans 공식문서 바로가기

- Python에서 기본 라이브러리로 urllib가 제공되지만, 이보다 더 간결한 코드로 다양한 HTTP요청을 할 수 있는 라이브러리 이기 때문에 사용
- JavaScript처리가 필요한 경우에 selenium을 고려할 수 있음. 하지만 requests에서도 적용이 가능한 부분도 있으며, 이는 requests 사용시 크롤링 할 페이지에 대해 다방면의 검토가 필요하다고 볼 수 있음.
- 크롤링을 할때 요청에서 requests를 사용한다면 가장 효율적인 처리라고 할 수 있음 

단순 GET 요청

사용방법

1
2
3
4
5
6
7
8
9
import requests
 
response = requests.get('https://www.naver.com')
                    .post
                    .put
                    .delete
                    .head
                    .options
                    .trace
cs
1번째줄: import로 사용을 먼저 하고
3번째줄: request.get이라던가 여러가지 메소드들을 넣어 사용할 수 있음('요청할 url')

실행

주의사항

그대로 받아와서 response를 출력한다면 1과 같이 응답 코드가 등장하므로

2번과 같이 .text를 통하여 변환을 시켜주고 출력하셔야 정상적으로 출력이 됩니다.


html코드는 어디서 오는것일까?

크롬환경에서 f12키를 누르시면 코드가 나오는데 이 코드를 가져오는것입니다.


beautiful soup을 활용한 크롤링 (많은 html코드중 제가 원하는 부분을 잘라보겠습니다)

설치방법은 커맨드창에서 pip3 install beautifulsoup4를 입력해 주세요

작업도중 알수없는 오류로 우분투 환경에서 진행하겠습니다.
우분투의 파이썬 버전은 3.5.2 입니다.

네이버 영화부분을 잘라보겠습니다

일단은 마우스 우클릭->페이지 소스보기로 살펴보겠습니다.



코드를 살펴보면 영화 순위 테이블을 발견할 수 있습니다

우리는 이것을 영화제목만 뽑아보겠습니다.



1
2
3
4
5
import requests
 
response = requests.get('http://movie.naver.com/movie/sdb/rank/rmovie.nhn?sel=cnt&date=20170714')
 
html = response.text
print(html)
cs

코드를 통해

실행을 확인해 보면

잘나오네요 여기서 이제 BeautifulSoup를 통해 원하는 랭킹만 뽑아보겠습니다.



자세히 보시면 div class="tit3">의 규칙성을 보이는것을 확인하실수 있습니다.

이제 이렇게 잘라보겠습니다.



1
2
3
4
5
6
7
8
9
import requests
 
response = requests.get('http://movie.naver.com/movie/sdb/rank/rmovie.nhn?sel=cnt&date=20170714')
 
html = response.text
 
from bs4 import BeautifulSoup    #BeautifulSoup import
soup = BeautifulSoup(html, 'html.parser'#html.parser를 사용해서 soup에 넣겠다
soup.select('div[class=tit3]')#div[class=tit3]인 애들만 선택해서 출력하
cs

코드 추가 하겠습니다.


출력이 잘되지만 불필요한 태그가 껴있으니 없애보겠습니다.



1
2
3
4
5
6
7
8
9
10
import requests
 
response = requests.get('http://movie.naver.com/movie/sdb/rank/rmovie.nhn?sel=cnt&date=20170714')
 
html = response.text
 
from bs4 import BeautifulSoup    #BeautifulSoup import
soup = BeautifulSoup(html, 'html.parser'#html.parser를 사용해서 soup에 넣겠다
for tag in soup.select('div[class=tit3]'):
    print(tag.text)
cs

반목문을 사용해서 tit3에 있는 텍스트만 뽑아보겠습니다



깔끔하게 출력되지만 공백이 아쉽네요

공백을 없애보겠습니다.



1
2
3
4
5
6
7
8
9
10
import requests
 
response = requests.get('http://movie.naver.com/movie/sdb/rank/rmovie.nhn?sel=cnt&date=20170714')
 
html = response.text
 
from bs4 import BeautifulSoup    #BeautifulSoup import
soup = BeautifulSoup(html, 'html.parser'#html.parser를 사용해서 soup에 넣겠다
for tag in soup.select('div[class=tit3]'):
    print(tag.text.strip())
cs

출력할때 공백을 제거하는 .strip()을 사용해봅시다



예쁘게 출력됩니다.



requests를 사용하지 않고 BeautifulSoup의 사용 바로가기

이 코드를 보시면 똑같은 영화 리스트를 출력하였는데 코드가 좀더 복잡한 것을 보실수 있습니다.

urllib를 사용하였는데 조금더 복잡한 것을 확인하실 수 있습니다.


때문에 저는 requests와 함께 사용하는 것을 추천하겠습니다.


GET요청시 커스텀헤더 지정

코드의 잔수정이 많은관계로 PyCharm으로 환경을 바꾸고, Window로 돌아와 작업하겠습니다.
클라이언트(사용자)가 직접 헤더를 커스텀해서 GET요청 해보는 실습입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
import requests
response = requests.get('http://movie.naver.com/movie/sdb/rank/rmovie.nhn?sel=cnt&date=20170714')
html = response.text
 
 
##==========커스텀 헤더============##
request_headers = {
    'User-Agent': ('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 '
                   '(KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36'),
    'Referer''http://movie.naver.com/movie/sdb/rank/rmovie.nhn?sel=cnt&date=20170714'# 영화랭킹
}
response = requests.get('http://movie.naver.com/movie/sdb/rank/rmovie.nhn?sel=cnt&date=20170714', headers=request_headers)
print(response)
cs

request_headers={}안에 ()는 1줄입니다.

만약 전에 사용하던 default헤더가 있다면 오버라이트와 함께 재정의됩니다.

User-Agent 출처는 nomade.kr 강의입니다.



결과확인

이렇게 사용자가 직접 헤더를 커스텀해서 요청을 보내도 올바른 응답이 오는것을 확인하실 수 있습니다.




GET요청시 GET인자 지정하기

GET요청시 인자는 dict과 Key,Value 형식의 tuple 두가지로 지정할 수 있습니다.
- params 인자로 dict지정: 동일 Key에 다수 지정 불가(Key : 인자 => 1 : 1)
- params 인자로 tuple지정: 동일 Key에 다수 지정 가능(Key : 인자 => 1 : N)

비교실험화면

@다수의 지정이 불가능한 dict 지정 예제

1
2
3
4
5
6
import requests
 
#get_params = (('k1', 'v1'), ('k1', 'v3'), ('k2', 'v2'))
get_params = dict([('k1''v1'), ('k1''v3'), ('k2''v2')])
response = requests.get('http://httpbin.org/get', params=get_params)
print(response.json())
cs
dict을 사용해 사전형태로 지정을 해보겠습니다.

결과값을 보시면 k1이라는 키에는 v3만 들어가 있습니다.

v1은 처음에 들어갔지만 v3으로 오버라이팅 되었기 때문에 v3만 표현되겠습니다.



@다수 지정이 가능한 tuple 지정 예제

1
2
3
4
5
import requests
 
get_params = (('k1''v1'), ('k1''v3'), ('k2''v2'))
response = requests.get('http://httpbin.org/get', params=get_params)
print(response.json())
cs

http://httpbin.org는 http의 응답을 보내주는 사이트입니다. 제가 요청했을때 응답이 오면 제대로 요청한 것을 확인할 수 있기 때문에 예제 사이트로 지정했습니다.

결과를 보시면 k1이라는 Key에 v1, v3의 값이 들어가는것을 확인할 수 있습니다.



웹에서는 동일키를 지원해주기 때문에 다수의 인자값 지정이 가능한 tuple타입으로 사용하는게 맞다고 보면 됩니다. 그렇다고 dict타입을 사용하지 않는게 아닙니다. 사용해야 할때도 있기 때문에 필요에 따라 사용하시면 되겠습니다.


응답헤더

- requests.structures.CaseInsensitiveDict 타입이며 일반 dict타입이 아님
Key문자열의 대소문자 구별을 하지 않음
- 각 헤더의 값은 헤더이름을 Key로 접근하여 획득함

실습

content-type이나 Content-Type 대소문자 가리지 않고 출력되는것을 확인하실 수 있습니다.



응답 body

응답에는 응답.content와 응답.text 두가지로 나눌 수 있습니다.
- bytes_data = response.content  #응답 Raw 데이터(bytes)
모든 요청에는 헤더와 바디가 있는데 바디부분은 bytes로 반환을 한다.
ex)만약 이미지 url로 요청을 했다면 응답 Raw데이터에는 이미지가 들어있게 된다.
html로 요청을 했다면 html의 문자열 bytes로 응답이 오게 되고
css로 요청을 했다면 css의 문자열 bytes데이터로 응답이 오게 된다.
- str_data = response.text    #response.encoding으로 디코딩하여 유니코드 변환
문자열응답인것을 알고 있을경우 html요청을 .text로 사용하면 바로 유니코드 반환하게 된다.

- 이미지 데이터일 경우에는 .content만 사용
1
2
3
4
with open('flower.jpg''wb') as f:
#flower.jpg를 열고 / f: 파일 오브젝트 획득
    f.write(response.content)
    #response.content이미지 데이터를 가지고 파일에다(f) 데이터를 씀(write)
cs

- 문자열 데이터일 경우에는 .text를 사용
1
2
3
4
5
6
7
html = response.text
#.text 사용해서 html코드 획득
 
html = response.content.decode('utf8')
#response.content를 사용하여 decode를 획득 가능
 
#두가지 방법중 원하는 것을 사용하면 
cs
둘중에 1번이 간결하고 좋은 코드입니다.

응답이 json포맷일경우

(1) json.loads(응답문자열)을 통해 직접 Deserialize를 수행
(2) 혹은 .json()함수를 통해 Deserialize를 수행
- 응답문자열이 json포맷이 아닐 경우 JSONDecodeError예외가 발생

json응답이 오는것을 확인

응답이 이와 같이 json으로 올경우를 args의 값만 출력해 보겠습니다.


(1) json.loads(응답문자열)을 통해 직접 Deserialize를 수행

이렇게 json파일도 원하는 값만 가져올 수 있는데 이것을 deserialize 라고 합니다.


(2) json()함수를 통해 deserialize를 수행


보기쉽죠?

이렇게 json파일도 원하는 값만 가져올 수 있는데 이것을 deserialize 라고 합니다.


(2)번이 조금다 간결해서 (2)사용을 추천합니다.




HTTP POST요청

단순 POST 요청

get과 똑같지만 get대신 post를 사용하는 점만 다릅니다. 참쉽죠?



POST요청시 커스텀헤어, GET인자 지정

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests
 
request_headers = {
    'User-Agent': ('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 '
                   '(KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36'),
    'Referer''http://httpbin.org',
}
get_params = {'k1''v1''k2''v2'}
response = requests.post(
    'http://httpbin.org/post',
    headers=request_headers,
    params=get_params)
 
print(response)
cs

GET요청시 커스텀헤더와 똑같습니다. 단지 get을 post로 바꿨다는 점 말고는요(9번째줄)

하지만 12번째 줄을 보시면 get_params로 해주는것에 의문이 생길수 있는데 왜 그런것일까요?


12번째줄의 params는 무조건 get_params만 올수 있습니다. 무조건 get인자이며 post는 다르게 해줘야함


그렇다면 get 인자가 도대체 무엇일까?

http://news.naver.com/main/main.nhn?mode=LSD&mid=shm&sid1=103 가 주소라는것은 다 아실것입니다.

그 주소 안에 ?부터 있는 mode=LSD&mid=shm&sid1=103 가 GET인자가 되는 것입니다.


그래서 이 주소에 대한 POST요청을 날린다고 말할수 있으니 POST요청에서는 GET인자를 사용할 수 있다 볼 수 있습니다.


정리하자면

params는 

GET인자일때는 GET인자만 지정이 가능하지만

POST인자 일때는 GET POST 둘다 지정이 가능합니다.


일반적인 form 전송/요청

ex)게시판 글쓰기
글쓰기의 경우 post요청이기 때문에 좋은 예가 될 수 있다.

- data인자로 dict지정: 동일 Key의 인자를 다수 지정 불가

앞서 GET요청의 GET인자의 경우 params=get_params였지만

글쓰기의 인자는 post로 하기 때문에 인자도 data=data or fields=fields로 지정해 줄 수 있습니다.


둘다 사용도 가능합니다

get인자와 post인자를 둘다 data로 지정할 경우 get인자로는 get인자로 post인자는 post로 data를 보내게 됩니다.


json으로 출력해보면 form으로 잘들어 오는것을 확인하실 수 있습니다.


- data인자로 (Key, value)형식의 tuple 지정: 동일 Key의 인자를 다수 지정 가능



이렇게 보낸 post방식은 application/x-www-form-urlencoded 방식으로 데이터 인코딩이 되는데,
그말이 무엇이냐 하면

?mode=LSD&mid=shm&sid1=103 이렇게 쓰는것을 말합니다.

key=value & key=value .....이며
mode(key)=LSD(value)&mid(key)=shm(value) 이것을 말하는것이죠
이것을 urlencoded방식으로 인코딩이 된것입니다.


json으로 출력했을때 form으로 잘 넘어오는 것을 확인하실 수 있습니다.


JSON POST 요청

JSON API 호출시

서버에서 JsonAPI를 지원하는데 그때 우리가 맞춰서 데이터를 올려 보내보겠습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
import requests
import json
 
json_data = {'k1''v2''k2': [1,2,3], 'name''rednooby'}
 
# json포맷 문자열로 변환 후, data인자로 지정
json_string = json.dumps(json_data)#문자열로 변환
response = requests.post('http://httpbin.org/post', data=json_data)#서버로 보낸다
 
response = requests.post('http://httpbin.org/post', json=json_data)
#json= 으로 처리해주면 내부에서 자동으로 json.dump 처리를 하기때문에 7째줄이 필요없겠죠
print(response)
print(response.json())

cs

코드입니다.




무엇이 다른가 살펴보면 앞에서 한 출력값들은 args나 form에 데이터들이 들어있었지만 이번에는 data에 들어있는 것을 확인할 수 있습니다.



파일 업로드 요청

글쓰기 자동화 할때 ex)게시판에 파일첨부를 같이 하고 싶을때 사용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests
from bs4 import BeautifulSoup
 
files = {
    'photo1'open('C:\\f1.jpg''rb'),#rb: 읽기 바이러니모드
    'photo2'open('C:\\f2.jpg''rb'),
    'photo3': ('f3.jpg'open('C:\\f3.jpg''rb'), 'image/jpeg', {'Expires''0'}),
#                   파일명        파일오브젝트         컨텍트타입        헤더
#photo1과 2는 단순하기 때문에 3처럼 넣는것이 베스트
}
post_params = {'k1''v1'}
response = requests.post('http://httpbin.org/post', files=files, data=post_params)
print(response)
print(response.json())
cs
코드


files에 파일이 전송된것을 확인할 수 있습니다.

파일은 경로내에 동일한 이름의 파일이 있어야 하며 files에 있는 값은 json으로 변경된 값입니다.




post_params는 form에 응답이 잘 와있습니다.




이것을 왜 알아야 하는가?(마무리 정리)

크롤링에서는 POST방식 보다는 GET요청을 더 사용을 많이 하는 편이라 정의할 수 있으며
(1) 필요한 헤더를 지정(커스텀헤더 지정)
(2) get parameter필요시 넣기(get_params)
(3) 상태코드 체크 print(response)해서 나오는 코드 or response.ok 로 True, False로 체크도 가능
(4) 응답 body에서 문자열(str_data) 혹은 데이터(bytes_data)을 받아서 처리

여기까지만 잘 처리해도 크롤링 하는데는 문제가 없을것 입니다.