크롤링 시작하기

Intro


이 포스트는 BS4 활용하기 포스트에 의존합니다. 이전 포스트를 먼저 보시는걸 추천합니다.


웹 크롤러 라는 이름에서 알 수 있듯이 URL에서 페이지를 가져오고, 그 페이지를 검사해 다른 URL을 찾고, 다시 그 페이지를 가져오는 작업을 무한히 반복한다. 그러므로 대역폭에 주의를 기울여야하고, 타겟 서버의 부하를 줄일 방법을 항상 생각해야한다.

안그럼 서버 관리자한테 전화가 올지도..


단일 도메인 내 이동


이제 우리는 임의의 위키 페이지를 가져와서 페이지에 들어있는 링크 목록을 가져오는 스크립트 정도는 쉽게 만들 수 있다.

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/로 시작한다

이들 규칙을 정규표현식으로 표현하여 코드를 수정하면

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함수

다음과 같이 작성하면 된다.

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페이지를 가지고있는 사이트는 없다. 내부의 링크가 중복되기 때문이다.

같은 페이지를 두번 크롤링하지 않으려면 발견되는 링크들을 리스트에 보관하는게 좋다. 새로운 링크인지 비교해야 하기 때문이다.

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, "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앞에 http://en.wikipedia.org을 붙여 위키백과 첫 페이지 URL로 바꾼다.
그 다음 첫 번째 페이지의 각 링크를 순회하며 전역변수 page에 들어있는지 아닌지를 검사, 없다면 리스트에 추가하고 화면에 출력한다음 다시 함수를 호출한다.


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


방금 만든 스크레이퍼는 페이지와 페이지를 옮겨다닐뿐 아무 행동도 하지않는다. 코드를 조금 고쳐서 페이지 제목, 첫 번째 문단, 편집 페이지를 가리키는 링크를 수집하는 스크레이퍼를 만들어 보자.

위키백과의 페이지는 다음과같은 규칙이있다.

  • 항목 페이지든 편집 페이지든 제목은 항상 h1태그 안에 있고, 페이지당 하나만 존재한다
  • 모든 바디 텍스트는 div#bodyContent태그에 들어있다
  • 편집 링크는 항목 페이지에만 존재한다

위의 규칙들을 이용해서 데이터를 수집해보자.

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, "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. 만약 당신의 크롤러의 존재를 웹 마스터가 알아차렸다면 당신을 법적으로 보호할수 있는가(이 문제는 마지막에 다루도록 하자)

파이썬 내장함수와 결합하면 다양한 웹 스크레이핑을 하는 코드를 쉽게 만들 수 있다.

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("http://oreilly.com")

이 프로그램은 http://oreilly.com 에서 시작해 외부 링크에서 외부링크로 무작위 이동한다. 첫 페이지에 외부링크가 항상 있을수는 없기때문에 외부링크를 찾을 때 까지 내부로 파고드는 방법을 사용했다.


다음포스트 - 긁어온 데이터 저장하기