YUDA't

TL;DR

2016년 BeautifulSoup로 YouTube에서 영상 정보를 크롤링 해보자 라는 글을 올린 적 있는데 이게 의외로 스테디셀러(?)라 2019년 버전으로 업데이트하고자 한다. 초심자들을 위한 글이기 때문에 난이도는 굉장히 낮다.

 

다만 그동안 유튜브의 HTML 구조가 바뀌어서 초심자가 크롤링하기에는 좀 번거롭고 향후 또 변경될 여지가 많아 대상을 프로젝트 오일러 한국 사이트로 변경했다. 이곳을 선택한 이유는 관리자들이 사이트를 절대 업데이트하지 않아서(ㅋㅋㅋ).

 

근데 그전에 이 사이트가 크롤링을 허용하는지부터 알아보자. 대부분의 사이트는 그들의 루트 경로 아래에 robots.txt라는 파일을 두어 크롤링에 대한 규약을 써놓는데, 보통 크롤링을 허용하거나 허용하지 않는 페이지들이 적혀있다. 사실 이건 의무가 아닌 권고사항이기 때문에 반드시 지킬 필요까지는 없지만 지금처럼 실습해보는 수준에서는 해당 페이지들을 피해주자.

그딴 거 없다

....관리도 안 하는 사이트인데 있을리가 없다. 맘 놓고 크롤링해도 된다는 뜻이겠지!

 


 

크롤링할 URL은 아래와 같다.

알고리즘 문제들이 나열돼 있는 페이지인데 크롤링하기 좋아보인다. 오늘 여기서 각 문제의 번호문제 요약푼 사람의 수를 가져올 거다. 그럼 시작!


 

0)

이 실습에서 사용할 패키지는 이렇다.

  • BeautifulSoup4 (설치 필요 O)
  • urllib (설치 필요 X)

BeautifulSoup는 HTML이나 XML 파일로부터 데이터를 가져올 수 있는 끝내주는 라이브러리다.

urllib는 URL 작업을 위한 패키지로 URL을 열고 읽어오는 데 사용한다.

urllib은 파이썬 내장 모듈이기 때문에 따로 설치할 필요는 없고, BeautifulSoup4 패키지만 설치하면 된다.

$ pip install beautifulsoup4

 

1)

받았다면 아래 코드를 실행해 해당 페이지로부터 읽어온 데이터를 BeautifulSoup 객체로 만들어주자. soup(BeautifulSoup 객체)를 프린트해보면 페이지의 HTML 소스가 출력될 것이다.

from bs4 import BeautifulSoup
import urllib.request

target_url = 'http://euler.synap.co.kr/prob_list.php'
html = urllib.request.urlopen(target_url).read()
soup = BeautifulSoup(html, 'html.parser')

print(soup)

 

2)

그럼 이제 BeautifulSoup의 함수들을 적절히 사용해 원하는 데이터를 뽑아내자. 일단 그 데이터가 있는 부분의 태그를 알아야 하는데 이때 개발자 도구창을 유용하게 사용할 수 있다.

크롬(혹은 다른 브라우저)에서 F12 키를 누르면 개발자 도구창을 열 수 있다. 아래 사진에서 오른쪽이 개발자 도구창인데 여기서 왼쪽 위에 있는 버튼을 클릭하면, 내가 마우스로 가리키는 부분의 태그를 알 수 있다.

 

이로써 내가 가져올 문제 데이터가 담긴 태그가 <table class="grid" style="width:800px">이라는 것을 확인했다.

 

BeautifulSoup의 find() 함수는 soup 객체 아래 특정 조건에 맞는 엘레멘트를 가져온다. 예를 들어 아래의 코드는 class 속성 값이 grid인 <table> 태그를 가져온다.

table = soup.find('table', {'class': 'grid'})
print(table)

결과:

<table class="grid" style="width:800px">
    <tr height="30">
        <td>
            <div style="text-align:center;">
                <b>1</b>
            </div>
        </td>
        <td style="padding-left:5px;">
            <a href="prob_detail.php?id=1" style="text-decoration:none;" title="2012-01-03 19:11:35">1000보다 작은 자연수 중에서 3 또는 5의 배수를 모두 더하면?</a>
        </td>
        <td>
            <div style="text-align:center;">8855</div>
        </td>
    </tr>

....

</table>

훨씬 보기가 쉬워졌다! <tr> 아래에는 3개의 <td>가 있는데, 여기에 각각 문제의 1) 번호, 2) 문제 요약, 3) 푼 사람의 정보가 담겨 있다.

 

3)

이제 <td>에 담겨 있는 정보들을 뽑아내기만 하면 된다. 이건 설명하기보다 코드로 보는 게 더 나을 것 같다. (참고로 첫 번째 <td>에는 <th> 정보가 담겨있어 제외했다.)

table = soup.find('table', {'class': 'grid'})
trs = table.find_all('tr')
for idx, tr in enumerate(trs):            # enumerate를 사용하면 해당 값의 인덱스를 알 수 있다.
    if idx > 0:
        tds = tr.find_all('td')
        sequence = tds[0].text.strip()    # 앞뒤 여백이 있어 strip()을 사용했다.
        description = tds[1].text.strip()
        solved_num = tds[2].text.strip()
        print(sequence, description, solved_num)

그럼 결과가 이렇게.

1 1000보다 작은 자연수 중에서 3 또는 5의 배수를 모두 더하면? 8855
2 피보나치 수열에서 4백만 이하이면서 짝수인 항의 합 6800
3 가장 큰 소인수 구하기 4754
4 세자리 수를 곱해 만들 수 있는 가장 큰 대칭수 3951
.....

전체 코드는 이렇다.

from bs4 import BeautifulSoup
import urllib.request

target_url = 'http://euler.synap.co.kr/prob_list.php'
html = urllib.request.urlopen(target_url).read()
soup = BeautifulSoup(html, 'html.parser')

table = soup.find('table', {'class': 'grid'})
trs = table.find_all('tr')
for idx, tr in enumerate(trs):
    if idx > 0:
        tds = tr.find_all('td')
        sequence = tds[0].text.strip()
        description = tds[1].text.strip()
        solved_num = tds[2].text.strip()
        print(sequence, description, solved_num)

 

+4)

지금껏 작성한 코드로는 첫 번째 문제 페이지의 정보만을 가져온다. 만약 홈페이지에 존재하는 6번째 페이지까지의 데이터를 가져오고 싶다면 다음과 같이 하면 된다. (보기 쉽게 함수를 나눴다)

from bs4 import BeautifulSoup
import urllib.request

def get_soup(target_url):
    html = urllib.request.urlopen(target_url).read()
    soup = BeautifulSoup(html, 'html.parser')
    return soup

def extract_data(soup):
    table = soup.find('table', {'class': 'grid'})
    trs = table.find_all('tr')
    for idx, tr in enumerate(trs):
        if idx > 0:
            tds = tr.find_all('td')
            sequence = tds[0].text.strip()
            description = tds[1].text.strip()
            solved_num = tds[2].text.strip()
            print(sequence, description, solved_num)

for i in range(1, 7):
    target_url = 'http://euler.synap.co.kr/prob_list.php?pg={}'.format(i)
    soup = get_soup(target_url)
    extract_data(soup)

 

물론 사이트마다 페이지 렌더링을 다루는 방식은 다르다. 오늘 크롤링한 웹사이트는 URL에 pg 파라미터 값을 설정함으로써 다른 페이지들을 불러올 수 있었지만 이 파라미터가 page가 될 수도, num이 될 수도, 혹은 아예 없을 수도 있다.

 

이렇게 크롤링을 하다 보면 HTML 파싱만으로 찾아내기 힘든 값이 많은데, 이를 위해 웹브라우저를 직접 컨트롤할 수 있는 selenium이라는 패키지가 있다.

 

브라우저를 직접 띄워 사용하기 때문에 BeautifulSoup보다는 느리지만, 버튼을 누르고 스크롤을 움직이는 등 갖가지 동작을 할 수 있어 매우 유용하다. 후에 짬이 난다면 selenium 튜토리얼도 올려보려 한다.

구글링하면 오만가지 나옴