[Crawling] 네이버 뉴스 기사 크롤링

김소정·2022년 5월 20일
2
post-thumbnail

DA팀 주니어로서의 마지막 활동인 주니어 프로젝트가 시작되었다. 우리 조는 데이터 뉴스레터라는 주제로 프로젝트를 진행하기로 하였다.

데이터 구축부터 모델링, 프론트엔드를 이용한 웹 구현까지 데이터 분석의 전반적인 과정을 모두 경험해보며 DA팀이 지향하는 바이기도 한 All-Rounder로의 한걸음을 내딛을 수 있지 않을까 하는 기대를 가지고 있다.

첫 회의에서 주제를 정하고 대략적인 프로젝트 계획을 설계한 뒤에 조원들과 API 크롤링링크 크롤링 두가지 방법으로 크롤링을 시도해보기로 하였고, 그 중 링크 크롤링을 맡아 코드를 작성했다.

1차 코드

언론사별 1주일간 발행된 기사 언론사, 헤드라인, 링크, 발행일자, 본문

사실 1차에서는 헛짓거리를 좀 많이 했다😅

뉴스 기사 본문은 크롤링할 필요가 없었는데, 본문까지 크롤링 해야되는 줄 알고 다음과 같은 방향으로 코드를 작성했다.

  • 언론사별 날짜 및 본문 크롤링 코드
  • 모든 언론사의 코드 작성은 어렵다고 판단 → 일간지 총 15개 선택
    (이 와중에 3개 실패.. 주석 처리)

교육세션 때 부팀장님께서 발제해주셨던 크롤링 자료와 코드가 정말 많은 도움이 되었고, 이 외에도 다른 코드들도 많이 참고하였다.

코드 순서

검색어 및 발행일자 설정 → 언론사별 본문 및 발행일자 크롤링 코드 → 검색할 언론사 선택 → for문과 동적 제어를 이용해 웹 페이지에서 해당 언론사 찾기 → 언론사 / 헤드라인 / 발행일자 / 링크 / 본문 크롤링 → 엑셀 파일로 저장

전체 코드

import sys, os
from bs4 import BeautifulSoup
import requests
from selenium import webdriver
import selenium
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from webdriver_manager.chrome import ChromeDriverManager
from datetime import datetime, timedelta
from pandas import DataFrame
import time
from openpyxl.workbook import Workbook

sleep_sec = 0.5

wb = Workbook()

# User-Agent를 입력해주세요.
headers = {'User-Agent' : '________________'}


# 날짜 지정
query = '빅데이터'
today = datetime.today().strftime("%Y.%m.%d")
oneweek = (datetime.today() - timedelta(7)).strftime("%Y.%m.%d")

def crawling_main_text(url):

    req = requests.get(url, headers = headers)
    req.encoding = None
    soup = BeautifulSoup(req.text, 'html.parser')
    
    # 경향신문
    # if ('khan' in url):
    #     content_of_article = soup.select('div.art_body')
        
    #     str_list = []
    #     for item in content_of_article:
    #         string_item = item.find_all("p", {"class": "content_text"})
    #         str_list.append(string_item)
        
    #     text = " ".join(str_list)
        
    # 국민일보
    if '://news.kmib' in url: 
        text = soup.find('div', {'class' : 'tx'}).text
        date = soup.find('span', {'class': 't11'}).text
        date = date[0:10]
        
    # 내일신문
    elif 'naeil.com' in url:
        text = soup.find('div', {'class' : 'article'}).text
        date = soup.find('div', {'class': 'articleArea'}).find('div', {'class': 'date'}).text
        date = date[0:10]
        
    # 동아일보
    elif 'donga.com' in url:
        text = soup.find('div', {'class' : 'article_txt'}).text
        date = soup.find('span', {'class': 'date01'}).text
        date = date[3:13]
        
    # 매일일보
    elif 'm-i.kr' in url:
        text = soup.find('div', {'itemprop' : 'articleBody'}).text
        date = soup.find('div', {'class': 'info-text'}).find_all('li')[1].text
        date = date[4:14]
    
    # 문화일보
    # elif 'munhwa.com' in url:
    #     text = soup.find('div', {'id' : 'NewsAdContent'}).text
    #     date = soup.find_all('td', {'align': 'right'})[15].text
    #     date = date[3:14]
        
    # 서울신문
    elif 'seoul.co.kr' in url:
        text = soup.find('div', {'itemprop' : 'articleBody'}).text
        date = soup.find('span', {'itemprop': 'datePublished'}).text
        date = date[0:10]
        
    # 세계일보
    elif 'segye.com' in url:
        text = soup.find('article', {'class' : 'viewBox2'}).text
        date = soup.find('p', {'class': 'viewInfo'}).text
        date = date[5:15]
    
    # 아시아투데이
    elif 'asiatoday' in url:
        text = soup.find('div', {'class' : 'news_bm'}).text
        date = soup.find('span', {'class': 'wr_day'}).text
        date = date[5:17]
    
    # 전국매일신문
    elif 'jeonmae' in url:
        text = soup.find('div', {'itemprop' : 'articleBody'}).text
        date = soup.find('div', {'class': 'info-text'}).find_all('li')[1].text
        date = date[4:14]
    
    # 조선일보
    # elif 'chosun' in url:
    #     text = soup.find('section', {'class': 'article-body'}).text
    #     date = soup.find_all('span', {'class': 'font--size-sm-14.font--size-md-14.text--grey-60'})[0][1].text
    #     date = date[0:10]
        
    # 중앙일보
    elif 'joongang.co.kr' in url:
        text = soup.find('div', {'itemprop' : 'articleBody'}).text
        date = soup.find('div', {'class': 'time_bx'}).text
        date = date[4:14]
        
    # 천지일보
    elif 'newscj' in url:
        text = soup.find('div', {'itemprop' : 'articleBody'}).text
        date = soup.find('div', {'class': 'info-text'}).find_all('li')[1].text
        date = date[4:14]
        
    # 한겨레
    elif 'hani' in url:
        text = soup.find('div', {'itemprop' : 'articleBody'}).text
        date = soup.find('p', {'class': 'date-time'}).text
        date = date[4:14]
        
    # 한국일보
    elif 'hankookilbo' in url:
        text = soup.find('div', {'itemprop' : 'articleBody'}).text
        date = soup.find('div', {'class': 'info'}).find('dd').text
        date = date[0:10]
        
    # 그 외
    else:
        text == None
        
    return (text.replace('\n','').replace('\r','').replace('<br>','').replace('\t',''), date)


press_nms = ['국민일보', '내일신문', '동아일보', '매일일보', '서울신문', '세계일보', '아시아투데이', '전국매일신문', '중앙일보', '천지일보', '한겨레', '한국일보']


def news_crawling(press_nm):
    
    service = Service(executable_path=ChromeDriverManager().install())
    browser = webdriver.Chrome(service=service)

    print('검색할 언론사 : {}'.format(press_nm))


    print('브라우저를 실행시킵니다(자동 제어)\n')

    news_url = 'https://search.naver.com/search.naver?where=news&sm=tab_pge&query={0}&sort=1&photo=0&field=0&pd=1&ds={1}&de={2}'.format(query, oneweek, today)
    browser.get(news_url)
    time.sleep(sleep_sec)


    ################# 언론사 선택 #################
    print('설정한 언론사를 선택합니다.\n')

    bx_press = browser.find_element_by_xpath('//div[@role="listbox" and @class="api_group_option_sort _search_option_detail_wrap"]//li[@class="bx press"]')

    # 언론사 분류순 클릭
    press_tablist = bx_press.find_elements_by_xpath('.//div[@role="tablist" and @class="option"]/a')
    press_tablist[1].click()
    time.sleep(sleep_sec)

    # 언론사 분류 선택
    bx_group = bx_press.find_elements_by_xpath('.//div[@class="api_select_option type_group _category_select_layer"]/div[@class="select_wrap _root"]')[0]

    press_kind_bx = bx_group.find_elements_by_xpath('.//div[@class="group_select _list_root"]')[0]
    press_kind_btn_list = press_kind_bx.find_elements_by_xpath('.//ul[@role="tablist" and @class="lst_item _ul"]/li/a')


    for press_kind_btn in press_kind_btn_list:
        
        # 언론사 종류를 순차적으로 클릭
        press_kind_btn.click()
        time.sleep(sleep_sec)
        
        # 언론사 선택
        press_slct_bx = bx_group.find_elements_by_xpath('.//div[@class="group_select _list_root"]')[1]
        press_slct_btn_list = press_slct_bx.find_elements_by_xpath('.//ul[@role="tablist" and @class="lst_item _ul"]/li/a')
        press_slct_btn_list_nm = [psl.text for psl in press_slct_btn_list]
        
        # 언론사 이름 딕셔너리 생성
        press_slct_btn_dict = dict(zip(press_slct_btn_list_nm, press_slct_btn_list))
        
        # 원하는 언론사가 해당 이름 안에 있는 경우 클릭 후 탐색 중지
        if press_nm in press_slct_btn_dict.keys():
            print('<{}> 카테고리에서 <{}>를 찾았으므로 탐색을 종료합니다'.format(press_kind_btn.text, press_nm))
            
            press_slct_btn_dict[press_nm].click()
            time.sleep(sleep_sec)
            
            break
        

    ################# 뉴스 크롤링 #################

    print('\n크롤링을 시작합니다.')

    #####동적 제어로 페이지 넘어가며 크롤링
    news_dict = {}
    idx = 1
    cur_page = 1
    news_num = 100

    while True:

        table = browser.find_element_by_xpath('//ul[@class="list_news"]')
        li_list = table.find_elements_by_xpath('./li[contains(@id, "sp_nws")]')
        area_list = [li.find_element_by_xpath('.//div[@class="news_area"]') for li in li_list]
        a_list = [area.find_element_by_xpath('.//a[@class="news_tit"]') for area in area_list]
    
        for n in a_list[:min(len(a_list), news_num-idx+1)]:
            n_url = n.get_attribute('href')
            news_dict[idx] = {'언론사': press_nm,
                            '타이틀' : n.get_attribute('title'),
                            '발행일자' : crawling_main_text(n_url)[1],
                            '링크' : n_url,
                            '본문' : crawling_main_text(n_url)[0]}
            
            idx += 1
            
        try:
            next_btn = browser.find_element(By.CSS_SELECTOR, 'a.btn_next')
            next_btn.click()
        
            cur_page +=1

            pages = browser.find_element_by_xpath('//div[@class="sc_page_inner"]')
            next_page_url = [p for p in pages.find_elements_by_xpath('.//a') if p.text == str(cur_page)][0].get_attribute('href')

            browser.get(next_page_url)
            time.sleep(sleep_sec)
            
        except:
            print('\n브라우저를 종료합니다.\n' + '=' * 100)
            time.sleep(0.7)
            browser.close()
            break
        
    ################# 데이터 전처리 #################

    print('데이터프레임 변환\n')
    news_df = DataFrame(news_dict).T

    folder_path = os.getcwd()
    xlsx_file_name = '{}_{}.xlsx'.format(query, press_nm)

    news_df.to_excel(xlsx_file_name, index=False)

    print('엑셀 저장 완료 | 경로 : {}\\{}\n'.format(folder_path, xlsx_file_name))
    
    
for pn in press_nms:
    news_crawling(pn)

코드 설명

웹페이지에서 언론사를 선택하는 부분과 엑셀 파일 변환은 다른 분의 코드를 참고하여 작성하였고, 언론사별로 본문 및 발행일자를 뽑아내는 과정은 간단하면서도 번거로웠다.

일주일간의 기사를 크롤링하는 것이었기 때문에, 함수를 정의해놓으면 언제든 오늘부터 일주일 전까지의 기사를 크롤링할 수 있게끔 datetime 라이브러리를 이용해 날짜를 url에 반영하는 방식으로 코드를 작성하였다.

또한, 정렬은 최신순으로 설정했는데 이 부분은 2차 코드에서 수정되었다.

엑셀 파일은 언론사별로 저장될 수 있도록 설정했고, 이에 따라 설정한 언론사 개수만큼 파일이 생성되었다.

네이버 뉴스의 마지막 페이지까지 넘기려면..

1차 코드를 작성하며 가장 오래한 고민이 아닐까 싶다. 네이버 뉴스 기사 크롤링에 관한 포스팅을 정말 많이 읽었는데, 보통 뉴스의 개수를 설정하거나 페이지 수를 설정해서 크롤링 코드를 설계하신 분들이 대부분이었다.

그래서 여러 방법을 생각해보다가, 불현듯 try/except을 사용하면 되지 않을까 하는 생각이 들어 시도를 해봤는데 다행히도 한번만에 코드가 제대로 돌아갔다!

돌아보니 매우 간단했던 거 같지만.. 크롤링 병아리인 나에겐 오래 걸렸던 1차 코드 작성이 끝이 났고, 두번째 회의를 끝낸 뒤 수정 사항을 반영하여 2차 코드를 작성했다.

2차 코드

전날 하루 동안 발행된 기사 헤드라인, 링크, 썸네일 링크

수정 사항

  • 검색어 : 빅데이터 → 데이터
  • 발행 기간 : 일주일 → 전날 하루
  • 관련도순 정렬
  • 썸네일 이미지 링크 크롤링
  • 본문(원래 안 하는 거였지만 내가 해버렸던 것), 언론사, 발행일자 X

회의를 통해 프로젝트의 몇 부분이 수정되었다. 먼저 전체적인 틀에 변화가 생기면서 빅데이터에서 데이터로 검색어의 범위를 확장했고, 범위가 확장됨에 따라 발행 기간은 7일이 아닌 1일로 단축했다. (하루만 해도 대략 2~3000건의 기사가 발행된다.)

코드 순서

검색어 및 발행일자 설정 → 헤드라인 / 링크 / 썸네일 링크 크롤링 → 엑셀 파일로 저장

전체 코드

import sys, os
from bs4 import BeautifulSoup
import requests
from selenium import webdriver
import selenium
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from webdriver_manager.chrome import ChromeDriverManager
from datetime import datetime, timedelta
from pandas import DataFrame
import time
from openpyxl.workbook import Workbook

sleep_sec = 0.5

wb = Workbook()

# User-Agent를 입력해주세요.
headers = {'User-Agent' : '________________'}

query = '데이터'
yesterday = (datetime.today() - timedelta(1)).strftime("%Y.%m.%d")

def news_crawling():
    
    service = Service(executable_path=ChromeDriverManager().install())
    browser = webdriver.Chrome(service=service)


    print('브라우저를 실행시킵니다(자동 제어)\n')

    news_url = 'https://search.naver.com/search.naver?where=news&query={0}&sm=tab_opt&sort=0&photo=0&field=0&pd=3&ds={1}&de={1}]'.format(query, yesterday)
    browser.get(news_url)
    time.sleep(sleep_sec)
    

    print('\n크롤링을 시작합니다.')

    #####동적 제어로 페이지 넘어가며 크롤링
    news_dict = {}
    idx = 1
    cur_page = 1
    news_num = 1000000

    while True:

        table = browser.find_element_by_xpath('//ul[@class="list_news"]')
        li_list = table.find_elements_by_xpath('./li[contains(@id, "sp_nws")]')
        area_list = [li.find_element_by_xpath('.//div[@class="news_wrap api_ani_send"]') for li in li_list]
                
        for a in area_list[:min(len(area_list), news_num-idx+1)]:
            n = a.find_element_by_xpath('.//a[@class="news_tit"]')
            n_url = n.get_attribute('href')
                    
            try:
                img = a.find_element(By.CSS_SELECTOR,'a.dsc_thumb ').find_element(By.CSS_SELECTOR, 'img')
                img = img.get_attribute('src')
                
            except:
                img = " "
                    
            news_dict[idx] = {'Title' : n.get_attribute('title'),
                            'url' : n_url,
                            'thumbnail': img}
            
            idx += 1
            
            
        try:
            next_btn = browser.find_element(By.CSS_SELECTOR, 'a.btn_next')
            next_btn.click()
        
            cur_page +=1

            pages = browser.find_element_by_xpath('//div[@class="sc_page_inner"]')
            next_page_url = [p for p in pages.find_elements_by_xpath('.//a') if p.text == str(cur_page)][0].get_attribute('href')

            browser.get(next_page_url)
            time.sleep(sleep_sec)
            
        except:
            print('\n브라우저를 종료합니다.\n' + '=' * 100)
            time.sleep(0.7)
            browser.close()
            break
        
    print('데이터프레임 변환\n')
    
    news_df = DataFrame(news_dict).T

    folder_path = os.getcwd()
    xlsx_file_name = '{}_{}.xlsx'.format(query, yesterday)

    news_df.to_excel(xlsx_file_name, index=False)

    print('엑셀 저장 완료 | 경로 : {}\\{}\n'.format(folder_path, xlsx_file_name))
    
news_crawling()

코드 설명

크롤링 대상에서 본문을 제외하니까 코드가 비교적 (정말 많이) 짧아졌다. 다른 부분은 모두 간단하게 수정이 가능했는데, 썸네일 링크를 뽑는 과정에서 여러 시행착오가 있었다.

  • HTML 태그를 긁어오는 과정에서 1차 코드와 똑같이 list를 설정해줄시, 썸네일은 해당 div에 포함되지 않아 긁어올 수가 없었다.
  • 이에 썸네일만 범위를 따로 정의하여 for 문을 별개로 작성해보았는데, 이는 news_dict의 기존 값이 사라지는 동시에 idx 정의에 관한 문제가 발생했다.
  • 따라서 헤드라인 / 링크 / 썸네일을 모두 포괄할 수 있는 범위로 태그 리스트를 다시 정의했다.
  • 또한 기사에 이미지(썸네일)가 없는 경우에는 공백으로 처리하기 위해 try/except를 이용해주었다.

간단한 작업인데 어디서 그렇게 에러가 많이 났던 걸까..

별도의 입력값 없이 news_crawling() 함수를 실행하면 전날 하루 동안의 데이터 기사 관련 정보가 크롤링되어 엑셀 파일로 저장된다.

결과

2022.05.18(어제 기준 하루 전) 네이버의 데이터 관련 기사 263페이지, 총 2621건

어제(5.19) 기준 전날인 5월 18일 하루 동안 발행된 데이터 관련 기사의 헤드라인 / 링크 / 썸네일 링크 (없을시 공백 처리)를 크롤링한 예시 파일이다.

열 이름을 포함한 2622행이 엑셀 파일에 잘 저장되었고, 이를 통해 에러 없이 코드가 잘 돌아가는 것을 확인할 수 있었다.

profile
Yonsei University, Applied Statistics

0개의 댓글