[python] 파이썬 웹 크롤링(1): 크롤링 기본 원리

업데이트:

파이썬 웹 크롤링(1): Part1 크롤러 만들기

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

참고 링크

Chapter1. 첫 크롤러

1.1 크롤링 할 때 해야할 것과 알아야 하는 것

크롤링(crawling)은 무엇을 하는 것일까요? 크롤링의 기본 메커니즘은 다음과 같습니다.

  • 파이썬으로 웹서버에 정보 요청하기

  • 서버 응답을 받은 후 데이터 핸들링

  • 웹사이트와 상호작용

따라서 크롤링은 이용하면 인터넷을 돌아다니고, 정보를 모으고, 저장한 데이터를 다른 곳에 사용할 수도 있습니다. 그렇다면 크롤링을 위해 알아야할 것은 무엇일까요?

  • HTML 데이터 가져오기

  • 데이터 파싱(parsing)

  • 타겟 정보 저장

  • (옵션) 다른 페이지로 이동하기

크롤링을 위해서 알아야 할 것은 위와 같습니다. 그럼 이제부터 본격적으로 크롤링에 대해 알아보겠습니다.

1.2 크롤링의 시작

1.2.1 네이버 메인 페이지 읽어 들이기

이번 챕터 부터는 간단한 크롤링을 해보겠습니다. 이번 챕터에서 할 일은 아래와 같습니다.

  • 웹서버에 GET 요청 보내기(특정 페이지에 대해)

  • HTML 아웃풋 읽기

  • 내용 중에 자신이 원하는 내용 추출하기

아래와 같은 간단한 코드로 네이버 메인페이지의 내용을 요청해보겠습니다.

>>> from urllib.request import urlopen
>>> html = urlopen("https://www.naver.com")
>>> print(html.read())
b'\n<!doctype html><html lang="ko" data-dark="false">
...중략...

위 코드는 네이버 메인페이지의 내용을 HTML 형식으로 가져오는 코드 입니다. 가장 먼저 urllib 라이브러리에서 웹서버에 요청을 보내기 위해 필요한 함수를 불러오고 urlopen을 이용해 원하는 사이트를 오픈한 결과를 읽어들여 html로 저장합니다. 아웃풋 결과를 보면 HTML 코드라는 것을 알 수 있습니다. 만약 HTML 코드에 이미지 소스 파일이 있다면 어떨까요? 한번의 urlopen으로 이미지 파일 까지 가져올 수 있을까요? 답은 그렇지 않다입니다. 현재까지는 한번의 크롤링으로는 HTML 파일 하나만 가지고 올 수 있으며 HTML에 걸려있는 추가적인 이미지파일까지 랜더링하기 위해서는 추가적인 작업이 필요합니다. 참고로 위에서 urllib 라이브러리를 사용했는데 만약 파이썬2 사용자라면 urllib2 라이브러리를 사용해야할 것입니다. urllib 라이브러리는 기본적인 라이브러리 이므로 따로 설치할 필요 없습니다.

1.2.2 BeautifulSoup?

BeautifulSoup은 파이썬 크롤링에서 중요한 역할을 하는 라이브러리 입니다. BeautifulSoup은 파이썬에 기본으로 장착된 라이브러리가 아니므로 설치 과정이 필요합니다. 아래와 같은 코드를 입력해 설치합시다.

$ pip install beautifulsoup4

만약 파이썬 가상환경 설치과정이 궁금하다면 파이썬 가상환경 설치하기를 참고해주세요. BeautifulSoup 설치를 성공하셨다면 파이썬을 실행해 BeautifulSoup 라이브러리를 불러와보겠습니다.

from bs4 import BeautifulSoup

자, 그럼 이번에는 BeautifulSoup을 이용해 네이버 메인 페이지를 긁어와 보겠습니다.

>>> from urllib.request import urlopen
>>> from bs4 import BeautifulSoup
>>> html = urlopen("https://www.naver.com")
>>> bsObj = BeautifulSoup(html.read())
>>> print(bsObj)
<!DOCTYPE html>
 <html data-dark="false" lang="ko">

앞선 예제와 다른 점은 html.read()에 대해 BeautifulSoup 함수를 사용한다는 것입니다. BeautifulSoup 함수는 html 내용을 읽어 들인 후 BeautifulSoup 객체로 바꾼다는 의미입니다. 그렇다면 html.read()와 BeautifulSoup(html.read())는 어떻게 다를까요?

>>> print(html.read())
content="\xeb\x84\xa4\xec\x9d\xb4\xeb\xb2\x84...중략...
>>> bsObj = BeautifulSoup(html.read())
>>> print(bsObj)
content="네이버 메인에서 다양한 정보와 유용한 컨텐츠를 만나 보세요"

위 결과와 같이 html.read()는 html 파일 내용을 읽은 후 문자열을 바이트 코드로 나타내지만 BeautifulSoup(html.read())를 이용한 결과는 string을 보여줍니다.

1.2.3 예외 처리

지금까지 따라 오셨다면 아마 이런 상상을 하실겁니다.

크롤러에게 일을 시켜놓고 잠을 자러 갑니다. 그리고 다음날 눈을 떠보니 크롤러가 모든 일을 끝냅니다.

하지만 현실은 이렇습니다.

무언가의 이유로 크롤러 에러 발생?!

네, 그렇습니다. 웹 사이트는 상당히 지저분합니다. 따라서 크롤러는 예상치못한 데이터 포맷을 만난다던가 하는 여러가지 이유로 에러를 뱉어내며 멈춰버릴 가능성이높습니다.

html = urlopen("https://www.naver.com")

위 문장에서 에러가 발생한다면 어떤 가능성이 존재할까요?

  • 서버 내부에 해당 페이지가 존재하지 않는 경우

  • 서버를 찾을 수 없을 경우

한줄짜리 간단한 코드라도 위와 같은 이유로 에러가 날 수 있습니다. 그리고 첫번째 에러의 경우, 즉 해당 페이지가 존재하지 않는다면 404, 500에러 같은 에러를 뱉어냅니다. 에러 메시지를 본다면 왜 에러가 났는지도 알 수 있겠죠. 아무튼 에러가 발생하더라도 프로그램이 멈추지 않게 예외처리를 해줍니다. 파이썬의 try except 구문을 사용합시다.

try:
    html = urlopen("https://www.naver.com")
except HTTPError as e:
    print(e)
else:
    # program continue...

위 코드는 네이버 메인 사이트를 열어 html 내용을 읽어들이되 에러가 발생하면, 에러 메시지를 출력하라는 뜻입니다.

그렇다면 두번째 에러는 어떨까요? 두번째 에러는 서버를 찾을 수 없는 경우 입니다. 이 경우에는 html값이 None값을 가집니다. 따라서 두번쨰 에러를 대비해 아래와 같이 코드를 짭니다.

if html is None:
    print("URL is not found")
else:
    #program continue...

위 코드는 if else 구문으로 간단하게 구현 가능합니다. html이 None값을 가진다면 “URL is not found”를 출력하라는 뜻입니다.

위와 같이 에러 처리를 해보았는데, 모든 에러에 대해 위와 같이 처리한다는건 여간 귀찮은 일이 아닙니다. 하지만 다행스럽게도 BeautifulSoup에서는 접근한 태그의 존재 유무 체크에 대해 편리한 방법을 제공합니다.

>>> print(bsObj.nonExistentTag)
None

위에서 네이버 메인 페이지는 존재했기 때문에 해당 태그가 존재 유무를 체크하는 nonExistentTag를 실행하면 None값을 출력합니다. 문제는 bsObj.nonExistentTag가 출력하는 값에 상관없이 또다른함수를 실행할 때 입니다.

>>> print(bsObj.nonExistentTag.someTag)
AttributeError: 'NoneType' object has no attribute 'someTag'

위 코드에서 bsObj.nonExistentTag는 이미 None값을 가집니다. 그런데 None값을 가지는 값에 대해 someTag를 실행했으니 에러가 발생합니다.

그럼 이제 위와 같은 경우를 커버할 수 있는 코드를 짜보겠습니다.

try:
    badContent = bsObj.nonExistingTag.anotherTag
except AttributeError as e:
    print("Tag was not found")
else:
    if badContent == None:
        print("Tag was not found")
    else:
        print(badContent)

하지만 위 코드는 다소 복잡해 보입니다. 따라서 좀 더 간소하게 짠다면 아래와 같이 코드 작성할 수 있습니다.

from urllib.request import urlopen
from urllib.error import HTTPError
from bs4 import BeautifulSoup

def getTitle(url):
    try:
        html = urlopen(url)
    except HTTPError as e:
        return None
    try:
        bsObj = BeautifulSoup(html.read())
        title = bsObj.body.h1
    except AttributeError as e:
        return None
    return title

위 코드에서 만든 getTitle 함수는 해당 페이지의 타이틀을 가져오는 함수입니다. 만약 어떤 문제가 발생한다면 None값을 가지게 합니다. 예외처리로는 HTTPError를 체크합니다.

title = getTitle("https://www.naver.com")
if title == None:
    print("Title could not be found")
else:
    print(title)
<h1 class="logo_default">
<a class="logo_naver" data-clk="top.logo" href="/">
<span class="blind">네이버</span></a>
</h1>