개발자의 취미생활

[Python] BeautifulSoup4 라이브러리와 기초 (멜론차트 크롤링) 본문

파이썬 프로그래밍/파이썬 크롤링

[Python] BeautifulSoup4 라이브러리와 기초 (멜론차트 크롤링)

늅이 2017. 7. 17. 16:12

requests가 아닌 BeautifulSoup를 사용하는 이유?

requests 설치와 라이브러리 포스트 바로가기

간단한 requests를 사용한 코드입니다.

requests를 사용해서 https://www.naver.com을 text로 변환하여 html변수에 집어넣고

그것을 print한 코드이며 실행을 시켜보면 www.naver.com의 html코드가 잘 출력이 됩니다.


하지만 너무 복잡하죠? BeautifulSoup에서는 이런 복잡한 코드에서 사용자가 원하는 부분을 크롤링 할 수 있게 도와주는 모듈입니다.


BeautifulSoup를 사용해서 크롤링을 해보겠습니다.

 

BeautifulSoup를 배우기 전에

HTTP 응답

- 클라이언트(사용자)가 웹서버(ex. naver, google ...)에 요청을 했을때, 서버에서는 일반적으로 HTML, CSS, JavaScript, Image 형식으로 응답을 하게 됩니다.
- HTML 문서는 중첩된 태그로 구성된 계층형 구조 입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!doctype html>
<html>
    <head>
        <meta charset="utf-8>
        <title>환영합니다</title>
    </head>
    <body>
        <h1>안녕하세요</h1>
        <ul id='homepage_law'>홈페이지 규칙
            <li class='law'>욕설 금지</li>
            <li class='law'>비속어 사용 금지</li>
        </ul>    
    </body>
</html>
cs

이것은 간단한 HTML구조입니다.

HTML구조는 사람들이 보기엔 나눠저 있는것 처럼 보이지만 하나의 문자열로 보기 때문에 이것을 사람이 원하는 부분만 알아서 보기는 힘듭니다.

하지만 크롤링을 해서 사용자가 홈페이지 규칙을 가져오고 싶을경우 사용을 할수 있겠습니다.


DOM문서

The Document Object Model(DOM)
브라우저는 위에 보이는 HTML문자열을 DOM Tree로 변환하여 문서를 표현하게 됩니다.

무슨말인가 하면

<페이지소스보기>
1
2
3
4
5
<table>
    <tr>
        <td>테이블 컬럼</td>
    </tr>
</table>
cs

예를 들어 서버에서 이런 응답을 받았다면


<개발자도구로보기>

1
2
3
4
5
6
7
<table>
    <tbody>
        <tr>
            <td>테이블 </td>
        </tr>
    </tbody>
</table>
cs

브라우저는 이렇게 변환해서 문서를 가지고 있게 됩니다.

다른점은 2,6째 줄에 <tbody>가 추가된것을 볼 수 있습니다.

이것이 브라우저나름의 해석이 하는 DOM문서가 되겠습니다.


이것을 왜 알아야 하는가?

requests 라이브러리를 통한 응답에서 HTML은 페이지 소스보기를 참고해야 하는데
페이지 소스보기로 하는 응답은 위에 있는 코드가 되고, 개발자 도구에서 보는 응답은 아래 코드가 되겠습니다.

즉, 페이지 소스보기는 서버에서 최초로 응답을 준 코드가 되겠고

개발자도구에서는 브라우저가 해석을 한 코드가 되겠습니다. (이것을 DOM Tree)


그렇다면 requests를 사용한 크롤링은 사용할때는 서버가 최초로 응답을 준 코드인 페이지 소스보기를 통해서 사용하는게 알맞겠죠?


그러면 개발자 도구는 언제 사용하는가?

selenium을 사용할때 쓰시면 됩니다.

왜냐하면 selenium은 브라우저를 통한 자동화이기 때문입니다.


복잡한 HTML문자열에서 내가 원하는 문자열 가져오는 방법

1. 정규표현식 활용
- 가장 빠른 처리가 가능하나, 정규표현식의 룰을 만드는 것이 번거롭고 복잡하여 다양한 처리를 하기 어려운 점이 있습니다.
- 때에 따라 필요할 수 있습니다.

2. HTML Parser 라이브러리를 활용(V)
- DOM Tree를 탐색하는 방식으로 적용이 쉬운 장점이 있습니다.
- Ex) BeautifulSoup4, lxml .......


BeautifulSoup4 기초

BeautifulSoup는 HTML/XML Parser이기 때문에 HTML/XML문자열에서 원하는 태그정보를 뽑아줍니다.

설치

윈도우 cmd창에서
pip3 install beautifulsoup4 입력
pip3 install beautifulsoup을 입력할 경우 버전3이 설치되므로 주의하셔야 합니다.

사용

BeautifulSoup의 인스턴스 생성


코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from bs4 import BeautifulSoup
html = '''
<ol>
    <li>NEVER - 국민의 아들</li>
    <li>SIGNAL - TWICE</li>
    <li>LONELY - 씨스타</li>
    <li>I LUV IT - PSY</li>
    <li>New Face - PSY</li>
</ol>
'''
 
soup = BeautifulSoup(html, 'html.parser')
for tag in soup.select('li'):
    print(tag.text)
cs

2째줄: html에 이런식으로 html태그들이 문자열로 들어가게 됩니다.

12째줄: BeautifulSoup의 인스턴스 생성
13째줄: li태그를 반복으로 반환하는데, select는 li를 반환한다고 이해하시면 됩니다.
그럼 결국 각각의 태그를 돌며 li태그만 순회를 돌며 출력하게 됩니다.
결과


Parser

1. Parser / Python's html.parser

- BeautifulSoup4의 내장파서

- 샘플코드

1
soup = BeautifulSoup(파싱할 문자열, 'html.parser')
cs

2. Parser / lxml's HTML parser

- lxml HTML파서를 사용하며 이는 외부 C 라이브러리

(html.parser보다 유연하고 빠른 처리)
- 설치법(따로 설치해야함)
cmd창에서
pip3 install lxml
-샘플코드
1
soup = BeautifulSoup(파싱할 문자열, 'lxml')
cs

왜 2개를 사용하는가?

- Python's html.parser는 파이썬 코드로 되어있기 때문에 C의 라이브러리를 활용할수 활용할 수 없는곳이나 굳이 xml을 사용하지 않고 심플하게 하고싶을때 사용하고
- lxml's html.parser는 lxml설치가 가능하고 좀더 유연한 처리가 가능할때 사용(대부분 lxml은 설치 가능)

보통은 Python's html.parser로 충분히 사용 가능하지만 정확하게 html로 마크업이 안되어 있을경우 lxml을 사용하여 처리가 가능할 수 있습니다.

태그찾는 방법

1. find를 통해 태그를 하나씩 찾기
2. 태그 관계를 지정해서 찾기(CSS Selector 사용)

1. find를 통해 태그 하나씩 찾기(멜론 TOP 100 크롤링)

이 화면입니다. 이 화면을 크롤링 해보겠습니다.


개발자도구 모드로 부분을 찾아냅니다.

(페이지 소스보기로 보시면 안나옵니다)


좀 확대해보겠습니다.

table안에 출력이 되는데, tbody 부분이 당연히 목록이겠죠?

tbody id는 chartListObj

그안에 <tr>


더 자세히 들어가 보면..

div class="wrap_song_info" 에서 더이상 쪼갤수는 없습니다.

그럼 이 세가지


먼저 tbody id = "chartListObj"

그안에 <tr></tr>

그안에 div class="wrap_song_info"

를 기억해서 크롤링 해보겠습니다.


깔끔한코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import requests
from bs4 import BeautifulSoup
 
html = requests.get('http://www.melon.com/chart/index.htm').text
soup = BeautifulSoup(html, 'html.parser')
 
tag_list = []
for tr_tag in soup.find(id='chartListObj').find_all('tr'):
    tag = tr_tag.find(class_='wrap_song_info')
    if tag:
        tag_sub_list = tag.find_all(href=lambda value: (value and 'playSong' in value))
        tag_list.extend(tag_sub_list)
 
for idx, tag in enumerate(tag_list, 1):
    print(idx, tag.text)
cs


설명달린코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import requests
from bs4 import BeautifulSoup
 
html = requests.get('http://www.melon.com/chart/index.htm').text
soup = BeautifulSoup(html, 'html.parser')
 
tag_list = []
for tr_tag in soup.find(id='chartListObj').find_all('tr'):
#첫째로 id가 chartListObj인 태그를 찾는다, 그안의 tr태그를 다찾는다.
 
    tag = tr_tag.find(class_='wrap_song_info')
#각각의 tr태그 안에서 class이름이 wrap_song_info인 애들을 찾는다.
#class_ 언더바 이유: 클래스를 만드는 예약어가 있기 때문에 class는 사용불가
 
    if tag:
    #정보가 있으면 코드실행, 없으면 None
 
        tag_sub_list = tag.find_all(href=lambda value: (value and 'playSong' in value))
        # 그안에 href속성에 속성을 받아와서 매번 함수(lambda함수)가 호출되도록한다.
 
 
        tag_list.extend(tag_sub_list)
        #미리 만든 list인 tag_list에 뽑아낸 내용을 추가(extend)시킨다.
 
#enumerrate를 사용한 넘버링 사용
for idx, tag in enumerate(tag_list, 1):
    print(idx, tag.text)
cs


결과

1위~100위까지 잘 출력이 되었습니다.


2. 태그관계를 지정해서 찾기(CSS Selector사용)

- CSS Selector를 통한 Tag찾기 지원
- tag name: "tag_name"
- tag id: "#tag_id"  (#을 사용하면 id)
- tag class names: ".tag_class"  (.을 사용하면 class)

CSS Selector Syntax(문법)

- *: 모든 Tag
- tag: 해당 모든 Tag
- Tag1 > Tag2: Tag1의 직계인 모든 Tag2
Ex)
1
2
3
4
5
6
7
8
9
<!doctype html>
<html>
    <head>
        <meta>
    </head>
    <body>
 
    </body>
</html>
cs

Tag1이 <html>이면 그 직계인 Tag2는 <head>와 <body>가 있습니다.

직계 이기 때문에 <meta>는 매칭되지 않습니다.

- Tag1 Tag2: Tag1의 자손인 모든 Tag2 (직계임이 요구되지 않음)
Ex)

1
2
3
4
5
6
7
8
9
<!doctype html>
<html>
    <head>
        <meta>
    </head>
    <body>
 
    </body>
</html>
cs
바로 위에서는직계만 매칭이 되서 <meta>는 매칭이 되지 않았지만 이렇게 사용하면 자손 모두를 매칭하기 때문에 <meta>도 매칭하게 됩니다.

- Tag1, Tag2: Tag1 이거나 Tag2인 모든 Tag
Ex)

1
2
3
4
5
6
7
8
9
<!doctype html>
<html>
    <head>
        <meta>
    </head>
    <body>
 
    </body>
</html>
cs
<html>(tag1)이거나 <head>이거나 모든 Tag


- tag[attr]: attr속성이 정의된 모든 Tag
- tag[attr="bar"]: attr속성이 "bar"문자열과 일치하는 모든 Tag
- tag[attr*="bar"]: attr속성이 "bar"문자열과 부분 매칭되는 모든 Tag
- tag[attr^="bar"]: attr속성이 "bar"문자열로 시작되는 모든 Tag
- tag[attr$="bar"]: attr속성이 "bar"문자열로 끝나는 모든 Tag

예제)
- tag#tag_id: id가 tag_id인 모든 Tag
- tag.tag_class: 클래스명 중에 tag_class가 포함된 모든 Tag
- tag#tag_id.tag_cls1.tag_cls2
id가 tag_id이고, 클래스명중에 tag_cls1과 tag_cls2가 모두 포함된 Tag
-tag.tag_cls1.tag_cls2
클래스명 중에 tag_cls1과 tag_cls2가 포함된 모든 Tag
-tag.tag_cls1 .tag_cls2
클래스명 중에 tag_cls1이 포함된 Tag의 자식중(직계아니여도됨) 클래스 명에 tag_cls2가 포함된 모든 Tag

방금한 멜론 TOP 100차트를 CSS Selector를 사용하여 더 간단하게 해보겠습니다.
코드
1
2
3
4
5
6
7
8
9
10
import requests
from bs4 import BeautifulSoup
 
html = requests.get('http://www.melon.com/chart/index.htm').text
soup = BeautifulSoup(html, 'html.parser')
 
tag_list = soup.select('#chartListObj tr .wrap_song_info a[href*=playSong]')
 
for idx, tag in enumerate(tag_list, 1):
    print(idx, tag.text)
cs
방금 사용한 그 반복문들이 CSS Selector를 사용하니 7번째줄 한줄로 끝나버립니다.

결과

결과화면도 똑같이 나옵니다.


CSS Selector 주의사항

- 패턴을 너무 타이트하게 지정하면 안됩니다.
HTML 마크업이 조금만 변경되어도 태그를 찾을수 없게 되는 경우가 생깁니다.
- 적절히 최소한의 패턴으로 지정합니다.

ex)

좀전에 쪼개 최하위 입니다

wrap_song_info이죠

1
2
tag_list = soup.select('#chartListObj tr .wrap_song_info a[href*=playSong]')
 
cs

이 코드입니다.


하지만

더 정확하게 하겠다고 이렇게 테이블을 추가하고 지나치게 타이트하게

1
2
tag_list = soup.select('#tb_list #frm .tb_list #chartListObj tr .wrap_song_info a[href*=playSong]')
 
cs
이런식으로 해도
출력은 됩니다.
하지만 웹페이지의 특성상 마크업이 바뀌거나 할 경우가 있기때문에 적당히 분석을 하고 최소한으로 패턴을 지정하시길 추천드립니다.

lxml HTML을 통한 Parser 실습

설치



어떨때 사용할까요?

Google Finance를 예로 들겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<div id=prices class="gf-tablewrapper sfe-break-bottom-16">
<table class="gf-table historical_price">
    <tr class=bb>
        <th class="bb lm lft">Date
        <th class="rgt bb">Open
        <th class="rgt bb">High
        <th class="rgt bb">Low
        <th class="rgt bb">Close
        <th class="rgt bb">Volume
    <tr>
        <td class="lm">Feb 28, 2014
        <td class="rgt">100.71
        <td class="rgt">100.71
        <td class="rgt">100.71
        <td class="rgt rm">0
</table>
cs

자세히 보시면 </tr>, </td>가 없는것을 확인하실 수 있습니다.

이때 lxml parser를 사용하시면 되겠습니다.



파라미터값 넣는 이유

주소로 가시면 q와 똑같은 이름으로 검색을 하고


여기서 startdate와 enddate의 파라미터를 넣기 때문에 파라미터를 보내는 것입니다.


코드실습

HTML.Parser을 쓸경우

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import requests
from bs4 import BeautifulSoup
 
params = {
    'q''EPA:BRNTB',
    'startdate''Jan 01, 2016',
    'enddate''Jun 02, 2016',
}
response = requests.get('https://www.google.com/finance/historical', params=params)
print(response.request.url)
html = response.text
soup = BeautifulSoup(html, 'html.parser'#여기 봐주세요)
for tr_tag in soup.select('#prices > table > tr'):
    row = [td_tag.text.strip() for td_tag in tr_tag.select('th, td')]
    print(row)
cs

html.parser를 사용한 코드입니다.


결과값이 제대로 안나오죠?


html.parser의 경우 태그가 정확하게 닫혀야 제대로 나오는데 이번의 경우는 그렇지 않았죠

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<div id=prices class="gf-tablewrapper sfe-break-bottom-16">
<table class="gf-table historical_price">
    <tr class=bb>
        <th class="bb lm lft">Date
        <th class="rgt bb">Open
        <th class="rgt bb">High
        <th class="rgt bb">Low
        <th class="rgt bb">Close
        <th class="rgt bb">Volume
    <tr>
        <td class="lm">Feb 28, 2014
        <td class="rgt">100.71
        <td class="rgt">100.71
        <td class="rgt">100.71
        <td class="rgt rm">0
</table>
cs
다시한번 확인해 보시기 바랍니다. </tr>과 </td>가 없기때문에 그렇습니다.


lxml.Parser 사용

그래서 이런 페이지가 있어서 파싱이 되지 않을때 바로 lxml.parser를 사용하시면 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import requests
from bs4 import BeautifulSoup
 
params = {
    'q''EPA:BRNTB',
    'startdate''Jan 01, 2016',
    'enddate''Jun 02, 2016',
}
response = requests.get('https://www.google.com/finance/historical', params=params)
print(response.request.url)
html = response.text
soup = BeautifulSoup(html, 'lxml'#'html.parser')
for tr_tag in soup.select('#prices > table > tr'): # > : 직계
    row = [td_tag.text.strip() for td_tag in tr_tag.select('th, td')]#, : ㅐㄱ
                    #strip: 좌우공백 제거
    print(row)
cs

코드는 간단합니다. 14번째줄 #html.parser을 lxml로 바꿔주시면 됩니다.


결과


비교

크롤링에 성공했습니다!




만약 lxml로도 되지 않는다면?

- 정규표현식을 통해 미리 전처리를 해주고 BeautifulSoup에 넘겨주는 방법이 있습니다.





본 게시글은 nomade.kr의 동영상 강의를 시청하고 제가 정리하여 올린 게시글입니다.

3 Comments
댓글쓰기 폼