본문 바로가기
코딩 연습/코딩배우기

[python] 파이썬 웹 크롤링(Web Crawling) 알아보기 #1

by good4me 2020. 10. 15.

goodthings4me.tistory.com

 

■ HTTP 요청 및 응답

웹 요청/응답 처리 시 HTTP 프로토콜의 문자열을 서로 주고 받아야 하며(웹 브라우저의 기능), 크롤링을 위해서 웹브라우저가 아닌 파이썬 라이브러리를 이용해서 요청/응답 처리할 수 있다.

▷ HTTP(S) 요청 방법 및 시기

  • 웹브라우저 접속 시
  • 새로고침
  • 자바스크립트를 통한 요청(예로, Ajax)
  • 앱 API
  • HTML Form 전송

 

▷ HTTP 메서드(패킷을 어떻게 구성하느냐의 차이)

  • GET : 리소스 요청(조회)
  • POST : 리소스 추가, 수정, 삭제 목적(파일 업로드)
  • PUT:  : 리소스 수정 요청
  • DELETE : 리소스 삭제 요청
  • HEAD : 헤더 정보만 요청, 해당 자원 존재 여부 확인 목적
  • OPTIONS : 웹서버 지원 메서드 종류 반환 요청
  • TRACE : 클라이언트 요청을 그대로 반환

 

헤더(Header)

  • User-Agent : 브라우저 종류
  • Referer : 이전 페이지 URL(어떤 페이지를 거쳐서 왔는가?)
  • Accept-Language : 어떤 언어의 응답을 원하는가?
  • Authorization : 인증정보
  • 크롤링 시에는 User-Agent와 Referer를 커스텀하게 설정할 필요가 있다.

good4me.co.kr

 

파이썬 requests 라이브러리

파이썬은 기본 라이브러리로 urllib가 제공되지만, 간결한 코드로 HTTP 요청이 가능한 requests 라이브러리를 사용하는 것이 편리함

 Crawling 관련 라이브러리 설치

pip install requests 또는 python -m pip install requests
pip install BeautifulSoup4

import requests

response = requests.get('https://askdjango.github.io/lv1/')  
# post 방식일 경우, .post
print(response)  # <Response [200]>
print(response.status_code)  # 200
#print(response.headers)
for k, v in response.headers.items():
    print('{}: {}'.format(k, v))
    
    
[실행 결과]
Content-Length: 1783
Content-Type: text/html; charset=utf-8
Server: GitHub.com
Last-Modified: Fri, 08 Mar 2019 08:06:47 GMT
ETag: W/"5c822297-1a23"
Access-Control-Allow-Origin: *
Expires: Tue, 13 Oct 2020 06:06:21 GMT
Cache-Control: max-age=600
Content-Encoding: gzip
X-Proxy-Cache: MISS
X-GitHub-Request-Id: 2A7E:42C0:8D1AC:AEEC1:5F854185
Accept-Ranges: bytes
Date: Tue, 13 Oct 2020 06:42:08 GMT
Via: 1.1 varnish
Age: 0
X-Served-By: cache-tyo19928-TYO
X-Cache: HIT
X-Cache-Hits: 1
X-Timer: S1602571328.901746,VS0,VE184
Vary: Accept-Encoding
X-Fastly-Request-ID: 5266c875a5e7513bb1e3eb7b8020a2b7760da40d

 

import os
import requests
image_urls = [
    'https://image-comic.pstatic.net/webtoon/119874/1417/20191229203111_\
195aed9c04e9a20c27d925ed9c22bd53_IMAG01_1.jpg',
    'https://image-comic.pstatic.net/webtoon/119874/1417/20191229203111_\
195aed9c04e9a20c27d925ed9c22bd53_IMAG01_2.jpg',
    'https://image-comic.pstatic.net/webtoon/119874/1417/20191229203111_\
195aed9c04e9a20c27d925ed9c22bd53_IMAG01_3.jpg',
    'https://image-comic.pstatic.net/webtoon/119874/1417/20191229203111_\
195aed9c04e9a20c27d925ed9c22bd53_IMAG01_4.jpg'
]

for image_url in image_urls:
    headers = {
        'Referer': 'https://comic.naver.com/webtoon/list.nhn?titleId=119874'
    }
    
    response = requests.get(image_url, headers = headers)
    image_data = response.content
    filename = os.path.basename(image_url)
    with open(filename, 'wb') as f:
        print('{} ({} bytes)'.format(filename, len(image_data)))
        f.write(image_data)
        

[실행 결과]
20191229203111_195aed9c04e9a20c27d925ed9c22bd53_IMAG01_1.jpg (229399 bytes)
20191229203111_195aed9c04e9a20c27d925ed9c22bd53_IMAG01_2.jpg (312777 bytes)
20191229203111_195aed9c04e9a20c27d925ed9c22bd53_IMAG01_3.jpg (315490 bytes)
20191229203111_195aed9c04e9a20c27d925ed9c22bd53_IMAG01_4.jpg (273366 bytes)

- 다운로드된 파일이 안보일 경우, requetsget() 인자로 Referer가 필요함 (headers)
- 서비스에 따라 User-Agent헤더와 Referer헤더를 통해 응답을 거부하기도 함.(네이버 웹툰 등의 서버에서 Referer 체크토록 구현된 경우, Referer 추가 필요)

 

 페이지 소스보기와 개발자 도구(F12)

- 페이지 소스보기는 서버에 접속하여 받은 최초의 HTML 소스임(크롤링으로 받는 문자열)
- 개발자 도구의 소스는 페이지 접속 시 자바스크립트가 구동된 소스임(브라우저의 DOM Tree 내역)
- requests를 통한 응답에서 HTML은 페이지 소스보기를 참조해야 함

import requests

url = 'https://news.naver.com/main/list.nhn'
request_headers = {
    'User-Agent' : ('Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537\
                .36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36'),
    'Referer' : 'https://news.naver.com/'
}

# Referer헤더만으로 안될 경우, User-Agent헤더도 추가한다.

response = requests.get(url, headers = request_headers)

print(response.status_code)  # 200
print(response.ok)  # True
print(response.headers)

''' print 결과
{'date': 'Thu, 15 Oct 2020 06:38:08 GMT', 'cache-control': 'no-cache', 'expires':\
 'Thu, 01 Jan 1970 00:00:00 GMT', 'set-cookie': 'JSESSIONID=4CB01EBEE40A7FDEAA926D\
 6DEDAF6B7C; Path=/main; HttpOnly', 'content-language': 'ko-KR', 'vary': 'Accept-\
 Encoding', 'content-encoding': 'gzip', 'transfer-encoding': 'chunked', 'content-\
 type': 'text/html;charset=EUC-KR', 'referrer-policy': 'unsafe-url', \
 'server': 'nfront'}
'''
print(response.headers.__class__)  # 대소문를 구별하지 않는 CaseInsensitiveDict
# <class 'requests.structures.CaseInsensitiveDict'>

print(response.encoding)  # EUC-KR
# response.encoding 값은 Content-Type 헤더의 charset 값을 가져오는데,
# 값이 없을 경우 iso-8859-1로 처리되거나 None이 출력될 수 있음

for k, v in response.headers.items():
    print('{}: {}'.format(k, v))

''' print 결과
date: Tue, 13 Oct 2020 07:12:51 GMT
cache-control: no-cache
expires: Thu, 01 Jan 1970 00:00:00 GMT
set-cookie: JSESSIONID=4FF383438A4A6DE219FAFF0235F9299C; Path=/main; HttpOnly
content-language: ko-KR
vary: Accept-Encoding
content-encoding: gzip
transfer-encoding: chunked
content-type: text/html;charset=EUC-KR
referrer-policy: unsafe-url
server: nfront
'''

html = response.text
#print(html)

# 응답 Body는 .content(Raw 데이터, bytes) 와 .text(.encoding으로 디코딩, 유니코드)로
# 각각 이미지 데이터와 문자열 데이터를 처리할 경우 사용함
# 한글이 깨지는 경우, response.encoding = 'euc-kr' 처리 후 html = response.text 함


from bs4 import BeautifulSoup as btsoup
soup = btsoup(html, 'html.parser')
#print(soup)

# <a>태그 href에 read.nhn 문자열 포함되는 것 추출
#tag_list = soup.select('a[href*=read.nhn]') # 에러 발생

# 정규표현식 .에 대해 처리해야 함
tag_list = soup.select('a[href*=read\.nhn]') # 또는 'a[href*="read.nhn"]'

for tag in tag_list:
    print(tag.text.strip())  # 추출된 tag에서 텍스트만 좌우공백 제거 후 출력 

 

▷ GET 요청 시 params 인자로 지정하는 dict는 동일 Key의 인자를 다수 지정 불가한 반면, (Key, Value) 튜플 형식은 다수의 동일 Key 인자 지정이 가능함

url = 'http://httpbin.org/get'
#get_params = dict([('k1', 'v1'), ('k1', 'v3'), ('k2', 'v2')])
get_params = (('k1', 'v1'), ('k1', 'v3'), ('k2', 'v2'))
response_p = requests.get(url, params = get_params)
print(response_p.text)

''' print 결과
{
  "args": {
    "k1": [
      "v1", 
      "v3"
    ], 
    "k2": "v2"
  }, 
  "headers": {
    "Accept": "*/*", 
    "Accept-Encoding": "gzip, deflate", 
    "Host": "httpbin.org", 
    "User-Agent": "python-requests/2.21.0", 
    "X-Amzn-Trace-Id": "Root=1-5f87ec33-0d5ebf780xxxxxx58304f1850"
  }, 
  "origin": "221.158.xxx.xxx", 
  "url": "http://httpbin.org/get?k1=v1&k1=v3&k2=v2"
}
'''


# json 포맷 응답으로 처리해야 할 경우

print(response_p.json())
# .text에서 args 값만 가져오기 : .json() 객체로 변환하는 과정(deserialize) 필요

''' print 결과
{'args': {'k1': ['v1', 'v3'], 'k2': 'v2'}, 'headers': {'Accept': '*/*', 'Accept\
-Encoding': 'gzip, deflate', 'Host': 'httpbin.org', 'User-Agent': 'python-\
requests/2.21.0', 'X-Amzn-Trace-Id': 'Root=1-5f87ecdd-1aca18d96f45a2b306e378a3'\
}, 'origin': '221.158.91.10', 'url': 'http://httpbin.org/get?k1=v1&k1=v3&k2=v2'}
'''
print(response_p.json()['args'])
# {'k1': ['v1', 'v3'], 'k2': 'v2'}


# json.loads() 통해 직접 deserialize 해도 됨
import json
print(json.loads(response_p.text))
print(json.loads(response_p.text)['args'])

 

▷ post 요청

  • post 요청은 .get() 대신에 .post()로 하며, post 요청 시 Key, Value는 'data=' 또는 'files=' 인자로 처리
  • json post 요청은 json 포맷 문자열로 변환 후 data 인자로 지정하여 처리
  • 객체를 json 인자로 지정하면 내부적으로 json.dumps 처리

 

 

■ BeautifulSoup 라이브러리

▷ HTTP 응답 / HTML Parser

  • 응답하는 html은 중첩된 태그로 구성된 계층적인 하나의 거대한 문자열이며, 브라우저는 HTML문자열을 DOM Tree로 변환하여 문서를 표현
  • 거대하고 복잡한 HTML 문자열에서 특정 문자열 정보를 추출하려면 정규표현식 Rule을 만들어 적용하는 방법과 HTML Parser 라이브러리를 활용하는 방법이 있음 
  • HTML/XML Parser는 문자열에서 원하는 태그 정보를 추출함
from bs4 import BeautifulSoup

html = '''
<ol>
    <li>슈퍼맨이 돌아왔다 - 도경완</li>
    <li>트롯신이 떴다 - 장윤정</li>
    <li>백종원의 골목식당 - 백종원</li>
    <li>놀면 뭐하니 - 유재석</li>
</ol>
'''

soup = BeautifulSoup(html, 'html.parser')  # parser 중 'lxml'도 있음
for tag in soup.select('li'):
    print(tag.text)

''' print 결과
슈퍼맨이 돌아왔다 - 도경완
트롯신이 떴다 - 장윤정
백종원의 골목식당 - 백종원
놀면 뭐하니 - 유재석
'''

# lxml parser(외부 C라이브러리, 불완전한 html 등)를 사용하려면 pip install lxml 함


# 태그 찾는 방법은 find를 통해 찾거나 태그 관계를 지정(CSS Selector를 사용)하여 찾는다


import requests
from bs4 import BeautifulSoup as btsoup

get_url = 'https://www.melon.com/chart/index.htm'
#response = requests.get(get_url)
#print(response)  # <Response [406]>

request_headers = {
    'User-Agent' : ('Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36\
                    (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36'),
}
response = requests.get(get_url, headers = request_headers)
print(response)  # <Response [200]>

html = response.text
soup = btsoup(html, 'html.parser')
music_chart = []


# find사용
for tr_tag in soup.find('tbody').find_all('tr'):
    tag = tr_tag.find(class_='ellipsis rank01')
    if tag:
        tag_list = tag.text.strip()
        music_chart.append(tag_list)
    
for no, music in enumerate(music_chart, 1):
    print(no, music)


# CSS Selector 사용
music_list = []

for tag in soup.select('#tb_list tr .wrap_song_info a[href*=playSong]'):
    music_list.append(tag.text)

for no, music in enumerate(music_list, 1):
    print(no, music)
[출력 결과]

1 DON'T TOUCH ME
2 Dynamite
3 Lovesick Girls
4 취기를 빌려 (취향저격 그녀 X 산들)
5 When We Disco (Duet with 선미)
6 오래된 노래
7 눈누난나 (NUNU NANA)
8 내 마음이 움찔했던 순간 (취향저격 그녀 X 규현)
9 Savage Love (Laxed - Siren Beat) (BTS Remix)
10 마리아 (Maria)
11 How You Like That
12 에잇(Prod.&Feat. SUGA of BTS)
13 Dolphin
14 다시 여기 바닷가
15 아로하
16 잠이 들어야 (Feat. 헤이즈)
17 Downtown Baby
18 홀로
19 어떻게 지내 (Prod. By VAN.C)
20 Blueming
21 Dance Monkey
22 늦은 밤 너의 집 앞 골목길에서
23 작은 것들을 위한 시 (Boy With Luv) (Feat. Halsey)
24 Memories
25 어떻게 이별까지 사랑하겠어, 널 사랑하는 거지
26 살짝 설렜어 (Nonstop)
27 METEOR
28 사랑은 지날수록 더욱 선명하게 남아
29 흔들리는 꽃들 속에서 네 샴푸향이 느껴진거야
30 거짓말이라도 해서 널 보고싶어
31 모든 날, 모든 순간 (Every day, Every Moment)
32 덤디덤디 (DUMDi DUMDi)
33 사랑하게 될 줄 알았어
34 Ice Cream (with Selena Gomez)
35 Not Shy
36 Summer Hate (Feat. 비)
37 오늘도 빛나는 너에게 (To You My Light) (Feat.이라온)
38 2002
39 Don't Start Now
40 우리 왜 헤어져야 해
41 봄날
42 처음처럼
43 마음을 드려요
44 ON
45 Bad Boy
46 보라빛 밤 (pporappippam)
47 서면역에서
48 안녕
49 아무노래
50 가을 타나 봐
51 시작
52 Love poem
53 12:45 (Stripped)
54 Maniac
55 이제 나만 믿어요
56 그 여름을 틀어줘
57 축하해
58 좋은 사람 있으면 소개시켜줘
59 Bet You Wanna (Feat. Cardi B)
60 Into the I-LAND
61 Psycho
62 Paris In The Rain
63 취했나봐
64 밤새 (취향저격 그녀 X 카더가든)
65 Pretty Savage
66 그때 그 아인
67 별을 담은 시 (Ode To The Stars)
68 신난다 (Feat. 마마무)
69 나비와 고양이 (feat.백현 (BAEKHYUN))
70 너를 만나
71 00:00 (Zero O’Clock)
72 bad guy
73 Painkiller
74 너를 사랑하고 있어
75 화려하지 않은 고백
76 밤하늘의 저 별처럼
77 너의 밤은 어때 (취향저격 그녀 X 정은지)
78 Black Swan
79 Stuck with U
80 숲의 아이 (Bon voyage)
81 시든 꽃에 물을 주듯
82 친구
83 반만
84 여름 안에서 by 싹쓰리 (Feat. 황광희)
85 이제서야 (Feat. 카더가든)
86 귀가
87 사랑이란 멜로는 없어
88 MORE & MORE
89 LINDA (Feat. 윤미래)
90 Filter
91 Moon
92 PLAY (Feat. 창모)
93 첫 줄
94 To Die For
95 어떻게 지내 (답가)
96 하루도 그대를 사랑하지 않은 적이 없었다
97 HIP
98 어느 60대 노부부이야기
99 내 마음을 누르는 일 (Daystar)
100 Make A Wish (Birthday Song)

 

[참고] askcompany.kr - 크롤링 차근차근 시작하기

 

댓글