(220901) 네이버 랭킹뉴스 크롤링 및 워드클라우드 시각화

이은경·2022년 9월 1일
0

오늘 오소진 교수님께 ZOOM으로 크롤링 및 워드클라우드 시각화를 배웠다✌

항상 대면으로 수업하다가 온라인으로 수업을 해보니...

수업 중 코드가 돌아가지 않아 질문을 하고 싶어도 세종 반도 같이 딜레이가 된다는 생각에 도저히 질문🙋‍♀️을 할 수 없었다. 결국 우리 교육생들끼리 문제를 해결하려다 보니 전보다 더 많은 시간이 걸렸다😭

어찌저찌 수업을 따라갔고, 타슈 정류장 지도 시각화 때 배웠던 크롤링과 url 오픈으로 크롤링이 어려울 때 시도할 만한 크롤링 방식 그리고 워드 클라우드 시각화를 구현했다. 기술 블로그를 쓰면서 또 한 번 느끼지만 확실히 대면 수업 때보다 온라인 수업 내용이 복습하는 데 더 어려움이 있었다.



교육이 진행될수록 임포트하는 라이브러리가 켜켜이 쌓이는 느낌으로 늘어간다😆

from urllib.request import urlopen
from bs4 import BeautifulSoup

import pandas as pd
import datetime # 크롤링 기준 시간을 저장! 현재 내 시간을 가져옴
from pytz import timezone
  • 웹페이지의 HTML자료를 파싱해오기 위한 'BeautifulSoup'
  • 크롤링하는 기준 시간(그 시각)을 저장하는 'datetime'
  • 기준 일자 및 시간 저장 시, 서울 기준으로 바꿔주는 'timezone'
# 1) 데이터 프레임 생성
data = pd.DataFrame(columns=['언론사명', '순위', '기사제목', '기사링크', '수집일자'])

# 2) 네이버 랭킹뉴스 접속 주소 준비: 
url = 'https://news.naver.com/main/ranking/popularDay.naver'

# 3) url에서 HTML 가져오기
html= urlopen(url)

# 4) HTML을 파싱할 수 있는 Object로 변환
bsObject = BeautifulSoup(html, 'html.parser', from_encoding ="UTF-8")
  • 크롤링해서 자료를 담을 데이터 프레임을 컬럼명을 포함해 설정한다.
  • 크롤링할 주소를 url에 지정한다.
  • urlopen을 통해 페이지의 HTML을 가져온다.
  • UTF-8로 인코딩 되어 있는 HTML 자료를 파싱해온다.
# 5) 네이버 랭킹뉴스 정보가 있는 div만 가져오기(class를 페이지 소스에서 찾아서 입력)
     -> 12개의 div를 담을 것이라고 추정
  
div = bsObject.find_all('div',{'class', 'rankingnews_box'})

# 6) 네이버 랭킹뉴스 언론사 상세 정보 추출
for index_div in range(0, len(div)):
  #6-1) 언론사명 추출
  strong = div[index_div].find('strong', {'class', 'rankingnews_name'})
  press = strong.text #strong class 태그가 감싸고 있는 text 수집 '헤럴드 경제'  
  • 랭킹뉴스의 페이지 소스를 확인해보니 랭킹뉴스의 언론사가 12개이고 언론사별로 div가 나뉘어 있는 것을 확인했다.
  • 그 내용이 div 태그로 감싸져 있어 그 div의 class인 'rankingnews_box'로 모든 div를 가져왔다.
  • 해당 페이지에서 'rankingnews_box'를 class명으로 가진 모든 div가 있는 수 만큼 for문을 돌면서 strong태그의 class인 'rankingnews_name'을 가진 태그를 읽고 그 태그가 포함하고 있는 text(언론사명)를 press로 수집한다.
#6-2) 언론사별 랭킹 뉴스정보 추출
  ul = div[index_div].find_all('ul',{'class', 'rankingnews_list'})
  for index_r in range(0, len(ul)):
    li = ul[index_r].find_all('li')
    for index_l in range(0,len(li)):
      try: # 오류방지를 위한 예외구문1: 이 구문에서 사용하기 어려운 경우 except로 내려간다.
        # 순위
        rank = li[index_l].find('em', {'class', 'list_ranking_num'}).text 
               #text 안쓰면 태그 통째로 들어옴 
        # 뉴스 제목
        title = li[index_l].find('a').text
        # 뉴스 링크
        link = li[index_l].find('a').attrs['href']
        #7) dataframe 저장(append): 데이터 수집
        data = data.append({'언론사명': press, '순위': rank, '기사제목': title, 
                           '기사링크': link, '수집일자': datetime.datetime.now
                           (timezone('Asia/Seoul')).strftime('%Y-%m-%d %H:%M:%S')},
                           ignore_index=True) 
        
      except:# 오류방지를 위한 예외구문2
        pass # 오류방지를 위한 예외구문3 ignore 하고 for 문으로 돌아가
                                     (반면,오류나서 raise할 때는 break)

      print('Completes of' + rank + ':' + title)

print('--------------------------------------------------------')
print(data)
  • for문을 돌면서 rankingnews_list'라는 클래스명을 가진 ul(unordered list)을 모두 찾고 언론사별 상위 5개 뉴스 리스트가 담긴 목록 태그를 읽어온다.
  • 하위 for문을 돌면서 UL의 하위 항목인 5개의 Li태그에 담긴 자료를 읽는다.
  • li태그의 하위 태그인 'em'에서 랭킹 순위를 가져오고, a태그가 포함한 text, 즉 기사제목을 수집한 후 그 태그의 속성인 href를 활용해 기사 링크를 수집한다.
  • 그리고 마지막으로 해당 추출한 자료들을 맨 처음에 생성해놓은 df에 append한다.
  • 여기서 try와 except 구문은 오류가 발생할 시 except 구문으로 처리할 수 있게 조치하는 방법이다.

자료를 모두 수집한 후 data.info()와 data.head()로 정보를 확인하고 csv로 저장한다. 주의할 점은 UTF-8-sig로 저장해서 한글이 깨지지 않도록 하는 것😎

크롤링한 데이터를 이용해 이제 워드클라우드⛅를 그려보자.
import matplotlib.pyplot as plt
from wordcloud import WordCloud, STOPWORDS, ImageColorGenerator
  
day_text = " ".join(li for li in day_df.기사제목.astype(str))
day_df['기사제목'].replace('[^\w]', ' ', regex=True, inplace = True)
  • 워드클라우드를 그리기 위해 필요한 라이브러리 임포트
  • 각각의 기사제목을 join을 활용해 한 줄로 모두 합쳐준다. 이 때 기사제목 간 구분을 위해 공백을 포함한다.
  • 보통 기사제목에 포함된 특수기호(',,)를 삭제하기 위해 정규식 문법을 이용해 특수기호를 공백으로 치환; 제목 가독성 유지 위함
plt.subplots(figsize=(15,15))
wordcloud = WordCloud(background_color='white', width = 1500,
                      height=1500, font_path=fontpath).generate(day_text)
plt.axis('off')
plt.imshow(wordcloud, interpolation='bilinear')
plt.show()
  • 보통 plot차트 그릴 때 x와 y축을 설정하지만 여기서는 off로 설정한다. interpolation을 bilinear로 준 것은 '1차원에서의 선형 보간법을 2차원으로 확장'한 것이라는데...(생략)
  • 결과물은 아래와 같다. 가장 많이 본 뉴스의 기사제목을 활용해 순서대로 눈에 띄는 밝은 색의 두꺼운 큰 글자부터 점차 옅고 작은 글자로 그려진다. 이 때 색 설정은 시스템에서 자동으로 처리한다.
import numpy as np
from PIL import Image #PIL : Python Imaging Library

mask = Image.open('/content/sphx_glr_masked_002.png')
mask = np.array(mask)
  • 기본은 위와 같이 사각형의 모습인데 해당 모양을 바꾸려면 위와 같이 두 라이브러리를 임포트 한 후 mask를 설정해주고, 워드클라우드 실행 시 아래와 같이 mask 항목을 포함해준다.
wordcloud = WordCloud(background_color='white', width = 1500,
            height=1500, mask = mask, font_path=fontpath).generate(day_text)

  • 이렇게 이미지 모양대로 워드 클라우드를 구현해준다.

위처럼 url만 붙여서 파싱해온 데이터가 모든 정보를 포함하고 있으면 얼마나 좋을까. 역시나 교수님께서 말씀하신 것처럼 크롤링을 막아놓은 웹사이트가 있었다.

바로 네이버 랭킹뉴스 연예부문이었다.

이 쉽지 않은 크롤링을 해내기 위해서 교수님께서 일전에 말씀하셨던 사람이 직접 크롬을 열어 데이터를 수집하는 것처럼 "보이게 하는" 코드를 실습했다😁

그럼 파싱해 온 데이터가 모든 자료를 포함하고 있는지 어떻게 알 수 있을까.

지금까지 파싱해 온 데이터에서 div, ul, li, a태그에 적용된 class name을 기준으로 삼아 자료를 읽고 필요한 부분을 수집했었다. 하지만 신기하게도 일부 웹사이트는 그 class name을 의도적으로 닫아놓음으로써 자동으로 자료를 수집하지 못하게 처리해놓았다.

  • F12키로 확인한 개발자 화면
    분명 각 순위별 기사 목록을 가진 ul의 class name을 확인할 수 있다.

  • 우클릭으로 확인한 페이지 소스
    하지만 이 화면에서는 ul의 class name이 보이지 않는다.

그래도 기존대로 파싱을 진행해보자. 될지도 모르니까요(?)

# 1) 데이터 프레임 생성
data = pd.DataFrame(columns=['언론사명', '순위', '기사제목', '기사링크', '수집일자'])

# 2) 네이버 랭킹 연예뉴스 접속 주소 준비: https://entertain.naver.com/ranking
url = 'https://entertain.naver.com/ranking'

# 3) url에서 HTML 가져오기
html= urlopen(url)

# print(html) #크롤링 방지를 위해 막히지 않았는지 확인 

# 4) HTML을 파싱할 수 있는 Object르 변환
bsObject = BeautifulSoup(html, 'html.parser', from_encoding ="UTF-8")

print(bsObject)

#아래 결과값 확인해보면 ul이 의도적으로 닫혀있어 li 값으로 찾을 수 없다. 
 개발자 모드로 클릭할 때만 페이지 소스를 확인할 수 있도록 만든 사이트
  
#새로운 라이브러리를 사용해야 함
  • 위 코드로 파싱해온 페이지 소스
    두둥(!) 마찬가지로 이 화면에서도 ul의 class name이 보이지 않는다.😣

그럼 어떤 라이브러리를 사용해서 파싱해와야 할까🤔

from selenium import webdriver

이 셀레니움 웹드라이버를 활용하는 것이다. 물론 1도 모르지만..ㅎ 오소진 교수님께서 친절하게 다 알려주심😊 아래의 코드를 작성하면 컴퓨터가 아닌 사람이 조작하는 것처럼 크롤링이 가능하다.

#이 부분은 처음 한번만 실행하면 된다. 사람인척 홈페이지를 클릭하여 수집하는 프로그램
!pip install selenium
!apt-get update
!apt install chromium-chromedriver
!cp /usr/lib/chromium-browser/chromedriver /usr/bin
#1) 데이터 프레임 준비
data=pd.DataFrame(columns=['순위', '기사제목', '기사링크', '기사내용기사링크', '수집일자'])

options = webdriver.ChromeOptions()
options.add_argument('--headless')        # Head-less 설정
options.add_argument('--no-sandbox')
options.add_argument('--disable-dev-shm-usage')

driver = webdriver.Chrome('chromedriver', options=options) # 크롬을 열면서 아래 주소로 접속함
driver.get("https://entertain.naver.com/ranking")
driver.implicitly_wait(3) # 바로 움직이면 컴퓨터로 인식하니까 잠시 멈춤

time.sleep(1.5)

driver.execute_script('window.scrollTo(0,800)')
time.sleep(3)

html_source = driver.page_source 
#봤던 화면의 소스를 가져와 종전에는 주소를 읽었는데 이제는 사람처럼 수집.
soup = BeautifulSoup(html_source, 'html.parser')

print(soup)
  • url open방식이 아닌 크롬으로 웹페이지를 들어가고 접속한 화면의 소스를 가져온다. 파싱한 정보를 확인해보면 아래와 같이 ID나 CLASS name이 수집된 것을 알 수 있다.
  • 보통 class name은 유일무이하지 않은 경우가 많아 정확한 자료 수집을 위해선 아래와 같이 ID name을 사용하는 것이 좋다. 또한 기존처럼 find 모듈이 아닌 select 모듈을 사용했다.
  • 특정 객체의 태그를 반환하고 싶을 때, find는 반복적으로 코드를 작성하는 것에 반해, select는 하위경로를 직접 설정하여 편리하다는 장점이 있다.

    참고.
    #find의 경우, soup.find('div').find('p') /
    #select의 경우, soup.select_one('div > p')
    출처: https://desarraigado.tistory.com/14

li= soup.select('ul#ranking_list > li') #ID 태그 사용 경우
# li= soup.select('ul.news_lst news_lst3 rank_news > li') #class 태그 사용 경우
# class로 CSS가 정의되어 있으면 .class name으로 선언되어 있음 
  / class는 다른 태그도 중복 사용하는 경우가 있다.(템플릿에 적용되는 경우 많음)
# ID로 CSS가 정의되어 있으면 # idname으로 선언되어 있음 
  / ID는 유니크하게 사용하는 것 따라서 ID로 찾는 것이 낫다.
# "div > ul > li" find함수와 다르게 select 태그만 가능 
  즉, 상위태그 > 자식태그> 그 자식의 태그 리스트를 for문을 쓰지 않고 가져올 수 있음

for index_l in range(0,len(li)):
  try:
    #순위
    rank = li[index_l].find('em',{'class', 'blind'}).text.replace('\n','').replace('\t','').strip() 
    #뉴스 제목
    title = li[index_l].find('a',{'class', 'tit'}).text.replace('\n','').replace('\t','').strip() 
    #뉴스 내용
    summary = li[index_l].find('p',{'class', 'summary'}).text.replace('\n','').replace('\t','').strip() 
    #\n은 줄바꿈, \t은 탭, 그리고 공백을 삭제    
    #뉴스 링크
    link = li[index_l].find('a').attrs['href']
    #dataframe 저장(append)
    data = data.append({'순위': rank, '기사제목': title, '기사링크': 'https://entertain.naver.com'+link, 
    '기사내용': summary, '수집일자' : datetime.datetime.now(timezone('Asia/Seoul')).strftime
    ('%Y-%m-%d %H:%M:%S')}, ignore_index=True) 
    
  except:# 오류방지를 위한 예외구문1
    pass # 오류방지를 위한 예외구문2 ignore 하고 for 문으로 돌아가(오류나서 raise할 때는 break)

  print('Completes of' + rank + ':' + title)

print('----------------------------------')
print(data)
  • 이후 파일 저장하여 모든 기사제목을 또는 기사내용의 특수문자를 공백으로 치환하고 한 문자열로 만들어 워드 클라우드 시각화가 가능하다.

복습 중 한 가지 실수를 범했는데, 특수문자를 공백으로 치환하는 대신 아예 없애버리면 그 기사제목이 한 문자열이 되어버려서 워드클라우드 때 문장을 한 단위로 그리게 된다.

수업 때 그 부분을 정정하셨었는데 그렇게 하면 안 되는 이유를 복습을 통해 알게 될 줄이야🤗 역시 실수를 통해 더 정확히 배우게 된다는 걸 또 한 번 깨달았다. 아래는 마스크를 변경한 워드클라우드이다.

0개의 댓글