ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 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를 복사할 수 있습니다.

    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

     

    댓글 편은 다음 시간에 마무리 하겠습니다. 

    반응형
Designed by Tistory.