끄적거림

[Naver News Crawling] 네이버 뉴스 기사와 댓글 크롤링 in python(feat.selenium) 본문

Python/Crawling

[Naver News Crawling] 네이버 뉴스 기사와 댓글 크롤링 in python(feat.selenium)

Signing 2020. 11. 30. 17:22
728x90
반응형

[API] Naver 뉴스 API로 데이터 받아오기 in python

[리뷰 크롤링] PlayStore 댓글 크롤링하기 in python 1(feat. selenium)


최근 크롤링 관련하여 이것저것 진행하다보니 모든 플랫폼에 대해서 크롤링을 진행하는 듯하다.

 

이번에는 네이버 뉴스 기사 원문과 그에 달린 댓글을 가져오려 한다.

 

이번 포스팅은 사실 네이버 뉴스를 API로 받아온 결과를 바탕으로 수집할 예정이다.

 

그러니 위의 포스팅을 한 번씩 읽어보고 오면 좋을 듯하다.

 

위의 포스팅의 결과로 다음과 같은 결과물을 얻었다.

Naver검색API의 결과

보면 다음과 같은 항목들을 얻을 수 있다.

  • title : 기사의 제목
  • originallink : 기사의 원문의 링킁(신문사에서 온라인으로 발간한 신문사의 뉴스 링크)
  • link : 해당 기사가 네이버 뉴스 플랫폼에 게제될 때의 링크
  • description : 기사 본문 초입(첫 2~3 줄 정도)
  • pubDate : 기사 발간 날짜
  • keyword : 검색 당시 검색어

링크가 두 개다.

나도 이번 크롤링을 통해 안 새로운 사실이지만, 온라인 뉴스가 다소 복잡한 과정을 거친다는 것을 알게되었다.

 

네이버 뉴스가 우리가 읽을때까지의 과정은 다음과 같다.

온라인 뉴스 발간 과정

먼저 신문사에서 자체 온라인 뉴스링크 페이지를 만들어 신문을 게제한다.

그 다음, 이를 네이버 뉴스 플랫폼으로 보내면, 네이버 자체 뉴스 플랫폼에 새로운 링크를 달고 뉴스가 게제가 된다.

만약, 뉴스 구독자가 네이버 플랫폼을 통해 뉴스를 읽고 그에 대한 댓글을 적는다면, 이는 신문사에게 댓글이 남겨지는 것이 아니라, 네이버 플랫폼 상에 댓글이 남겨지게 되는 것이다.

 

한편, 신문사들은 저마다의 온라인 페이지 양식이 존재하기 때문에(심지어 한 두개가 아니다...) 뉴스 원문과 댓글을 가져오려면 해당 양식마다의 크롤러를 커스텀하게 만들어야한다.

 

하지만, 이런 과정은 매우 불편하고 시간도 오래걸리고 에러도 날 확률이 크기 때문에, 일정한 양식을 가진 네이버 뉴스 플랫폼으로 접근하여 원문과 댓글을 수집하는 편이 더 낫다고 생각한다.

댓글 역시 네이버 플랫폼에 훨씬 더 많다고 생각한다.

 

그렇다면 이제 본격적인 코딩을 진행해보자.

 


전체 코드는 맨 마지막에 적겠다.


 

 

1. 사전 준비

from selenium import webdriver as wd

import pandas as pd
import numpy as np
import os
import collections as co
from datetime import datetime

import time
from datetime import datetime

pd.options.display.max_colwidth = 10000

driverPath = "~~~~~/chromedriver.exe"   # 크롬 드라이버 exe 파일의 위치

naverURL = pd.read_csv('./Naver_News/Naver_News_URLs.csv', encoding='cp949')
  • pd.options.display.max_colwidth = 10000
    • DataFrame에 뉴스 원문을 담았을 때 10000자리까지 print해서 볼 수 있게 하는 옵션이다.
  • naverURL
    • Naver News API를 이용해서 가져온 뉴스의 정보들이다.(전 포스팅 참고)

 

 

 

2. 전체 코드의 수도 코드

article = co.deque([])
reples = co.deque([])

log = open('./NaverNews_log/log_'+datetime.strftime(datetime.now(), "%Y%m%d_%H%M%S")+'.txt', 'w')
log.write('start program: '+datetime.strftime(datetime.now(), "%H:%M:%S.%f"))
print('start program: '+datetime.strftime(datetime.now(), "%H:%M:%S.%f"))

for iteration in zip(URLs['index'].iloc[start:end], URLs['link'].iloc[start:end]):
    i, url = iteration[0], iteration[1]
    message = '\n'+datetime.strftime(datetime.now(), "%H:%M")+'    '+str(i)+'th loop | '
    
    # Back-up function
    
    # Article body function
    
    # Article reple function

본격적인 코드에 들어가기 앞서 대략적인 수도 코드이다.

 

  • 먼저 백업부터 보자면, 많은 request를 날리기 때문에 언제 네이버가 날 차단할지, 그리고 어떤 오류로 인해 루프가 종료될지는 모르기 때문에 중간중간마다 쌓였던 데이터를 로컬영역에 떨궈줌으로써 백업을 할 예정이다.
  • 그 다음으로 기사의 본문을 수집하는 함수를 작성하여 매 루프마다 호출하는 방식으로 운영할 예정이다.
  • 기사에 달린 댓글도 마찬가지이다. 하지만 여기서 중요한 것이, 하나의 기사에 대해 여러 댓글이 달린 경우 이를 본문 데이터와 함께 dataframe형태로 적재한다면 매우 큰 리소스 낭비가 될 것임으로, 본문의 url을 키로 두어 나중에 필요에 의하면 join하여 볼 수 있게끔 진행할 예정이다.
  • 마지막으로, for loop를 돌릴 예정인데 굳이 zip을 이용한 iteration으로 돌리는 이유는 백업과 퍼포먼스 때문이다.
    • 백업을 진행할 때, 특정 건수가 쌓이면 파일을 쓰고 메모리를 free 시킬 예정이기에 백업 파일 이름에 규칙을 부여해야한다.
    • 그 규칙성으로 나는 해당 naver news link의 index를 붙여서 몇번째 루프에서 백업 파일을 생성했는지 확인하기 위함이다.
    • 루프를 돌릴때 흔히 index까지 고려하는 상황이라면, enumerate를 사용하곤한다.
    • enumerate는 간편성과 가독성 면에서 우수하지만, 퍼포먼스를 생각하면 좋은 편은 아니라고 한다.
    • 참고: stackoverflow.com/questions/16476924/how-to-iterate-over-rows-in-a-dataframe-in-pandas
    • DataFrame을 loop를 돌릴때의 효율에 대해선 나중에 포스팅해볼 예정이다.
  • message는 루프를 돌릴때 모니터링을 하기 위해서 만든 메세지이다.

 

 

 

3. Back-up function

# Back-up
def backup(i, article, reples):
  if (int(i)-1) % 10 == 0:
    # free article body
    res_article = pd.DataFrame(article)
    res_article.to_csv('./NaverNews_body_reple/NaverNews_Exist_article'+str(i)+'.csv', index=False, encoding='utf-8-sig')
    del res_article, article
    log.write(message+'Delete res, article and free memory')
    log.write(message+'Re-create article')
    article = co.deque([])

    # free article reple
    res_reple = pd.concat(reples)
    res_reple.to_csv('./NaverNews_body_reple/NaverNews_Exist_reple_'+str(i)+'.csv', index=False, encoding='utf-8-sig')
    del res_reple, reples
    log.write(message+'Delete res, article and free memory')
    log.write(message+'Re-create article')
    reples = co.deque([])

인자로 i, article, reples를 받아서 다음과 같은 작업을 진행한다.

  • i : iteration
  • article : 뉴스의 본문
  • reples : 뉴스의 댓글
  • 각각을 원하는 위치에 csv 파일로 떨군 다음 del을 적용하여 메모리부터 free를 시킨다. 만약 많은양의 데이터를 한 번에 한 객체에 담고 진행하면 append memory error가 발생할 수 있다.
  • 이 때, 많은 데이터가 쌓이고 이를 한번에 append 시키는 과정을 다음과 같이 진행한다.(댓글을 예로 들면)
    • 많은 row의 dataframe 객체를 매 루프마다 list 객체에 append 시킨다.
    • 일정 루프를 돌면 list를 DataFrame으로 convert시킨다.
    • 이때, pd.concat( ) 함수를 적용하여 많은 dataframe들을 appnd한다.

 

 

4. Enter the URL

# Enter the URL

def enter_URL(url):
  try:
    driver = wd.Chrome(executable_path=driverPath)
    driver.get(url)
  except:
    log.write(message+'error with get URL... maybe Blocked IP... sleep 10 sec...')
    time.sleep(600)
    log.write(message+'restart')
    driver = wd.Chrome(executable_path=driverPath)
    driver.get(url)
  • try ... except ... 구문을 사용하여 혹시 모를 에러에 대비하자.
  • 내가 대비한 에러는 과도한 request시 네이버가 나의 ip를 막고, 더 이상 루프를 돌리기 어려울 때이다.
  • 혹시 모를 ip block에 대비하여 10분 동안의 휴식을 갖고 다시 돌리는 계획을 세웠다.

 

 

 

 

 

5. Article Body

# Get data - Article body

def Get_Article_Body(url):
  body = driver.find_elements_by_xpath("//div[@id='articleBodyContents']")
  text = ""
  for b in body:
    text = text.join(b.text)

  # re-enter
  if text.strip(url) == '':
    log.write(message+'there`s no article... maybe loading error')
    driver.refresh()
    log.write(message+'refresh...')
    log.write(message+'sleep 5 sec...')
    time.sleep(5)
    log.write(message+'try again...')
    driver.get(url)
    body = driver.find_elements_by_xpath("//div[@id='articleBody']")
    text = ""
    for b in body:
      text = text.join(b.text)

    tmp_article = {
      'NaverURL':url,
      'body':text
    }

    article.append(tmp_article)
  • 해당 url에 접속했을 때, 가끔 크롬이 굼뜨게 켜지는 경우가 있다. 이 때문에 데이터를 제대로 못긁어오는 현상을 방지하고자, re-enter을 구상했다.
  • xpath로 원문을 긁어왔을 때, 아무것도 긁어온 데이터가 없다면 드라이버의 refresh함수를 사용해 다시 크롬을 새로고침하는 것이다.
  • 경험상 이렇게하면 좀 더 빠르게 기사 원문을 끌어올 수 있다.
  • 그렇게 body 객체 안에 기사 원문을 가져왔다면, 이를 for문을 다시 한 번 이용하여 원문을 하나의 큰 string 객체로 로 만들어줘야한다.
  • 긁어온 기사의 원문을 보면 문단 단위, 문장단위로 띄어져 있기 때문에 모든 문장을 붙여주는 작업을 진행하는 것이다.

 

 

 

 

6. Article Reples

# Get data - Article reple
def ariticle_reple():
  log.write(message+'sleep 2 sec for scrap reples...')
  time.sleep(2)

  noReple = False

  no_reple = pd.DataFrame({
    'repleID':[None],
    'reple':[None],
    'time':[None],
    're_reple':[None],
    'like':[None],
    'dislike':[None],
    'NaverURL':[None]
  })

  try:
    # Click more reple
    driver.find_elements_by_xpath("//span[@class='u_cbox_in_view_comment']")[0].click()
  except:
    noReple = True

  if noReple:
	tmp_reple = no_reple
  else:
    log.write(message+'there are some reples... refresh and sleep 2 sec to scrap all data...')
    driver.refresh()
    time.sleep(2)

    # 총 댓글 갯수 확인
    Nreple = driver.find_element_by_xpath("//span[@class='u_cbox_count']")
    Nreple = Nreple.text
    Nreple = int(Nreple.replace(',',''))
    print(Nreple)
    if Nreple > 20:
      NrepleLoop = (Nreple//20) if Nreple%20 > 0 else (Nreple//20)-1
      for i in range(NrepleLoop):
        driver.find_elements_by_xpath("//span[@class='u_cbox_page_more']")[0].click()
        time.sleep(1)
        
	# 댓글 수집
    reple = driver.find_elements_by_xpath('//div[@class="u_cbox_comment_box"]')
    if len(reple) > 0:            # 댓글이 적어도 하나라도 있는 경우: 클린봇 X, 삭제 X
      tmp_reple = co.deque([])
      for r in reple:
        tmp_list = r.text.split('\n')
        if len(tmp_list) == 11:
          tmp_reple.append(tmp_list)
      tmp_reple = pd.DataFrame(tmp_reple)
      if len(tmp_reple) == 0:     # 댓글 하나만 있는데 삭제되었을 때
        tmp_reple = no_reple
      else:                
        tmp_reple = tmp_reple[[0,3,4,5,8,10]].rename(columns={
          0:'repleID',
          3:'reple',
          4:'time',
          5:'re_reple',
          8:'like',
          10:'dislike'
        })
        tmp_reple['NaverURL'] = url
    else:
      tmp_reple = no_reple

  reples.append(tmp_reple)
  • 제일 까다로운 부분이다.
  • 우선 다음과 같은 댓글 상황이 존재한다.
    1. 댓글이 아예 없는 상황
    2. 댓글이 있지만 전부 클린봇이나 작성자에 의해 삭제가 된 상황
    3. 일반적인 댓글과 삭제된 댓글이 혼재된 상황
    4. 일반적인 댓글만 있는 상황
    5. 댓글이 매우 많아서 댓글 더보기 클릭해야하는 상황 
  • 일단은 댓글의 상태와 관련없이, 삭제된 댓글이라도 1건 이상만 있다면, <댓글 더보기> 버튼을 클릭하여 댓글만 있는 페이지로 넘어갈 수 있다.
    • 따라서, 먼저 <댓글 더보기> 버튼을 클릭해보고, 에러가 난다면 댓글이 없는 경우라고 생각할 수 있다.
    • 그래서 먼저 try를 실행해보고 에러가 발생하면 except문으로 받아서 댓글이 없는 상황임을 noReple = True로체크한다.
  • 댓글이 없다면, NA값을 채운 데이터 프레임을 tmp_reple 객체에 넣어주고,
  • 댓글이 있다면, 어떤 댓글들인지를 파악해야 한다.
    • 먼저, 총 댓글의 갯수가 몇 개인지를 알아야한다. 그래야 <댓글 더보기> 버튼을 몇 번 누를지를 판단하여 리소스 낭비를 줄일 수 있다.
    • 만약 댓글 수가 20개가 넘는다면 <댓글 더보기> 버튼을 클릭해야하므로 몇 번 클릭할지를 삼항연산자를 이용해 구할 수 있다.
  • 그렇게해서 모든 댓글들을 페이지에 띄운다음에 모든 댓들들을 수집해온다.
    • 이때, driver.find_elements_by_xpath( ) 함수를 사용하기 때문에 반드시 루프를 한 번 더 사용할 수 밖에 없는 상황이다.
    • 기왕 루프를 돌 때, 댓글의 상태를 확인하여 삭제된 댓글인지를 확인하자.
      • 기본적으로 정상적인 댓글은 \n 를 구분자로 11개의 컬럼을 가지고 있다. 물론 이중엔 쓸모 없는 컬럼도 있다.
      • 하지만, 작성자에 의해 제거된 댓글이나 클린봇에 의해 삭제된 댓글이라면 4~5개의 컬럼을 갖는다.
      • 이 규칙을 이용하여 11개 보다 적은 값을 가지고 있다면 skip하고, 아닌 경우면 제대로 적재하는 상황을 만들었다.

 

 

 

 

 

7. Final step

driver.quit()
message2 = '\n'+datetime.strftime(datetime.now(), "%H:%M")+'    '+str(i)+'th loop | '
log.write(message2+'NaverURL: '+url)
print(message2+'NaverURL: '+url)
  • driver.quit( ) 함수는 selenium에 의해 실행되었던 크롬창을 닫는 동작을 수행한다.
  • 나머지는 로깅을 통해 모니터링을 위한 코드이다.

 

 

8. end program

res_article = pd.DataFrame(article)
res_reple = pd.concat(reples)
res_article.to_csv('./NaverNews_body_reple/NaverNews_Exist_article.csv', index=False, encoding='utf-8-sig')
res_reple.to_csv('./NaverNews_body_reple/NaverNews_Exist_reple.csv', index=False, encoding='utf-8-sig')

log.write('\n end program: '+datetime.strftime(datetime.now(), "%H:%M:%S.%f"))
print('end program')

log.close()
  • 이 코드는 루프에서 빠져나와서 갯수를 채우지 못하고 쌓인 나머지 기사들을 의한 코드이다.

 

 

 

 

 

9. Full Code

from selenium import webdriver as wd

import pandas as pd
import numpy as np
import os
import collections as co
from datetime import datetime

import time
from datetime import datetime

pd.options.display.max_colwidth = 10000

driverPath = "./chromedriver.exe"

naverURL = pd.read_csv('./Naver_News/Naver_News_URLs.csv', encoding='cp949')

URLs = naverURL[['link']].reset_index()

# Back-up
def backup(i, article, reples):
  if (int(i)-1) % 10 == 0:
    # free article body
    res_article = pd.DataFrame(article)
    res_article.to_csv('./NaverNews_body_reple/NaverNews_Exist_article'+str(i)+'.csv', index=False, encoding='utf-8-sig')
    del res_article, article
    log.write(message+'Delete res, article and free memory')
    log.write(message+'Re-create article')
    article = co.deque([])

    # free article reple
    res_reple = pd.concat(reples)
    res_reple.to_csv('./NaverNews_body_reple/NaverNews_Exist_reple_'+str(i)+'.csv', index=False, encoding='utf-8-sig')
    del res_reple, reples
    log.write(message+'Delete res, article and free memory')
    log.write(message+'Re-create article')
    reples = co.deque([])

# Enter the URL
def enter_URL(url):
  try:
    driver = wd.Chrome(executable_path=driverPath)
    driver.get(url)
  except:
    log.write(message+'error with get URL... maybe Blocked IP... sleep 10 sec...')
    time.sleep(600)
    log.write(message+'restart')
    driver = wd.Chrome(executable_path=driverPath)
    driver.get(url)

# Get data - Article body
def Get_Article_Body(i, article, url):
  body = driver.find_elements_by_xpath("//div[@id='articleBodyContents']")
  text = ""
  for b in body:
    text = text.join(b.text)

  # re-enter
  if text.strip(url) == '':
    log.write(message+'there`s no article... maybe loading error')
    driver.refresh()
    log.write(message+'refresh...')
    log.write(message+'sleep 5 sec...')
    time.sleep(5)
    log.write(message+'try again...')
    driver.get(url)
    body = driver.find_elements_by_xpath("//div[@id='articleBody']")
    text = ""
    for b in body:
      text = text.join(b.text)

    tmp_article = {
      'NaverURL':url,
      'body':text
    }

    article.append(tmp_article)


# Get data - Article reple
def Get_Ariticle_reple(i, reples):
  log.write(message+'sleep 2 sec for scrap reples...')
  time.sleep(2)

  noReple = False

  no_reple = pd.DataFrame({
    'repleID':[None],
    'reple':[None],
    'time':[None],
    're_reple':[None],
    'like':[None],
    'dislike':[None],
    'NaverURL':[None]
  })

  try:
    # Click more reple
    driver.find_elements_by_xpath("//span[@class='u_cbox_in_view_comment']")[0].click()
  except:
    noReple = True

  if noReple:
	tmp_reple = no_reple
  else:
    log.write(message+'there are some reples... refresh and sleep 2 sec to scrap all data...')
    driver.refresh()
    time.sleep(2)

    # 총 댓글 갯수 확인
    Nreple = driver.find_element_by_xpath("//span[@class='u_cbox_count']")
    Nreple = Nreple.text
    Nreple = int(Nreple.replace(',',''))
    print(Nreple)
    if Nreple > 20:
      NrepleLoop = (Nreple//20) if Nreple%20 > 0 else (Nreple//20)-1
      for i in range(NrepleLoop):
        driver.find_elements_by_xpath("//span[@class='u_cbox_page_more']")[0].click()
        time.sleep(1)
        
	# 댓글 수집
    reple = driver.find_elements_by_xpath('//div[@class="u_cbox_comment_box"]')
    if len(reple) > 0:            # 댓글이 적어도 하나라도 있는 경우: 클린봇 X, 삭제 X
      tmp_reple = co.deque([])
      for r in reple:
        tmp_list = r.text.split('\n')
        if len(tmp_list) == 11:
          tmp_reple.append(tmp_list)
      tmp_reple = pd.DataFrame(tmp_reple)
      if len(tmp_reple) == 0:     # 댓글 하나만 있는데 삭제되었을 때
        tmp_reple = no_reple
      else:                
        tmp_reple = tmp_reple[[0,3,4,5,8,10]].rename(columns={
          0:'repleID',
          3:'reple',
          4:'time',
          5:'re_reple',
          8:'like',
          10:'dislike'
        })
        tmp_reple['NaverURL'] = url
    else:
      tmp_reple = no_reple

  reples.append(tmp_reple)












article = co.deque([])
reples = co.deque([])

log = open('./NaverNews_log/log_'+datetime.strftime(datetime.now(), "%Y%m%d_%H%M%S")+'.txt', 'w')
log.write('start program: '+datetime.strftime(datetime.now(), "%H:%M:%S.%f"))
print('start program: '+datetime.strftime(datetime.now(), "%H:%M:%S.%f"))

start = 0
end = 1500
for iteration in zip(URLs['index'].iloc[start:end], URLs['link'].iloc[start:end]):
    i, url = iteration[0], iteration[1]
    message = '\n'+datetime.strftime(datetime.now(), "%H:%M")+'    '+str(i)+'th loop | '
    
    # Back-up
    backup(i,article, reple)
    
    #
    enter_URL(url)
    
    #
    Get_Article_Body(i, article, url)
    
    #
    Get_Ariticle_reple(i, reples)
    
    driver.quit()
    message2 = '\n'+datetime.strftime(datetime.now(), "%H:%M")+'    '+str(i)+'th loop | '
    log.write(message2+'NaverURL: '+url)
    print(message2+'NaverURL: '+url)

res_article = pd.DataFrame(article)
res_reple = pd.concat(reples)
res_article.to_csv('./NaverNews_body_reple/NaverNews_Exist_article.csv', index=False, encoding='utf-8-sig')
res_reple.to_csv('./NaverNews_body_reple/NaverNews_Exist_reple.csv', index=False, encoding='utf-8-sig')

log.write('\n end program: '+datetime.strftime(datetime.now(), "%H:%M:%S.%f"))
print('end program')

log.close()

 

 

 

 

 

 

 

 

728x90
반응형
Comments