requests가 아닌 BeautifulSoup를 사용하는 이유?
간단한 requests를 사용한 코드입니다.
requests를 사용해서 https://www.naver.com을 text로 변환하여 html변수에 집어넣고
그것을 print한 코드이며 실행을 시켜보면 www.naver.com의 html코드가 잘 출력이 됩니다.
하지만 너무 복잡하죠? BeautifulSoup에서는 이런 복잡한 코드에서 사용자가 원하는 부분을 크롤링 할 수 있게 도와주는 모듈입니다.
BeautifulSoup를 사용해서 크롤링을 해보겠습니다.
BeautifulSoup를 배우기 전에
HTTP 응답
이것은 간단한 HTML구조입니다.
HTML구조는 사람들이 보기엔 나눠저 있는것 처럼 보이지만 하나의 문자열로 보기 때문에 이것을 사람이 원하는 부분만 알아서 보기는 힘듭니다.
하지만 크롤링을 해서 사용자가 홈페이지 규칙을 가져오고 싶을경우 사용을 할수 있겠습니다.
DOM문서
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문서가 되겠습니다.
이것을 왜 알아야 하는가?
즉, 페이지 소스보기는 서버에서 최초로 응답을 준 코드가 되겠고
개발자도구에서는 브라우저가 해석을 한 코드가 되겠습니다. (이것을 DOM Tree)
그렇다면 requests를 사용한 크롤링은 사용할때는 서버가 최초로 응답을 준 코드인 페이지 소스보기를 통해서 사용하는게 알맞겠죠?
그러면 개발자 도구는 언제 사용하는가?
selenium을 사용할때 쓰시면 됩니다.
왜냐하면 selenium은 브라우저를 통한 자동화이기 때문입니다.
복잡한 HTML문자열에서 내가 원하는 문자열 가져오는 방법
BeautifulSoup4 기초
설치
사용
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태그들이 문자열로 들어가게 됩니다.
Parser
1. Parser / Python's html.parser
- BeautifulSoup4의 내장파서
- 샘플코드
1 | soup = BeautifulSoup(파싱할 문자열, 'html.parser') | cs |
2. Parser / lxml's HTML parser
- lxml HTML파서를 사용하며 이는 외부 C 라이브러리
1 | soup = BeautifulSoup(파싱할 문자열, 'lxml') | cs |
왜 2개를 사용하는가?
태그찾는 방법
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 Syntax(문법)
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>는 매칭되지 않습니다.
1 2 3 4 5 6 7 8 9 | <!doctype html> <html> <head> <meta> </head> <body> </body> </html> | cs |
1 2 3 4 5 6 7 8 9 | <!doctype html> <html> <head> <meta> </head> <body> </body> </html> | cs |
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 주의사항
좀전에 쪼개 최하위 입니다
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 |
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의 동영상 강의를 시청하고 제가 정리하여 올린 게시글입니다.
'파이썬 프로그래밍 > 파이썬 크롤링' 카테고리의 다른 글
[Python] Pillow를 활용한 이미지 썸네일/다운로드 처리 크롤링 (0) | 2017.07.19 |
---|---|
[Python] 크롤링 연습문제. reddit 크롤링 풀이 (0) | 2017.07.18 |
[Python] requests 기초와 beautiful soup를 활용한 크롤링, [크롤링 준비] (1) | 2017.07.15 |
[Python] 크롤링 기초 개념과 requests를 이용한 기초실습(설치부터) (1) | 2017.07.15 |
[Python] 파이썬의 Beautiful Soup를 이용한 파싱 (9) | 2016.05.19 |