파이문

파이썬으로 크롤러 만들기 본문

Python/Project

파이썬으로 크롤러 만들기

민Z 2017. 1. 16. 23:40

파이썬으로 크롤러 만들기

(Python web crawler)


크롤러는 크게 두 가지 일을 한다. 

  • 웹 페이지 다운로드
  • 다운로드한 웹 페이지 파싱

여기서 예제로 구현할 크롤러 역시 위의 두 가지의 기능만을 갖고 있을 것이다. (다운로드할 url을 이미 갖고 있다는 전제에서 시작하겠다.)


예제로 구현할 나의 컴퓨터 환경은 OS의 경우 Windows 10이며 파이썬은 3.5버전이다. 그러나 2.7이랑 차이가 많이 나는 메서드로 구현하지 않을 것이기 때문에, print 함수만을 제외하면 동일하게 돌아갈 것이다.


라이브러리의 경우 다운로드는 requests를, 파서는 BeautifulSoup4를 사용할 것이다. 예제로 구현할 웹 페이지는 나무 위키를 선택하였으나 너무 느려서 그냥 오버워치 인벤으로 바꿨다.


선수 지식으로는 파이썬 문법과 HTML을 조금 알면 이해할 수 있다.


우선 다운로드 함수를 구현해 보자. 내장형 모듈인 urllib를 이용하여도 구현할 수 있지만 python3.x 에서는 사용해본 적이 없어서 requests를 이용하였다. 사실 requests가 거의 내장형 모듈이나 다름 없을 정도로 많이 사용하고 정말 좋다(??)

import requests


def page_download(url):
    response = requests.get(url)
    print(response)


if __name__ == '__main__':
    page_download("http://overwatch.inven.co.kr")

requests 모듈을 import하고 page_download 함수를 만들어서 url을 다운로드 하여 response를 출력하는 간단한 예제를 작성하였다.

request.get은 http code를 리턴한다. 고로 출력은 <Response [200]> 라고 나온다.


사실 이게 1/3은 했다고 볼 수 있다. 나머지 1/3은 파서 만들기고 그 외의 기타로 문자열 깨진 거 해결하기, url 수집하기, db에 저장하기 등등 부가적인 문제만이 남은 것이다.


url의 실제 html 내용은 print(response.text) 를 하면 볼 수 있다.




다운로드는 이 처럼 별로 어렵지 않다. 워낙 모듈들을 잘 만들어 놓았기 때문이다. 문제는 바로 파서다. html 페이지는 가져 왔지만 이 html 페이지에서 원하는 데이터만 뽑아 가공하는 것이 어렵다. 처음엔 오버워치 인벤의 메인 페이지를 가져오려고 했지만 예제로 작성하는 것이니 기왕 텍스트 많은 오버워치 인벤 뉴스의 한 기사로 파서를 만들도록 하겠다.


파서 만들기에 제일 먼저 알아봐야 할 것은 원하는 텍스트(데이터)가 웹 페이지 어디에 위치하느냐인 것이다.(그래서 html에 관한 지식이 조금 필요하기도 하고.) 내가 가져와야 할 데이터가 class로 되어있는지, id로 되어있는지에 따라 난이도가 달라진다. 왜냐하면 id는 해당 웹 페이지의 고유 값이고 class는 중복될 수도 있기 때문이다.


오버워치 인벤 뉴스의 제목은 불행히도 class로 구성되어 있었~지만 페이지의 title이 곧 뉴스 제목이라 쉽게 뽑아올 수 있다. (이것 역시 웹 페이지의 title 태그는 하나밖에 없기 때문에 가능하다.)

def page_parse(html):
    parser = BeautifulSoup(html, "html.parser")
    print(parser.title.string) # 기사 제목

인자 값으로는 response.text를 넘겨주면 된다.


타이틀은 쉽게 가져왔지만 나머지 기자, 글쓴 시각, 내용은 전부 class로 이루어져있었다. 일단 하나씩 살펴보도록 하자.


오버워치 인벤 뉴스글의 글쓴 시각은 

<dl class="date">
	<dt>날짜 : </dt>
	<dd>2017-01-16 11:48</dd>
</dl>

이렇게 구성되어 있다. date 클래스가 하나라면 좋겠지만 html을 살펴본 결과 동일한 클래스 명이 최소 두개라는 것을 발견하였다. 그러나 내가 가져오려는 date는 dl로 되어 있고 또 다른 date는 ul로 되어 있기 때문에 아래처럼 구현하면 위의 html과 동일하게 나올 것이다.

for data in parser.find_all('dl', class_="date"):
    print(data)

사실 전체 dl 안에를 가져올 필요는 없고 2017-01-16 11:48 만 가져오면 된다. 몇 가지 방법이 있는데 정규식을 사용할 수도 있고 split을 할 수도 있고 원하는 방식으로 구현하면 된다. (참고로 data는 str이 아니고 bs4.element.Tag 이다.) 

print(parser.find('dl', class_="date").find('dd').string)

한 줄로는 위 처럼 구현할 수 있다. dl의 date 클래스를 가져와서 dd의 string만을 출력하는 것이다.


기자 이름 역시 한 줄로 쉽게 구현할 수 있다.

print(parser.find('div', class_="writer").text)


여기까지 보면 의문이 들 것이다. 왜 위에는 string을 호출하고 아래는 text를 호출하였을까? 기자 이름 출력에서 text를 string으로 바꾸면 None을 출력하는 것을 볼 수 있다. 이는 string의 경우 return 타입이 NavigableString이지만, text는 unicode이며, 모든 자식 클래스의 string을 합쳐놓은 것이기 때문이다. 더 자세한 것 은 http://stackoverflow.com/questions/25327693/difference-between-string-and-text-beautifulsoup 에서 확인할 수 있다.


기사 글 역시 동일한 방식으로 구현하면 된다.

print(parser.find('div', class_="contentBody").text.replace("\r\n", " "))

(replace를 한 이유는 문장 사이에 빈 줄이 들어가있었기 때문이다.)




여기까지 했으면 사실 다 끝났다. 하나의 소스 코드로 보자면 아래와 같다.

import requests
from bs4 import BeautifulSoup


def page_download(url):
    response = requests.get(url)
    return response.text


def page_parse(html):
    ret_dict = {}
    parser = BeautifulSoup(html, "html.parser")
    ret_dict["title"] = parser.title.string

    ret_dict["date"] = parser.find('dl', class_="date").find('dd').text.strip()
    ret_dict["writer"] = parser.find('div', class_="writer").text.strip()
    ret_dict["body"] = parser.find('div', class_="contentBody").text.replace("\r\n", " ").strip()

    return ret_dict


if __name__ == '__main__':
    html = page_download("http://www.inven.co.kr/webzine/news/?news=170888&site=overwatch")
    result = page_parse(html)
    for key, value in result.items():
        print(key, ":", value)



여기서 더 생각해볼 수 있는 것은 다음과 같다.

  1. 다운로드해야 할 링크를 어떻게 수집하는가?

  2. 가공한 데이터를 어떻게 저장하는가?


1번의 경우 크게 두 가지 방법이 있다. 첫번째는 노가다로 수집하는 것이고 두번째는 크롤러가 페이지를 다운하여 원하는 데이터를 파싱하여 가져왔듯이, 페이지에서 링크를 (a 태그) 파싱하여 결과 값을 받아온 후 따로 DB에 저장하든지 아님 크롤러가 계속 url을 들고 있으면서 페이지를 다운로드 하든지 하는 방법이 있다. 


실제 포털 사이트의 크롤러는 이를 url 프론티어 혹은 seed라고 부르는데 http://nlp.stanford.edu/IR-book/html/htmledition/the-url-frontier-1.html 여기서 한번 읽어보면 좋을 듯 싶다.


2번의 경우 나는 주로 DB에 저장하곤 하는데 이것은 데이터가 어떤 형식이냐에 따라 다르므로 케이스 바이 케이스인 것 같다. 



모든 웹 페이지가 이처럼 쉬우면 좋겠지만, 불행히도 로그인 해서 가져와야 하는 데이터가 있는 페이지, html이 전부 table로 이루어져있는 아주~ 지저분한 페이지 등등 쉽게 접근 혹은 파서를 만들지 못하는 페이지가 수두룩하다.


그래서 다음에 작성할 글은 바로 로그인 해야 가져올 수 있는 페이지, 동적(ajax, javascript)으로 구성되어 있는 페이지에서 데이터를 가져오는 것으로 하겠다.

Comments