Python 웹 크롤링 튜토리얼
Python을 사용 한 웹 스크레이핑, 웹 크롤링 튜토리얼.
Intro
웹이라는 것은 우리생각보다 간단하지 않다. 데이터 형식은 제 멋대로이고 닫는 태그도 빠져있는 경우가 많다. 그런 웹 환경에서 원하는 데이터를 긁어오려면 많은 상황과 변수를 예상할 수 있어야한다.
아래 서술 할 웹 크롤링 포스트는 O’REILLY - Web Scraping with Python을 공부하며 정리하는 글이다.
첫 웹 스크레이핑
웹 동작의 이해
일단 우리가 매일 사용하는 브라우저
가 얼마나 많은 일을 하고있는지 알아보자.
영희는 웹 서버를 가지고있다. 철수는 데스크톰 컴퓨터를 가지고 영희의 서버에 연결하려 하고있다. 철수의 컴퓨터가 영희의 서버에 연결하려면 다음과같은 과정이 필요하다.
- 철수의 컴퓨터는 1과0으로 된 비트 스트림을 보낸다. 각 비트는 전압으로 구별되고 정보를 구성하며, 헤더와 바디도 이런 정보의 조합으로 표현된다. 헤더에는 바로 다음목표인 철수의 라우터 MAC주소와 최종 목적지인 영희의 IP주소가 들어있다. 바디에는 철수가 영희의 서버 애플리케이션에 요청하는 내용이 들어있을 것 이다.
- 철수의 라우터는 비트 스트림을 받아 철수의 MAC 주소에서 영희의 IP주소로 가는 패킷으로 해석한다. 철수의 라우터의 고유 IP주소를 패킷에 발신자 주소로 기록한 다음 이 패킷을 인터넷에 보낸다.
- 철수의 패킷은 여러 중간 서버를 거치며 영희의 서버를 향해 패킷을 보낸다.
- 영희의 서버는 그 패킷을 받는다.
- 영희의 서버는 헤더에서 포트 번호를 찾고 적절한 애플리케이션에 보낸다.(보통 80포트=웹 애플리케이션)
- 웹 서버 애플리케이션은 데이터 스트림을 받는다. 이 데이터에는 다음과같은 정보가 들어있다.
- 이 요청은 GET요청이다.
- 요청하는 파일은 index.html 이다.
- 웹 서버는 해당하는 HTML 파일을 찾아 새 패킷으로 묶은 뒤 영희의 라우터를 통해 철수의 컴퓨터로 전송한다. 이 패킷은 철수가 보낸 패킷과 비슷한 과정을 거쳐 철수의 컴퓨터에 도달한다.
전체과정이 1초도 안되서 이루어진다
여기서 브라우저
는 패킷을 만들고, 보내고, 돌아온 데이터를 해석해 사진, 소리, 비디오 텍스트 등으로 표현하는 프로그램이다.
이 브라우저
는 프로세서에 명령을 내려 데이터를 애플리케이션에 보내서 유/무선 인터페이스로 처리할 수 있고, 그런 일을 하는 라이브러리가 여러 언어에 존재한다.
물론 Python
에서도 가능하다.
1
2
3
4
from urllilb.request import urlopen
html = urlopen("https://google.com/")
print(html.read())
이 코드를 test.py
로 저장하고 실행시켜보자.
출력결과는 www.google.com
페이지의 HTML 코드 전체이다. urllib
는 파이썬 표준 라이브러리이므로 따로 설치할 것은 없다.
BeautifulSoup 라이브러리
아름다운 수프
이름 그대로 이 라이브러리는 잘못된 HTML을 수정하여 아름답게 만드는 역할을한다. 기본 파이썬 라이브러리가 아니므로 반드시 설치를 해야한다.
$ pip install beautifulsoup4
BeautifulSoup4(BS4)를 본격적으로 활용해보자.
1
2
3
4
5
6
7
from urllib.request import urlopen
from bs4 import BeautifulSoup
html = urlopen("https://en.wikipedia.org/wiki/Web_scraping")
bsObj = BeautifulSoup(html.read(), "html.parser")
print(bsObj.h1)
출력 결과는 다음과 같다.
1
<h1>Web scraping\</h1>
HTML 분석 시작하기
BS4 라이브러리를 제대로 활용하는 법을 알아보기 전에 한가지 짚고 넘어가자.
웹은 엉망진창이다. 전 시간에도 말했듯이 데이터 형식과 태그들은 대부분 웹 개발자 마음대로 작성되어 있다. 이런 웹 환경에서 웹 스크레이퍼가 예기치 못한 데이터 형식에 부딪혀 에러를 일으키고는 죽어버리기 일쑤다.
이런 상황에 대비하여 데이터를 긁어오지는 못하더라도 최소한 죽지는 않도록 예외처리를 해 줄 필요가 있다.
예외처리
1
html = urlopen("https://www.scraping.com/pages/page1.html")
여기서 문제가 생길 수 있는 부분은 크게 두 가지 이다.
- 페이지를 찾을 수 없거나, URL 해석에서 에러가 생긴경우
- 서버를 찾을 수 없는 경우
첫 번째 상황에서는 HTTP 에러가 반환된다. 404 Page Not Found
라던가 500 Internal Server Error
같은 것들 이다.
이런 에러들은 다음과 같이 처리한다.
1
2
3
4
5
6
7
8
9
10
11
12
from urllib.request import urlopen
from urllib.request import HTTPError
from bs4 import BeautifulSoup
try:
html = urlopen("https://www.scraping.com/pages/error.html")
except HTTPError as e:
print(e)
# null을 반환하거나 break 문을 실행하거나 하는 방법을 사용한다.
else:
# 프로그램을 계속 실행한다. except에서 return이나 break를 사용했다면
# 이 else문은 필요없다.
스크레이퍼를 만들 때는 코드의 전반적 패턴에 대해 생각해야 예외도 처리하고 읽기도 쉽게 만들 수 있다.
getSiteHtml
이나 getTitle
같은 범용 함수를 만들고 여기에 예외 처리를 빡세게 만들어두면 재사용하기 쉽고 믿을 수 있는 웹 스크레이퍼를 만들 수 있다.
고급 분석 전략
BS4를 통해 웹사이트를 분석할 때 CSS 코드는 아주 큰 도움이 된다.
<span class="green">...</span>
그리고
<span class="red">...</span>
이 두가지 경우는 클래스를 이용해 쉽게 이 태그들을 구분할 수 있다.
초록색만 수집하고 빨간색은 건너뛸 수도 있다.
find 와 findAll 함수 사용
이 두 함수를 통해 HTML 페이지에서 원하는 태그를 다양한 속성에 따라 쉽게 필터링할 수 있다. 함수의 인터페이스는 다음과같다.
1
2
findAll(tag, attributes, recursive, text, limit, keywords)
find(tag, attributes, recursive, text, keywords)
실제로 이 함수를 쓸 때는 거의 항상 처음 두 매개변수인 tag와 attributes만 쓰게 된다. 그래도 각각의 매개변수 모두 알아보자.
tag
tag 매개변수는 이미 봤다. 태그 이름인 문자열을 넘기거나, 태그 이름으로 이루어진 리스트를 넘길 수도 있다.
attributes
attributes 매개변수는 속성으로 이루어진 딕셔너리를 받고, 그중 하나에 일치하는 태그를 찾는다. 예를 들어 다음 함수는 HTML 문서에서 초록색과 빨간색 태그를 모두 반환한다.
.findAll(“span”, {“class”:{“green”, “red”}})
recursive
recursive 매개변수는 불리언타입. 문서에서 얼마나 깊이 찾아 들어가고 싶은지를 정한다.
True이면 findAll()함수는 일치하는 태그를 찾아 자식의 자식의 자식까지 파고들어간다. False이면 최상위 태그만 찾는다. 기본값은 True이다.
limit
limit 매개변수는 findAll()에서만 쓰인다. limit을 1로 지정하면 find()함수와 같다.
페이지의 항목 처음 몇 개에만 관심이 있을 때 사용한다.
keyword
keyword 매개변수는 특정 속성이 포함된 태그를 선택할 때 사용한다.
1
2
allText = bsObj.findAll(id="text")
print(allText[0].get_text())
정규 표현식 사용하기
정규표현식에 익숙하다면 이것이 얼마나 강력한지 잘 알것이다. 수십줄, 심하면 백몇줄의 복잡한 검색과 필터링 함수를 단 한 줄의 정규 표현식으로 끝낼 수 있다는 건 정말 매력적이다.
간단한 예시로
- 글자 a를 최소한 한 번 써라
- 그 뒤에 b를 정확히 다섯 개 써라
- 그 뒤에 c를 짝수 번 써라
- 마지막에 d가 있어도 되고 없어도 된다
이 규칙을 따르는 문자열은 다음과같은 정규표현식으로 표현할 수 있다.
aa*bbbbb(cc)*(d | )
이 한줄이면 끝날 것을 조건문으로 필터링하는걸 상상해보면..끔찍
한번 배워두면 아주 유용하기때문에 익혀 두는걸 추천한다.
크롤링 시작하기
웹-크롤러 라는 이름에서 알 수 있듯이 URL에서 페이지를 가져오고, 그 페이지를 검사해 다른 URL을 찾고, 다시 그 페이지를 가져오는 작업을 무한히 반복한다. 그러므로 대역폭에 주의를 기울여야하고, 타겟 서버의 부하를 줄일 방법을 항상 생각해야한다.
안그럼 서버 관리자한테 전화가 오거나 내용증명이 날아 올 수도 있다..
단일 도메인 내 이동
이제 우리는 임의의 위키 페이지를 가져와서 페이지에 들어있는 링크 목록을 가져오는 스크립트 정도는 쉽게 만들 수 있다.
1
2
3
4
5
6
7
8
from urllib.request import urlopen
from bs4 import BeautifulSoup
html = urlopen("https://en.wikipedia.org/wiki/Kevin_Bacon")
bsObj = BeautifulSoup(html, "html.parser")
for link in bsObj.findAll("a"):
if 'href' in link.attrs:
print(link.attrs['href'])
wikipedia의 Kevin_bacon 페이지 링크목록들이 출력될 것 이다.
하지만 원하지 않는 이상한 링크들도 모두 포함됬다. 사이드바, 푸터, 헤더 링크들과 카테고리 페이지 등등 우리가 관심없는 페이지 링크들을 걸러야한다.
우리가 원하는 항목 페이지들은 다음과 같은 공통점이 있다.
- 링크들의 id가 bodyContent인 div 안에 있다
- URL에는 세미콜론이 없다
- URL은 /wiki/로 시작한다
이들 규칙을 정규표현식으로 표현하여 코드를 수정하면 다음과 같다.
1
2
3
4
5
6
7
8
9
10
from urllib.request import urlopen
from bs4 import BeautifulSoup
import re
html = urlopen("https://en.wikipedia.org/wiki/Kevin_Bacon")
bsObj = BeautifulSoup(html, "html.parser")
for link in bsObj.find("div", {"id":"bodyContent"}).findAll("a",
href=re.compile("^(/wiki/)((?!:).)*$")):
if 'href' in link.attrs:
print(link.attrs['href'])
이제 다른항목을 가리키는 링크들만 출력할 것 이다.
하지만 이 코드는 현실적으로 쓸모는 없다. 다음과 같이 바꿔야한다.
- 출력된 URL을 반환하는 getLinks 함수
- 시작시 getLinks를 호출하고 반환된 리스트에서 무작위로 항목을 선택해 getLinks를 다시 호출하는 작업을 반복하는 main함수
다음과 같이 작성하면 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from urllib.request import urlopen
from bs4 import BeautifulSoup
import datetime
import random
import re
random.seed(datetime.datetime.now())
def getLinks(articleUrl):
html = urlopen("https://en.wikipedia.org" + articleUrl)
bsObj = BeautifulSoup(html, "html.parser")
return bsObj.find("div", {"id":"bodyContent"}).findAll("a",
href=re.compile("^(/wiki/)((?!:).)*$"))
links = getLinks("/wiki/Kevin_Bacon")
while len(links) > 0:
newArticle = links[random.randint(0, len(links)-1)].attrs["href"]
print(newArticle)
links = getLinks(newArticle)
이 프로그램은 초기페이지에서 링크목록을 links
변수로 정의한다.
그리고 루프에서 항목 링크를 무작위로 선택후 선택한 링크에서 href
속성을 추출하고 페이지를 출력하고, 추츨한 URL에서 새 링크 목록을 가져오는 작업을 반복한다.
자 이제 단일 도메인에서 페이지를 돌아다니는 방법을 알게되었으니 데이터를 수집하는방법을 알아보자.
전체 사이트 크롤링
사이트 전체를 크롤링하려면 보통 홈페이지 같은 최상위 페이지에서 시작해, 내부링크를 모두 검색한다.
검색한 링크를 모두 탐색하고, 다시 링크가 발견되면 한단계 더 내려가는 식이다.
만약 홈페이지의 모든 페이지에 링크가 10개씩 있고 사이트가 5단계로 구성되어 있다면 최소 105페이지, 최대 100,000페이지를 찾아야 사이트를 모두 탐색 할 수 있다.
하지만 실제로 100,000페이지를 가지고있는 사이트는 없다. 내부의 링크가 중복되기 때문이다.
같은 페이지를 두번 크롤링하지 않으려면 발견되는 링크들을 리스트에 보관하는게 좋다. 새로운 링크인지 비교해야 하기 때문이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from urllib.request import urlopen
from bs4 import BeautifulSoup
import re
#집합 자료형.중복제거
pages = set()
def getLinks(pageUrl):
global pages
html = urlopen("https://en.wikipedia.org"+pageUrl)
bsObj = BeautifulSoup(html, "html.parser")
for link in bsObj.findAll("a", href=re.compile("^(/wiki/)")):
if 'href' in link.attrs:
if link.attrs['href'] not in pages:
#새 페이지.
newPage = link.attrs['href']
print(newPage)
pages.add(newPage)
getLinks(newPage)
getLinks("")
이 프로그램은 getLinks에 빈 URL을 넘겨 호출한다.
함수 내부에서 빈 URL앞에 https://en.wikipedia.org
을 붙여 위키백과 첫 페이지 URL로 바꾼다.
그 다음 첫 번째 페이지의 각 링크를 순회하며 전역변수 page에 들어있는지 아닌지를 검사, 없다면 리스트에 추가하고 화면에 출력한다음 다시 함수를 호출한다.
전체 사이트에서 데이터 수집하기
방금 만든 스크레이퍼는 페이지와 페이지를 옮겨 다닐뿐 아무 행동도 하지않는다. 코드를 조금 고쳐서 페이지 제목, 첫 번째 문단, 편집 페이지를 가리키는 링크를 수집하는 스크레이퍼를 만들어 보자.
위키백과의 페이지는 다음과같은 규칙이있다.
- 항목 페이지든 편집 페이지든 제목은 항상 h1태그 안에 있고, 페이지당 하나만 존재한다
- 모든 바디 텍스트는
div#bodyContent
태그에 들어있다 - 편집 링크는 항목 페이지에만 존재한다
위의 규칙들을 이용해서 데이터를 수집해보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from urllib.request import urlopen
from bs4 import BeautifulSoup
import re
#집합 자료형.중복제거
pages = set()
def getLinks(pageUrl):
global pages
html = urlopen("https://en.wikipedia.org"+pageUrl)
bsObj = BeautifulSoup(html, "html.parser")
try:
print(bsObj.h1.get_text())
print(bsObj.find(id ="mw-content-text").findAll("p")[0])
print(bsObj.find(id ="ca-edit").find("span").find("a").attrs['href'])
except AttributeError:
print("Error!")
for link in bsObj.findAll("a", href=re.compile("^(/wiki/)")):
if 'href' in link.attrs:
if link.attrs['href'] not in pages:
#새 페이지.
newPage = link.attrs['href']
print("--------------------\n" + newPage)
pages.add(newPage)
getLinks(newPage)
getLinks("")
h1과 첫번째 단락의 텍스트, 편집 링크를 출력하는 프로그램이다.
하지만 출력하기만 했을 뿐 ‘수집’하지는 않았다. 데이터를 데이터베이스에 저장하고 가공하는 방법은 다음 챕터에서 알아보자.
인터넷 돌아다니며 크롤링 하기
이제 우리가 만든 웹 크롤러도 링크를 따라 이동하는 능력이 있다. 이번에는 외부링크를 무시하지 않고 따라갈 것 이다.
단순히 외부 링크를 닥치는 대로 따라가는 크롤러를 만들기 전에 먼저 자신에게 다음과 같은 질문을 해보자.
- 내가 수집하려 하는 데이터는 어떤 것 인가? 정해진 사이트 몇개만 수집하면 되는가? (분명 더 쉬운방법이 있다) 아니면 전혀 새로운 사이트에도 방문하는 크롤러가 필요한가
- 크롤러가 새 링크에 도달하면 즉시 다른 링크를 따라가야하나? 아니면 조금 머물면서 데이터를 수집해야하나
- 특정 사이트를 제외할 필요가 없는가.(영어가 아닌 컨텐츠도 수집하는가)
- 만약 당신의 크롤러의 존재를 웹 마스터가 알아차렸다면 당신을 법적으로 보호할수 있는가 (이 문제는 마지막에 다루도록 하자)
파이썬 내장함수와 결합하면 다양한 웹 스크레이핑을 하는 코드를 쉽게 만들 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
from urllib.request import urlopen
from urllib.parse import urlparse
from bs4 import BeautifulSoup
import re
import datetime
import random
pages = set()
random.seed(datetime.datetime.now())
#페이지에서 발견된 내부 링크를 모두 목록으로 만든다.
def getInternalLinks(bsObj, includeUrl):
includeUrl = urlparse(includeUrl).scheme + "://" + urlparse(includeUrl).netloc
internalLinks = []
# /로 시작하는 링크를 모두 찾는다.
for link in bsObj.findAll("a", href=re.compile("^(/|.*"+ includeUrl +")")):
if link.attrs['href'] is not None:
if link.attrs['href'] not in internalLinks:
if(link.attrs['href'].startswith("/")):
internalLinks.append(includeUrl+link.attrs['href'])
else:
internalLinks.append(links.attrs['href'])
return internalLinks
#페이지에서 발견된 외부 링크를 목록으로 만든다.
def getExternalLinks(bsObj, excludeUrl):
externalLinks = []
#현재 URL을 포함하지 않으면서 http나 www로 시작하는 링크를 찾는다.
for link in bsObj.findAll("a",href=re.compile("^(http|www)((?!"+excludeUrl+").)*$")):
if link.attrs['href'] is not None:
if link.attrs['href'] not in externalLinks:
externalLinks.append(link.attrs['href'])
return externalLinks
def getRandomExternalLink(startingPage):
html = urlopen(startingPage)
bsObj = BeautifulSoup(html, "html.parser")
externalLinks = getExternalLinks(bsObj, urlparse(startingPage).netloc)
#외부링크가 없을 경우.
if len(externalLinks) == 0:
domain = urlparse(startingPage).scheme+"://"+urlparse(startingPage).netloc
internalLinks = getInternalLinks(bsObj, domain)
return getRandomExternalLink(internalLinks[random.randint(0, len(internalLinks)-1)])
else:
return externalLinks[random.randint(0, len(externalLinks)-1)]
def followExternalOnly(startingSite):
externalLink = getRandomExternalLink(startingSite)
print("Random external link is: "+externalLink)
followExternalOnly(externalLink)
followExternalOnly("https://oreilly.com")
이 프로그램은 https://oreilly.com
에서 시작해 외부 링크에서 외부링크로 무작위 이동한다. 첫 페이지에 외부링크가 항상 있을수는 없기때문에 외부링크를 찾을 때 까지 내부로 파고드는 방법을 사용했다.
크롤링 데이터 저장하기
지금까지 웹 크롤링 결과를 터미널에 출력하거나 txt
파일로 만들었다. 하지만 데이터를 수집하는데만 관심이 있다면 모를까 수집한 데이터를 분석하려면 이런 방법으로는 유용하지않다.
데이터를 관리하는 방법에대해 알아보자. 굳이 웹 스크레이핑이 아니더라도 다른 애플리케이션을 만들 때 데이터베이스는 필수이기 때문에 익숙해 질 필요가 있다.
미디어파일
미디어 파일을 저장하는 방법은 크게 두 가지이다.
하나는 참조를 저장하는 것, 다른 하나는 파일 자체를 저장하는 것 이다.
파일 참조 저장은 간단하다. 파일이 위치한 URL을 저장하기만 하면 끝. 이 방법에는 여러 장점이 있다
- 스크레이퍼가 파일을 내려받을 필요가 없으므로 빠르고 가볍다.
- URL만 저장하므로 컴퓨터의 공간도 확보할 수 있다.
- 코드짜기가 쉽다.
- 호스트 서버의 부하도 적다
물론 단점도 있다.
- 외부에 있는 파일은 언제든 바뀔 수 있다.(웹 마스터에 의해)
- 파일을 사용하려는 시점에 파일이 사라져있을수도 있다.
- 파일을 내려받으면 실제 사람이 사이트를 보는 것처럼 보일 수 있다.(장점이다!)
대부분의 사람은 스크레이핑 한 데이터를 단순히 저장만 해 놓고 만족하지는 않는다. 분석을 위해서는 실제 파일저장이 필수다.
파이썬 v3.X 이상 버전 에서는 url.request.urlretrieve
을 사용하여 원격 URL의 파일을 내려받을 수 있다.
1
2
3
4
5
6
7
8
9
from urllib.request import urlretrieve
from urllib.request import urlopen
from bs4 import BeautifulSoup
html = urlopen("html://www.pythonscraping.com")
bsObj = BeautifulSoup(html, 'html.parser')
imageLocation = bsObj.find("a", {"id":"logo"}).find("img")["src"]
urlretrieve (imageLocation, "logo.jpg")
이 코드는 https://www.pythonscraping.com
에서 로고를 내려받아 스크립트를 실행한 디렉터리에 logo.jpg
라는 이름으로 저장한다.
MySQL에 데이터 저장하기
오픈소스 관계형 데이터베이스 관리 시스템.
SQL 기본 명령어
일단 처음 로그인하면 데이터를 추가할 데이터베이스가 없으니 만들어야 한다.
1
CREATE DATABASE scraping;
그리고 이 데이터베이스를 사용하겠다고 알려준다.
1
USE scraping;
오! 아주 쉽다. 이제 스크랩한 웹 페이지들을 저장할 테이블을 만들자.
1
2
3
4
5
6
7
CREATE TABLE pages (
id BIGINT(7) NOT NULL AUTO_INCREMENT,
title VARCHAR(200),
content VARCHAR(10000),
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY(id)
);
후! 테이블에 목록들까지 추가했다. 각열의 정의는 세 부분으로 나뉜다
- 이름(id, title, created 등)
- 변수 타입(BIGINT(7), VARCHAR, TIMESTAMP)
- 옵션으로 추가속성(NOT NULL, AUTO_INCREMENT 등)
열 목록 마지막에는 반드시 테이블의 키를 정의해야 한다. MySQL
은 이 키를 사용해서 테이블 콘텐츠를 빨리 검색할 수 있도록 준비한다.
쿼리를 실행하고 나면 언제든 DESCRIBE
명령으로 테이블 구조를 확인할 수 있다.
이제 데이터를 삽입해보자.
1
2
3
4
5
6
INSERT INTO pages VALUE(
1,
'Test Page Title',
'this is test page content',
'2017-10-26-16:00:00'
);
성공했다면 삽입한 데이터를 확인해 본다.
1
SELECT * FROM pages WHERE id = 1;
결과
이 문법은 pages
테이블에서 id
가 1인 것을 모두 선택하라는 의미다.
이 외에도 DELETE
나 UPDATE
문도 사용법이 비슷하니 자세한 사용법은 생활코딩 튜토리얼을 참고 하자.
파이썬과 연결
아쉽게도 파이썬에는 MySQL
이 내장되어 있지 않다.
따로 설치해야하는데 pip install pymysql
한줄이면 끝난다.
다음 코드에서 비밀번호 부분은 각자 설정한 비밀번호로 바꾸자.
1
2
3
4
5
6
7
8
9
import pymysql
conn = pymysql.connect(host='localhost', user='root', password='None', db='mysql')
cur = conn.cursor()
cur.execute("USE scraping")
cur.execute("SELECT * FROM pages WHERE id = 1")
print(cur.fetchone())
cur.close()
conn.close()
여기에는 새로운 객체타입이 있다. 하나는 연결 객체(conn)이고, 하나는 커서 객체 (cur)이다.
연결하나에 커서 여러 개가 있을 수 있다. 연결 객체는 데이터베이스 연결에 관여하지만, 그 외에도 데이터베이스에 정보를 보내고, 롤백을 처리하고 새 커서 객체를 만드는 역할도 한다.
커서는 어떤 데이터베이스를 사용 중인지 같은 상태 정보를 추적한다. 데이터베이스가 여러개 있고 이들 전체에 정보를 저장해야 한다면 커서도 여러 개 필요하다.
커서를 다 사용하면 반드시 닫아야한다. 그렇지않으면 연결누수
현상이 발생하는데 이는 더는 사용하지않는 연결을 계속 유지하는 현상이 쌓여 결국 데이터베이스가 다운될 수도 있다.
데이터 입력
자 이제 스크레이핑 결과를 데이터베이스에 저장하는 일을 해보자. 코드는 이전에 만들었던 위키백과 스크레이퍼를 재활용 하자.
웹 스크레이핑을 하면서 유니코드 텍스트를 다루는일이 좀 어려운데 MySQL은 기본적으로 유니코드를 처리하지않는다..ㅠ
검색을 하다보니 기능을 켤수 있단다.
이제 우리 데이터베이스에 유니코드를 알려주자.
1
2
3
4
ALTER DATABASE scraping CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
ALTER TABLE pages CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE pages CHANGE title title VARCHAR(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE pages CHANGE content content VARCHAR(10000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
위의 코드는 데이터베이스와 테이블 두 열의 기본 문자셋을 utf8mb3
에서 utf8mb4_unicode_ci
로 바꾼다.
이제 준비가 끝났으니 다음과 같은 코드를 작성해서 실행해 보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
from urllib.request import urlopen
from bs4 import BeautifulSoup
import datetime
import random
import re
import pymysql
conn = pymysql.connect(host='localhost', user='root', password='None', db='mysql', charset='utf8')
cur = conn.cursor()
cur.execute("USE scraping")
random.seed(datetime.datetime.now())
def store(title, content):
cur.execute(
"INSERT INTO pages (title, content) VALUES (\"%s\",\"%s\")",
(title, content)
)
cur.connection.commit()
def getLinks(articleUrl):
html = urlopen("https://en.wikipedia.org" + articleUrl)
bsObj = BeautifulSoup(html, "html.parser")
title = bsObj.find("h1").find("span").get_text()
content = bsObj.find("div", {"id":"mw-content-text"}).find("p").get_text()
store(title, content)
return bsObj.find("div", {"id":"bodyContent"}).findAll("a",
href=re.compile("^(/wiki/)((?!:).)*$"))
links = getLinks("/wiki/Kevin_Bacon")
try:
while len(links) > 0:
newArticle = links[random.randint(0, len(links)-1)].attrs["href"]
print(newArticle)
links = getLinks(newArticle)
finally:
cur.close()
conn.close()
마찬가지로 password
부분은 각자 설정한 비밀번호를 입력해 둔다.
store
함수가 추가됬는데 이 함수는 문자열 변수 title
과 content
를 받고, 이 변수를 INSERT
문에 추가한다. 커서는 INSERT
문을 실행하고, 자신의 연결을 통해 데이터베이스에 보낸다.
커서는 데이터베이스와 자신의 컨텍스트에 관한 정보를 갖고 있지만, 정보를 데이터베이스에 보내고 삽입하려면 연결을 통해야한다. 커서와 연결이 어떻게 구분되는지 잘 보여주는 함수다.
마지막으로, 코드 마지막 부분에서 finally
문을 메인루프에 추가하여 어떤일이 일어나도 프로그램을 종료하기전에 반드시 커서와 연결을 닫는다. 웬만하면 try-finally
문을 사용하자.
이 외에도 pymysql
에는 유용한 함수가 아주아주아주 많다. 반복하지만 데이터베이스를 배워두면 대부분의 애플리케이션에서 활용할 수 있기때문에 배우는걸 추천한다!
크롤링 한 데이터 정리하기
지금까지 어느정도 정형화된 데이터 소스를 가지고 크롤링 실습을 해 보았다.
오류가있거나 정형화되지 않은 데이터는 아예 무시하고 크롤링하지 않았다. 하지만 이렇게 제한된 범위에서만 데이터를 수집하는걸로는 부족할 때가 있다.
잘못된 구두점, 일관성없는 대문자, 줄바꿈, 오타, 닫는 태그의 부재 등 지저분한 데이터는 웹 크롤링에서 큰 장애물이다.
이번에는 몇가지 도구와 테크닉-코드 작성 방법을 바꿔서 우리의 코드에서 문제가 발생하지 않도록하는 방법, 일단 DB에 들어온 데이터를 정리하는 방법을 알아보자.
코드로 정리하기
데이터 전처리
예외를 처리하는 코드도 중요하지만 예상 못 한 상황에 대응하는 방어적인 코드도 중요하다.
언어학에서 N-그램
은 텍스트나 연설에서 연속으로 나타난 단어 n개를 말한다.
예를들어 I love Python
이라는 문장에서 2-그램
으로 나누면 [I], [love]
, [love], [python]
이렇게 나뉘게 된다. 보통 자연어를 분석할때 공통적으로 나타나는 n-그램
으로 나누어 분석하는게 편리하다.
n-그램
을 분석하는 방법은 다음 챕터에서 알아보도록하고, 이번에는 정확한 형태를 갖춘 n-그램
을 찾는 데 집중하자.
다음 코드는 파이썬 프로그래밍 언어에 관한 위키백과 항목에서 찾은 2-그램 목록을 반환한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from urllib.request import urlopen
from bs4 import BeautifulSoup
import re
def ngrams(input, n):
input = input.split(' ')
output = []
for i in range(len(input)-n+1):
output.append(input[i:i+n])
return output
html = urlopen("https://en.wikipedia.org/wiki/Python_(programming_language)")
bsObj = BeautifulSoup(html, "html.parser")
content = bsObj.find("div",{"id":"mw-content-text"}).get_text()
ngrams = ngrams(content, 2)
print(ngrams)
print("2-grams count is : "+str(len(ngrams)))
ngrams 함수는 입력 문자열을 받고, 모든 단어가 공백으로 구분되었다고 가정하여 연속된 단어로 나눈 다음 n-그램 배열을 만들어 반환한다. 출력 결과는 다음과 같다.
제대로 된 2-그램 보다는 쓸모없는 것들이 잔뜩 들어있다. 마지막 단어를 제외하고 만나는 모든 단어에서 2-그램을 만들어 총 8782개의 2-그램이 만들어졌다.
정규표현식을 써서 \n
같은 이스케이프 문자를 제거하고 유니코드 문자도 제거하면 어느정도 정리가 될 것 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
def ngrams(input, n):
#줄바꿈Xn = 공백
input = re.sub('\n+', " ", input)
#공백Xn = 공백
input = re.sub(' +', " ", input)
input = bytes(input, "UTF-8")
input = input.decode("ascii", "ignore")
print(input)
input = input.split(' ')
output = []
for i in range(len(input)-n+1):
output.append(input[i:i+n])
return output
이 코드는 먼저 줄바꿈 문자를 모두 공백으로 바꾸고, 연속된 공백을 하나의 공백으로 합쳐서 모든 단어와 단어 사이에 공백이 하나만 있도록 만든다.
다음에는 컨텐츠 인코딩을 UTF-8
으로 바꿔서 이스케이프 문자를 없앤다.
이 단계를 거치면 함수의 출력 결과가 크게 개선되지만, 여전히 몇가지 문제가 있다.
1
\['Pythoneers.\[43]\[44]', 'Syntax'], \['7', '/'], \['/', '3'], \['3', '\=='], \['\==', '2']
이런 데이터를 처리하기 위해서는 몇가지 규칙이 더 필요하다.
- i와 a를 제외한 단 한 글자로 된 ‘단어’는 모두 버려야한다
- 위키백과 인용 표시인 대괄호로 감싼 숫자도 버려야한다
- 구두점도 버린다
위의 규칙까지 넣으면 코드가 좀 길어지니까 cleanInput
함수로 분리했다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from urllib.request import urlopen
from bs4 import BeautifulSoup
import re
import string
def cleanInput(input):
input = re.sub('\n+', " ", input)
input = re.sub('\[[0-9]*\]', "", input)
input = re.sub(' +', " ", input)
input = bytes(input, "UTF-8")
input = input.decode("ascii", "ignore")
cleanInput = []
input = input.split(' ')
for item in input:
item = item.strip(string.punctuation)
if len(item) > 1 or (item.lower() == 'a' or item.lower() == 'i'):
cleanInput.append(item)
return cleanInput
def ngrams(input, n):
input = cleanInput(input)
output = []
for i in range(len(input)-n+1):
output.append(input[i:i+n])
return output
import string
과 string.punctuation
으로 파이썬이 구두점이라 생각하는 모든 글자의 리스트를 얻었다.
파이썬 터미널에서 string.punctuation
의 결과를 확인할 수 있다.
컨텐츠의 모든 단어를 순회하는 루프 안에서 item.strip(string.punctuation)
을 사용하면 단어 양 끝의 구두점을 모두 없앨 수 있다. 물론 하이픈이 들어간 단어는 바뀌지 않는다.
이제 훨씬 깔끔한 2-그램을 얻을 수 있다.
데이터 정규화
데이터 정규화란 언어학적으로 또는 논리적으로 동등한 문자열이 똑같이 표시되도록, 최소한 비교할 때 같은 것이라고 판단하게 하는 작업이다.
앞에서 본 n-그램 코드를 사용하면 데이터 정규화 기능을 사용할 수 있다.
물론 이 코드는 중복된 2-그램이 많다는 문제가 있다.
2-그램을 만나면 리스트에 추가할 뿐 빈도를 기록하지도 않는다. 빈도를 기록하고 중복을 없앤다면 데이터 정리 알고리즘이나 정규화 알고리즘을 바꿨을 때 어떤 효과가 있는지 알아보는 데 유용하다.
하지만 파이썬 딕셔너리는 정렬되지 않는다. 그렇기때문에 collections
라이브러리에 들어 있는 OrderedDict
를 사용하여 이 문제를 해결한다.
1
2
3
4
5
6
7
from collections import OrderedDict
...
ngrams = ngrams(content, 2)
ngrams = OrderedDict(sorted(ngrams.items(), key=lambda t: t[1], reverse=True))
print(ngrams)
여기서 파이썬의 sorted
함수를 활용해 값을 기준으로 정렬해서 새 OrderedDict
객체에 넣었다.
사후 정리
코드에서 할 수 있는 일은 한계가있다. 내가 만들지 않았을 뿐더러 어떻게 처리할지 짐작도 안되는 데이터 셋을 다루게 될 수 도 있기때문이다.
이런 상황에서 많은 개발자들은 ‘스크립트를 만들자’라는 생각을 한다. 물론 뛰어난 해결책이 될 수도 있지만 우리는 다른 프로그램을 써보기로 하자.
OpenRefine 오픈리파인
오픈리 파인 은 메타웹 이라는 회사에서 2009년 시작한 오픈 소스 프로젝트이다. 현재는 구글이 인수하여 개발중이다.
예시로 작성하고 캡쳐한 이미지가 과거 버전이라 현재의 UI/UX 가 많이 다를 수도 있다.
오픈리파인의 인터페이스는 브라우저 안에서 동작하지만 데스크탑 애플리케이션 이므로 반드시 내려받아 설치해야 한다. 여기에서 리눅스와 윈도우, 맥 OS X용 애플리케이션을 내려받을 수 있다.
맥 사용자인데 파일을 열때 문제가 있다면
'시스템 환경설정 > 보안 및 개인정보 보호 > 일반'
에서'다음에서 다운로드한 App허용'
을 ‘모든 곳’으로 바꾸자.
OpenRefine-오픈리파인 사용하기
OpenRefine을 사용하려면 데이터를 CSV 파일로 바꿔야한다. 일단 CSV파일을 만들자.
위키백과 텍스트에디터 비교 테이블을 긁어와 CSV파일로 저장하는 예제이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import csv
from urllib.request import urlopen
from bs4 import BeautifulSoup
html = urlopen("https://en.wikipedia.org/wiki/Comparison_of_text_editors")
bsObj = BeautifulSoup(html, "html.parser")
table = bsObj.findAll("table",{"class":"wikitable"})[0]
rows = table.findAll("tr")
csvFile = open("./editors.csv", 'wt', encoding='UTF-8')
writer = csv.writer(csvFile)
try:
for row in rows:
csvRow = []
for cell in row.findAll(['td', 'th']):
csvRow.append(cell.get_text())
writer.writerow(csvRow)
finally:
csvFile.close()
이제 생성된 데이터를 오픈리파인에서 열어보자.
프로젝트를 새로 생성한 뒤의 화면이다. 각 열 레이블 다음에 있는 화살표는 필터링, 정렬, 변형, 데이터 제거가 가능한 도구 메뉴를 연다.
필터링
데이터 필터링에는 filter
와 facet
두 가지 방법이 있다. filter
는 정규 표현식을 써서 데이터를 거를 때 유용하다. 예를 들어 다음은 프로그래밍 언어 열에서 프로그래밍 언어 세 개 이상이 쉼표로 구분된 데이터만 보는 화면이다.
클릭! 하면
정규표현식 .+,.+,.+는 쉼표로 구분된 항목이 최소한 세 개 이상인 값만 선택한다.
facet
은 열의 콘텐츠 전체를 바탕으로 데이터를 제외하거나 포함하려 할 때 유용하다.
위 사진은 GPL이나 MIT 라이선스로 운영하는 에디터만 보는 화면이다.
데이터를 어떤 식으로 필터링 했더라도 언제든지 오픈리파인이 지원하는 형식으로 내보낼 수 있다. 오픈리파인은 CSV, HTML, HTML 테이블, 엑셀 등 여러가지 다른 형식을 지원한다.
form 입력과 로그인 문제 해결하기
웹에서는 로그인 해야만 얻을 수 있는 정보들이 많다.
우리가 지금껏 만든 스크레이퍼들은 HTTP GET 메소드를 써서 정보를 요청했다. 이번에는 웹 서버에서 저장하고 분석할 정보를 보내는 POST 메소드에 집중해보자.
기본적인 폼 전송
파이썬의 기본 라이브러리만으로도 웹 폼을 다룰수 있지만 비효율적 이다. 우리는 requests
라는 라이브러리를 사용하자.
언제나처럼 설치는 pip
1
pip install requests
유명한 사이트인 네이버부터 살펴보자.
로그인 폼에서 마우스 우클릭에 검사를 눌러 페이지 소스를 확인해 보면,
요런게 보인다.
입력필드의 변수 이름을 눈여겨 보자.
폼이 실제로 동작하는 곳은 숨겨져있다. 예를들면 login.php
같은 페이지.
폼에 post요청을 보낼때는 폼이있는 그 페이지가 아닌 데이터를 처리하는 페이지에 보내야 한다.
requests라이브러리로 폼을 보내는건 단 4줄이면 가능하다.
1
2
3
4
import requests
params = {'id': '네이버 아이디', 'pw': '네이버 암호'}
r = requests.post("https://naver.com/login.php", data=params)
print(r.text)
안타깝지만 이 코드는 실행이 안된다.
대부분의 유명한 사이트는 robots.txt
파일에서 로그인 폼에 접근하는 걸 거부하기 때문이다.
하지만 기본적인 페이지의 로그인과 보안이 높지않은 단순한 form 입력 페이지 상당부분에 적용할 수 있다.
단 2가지만 기억하면 된다.
- 데이터를 전송할 필드 이름.
- 폼 자체의
action
속성, 즉 폼을 실제 처리하는 페이지.
라디오 버튼, 체크박스 …
물론 모든 웹 폼이 텍스트 필드와 전송 버튼만으로 구성되는 건 아니다.
라디오버튼, 체크박스, 셀렉트 박스, 슬라이더, 이메일, 날짜 등 에다가 자바 스크립트를 사용하면 쓸 수 있는 필드가 무한히 늘어난다. 무엇이든 개발자 마음대로 만들 수 있다.
이렇게 폼 필드가 다양하더라도 신경 쓸 것은 필드 이름과 값 두가지다.
POST 폼이 너무 복잡해서 막혀있고 브라우저가 정확히 어떤 매개변수를 서버에 보내는지 알고싶다면, 브라우저의 개발자 도구를 보는게 좋다.
크롬 개발자도구로 네이버 페이지를 보면 대충 이렇게 생겼다.
로그인과 쿠키 처리
최신 웹사이트는 대부분 쿠키를 사용해서 누가 로그인했고 누가 안 했는지 추적한다.
일단 사용자가 로그인 요청을 인증하면, 사이트는 당신의 브라우저에 쿠키를 저장한다. 이런 쿠키에는 보통 서버에서 생산된 토큰, 만료일, 트래킹 정보가 들어있다.
사이트는 나중에 이 쿠키를 당신이 그 사이트에 머물며 방문하는 각 페이지에서 일종의 인증 증거로 사용한다.
쿠키에 관한 예제는 책에있는 예제를 활용했다.
여기 사이트에 단순한 로그인 폼이 있다. 사용자 이름은 아무거나 써도 되지만, 비밀번호는 반드시 password 여야 한다.
이 폼을 처리하는 페이지는 https://pythonscraping.com/pages/cookies/welcom.php
이고, 이페이지에는 ‘메인 사이트’ 페이지인 https://pythonscraping.com/pages/cookies/profile.php
를 가리키는 링크가 있다.
로그인하지 않고 환영 페이지나 프로필 페이지에 접근하려 하면 에러 메시지와 함께 먼저 로그인하라는 안내가 표시된다.
프로필 페이지에서는 로그인 페이지에서 브라우저 쿠키를 만들었는지 체크한다.
requests
라이브러리를 사용하면 쿠키 추적도 쉽다.
1
2
3
4
5
6
7
8
9
10
import requests
session = requests.Session()
params = {'username':'username', 'password':'password'}
s = session.post("https://pythonscraping.com/pagse/cookies/welcom.php", params)
print("Cookie is set to:")
print(s.cookies.get_dict())
print("-------------")
print("Going to profile page...")
s = session.get("https://pythonscraping.com/pages/cookies/profile.php")
print(s.text)
이 코드는 로그인 폼을 처리하는 환영 페이지에 로그인 매개변수를 보내고 마지막 요청 결과에서 쿠키를 가져와 출력으로 확인한다. 그런뒤에 session매개변수를 통해 그 쿠키를 프로필 페이지에 보낸다.
기타 폼 문제
웹 폼은 온갖 악의적 봇들이 들끓는 장소다. 봇이 사용자 계정을 만들고 서버 자원을 낭비하고, 블로그에 스팸을 뿌려대길 원하는 사람은 아무도없다.
이런 봇들을 차단하기위해 HTML 폼에는 즉시 드러나지 않은 보안 기능이 포함될 때가 많다.
자동 가입 방지 문자(captcha), 이미치 처리와 텍스트 인식에 관해서는 나중에 알아보자.
이상한 에러를 만나거나, 서버가 알 수 없는 이유로 당신의 크롤러를 차단한다면 허니팟과 숨겨진 필드, 다른 보안수단이 동원되었을 가능성이 높다.
Javascript 사용하기
Javascript 찍먹
사작하기에 앞서 Javasciprt
와 Ajax
, 동적 페이지
등의 개념을 익히고 가자.
클라이언트쪽 스크립트 언어는 웹 서버가 아니라 브라우저 자체에서 동작하는 언어다. 그중에 자바스크립트는 현재 웹에서 가장 널리 쓰이고 잘 지원하는 스크립트 언어다.
자바스크립트는 웹 페이지 대부분에 들어 있을정도로 단순하지만 강력하다. 페이지 소스 코드에서 <script>
태그 부분에 들어있다.
연산자나 루프, 배열 같은 문법적 요소는 일부 비슷하지만, 약한 타입과 스크립트에서 시작된 성격 때문에 다른 프로그래밍 언어에 익숙하다면 이해하는데 약간 노력이 필요할 수 있다.
다음 코드는 재귀함수로 피보나치 수열을 계산한 뒤 브라우저의 개발자 콘솔에 출력하는 코드다.
1
2
3
4
5
6
7
8
9
10
<script>
function fibonacci(a,b){
var nextNum = a + b;
console.log(nextNum + " is in the Fibonacci sequence")
if(nextNum < 100){
fibonacci(b, nextNum);
}
}
fibonacci(1, 1);
</script>
모든 변수앞에 var
가 있다. 자바스크립트에서는 함수도 변수처럼 선언하여 사용할 수 있다는 대단히 좋은 기능이 있다.
예를들어
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script>
var fibonacci = function(){
var a = 1;
var b = 1;
return function(){
var temp = b;
b = a + b;
a = temp;
return b;
}
}
var fibInstance = fibonacci();
console.log(fibInstance() + " is in the Fibonacci sequence");
console.log(fibInstance() + " is in the Fibonacci sequence");
console.log(fibInstance() + " is in the Fibonacci sequence");
</script>
변수 fibonacci
는 함수로 정의됐다. 이 함수가 반환하는 값은 함수이며, 반환된 함수는 피보나치 수열에서 점점 커지는 값을 출력한다.
Ajax?
여태까지 우리가 웹 서버와 한 통신은 페이지를 가져올 때 일종의 HTTP 요청을 보낸 것뿐이다. 페이지를 새로고침하지 않고 폼을 전송하거나 서버에서 정보를 가져오는 경험이 있다면 Ajax
를 통한 것 이다.
Ajax
는 언어가 아니라 특정 작업을 하기 위해 사용하는 기술의 묶음 정도이다.
Ajax
는 비동기 자바 스크립트와 XML(Asynchronous Javascript and XML)의 약자이며, 서버에 별도의 페이지를 요청하지 않고 정보를 주고받기 위해 사용한다. 그러니까
이 서버는 ‘Ajax’로 만든 겁니다.
가 아니라
이 폼은 ‘Ajax’를 써서 웹 서버와 통신합니다.
라고 해야한다.
마찬가지로 DHTML(Dynamic HTML)
도 같은 목적을 위해 사용하는 기술을 묶어 부르는 말이다.
DHTML
은 클라이언트 측 스크립트가 페이지의 HTML 요소의 변화에 따라 바뀌는 HTML이나 CSS이다.
커서를 움직여야 버튼이 나타나거나, 클릭하면 배경색이 바뀌거나, Ajax 요청으로 새로운 컨텐츠가 나타날 수도 있다.
이런 페이지들을 스크립팅 할때 무작정 페이지소스를 긁어오기만 한다면 로딩페이지만 긁어오거나 아무 의미없는 데이터만 가져올 수 있다.
이제 Ajax나 JQuery 뒤에 숨어있는 데이터를 긁어오는 방법을 알아보자.
React 등 CSR 로 동작하는 페이지의 데이터를 긁어오는 것으로 생각하면 된다.
Selenium-셀레니움
이전 시도에서 requests
를 이용해 네이버에 로그인 하려했지만 실패했었다. 프런트 단에서 자바스크립트를 통해 로그인처리를 하기 때문인데, 셀레니움을 사용하면 쉽게 가능하다.
1
$ pip install selenium
Selenium 은 원래 웹사이트 테스트 목적으로 개발됐지만, 강력한 웹 스크레이핑 도구로 사용할 수 있다.
셀레니움에는 자체적인 웹 브라우저가 들어 있지 않으므로 다른 브라우저가 있어야 동작한다.
보통 사용하는 파이어폭스나 크롬 등을 이용할 수 있다.
스크립트가 요란하게 브라우저를 띄우고 화면을 보여주는게 싫다면 조용히 백그라운드에서 실행되는 브라우저인 팬텀JS 라는 도구도 있다. 나는 크롬을 사용하겠다.
일단 네이버 로그인 폼에서 마우스 우클릭 후 검사.
개발자 콘솔에서 태그의 id
나 name
속성을 찾아본다. 그게 제일 중요하니까.
네이버는 아이디는 ‘id’ 비밀번호는 ‘pw’로 되어있는걸 볼 수 있다.
로그인 버튼을도 검사해 보자. 네이버 홈페이지가 아닌 폼 데이터를 처리하는 페이지에 요청을 보내야한다.
이제 스크립트를 통해 네이버에 자동으로 로그인하는 프로그램을 만들어보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from bs4 import BeautifulSoup
from selenium import webdriver
import time
#---------------------------------------
# 브라우저 열기
driver = webdriver.Chrome('C:\\크롬\\드라이버를\\저장한\\절대경로')
driver.get("https://nid.naver.com/nidlogin.login")
#---------------------------------------
#---------------------------------------
# 로그인 하기
driver.find_element_by_name('id').send_keys('네이버 ID')
driver.find_element_by_name('pw').send_keys('네이버 PW')
# 로그인 버튼 클릭
driver.find_element_by_xpath('//*[@id="frmNIDLogin"]/fieldset/input').click()
#---------------------------------------
로그인하기의 선택자들은 BS4
와 사용법이 비슷해서 이해가 쉽지만 마지막에 로그인 버튼 클릭하는 문법은 생소하다.
로그인 페이지 소스를 자세히보면 로그인 버튼에는 id
나 name
같은 선택자가 없다. 이런 경우에는 XPath(XML Path)
를 사용한다. XPath
는 XML 문서의 일부분을 탐색하고 선택하는데 사용하는 쿼리 언어다.
XPath
문법은 크게 네가지 개념으로 이루어진다.
- 루트 노드 대 루트가 아닌 노드
/div
는 오직 문서의 루트에 있는 div 노드만 선택한다.//div
는 문서의 어디에있든 모든 div노드를 선택한다.
- 속성 선택
//@href
는 href 속성이 있는 모든 노드를 선택한다.//a[@href='http:\/\/google.com']
는 구글을 가리키는 모든 링크를 선택한다.
- 위치에따른 노드 선택
(//a)[3]
는 문서의 세 번째 링크를 선택한다.(//table)[last()]
는 문서의 마지막 테이블을 선택한다.(//a)[position() < 3]
는 문서의 처음 두 링크를 선택한다.
- 애스터리스크(*) 는 어떤 문자나 노드의 집합이든 선택한다.
//table/tr/*
은 모든 테이블에서 모든 자식 tr태그를 선택한다.//div[@*]
는 속성이 하나라도 있는 모든 div태그를 선택한다.
물론 이것 말고도 고급기능이 많지만 이정도만 알아도 사용하는데 무리가 없다.
로그인 버튼의 태그 구조를 살펴보면
이렇게 생겼다.
이제 //*[@id="frmNIDLogin"]/fieldset/input
이 코드가 조금은 이해가 가겠다.
위 코드를 실행하면 신기하게 크롬 브라우저 창이 뜬 뒤에 자동으로 로그인 폼이 입력되고 로그인 버튼도 눌러준다.
여기서는 로그인하는것만 다루었지만 이전 포스트에서 설명한 여러 예제들과 결합하면 멋진 웹 크롤러가 탄생할 것이다.
Congratulation
우리는 이제 웹 페이지를 마음 껏 돌아다니며 원하는 데이터를 긁어올 수 있다!
계속 중요하게 이야기 했지만, 스크립트를
절대로
아무 사이트나 돌아니면서 실행하면 안된다절대로.
잘 보호된 일부 사이트는 폼을 너무 빨리 전송하거나 너무 빨리 행동한다면 막힐 수 있다. 일반적인 사용자보다 압도적으로 많은 정보를 가져가거나 요구해도 웹 마스터의 주의를 끌고 차단 당할 수 있다.
따라서 멀티스레드로 속도를 끌어올리는 것 보다는 time.sleep(3)
등의 방법으로 몇 초 정도 간격을 두는게 좋다.
다른건 몰라도 이것만은 꼭 지키자. 서버 자원을 과도하게 사용하면 법적으로 불리해질 뿐만 아니라, 소규모 사이트같은 경우에는 서버가 다운되게 할 수도 있기때문이다.
웹 사이트를 다운시키는 것은 윤리적으로 모호한 상황이 아니다. 그건 완전히
잘못된 일
이다. 주의하자.