여태까지 이 책에서 사용한 예제는 정적 페이지 하나만 분석하는 예제였고, 다소 인위적으로 만든 예제였습니다. 이 장에서는 여러 페이지, 여러 사이트를 이동하는 스크레이퍼를 통해 실제 문제를 살펴보겠습니다.
웹 크롤러라는 이름은 웹을 크롤링하기 때문에 붙은 이름입니다. 그 핵심은 재귀입니다. 웹 크롤러는 URL에서 페이지를 가져오고, 그 페이지를 검사해 다른 URL을 찾고, 다시 그 페이지를 가져오는 작업을 무한히 반복합니다.
하지만 조심하십시오. 웹 크롤링이 가능하다는 것과 웹 크롤링을 해야 한다는 것은 다른 이야기입니다. 이전 예제에서 사용한 스크레이퍼는 모든 데이터가 페이지 하나에 들어 있는 상황에는 잘 동작합니다. 웹 크롤러를 사용할 때는 반드시 대역폭에 세심한 주의를 기울여야 하며, 타겟 서버의 부하를 줄일 방법을 강구해야 합니다.
3.1 단일 도메인 내의 이동
‘위키백과의 여섯 다리’에 대해서는 못 들어봤더라도, 아마 ‘케빈 베이컨의 여섯 다리’에 대해서는 들어봤을 겁니다. 두 게임 모두 목표는 관계가 없어 보이는 두 대상을 연결하는 겁니다. 위키백과의 경우는 링크로 연결된 항목, 케빈 베이컨의 경우는 같은 영화에 등장한 배우라는 조건으로, 총 여섯 단계(시작과 목표를 포함) 안에 찾는 거죠.
예를 들어 에릭 아이들Eric Idle은 브렌던 프레이저Brendan Fraser와 함께 <폭소 기마 특공대>에 출연했고, 브렌던 프레이저는 케빈 베이컨과 함께 <내가 숨쉬는 공기>에 출연했습니다(http://oracleofbacon.org 에서 배우 사이의 관계에 대해 알 수 있었습니다). 이 경우 에릭 아이들과 케빈 베이컨을 잇는 고리는 세 단계밖에 되지 않습니다.
이 섹션에서는 위키백과의 여섯 다리를 푸는 프로젝트를 시작할 겁니다. 즉, 에릭 아이들의 페이지(https://en.wikipedia.org/wiki/Eric_Idle)에서 시작해 케빈 베이컨의 페이지(https://en.wikipedia.org/wiki/Kevin_Bacon)에 닿는 최소한의 클릭 수를 찾을 겁니다.
하지만 위키백과의 서버 부하에 대한 대책은?
위키백과 재단에 따르면 위키백과 방문자는 대략 초당 2,500명이며, 그중 99퍼센트 이상이 다른 위키백과 도메인으로 이동합니다(자세한 내용은 ‘숫자로 보는 위키미디어’ 페이지(http://bit.ly/2f2ZZXx)를 읽어보십시오). 워낙 트래픽이 대단하니, 우리가 만들 웹 스크레이퍼 정도는 위키백과 서버에 별 영향을 끼치지 않을 겁니다. 하지만 이 책의 코드 샘플을 집중적으로 사용하거나 위키백과 사이트를 스크랩하는 프로젝트를 만든다면 위키백과 재단에 기부(세액 공제도 됩니다)하는 것을 권합니다. 다만 몇 달러라도 당신이 차지하는 서버 부하를 경감하고 만인을 위한 교육 자원에 보탬이 될 겁니다.
지금쯤이면 임의의 위키백과 페이지를 가져와서 페이지에 들어 있는 링크 목록을 가져오는 파이썬 스크립트 정도는 쉽게 만들 수 있을 겁니다.
from urllib.request import urlopen
from bs4 import BeautifulSoup
html = urlopen("http://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'])
링크 목록을 살펴보면 예상대로 ‘Apollo 13’, ‘Philadelphia’, ‘Primetime Emmy Award’ 등이 모두 있을 겁니다. 하지만 원하지 않는 것들도 포함되어 있습니다.
//wikimediafoundation.org/wiki/Privacy_policy
//en.wikipedia.org/wiki/Wikipedia:Contact_us
사실 위키백과의 모든 페이지에는 사이드바, 푸터, 헤더 링크가 있고 카테고리 페이지, 토론 페이지 등 그 외에도 우리가 관심 있어 하는 항목이 아닌 페이지를 가리키는 링크가 많이 있습니다.
/wiki/Category:Articles_with_unsourced_statements_from_April_2014
/wiki/Talk:Kevin_Bacon
최근 필자의 친구는 이와 비슷한 위키백과 스크레이핑 프로젝트를 만들다가 내부 위키백과 링크가 항목 페이지인지 아닌지 판단하는 100행이 넘는 거대한 필터링 함수를 만들게 되었다고 말했습니다. 친구가 ‘항목 링크’와 ‘다른 링크’를 구분하는 패턴을 발견하는 데 시간을 많이 들인 것 같지는 않습니다. 그랬다면 뭔가 규칙을 발견했을 테니까요. 항목 페이지를 가리키는 링크에는 다른 내부 페이지를 가리키는 링크와 비교되는 세 가지 공통점을 찾을 수 있습니다.
이들 규칙을 활용하면 항목 페이지를 가리키는 링크만 가져오도록 코드를 수정할 수 있습니다.
from urllib.request import urlopen
from bs4 import BeautifulSoup
import re
html = urlopen("http://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'])
이 코드를 실행하면 케빈 베이컨의 위키백과 항목에서 다른 항목을 가리키는 모든 링크 목록을 볼 수 있습니다.
특정 위키백과 항목에서 다른 항목을 가리키는 모든 링크 목록을 가져오는 이 스크립트도 물론 흥미롭긴 하지만, 현실적으로는 별 쓸모가 없습니다. 이 코드는 다음과 같은 형태로 바꿀 수 있어야 합니다.
다음 코드가 그와 같은 코드입니다.
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("http://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)
이 프로그램이 필요한 라이브러리를 임포트한 후 처음 하는 일은 현재 시스템 시간을 가지고 난수 발생기를 실행하는 겁니다. 이렇게 하면 프로그램을 실행할 때마다 위키백과 항목들 속에서 새롭고 흥미로운 무작위 경로를 찾을 수 있습니다.
의사 난수와 무작위 시드
앞의 예제에서 필자는 파이썬의 난수 발생기를 사용해 각 페이지에서 무작위로 항목을 선택해, 위키백과를 무작위로 이동했습니다. 하지만 난수는 조심해서 써야 합니다.
컴퓨터는 정확한 답을 계산하는 데는 강하지만, 뭔가 창조하는 데는 심각하게 약합니다. 따라서 난수를 만드는 것도 매우 어려운 일입니다. 대부분의 난수 알고리즘은 균등하게 분배되고 예측하기 어려운 숫자들을 만들어내기 위해 최선을 다하며, 이런 알고리즘이 기동하기 위해서는 시드 숫자가 필요합니다. 시드 숫자가 일치하면 그 결과인 난수 배열도 항상 일치합니다. 따라서 시스템 시간으로 난수 배열을 만들면 항목도 무작위로 고를 수 있습니다. 이렇게 하면 프로그램도 좀 더 흥미진진해집니다.
궁금해할 사람들을 위해 좀 더 첨언하면, 파이썬의 의사 난수 발생기는 메르센 트위스터 알고리즘Mersenne Twister algorithm을 사용합니다. 이 알고리즘은 예측하기 어렵고 균일하게 분산된 난수를 만들긴 하지만, 프로세서 부하가 좀 있는 편입니다. 이렇게 훌륭한 난수를 공짜로 얻을 순 없는 거죠.
그다음 getLinks 함수를 정의합니다. 이 함수는 /wiki/... 형태로 된 URL을 받고 그 앞에 위키백과 도메인 이름인 http://en.wikipedia.org 를 붙여, 그 위치의 HTML에서 BeautifulSoup 객체를 가져옵니다. 그리고 앞에서 설명한 매개변수에 따라 항목 링크 태그 목록을 추출해서 반환합니다.
이 프로그램은 초기 페이지인 https://en.wikipedia.org/wiki/Kevin_Bacon 의 링크 목록을 links 변수로 정의하며 시작합니다. 그리고 루프를 실행해서 항목 링크를 무작위로 선택하고, 선택한 링크에서 href 속성을 추출하고, 페이지를 출력하고, 추출한 URL에서 새 링크 목록을 가져오는 작업을 반복합니다.
물론 단순히 페이지에서 페이지로 이동하는 스크레이퍼를 만들었다고 위키백과의 여섯 다리 문제가 풀리는 것은 아닙니다. 반드시 결과 데이터를 저장하고 분석할 수 있어야 합니다. 데이터의 저장과 분석은 5장에서 설명하겠습니다.
CAUTION_ 예외 처리를 잊지 마세요!
이 예제에서는 간결함을 위해 예외 처리를 대부분 생략했지만, 잠재적 함정이 많이 있음을 알아야 합니다. 예를 들어 위키백과에서 bodyContent 태그의 이름을 바꾼다면 어떻게 될까요? (힌트: 충돌이 일어납니다.)
주의 깊게 살펴보며 진행한다면 이 스크립트 예제도 별문제는 없지만, 자동으로 실행되는 실무 코드에서는 예외 처리가 이 책에서 다룰 수 있는 것보다 훨씬 더 많이 필요합니다. 이에 관해서는1장 을 다시 읽어보십시오.
이전 글 : 성장하는 개발팀 만들기
다음 글 : 이건 달이 아닙니다! 죽음의 별 드론입니다!
최신 콘텐츠