-
6. selenium 과 BeautifulSoup으로 daum 카페 크롤링 - 본문편Python/파이썬 웹 크롤러 2019. 5. 17. 20:36반응형
* 주의사항
과도한 크롤링은 법적인 문제가 될 수도 있습니다.
실습 시 대상 서버에 과도한 부하가 걸리지 않도록 주의합시다.
다음은 과도한 크롤링이 적발될 경우 계정을 정지시킵니다.미리 연습용 계정을 만드는 것도 괜찮겠네요.예제 주소:
https://github.com/pycrawling/crawling-tutorial/blob/master/daum-cafe-mobile-crawler-article.ipynb
* 모바일용 홈페이지를 공략하자.
모바일용 홈페이지가 PC용 홈페이지보다 단순한 구조를 가진 경우가 많습니다.
구조가 단순할 수록 크롤링이 쉽습니다.1. 필요한 라이브러리들을 불러옵니다.
예제 중 설명이 필요한 부분만 블로그에 올리겠습니다.
from selenium import webdriver from bs4 import BeautifulSoup import time
2. 셀레니움 웹드라이버를 이용해서 다음 카페 모바일의 로그인 페이지를 열어줍니다.
driver = webdriver.Chrome('./driver/chromedriver') # 설치 폴더에 주의합니다. driver.get('https://logins.daum.net/accounts/loginform.do?mobilefull=1&category=cafe&url=http%3A%2F%2Fm.cafe.daum.net%2F_myCafe%3Fnull') # 19년 5월부터 로그인 페이지 주소가 살짝 바뀌었네요. time.sleep(3) # 페이지 전환시에는 적당한 시간을 줍니다. # 1. 과도한 크롤링 방지. # 2. 페이지 전환이 완료되기 전에 다음 명령 실행되는 것 방지. # AJAX를 사용한 페이지는 페이지 전환시 딜레이가 꼭 필요한 경우도 있습니다.
3. send_keys()와 click()으로 아이디와 패스워드를 자동 입력합시다.
# 수동으로 웹 브라우저에 직접 입력 후 건너뛰어도 됩니다. driver.find_element_by_xpath("""//*[@id="id"]""").send_keys('') # id driver.find_element_by_xpath("""//*[@id="inputPwd"]""").send_keys('') # 패스워드 driver.find_element_by_xpath("""//*[@id="loginBtn"]""").click() # 입력 버튼 클릭. time.sleep(3)
* XPath가 등장했습니다.
이 부분이 XPath 입니다.
//*[@id="id"]
XPath는 엘리먼트가 문서의 어느 부분에 있는 지를 설명해주는, 주소 같은 개념입니다.
좀 더 자세히 설명하자면,
XPath는 (html을 구성하는 node의 attrib 중에)
id 값은 유일(unique)해야 한다는 점을 이용합니다.
예를 들자면, 어떤 ID의 몇 대째, 몇 번째 자식의 몇 번째 이웃.
이런 식으로 최대한 가까운 부모의 ID 값을 이용해 엘리먼트의 위치를 표현하는 겁니다.//*[@id="loginForm"]/fieldset/div[3]/div
위 xpath는 "loginForm"이란 id를 가진 엘리먼트 아래에, fieldset 태그 아래, 3번째 div 태그 아래에 있는,
div를 의미합니다. [3]이 4번째가 아니라 3번째임을 주의하셔야합니다.
엑스패스는 정말 편합니다.왜 노드를 찾아 해매었나 자괴감 들어...* html에서 XPath를 추출해 봅시다.
F12를 누르면 크롬의 개발자 도구가 열립니다.
개발자 도구의 좌 상단 '폰, 패드 아이콘'을 누르면좀 더 모바일 스러운 레이아웃이 적용됩니다.개발자 도구가 열려있지 않은 상태에서는 Ctrl+ Shift + C로 바로 실행가능합니다.
개발자 도구의 우측 상단 마우스 모양의 아이콘
(Select an element in the page to inspect it)을 누르고,
마우스 커서를 이리 저리 옮겨보면
HTML 요소(엘리먼트)의 코드 위치를 알 수 있습니다.개발자 도구에서 하늘색 배경으로 표시가 됩니다.
찾은 요소를 클릭하면 마우스 상태가 풀리면서 개발자 도구가 해당 소스 배경이 하늘색으로 고정됩니다.
이때 하늘색 배경을 마우스 우클릭을 하면 메뉴가 열리는데
Copy > XPath(제일 아래)를 선택해서 XPath를 복사할 수 있습니다.4. 카페 이름과 주소를 지정해 줍니다.
CAFE_NAME = 'ssaumjil' # 카페 이름을 지정한다. 예제는 이종격투기 BOARD_NAME = 'Jntr' # 게시판 주소의 마지막 4자리(?)를 지정한다. # http://m.cafe.daum.net/ssaumjil/Jntr
5. 게시판으로 이동합시다
driver.get('http://m.cafe.daum.net/%s/%s?boardType=' % (CAFE_NAME, BOARD_NAME)) time.sleep(3) # 위에서 지정한 까페의 게시판으로 이동합니다. # 바로 게시물로 이동해도 됩니만.. # 계정정지의 압봵이 있으니... 게시판 리스트를 보고 게시물 하나만 소박하게 스크래핑하는 식으로...
6. 캡쳐
inp_num = input('저장할 게시물 번호 + 엔터: ') num = int(inp_num) # 뒤에 DB에 정수로 저장할 거니까 미리 형변환 해줍시다. url = 'http://m.cafe.daum.net/%s/%s/%d' % (CAFE_NAME, BOARD_NAME, num) driver.get(url) # 게시물의 주소로 이동합니다. time.sleep(3) html = driver.page_source
* 페이지 소스를 BeautifulSoup(이하 BS)에게 넘기는 것을 마지막으로 셀레니움의 역할은 끝이 납니다.
7. Beautiful Soup 의 등장
soup = BeautifulSoup(html, 'html.parser') #repr(soup)
* 제목을 찾아봅시다.
개발자 도구에서 제목에 해당되는 부분을 찾을 수 있을 것입니다.
<h3 class="tit_subject"> ▶▶▶▶ 운영자입니다. </h3>
불행히도 BS에서는 XPath가 지원되지 않습니다.
이제 BS에서 노드를 찾는 방법을 알려드려야 하겠네요.
저는 다음 2가지 방법을 사용합니다.
subject = soup.body.find('h3', class_='tit_subject')
BS는 class명을 이용해서 h3을 바로 찾을 수 있습니다.
클래스 명은 ID와 달리 중복이 가능합니다.
태그와 클래스명까지 중복되어 겹치는 경우 첫번째 요소가 검색됩니다.
다행히 이 HTML 문서에서 'tit_subject' 클래스는 한번만 사용됩니다. ^^중복을 걱정할 필요가 없네요.
중복 여부는 개발자 도구의 검색 기능을 이용해 보십시오.
find_all() 메소드(함수)로 모두 찾아서 list로 리턴 받을 수도 있습니다.subject = soup.body.select_one('#mArticle > div.view_subject.\#subject_area > h3')
BS는 selector를 이용해서 노드를 찾을 수 있습니다.
개발자 도구에서 copy > selector 를 사용하면 됩니다.
select() 메소드(함수)는 해당되는 요소를 모두 찾아서 list로 리턴합니다.
select_one()는 첫번째 요소를 하나만 리턴해줍니다.* 연속 크롤링시에는 에러를 최대한 피해야 합니다.
if subject is None: print(url, '지워진 게시물입니다.') # 함수로 싸 주었다면 여기서 return을 걸면 딱 좋겠죠? else: # 지워진 게시물이 아닌 경우 이하를 계속 실행합니다. subject = subject.get_text(strip=True)
None 객체에 .get_text() 메소드를 사용하면 에러가 발생합니다.
* 작성자를 찾아봅시다.
<span class="txt_subject"> <span class="sr_only">작성자</span> 니**코 <span class="txt_bar">|</span> <span class="sr_only">작성시간</span> <span class="num_subject">18.09.11</span> <span class="txt_bar">|</span> <span class="sr_only">조회수</span> <span class="num_subject">12,548</span> </span>
헉 '니**코'는 직접 태그로 감싸지지 않은 부분입니다.
셀렉터로는 바로 검색이 안됩니다.
XPath는 되는데.. OTL.. 지원을 안하니..
이런 경우에는 옆 노드(<span class="sr_only">작성자</span>)를 찾은 뒤,
그 노드의 이웃으로 찾아야 합니다.
soup.select_one('#mArticle > div.view_subject.\#subject_area > span.txt_subject > span:nth-child(1)') # <span class="sr_only">작성자</span>
soup.select_one('#mArticle > div.view_subject.\#subject_area > span.txt_subject > span:nth-child(1)').next_sibling # '니**코' # next_sibling 으로 옆 노드를 찾을 수 있습니다. next_sibling.next_sibling 도 해보세요.
soup.body.find('span', class_='sr_only').next_sibling # find를 이용해서 찾을 수도 있습니다.
다음 카페는 일시적으로 익명 전환이 가능해서 작성자가 공란인 경우도 있습니다.
예외 처리 해줍니다.
if soup.body.find('span', class_='txt_subject').find('span', class_='sr_only').get_text() == '작성자': nick = soup.body.find('span', class_='sr_only').next_sibling else: nick = ''
* 작성시간과 조회수를 찾아봅시다.
클래스 명이 지정되어 있습니만, 작성시간, 조회수 2군데에서 사용되었습니다.
이런 경우는 find_all()로 찾아낸 뒤 [0], [1] 하나씩 뽑아내야합니다.num_subject = soup.body.find_all('span', class_='num_subject') write_time = num_subject[0].get_text() views = num_subject[1].get_text() print(num, subject, nick, write_time, views, url)
* 본문
본문은 친절하게 id='article' 안에 잘 들어있습니다.
id는 유일하니까, 바로 find로 찾으면 됩니다.본문 내 사용자가 퍼온 HTML이 있는 경우엔 id가 겹치는 경우도 있습니다. 조심하셔야 합니다. ㅠ,.ㅠ
contents = soup.body.find('div', id='article').get_text('\n', strip=True) print(contents)
* 브라우저를 닫습니다.
driver.close()
강좌가 프로그래밍보다 힘들군요..
크롤러를 한번도 만들어보지 않았던 상황에서 정보 수집에서, 완성까지 한나절 정도 걸렸는데..
강좌를 올리는 데는 며칠이 걸리는 군요.. OTL댓글 편은 다음 시간에 마무리 하겠습니다.
반응형