[python] 파이썬 웹 크롤링(3): 본격적인 크롤링

업데이트:

파이썬 웹 크롤링(3): 본격적인 크롤링

본 포스팅은 Web Scraping with Python by Ryan Mitchell을 참고하였습니다.

참고 링크

Chapter3: 본격적인 크롤링의 시작

지금까지는 단 하나의 정적인 페이지들을 크롤링 해보았습니다. 지금부터는 실제 인터넷 상에 널리 퍼져있는 다수의 웹 사이트와 다수의 페이지들을 크롤링 해보겠습니다. 크롤링의 핵심은 다수의 사이트들을 넘나들며 재귀적으로 사용하는 것입니다. 하나의 페이지 내용을 긁어오고, 다른 페이지를 조사하고 또 다시 긁어 옵니다. 하지만 꼭 그래야만 하는 것은 아니고 하나의 페이지에 모든 정보가 존재한다면 하나의 페이지만 긁어와도 충분합니다.

3.1 하나의 도메인 여행하기

“Six degrees of wikipedia” 라는 말이 있습니다. 이말은 위키피디아에서 6개의 링크 이내에 모든 컨텐츠가 연결 되어 있다는 뜻입니다. 이번 단원에서 할 일은 the Eric Idle page 에서 링크를 타고 들어가서 the Kevin Bacon page 로 이동하는 것입니다. 아래코드는 해당 페이지의 모든 링크 목록을 보여줍니다.

from urllib.request import urlopen
from bs4 import BeautifulSoup
html = urlopen("http://en.wikipedia.org/wiki/Kevin_Bacon")
bsObj = BeautifulSoup(html)
for link in bsObj.findAll("a"):
    if 'href' in link.attrs:
        print(link.attrs['href'])
/wiki/Wikipedia:Protection_policy#semi
#mw-head
#searchInput
/wiki/Kevin_Bacon_(disambiguation)
/wiki/File:Kevin_Bacon_TIFF_2015.jpg
/wiki/Philadelphia
/wiki/Pennsylvania
/wiki/Kyra_Sedgwick
/wiki/Sosie_Bacon
#cite_note-1
/wiki/Edmund_Bacon_(architect)
/wiki/Michael_Bacon_(musician)

위 코드 실행 결과를 보시면 여러분이 찾는 모든 링크가 있을 것입니다. “Apollo 13”, “Philadelphia”, “Primetime Emmy Award”, 등과 같이 말이죠. 우리가 원하는 모든 것이 있는 반면, 원하지 않는 것들도 많이 섞여 있습니다. 아래와 같은 것들 말이죠

”//wikimediafoundation.org/wiki/Privacy_policy”

”//en.wikipedia.org/wiki/Wikipedia:Contact_us”

사실, 위키피디아의 모든 사이드바(sidebar), 푸터(footer), 헤더(header)들은 모든 페이지에 존재합니다. 따라서 저런 불필요한 정보를 필터랑하고, 해댕 링크가 아티클 페이지 인지 아닌지 구분하기 위해 100줄이 넘는 필터링 코드를 짜기도합니다. 하지만 이런 식이라면 엄청난 시간이 소모될 뿐만 아니라 모든 패턴을 걸러낼 수도 없습니다. 만약 여러분이 아티클 페이지 인지 아닌지만 검사할 수 있다면 아주 편할 것입니다. 아티클 페이지는 아래와 같은 특징을 가집니다.

  • 아티클 페이지는 div 내부에 존재하며 id는 bodyContent로 세팅 되어 있습니다.
  • URL은 세미콜론을 포함하지 않습니다.
  • URL은 /wiki/로 시작합니다.

우리는 이러한 규칙을 이용해 우리가 원하는 코드를 짜보겠습니다.

from urllib.request import urlopen
from bs4 import BeautifulSoup
import re
html = urlopen("http://en.wikipedia.org/wiki/Kevin_Bacon")
bsObj = BeautifulSoup(html)
for link in bsObj.find("div", {"id":"bodyContent"}).findAll("a",href=re.compile("^(/wiki/)((?!:).)*$")):
    if 'href' in link.attrs:
        print(link.attrs['href'])

이제 위 코드를 실행하면 Kevin Bacon 페이지에서 연결되어있는 다른 위키피디아 아티클을 확인할 수 있습니다. 하지만 위 형식을 단지 하나의 사이트에서만 쓸수 있다면 사실상 쓸모없는 경우가 많습니다. 왜냐하면 다른 사이트에서는 못쓰니까요. 위 코드를 조금 수정해보겠습니다.


  • getLinks 함수를 사용해서 /wiki/ 폼을 사용하면 해당 폼에 해당하는 모든 URL 리스트를 보여줍니다.
  • 메인 함수는 시작 아티클에서 getLinks를 호출하는데, 랜덤리스트 중 하나를 보여줍니다. 그리고 다시 getLinks를 호출합니다. 그리고 이는 더이상 새로운 페이지가 없을 떄까지 반복됩니다.

자 지금까지, 한 페이지 내의 링크 목록을 가져오는 실습을 해보았습니다.

3.2 전체 사이트 크롤링하기

이전 섹션에서는 특정 페이지에서의 링크 목록을 가져와 보았습니다. 하지만 사이트 전체에 대해서 하려면 어떻게 할까요? 전체 사이트를 크롤링하는데는 메모리 자원이 많이 소모 됩니다. 이에 대해서는 이후에 챕터 5에서 자세히 다루도록 하겠습니다.

전체 사이트를 크롤링하면 뭐가 좋을까요?

  1. 사이트맵을 생성할 수 있습니다. 이는 사이트맵이 제공되지 않는 사이트에 유용합니다.
  2. 데이터 모으기가 유용합니다.

전체 사이트를 크롤링할 떄는 페이지 홈에서 시작합니다. 그리고 홈에 연결되어 있는 모든 페이지 목록을 수집합니다. 작업을 할 때는 크롤링할 대상이 폭발적으로 증가합니다. 각 페이지들 내부에 10개의 링크가 존재한다고 하면, 5개의 페이지만 딥하게 들어가도 내가 조사해야할 페이지 수는 100,000 페이지가 됩니다. 하지만 이 경우 100,000 페이지라고 하더라도 중복되는 페이지가 많습니다. 따라서 중복 수집을 막가위해 내부 링크 포맷을 일관성 있게 짜는 것이 중요합니다.

from urllib.request import urlopen
from bs4 import BeautifulSoup
import re
pages = set()
def getLinks(pageUrl):
    global pages
    html = urlopen("http://en.wikipedia.org"+pageUrl)
    bsObj = BeautifulSoup(html)
    for link in bsObj.findAll("a", href=re.compile("^(/wiki/)")):
        if 'href' in link.attrs:
            if link.attrs['href'] not in pages:
                #We have encountered a new page
                newPage = link.attrs['href']
                print(newPage)
                pages.add(newPage)
                getLinks(newPage)
getLinks("")

3.3 사이트 전체에서 데이터 수집하기

이번에는 단순히 페이지 이동이 아닌 해당 페이지에서 데이터도 수집해 보겠습니다.

  • 모든 타이틀은 h1 -> span 태그에 존재합니다.
  • body 텍스트는 div#bodyContent 태그 내에 존재합니다. 더 나아가서 첫번째 문단에만 접근하고 싶다면 div#mw-content-text->p를 사용합니다.
  • 페이지 링크는 li#ca-edit->span->a 하위에 존재하는 li#ca-edit를 이용합니다.
from urllib.request import urlopen
from bs4 import BeautifulSoup
import re

pages = set()
def getLinks(pageUrl):
  global pages
  html = urlopen("http://en.wikipedia.org"+pageUrl)
  bsObj = BeautifulSoup(html)
  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("This page is missing something! No worries though!")
  for link in bsObj.findAll("a", href=re.compile("^(/wiki/)")):
    if 'href' in link.attrs:
      if link.attrs['href'] not in pages:
        #We have encountered a new page
        newPage = link.attrs['href']
        print("----------------\n"+newPage)
        getLinks(newPage)
getLinks("")
pages.add(newPage)