DA팀 주니어로서의 마지막 활동인 주니어 프로젝트가 시작되었다. 우리 조는 데이터 뉴스레터라는 주제로 프로젝트를 진행하기로 하였다.
데이터 구축부터 모델링, 프론트엔드를 이용한 웹 구현까지 데이터 분석의 전반적인 과정을 모두 경험해보며 DA팀이 지향하는 바이기도 한 All-Rounder로의 한걸음을 내딛을 수 있지 않을까 하는 기대를 가지고 있다.
첫 회의에서 주제를 정하고 대략적인 프로젝트 계획을 설계한 뒤에 조원들과 API 크롤링과 링크 크롤링 두가지 방법으로 크롤링을 시도해보기로 하였고, 그 중 링크 크롤링을 맡아 코드를 작성했다.
언론사별 1주일간 발행된 기사 언론사, 헤드라인, 링크, 발행일자, 본문
사실 1차에서는 헛짓거리를 좀 많이 했다😅
뉴스 기사 본문은 크롤링할 필요가 없었는데, 본문까지 크롤링 해야되는 줄 알고 다음과 같은 방향으로 코드를 작성했다.
교육세션 때 부팀장님께서 발제해주셨던 크롤링 자료와 코드가 정말 많은 도움이 되었고, 이 외에도 다른 코드들도 많이 참고하였다.
검색어 및 발행일자 설정 → 언론사별 본문 및 발행일자 크롤링 코드 → 검색할 언론사 선택 → 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차 코드를 작성했다.
전날 하루 동안 발행된 기사 헤드라인, 링크, 썸네일 링크
회의를 통해 프로젝트의 몇 부분이 수정되었다. 먼저 전체적인 틀에 변화가 생기면서 빅데이터
에서 데이터
로 검색어의 범위를 확장했고, 범위가 확장됨에 따라 발행 기간은 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()
크롤링 대상에서 본문을 제외하니까 코드가 비교적 (정말 많이) 짧아졌다. 다른 부분은 모두 간단하게 수정이 가능했는데, 썸네일 링크를 뽑는 과정에서 여러 시행착오가 있었다.
썸네일
만 범위를 따로 정의하여 for 문을 별개로 작성해보았는데, 이는 news_dict
의 기존 값이 사라지는 동시에 idx
정의에 관한 문제가 발생했다.헤드라인 / 링크 / 썸네일
을 모두 포괄할 수 있는 범위로 태그 리스트를 다시 정의했다.try/except
를 이용해주었다.간단한 작업인데 어디서 그렇게 에러가 많이 났던 걸까..
별도의 입력값 없이
news_crawling()
함수를 실행하면 전날 하루 동안의 데이터 기사 관련 정보가 크롤링되어 엑셀 파일로 저장된다.
2022.05.18(어제 기준 하루 전) 네이버의 데이터 관련 기사 263페이지, 총 2621건
어제(5.19) 기준 전날인 5월 18일 하루 동안 발행된 데이터 관련 기사의 헤드라인 / 링크 / 썸네일 링크 (없을시 공백 처리)를 크롤링한 예시 파일이다.
열 이름을 포함한 2622행이 엑셀 파일에 잘 저장되었고, 이를 통해 에러 없이 코드가 잘 돌아가는 것을 확인할 수 있었다.