들어가며
본격적으로 데이터분석을 시작하는 첫째날이다. 네이버 랭킹뉴스 페이지를 바탕으로 크롤링부터 배우게 될텐데 과연 어떤 원리와 방식으로 이루어질지 알아보자.
HTML 기초
교수님께서는 크롤링을 하기 전에 먼저 알아야하는 HTML태그를 몇가지 알려주셨다. 저번 학기 수업으로 접한 적이 있어서인지 배웠던 내용들이 하나둘 씩 생각났다. 태그의 종류는 정말 많지만 그 중 생각나는 몇가지만 정리해보면 다음과 같다.
`<div></div>` | 문단을 구분하는 태그 |
`<a></a>` | 하이퍼링크를 구련하기 위한 태그 |
`<p></p>` | 문단을 구분하는 태그 |
`<input></input>` | 입력값을 받는 태그(텍스트, 숫자, 버튼 등) |
`<ul>, </ul>` `<ol></ol>` | ul은 순서를 부여하지 않는 목록 태그 ol은 순서를 부여하는 목록 태그 |
`<li></li>` | ul이나 ol안에 나열되는 항목 태그 |
웹페이지를 구현하는 것이 아닌 구조를 이해하고 데이터 수집을 하는 것이 목적이기 때문에 HTML 부분은 빠르게 넘어갔다.
네이버 언론사별 랭킹뉴스 크롤링 및 데이터 시각화
이제 위에서 다룬 태그들을 활용해서 네이버 랭킹 뉴스를 크롤링 해보자. 이번엔 Jupyter Notebook이 아닌 Google Colab에서 진행됐다.
1. 패키지 설치 및 임포트
한국어 정보 처리를 위한 `konlpy`와 matplotlib 사용 시 한국어 지원을 위한 `koreanize-matplotlib`을 설치해준다.
!pip install konlpy
!pip install koreanize-matplotlib
그리고 크롤링에 필요한 각종 라이브러리를 불러온다.
from urllib.request import urlopen # 웹페이지를 여는데 사용
from bs4 import BeautifulSoup # HTML 및 XML 문서를 파싱하는 데 사용
import pandas as pd # 데이터프레임 및 데이터 조작 및 분석 기능 활용
import datetime # 현재 시간을 가져오는데 사용
from pytz import timezone # 우리나라 기준 서버시간을 가져오는데 사용
import warnings # 경고메시지를 발행하는 방법 제공
warnings.filterwarnings('ignore') # 콘솔에 출력할 모든 경고를 '무시'
2. 네이버 언론사별 랭킹뉴스 - 많이 본 뉴스 크롤링
먼저 네이버 랭킹뉴스 페이지를 확인한다.
각 언론사 마다 HTML구조가 어떻게 이뤄져 있는지 개발자 모드를 통해 확인해본다. 아래는 각 언론사 박스의 구조를 간소화해서 그림으로 표현한 것이다.
우리는 `언론사명`, `순위`, `기사제목`, `기사링크`, `수집일자` 데이터를 얻을 것이기 때문에 먼저 데이터프레임을 생성해준다.
# 1) 데이터 프레임 생성
data = pd.DataFrame(columns=['언론사명', '순위', '기사제목', '기사링크', '수집일자'])
해당 웹페이지의 주소를 사용하기 때문에 url 변수에 주소를 저장한다.
# 2) 언론사별 랭킹 뉴스 URL
url = 'https://news.naver.com/main/ranking/popularDay.naver'
그리고 `urlopen()`함수를 이용하여 위 주소의 http 응답 객체 즉, html을 받아온다.
# 3) url 접속하여 html 가져오기
html = urlopen(url)
불러온 http 응답 객체를 사용하면 태그를 찾아서 구조화된 형태로 변환해줄 것이다. `html.parser`는 html을 파싱해주는 프로그램이라고 이해하면 될 것 같다.
# 4) HTML 태그 파싱하여 변환
soup = BeautifulSoup(html, 'html.parser') # html 태그를 찾아서 변환, <html.parser : HTML Parse를 행하는 프로그램>
이제 위에서 가져온 html에서 추출하고자 하는 데이터의 태그를 찾아 리스트로 변환해줄 수 있다. 게시물 초반에 html구조에서 `div` 태그로 언론사별 랭킹뉴스가 박스처럼 나눠져 있는 것을 확인했다. 또한 태그 옆에 class 이름이 `rankingnews_box`였기 때문에 class이름도 함께 지정해줄 것이다.
soup의 `find_all()`메서드를 사용하여 매개변수에 찾을 태그 이름 `div`와 class이름을 딕셔너리 형태로 넣어준다. 같은 class이름의 `div`태그를 가진 언론사별 랭킹뉴스 박스가 여러개이기 때문에 '모두' 찾기 위해 `find_all()`메서드를 사용했다.
# 5) 네이버 랭킹뉴스 정보가 들어있는 <div>만 추출 -> rankingnews_box 가져오기
div = soup.find_all('div', {'class':'rankingnews_box'}) # class가 rankingnews_box인 div `모두` 찾아서 리스트 형태로 반환
현재 `div` 변수에는 각 언론사의 랭킹뉴스 정보가 list 형태로 들어있다. 즉 list 길이는 추출된 언론사의 개수라고 생각하면 된다. 이제 각 언론사별로 필요한 정보들을 반복문을 통해서 가져올 것이다. 아래 `for`문 코드는 길이가 길기 때문에 블록을 나눠서 설명할 것이다.
class이름이 `rankingnews_name` 인 `strong` 태그에 언론사 명이 담겨져 있기 때문에 `find()`메서드를 이용해서 추출해준다. 언론사 명은 각 언론사마다 하나이기 때문에 `find_all()`이 아닌 `find()`를 사용한다. 그리고 `strong`태그 안에 존재하는 값(언론사 명)을 `text`메서드를 이용해서 찾은 후 `press`에 담아줬다.
# 6) 네이버 랭킹뉴스 기사 제목, 언론사 등 데이터 크롤링
for index_div in range(len(div)):
# 언론사 추출
strong = div[index_div].find('strong', {'class':'rankingnews_name'}) # claass가 rankingnews_name인 strong `하나`씩만 찾아서 div
press = strong.text # <태그>값</태그> 사이에 존재하는 값을 찾아서 담아줌
하나의 언론사 안에 5개 이내의 기사가 있기 때문에 현재 언론사 랭킹 박스의 `div`태그에서 5개의 기사가 묶인 `ul`태그를 찾는다. class이름은 `rankingnews_list`로 지정한다. 찾은 `ul`태그 에서 등재된 기사의 개수만큼 반복문을 통해서 모두 찾아줄 것이다. 각 기사항목은 class이름이 없는 `li`태그로 이루어져 있으므로, `find_all()`을 이용해서 모두 찾아준다.
# 5개 순위 기사 정보 추출
ul = div[index_div].find_all('ul', {'class':'rankingnews_list'})
for index_r in range(len(ul)):
li = ul[index_r].find_all('li') # class가 없기 때문에 미지정
언론사 별로 기사 개수가 5개라는 보장이 없기 때문에 추출된 기사 개수만큼 반복해줄 것이다. `try-except`문을 이용해서 실행될 코드에 대한 예외처리를 `pass`해준다. `try`안에서는 각 기사 항목의 순위(rank), 기사제목(title), 기사링크(link)를 `find()`로 찾아서 저장했다. 수집일자는 `datetime`의 `now()`메서드를 사용하여 서울 시간 기준 수집된 시점의 일시를 저장해줬다. 이렇게 추출된 각 변수의 데이터들을 이용해서 데이터프레임 형태로 초기화 해주고 인덱스를 순위로 지정한 후 임시변수 `temp_df`에 저장한다. 마지막으로 `temp_df`를 맨 위에서 선언한 `data`에 `concat()`을 통해 붙여주고 인덱스 지정은 하지 않는다.
for index_l in range(len(li)): # `5`로 하드코딩X, 리스트길이로 가변적으로 지정
try: # 코드 시도
rank = li[index_l].find('em', {'class':'list_ranking_num'}).text
title = li[index_l].find('a').text
link = li[index_l].find('a').attrs['href'] # href에 할당된 링크주소 찾아서 저장
# 데이터 프레임에 담기
temp_df = pd.DataFrame({'언론사명' : press,
'순위' : rank,
'기사제목' : title,
'기사링크' : link,
'수집일자' : datetime.datetime.now(timezone('Asia/Seoul'))
}, index=['순위'])
data = pd.concat([data, temp_df], ignore_index=True)
except: # 위에서 발생한 예외상황 처리
pass
3. 크롤링한 데이터 저장하기
위에서 추출한 데이터를 `to_csv()`를 이용해서 저장한다. 한글 지원을 위해 `utf-8-sig`를 지정하고 인덱스는 따로 지정하지 않는다.
# 인덱스 False지정, utf-8-bom으로 자동 변환
data.to_csv('./네이버랭킹뉴스_많이본뉴스_20240718.csv', encoding='utf-8-sig', index=False)
4. 크롤링한 데이터 시각화
위에서 크롤링한 데이터를 시각화하기 위한 라이브러리를 불러온다.
import matplotlib.pyplot as plt # 그래프를 그리기 위함1
import seaborn as sns # 그래프를 그리기 위함2
import koreanize_matplotlib # matplotlib에서 한글 지원
import konlpy # 한국어 정보 처리(기사나 댓글의 단어)
from wordcloud import WordCloud # 워드클라우드 시각화
워드 클라우드를 위한 전처리
핵심단어가 모여있는 기사제목을 공백을 기준으로 텍스트 뭉치로 변환해준다.
# 기사제목을 텍스트 뭉치로 변환해서 워드 클라우드 시각화
text = ' '.join(title for title in data['기사제목'].astype(str))
text
워드 클라우드 시각화
`WorldCould()`를 이용해서 그래프 크기 지정 및 `배민도현체` 폰트를 지정해줬다. 실행 결과 `한동훈`이라는 단어가 기사에서 가장 많이 언급된 것을 확인할 수 있다.
font_path = '/content/drive/MyDrive/ABC부트캠프/BMDOHYEON_ttf.ttf'
wc = WordCloud(width=1000, height=700, font_path=font_path).generate(text)
plt.axis('off') # 그래프 형식 표시 여부
plt.imshow(wc, interpolation='bilinear') # bilinear: 이미지 확대/축소시 픽셀 간 색상 부드럽게 보간
plt.show()
5. 많이 본 뉴스와 댓글이 많은 뉴스 비교하기
지금까지 많이 본 뉴스를 크롤링하고 워드클라우드까지 확인해보았다. 이제 댓글이 많은 뉴스도 같은 과정을 거친 후 두 워드클라우드를 비교해보자.
댓글이 많은 뉴스 크롤링
HTML구조가 같으므로, 데이터프레임 변수 이름만 다르게 지정해서 크롤링을 진행한다.
# 1) 데이터 프레임 생성
re_data = pd.DataFrame(columns=['언론사명', '순위', '기사제목', '기사링크', '수집일자'])
# 2) 언론사별 랭킹 뉴스 URL (댓글이 많은 랭킹 뉴스)
url = 'https://news.naver.com/main/ranking/popularMemo.naver'
# 3) url 접속하여 html 가져오기
html = urlopen(url)
# 4) HTML 태그 파싱하여 변환
soup = BeautifulSoup(html, 'html.parser') # html 태그를 찾아서 변환, <html.parser : HTML Parse를 행하는 프로그램>
# 5) 네이버 랭킹뉴스 정보가 들어있는 <div>만 추출 -> rankingnews_box 가져오기
div = soup.find_all('div', {'class':'rankingnews_box'}) # class가 rankingnews_box인 div `모두` 찾아서 리스트 형태로 반환
# 6) 네이버 랭킹뉴스 기사 제목, 언론사 등 데이터 크롤링
for index_div in range(len(div)):
# 언론사 추출
strong = div[index_div].find('strong', {'class':'rankingnews_name'}) # claass가 rankingnews_name인 strong `하나`씩만 찾아서 div
press = strong.text # <태그>값</태그> 사이에 존재하는 값을 찾아서 담아줌
# 5개 순위 기사 정보 추출
ul = div[index_div].find_all('ul', {'class':'rankingnews_list'})
for index_r in range(len(ul)):
li = ul[index_r].find_all('li') # class가 없기 때문에 미지정
for index_l in range(len(li)): # `5`로 하드코딩X, 리스트길이로 가변적으로 지정
try: # 코드 시도
rank = li[index_l].find('em', {'class':'list_ranking_num'}).text
title = li[index_l].find('a').text
link = li[index_l].find('a').attrs['href'] # href에 할당된 링크주소 찾아서 저장
# 데이터 프레임에 담기
temp_df = pd.DataFrame({'언론사명':press,
'순위':rank,
'기사제목':title,
'기사링크':link,
'수집일자':datetime.datetime.now(timezone('Asia/Seoul'))
}, index=['순위'])
re_data = pd.concat([re_data, temp_df], ignore_index=True)
except: # 위에서 발생한 예외상황 처리
pass
re_data
댓글이 많은 뉴스 워드 클라우드를 위한 전처리
같은 방법으로 기사제목들을 공백을 기준으로 텍스트 뭉치로 변환해준다.
# 기사제목을 텍스트 뭉치로 변환해서 워드 클라우드 시각화
re_text = ' '.join(title for title in re_data['기사제목'].astype(str))
re_text
댓글이 많은 뉴스 워드 클라우드
마찬가지로 `WordCloud()`를 이용해서 이미지 크기, 폰트, 생성할 텍스트를 지정한다.
re_wc = WordCloud(width=1000, height=700, font_path=font_path).generate(re_text)
많이 본 뉴스와 댓글이 많은 뉴스 워드 클라우드 시각화
이제 두 주제의 워드클라우드를 동시에 띄워서 비교해보는 과정이다. 두개 다 가장 핫한 키워드가 `한동훈`이고, 그 이외에는 `공소 취소`, `이재명` 등 소소하게 차이가 나는 부분을 확인해볼 수 있다.
fig = plt.figure(figsize=(15,5)) # 도화지 같은 개념, (가로 15, 세로 5)
rows, cols = 1, 2 # 1행 2열 크기
ax1 = fig.add_subplot(rows, cols, 1) # fig 1번째 칸에 추가
ax1.imshow(wc, interpolation='bilinear') # bilinear: 이미지 확대/축소 시 픽셀 간 색상 부드럽게 보간
ax1.set_title('언론사별 많이 본 뉴스')
ax1.axis('off') # 그래프 형식 표시 여부
ax2 = fig.add_subplot(rows, cols, 2) # fig 2번째 칸에 추가
ax2.imshow(re_wc, interpolation='bilinear') # bilinear: 이미지 확대/축소 시 픽셀 간 색상 부드럽게 보간
ax2.set_title('언론사별 댓글이 많은 뉴스')
ax2.axis('off') # 그래프 형식 표시 여부
plt.show()
많이 본 뉴스와 댓글이 많은 뉴스 명사 TOP 10 비교
두 주제의 기사제목에서 가장 많이 언급된 명사 상위 10개를 추출해서 비교해보자.
# 주제별 기사제목 텍스트 변환
d_text = ' '.join(title for title in data['기사제목'].astype(str))
m_text = ' '.join(title for title in re_data['기사제목'].astype(str))
# 명사 단어 추출
# Okt, Komoran : sns같은 플랫폼에서 자유롭게 사용되는 문자 추출
komoran = konlpy.tag.Komoran()
d_nn = komoran.nouns(d_text)
m_nn = komoran.nouns(m_text)
# 데이터프레임으로 변환
d_word_df = pd.DataFrame({'word': d_nn})
m_word_df = pd.DataFrame({'word': m_nn})
# 단어 수 컬럼을 추가
d_word_df['count'] = d_word_df['word'].str.len()
m_word_df['count'] = m_word_df['word'].str.len()
# 글자 수가 2개 이상인 단어만 추출
d_word_df = d_word_df.query('count >= 2')
m_word_df = m_word_df.query('count >= 2')
# 단어의 빈도표 만들기
d_group_df = d_word_df.groupby('word', as_index=False).agg(n=('word', 'count')).sort_values('n', ascending=False)
m_group_df = m_word_df.groupby('word', as_index=False).agg(n=('word', 'count')).sort_values('n', ascending=False)
# 단어 빈도 막대 그래프
# 가장 많이 본 뉴스와 댓글이 많은 뉴스의 단어 빈도수 비교
# 차트를 나눠서 그림 1행 2열, 15x5 크기(subplot에 비해 과정 간소화)
fig, axes = plt.subplots(1, 2, figsize=(15, 5))
plt.suptitle('가장 많이 본 뉴스와 댓글이 많은 뉴스의 단어 빈도수 비교')
sns.barplot(data=d_group_df.head(10), y='word', x='n', ax=axes[0])
axes[0].set_title('가장 많이 본 뉴스')
sns.barplot(data=m_group_df.head(10), y='word', x='n', ax=axes[1])
axes[1].set_title('댓글이 가장 많은 뉴스')
plt.show()
마무리
크롤링이 처음이라 따라가는데 애를 먹었다. 복습을 하다가 이해가 안가는 부분이 있었는데, 기사 크롤링 부분에서 `ul`태그를 찾을 때 왜 `find()`를 쓰면 오류가 나는지 모르겠다. 어차피 `ul`은 하나라서 여러개를 찾을 필요가 없지 않나 싶은데, 다음 수업 때 교수님께 여쭤봐야겠다.
'ABC부트캠프 테크노트' 카테고리의 다른 글
[13일차] ABC부트캠프 : 유튜브 댓글 수집 및 시각화 (6) | 2024.07.22 |
---|---|
[12일차] ABC부트캠프 : ESG포럼 & 세미나2 (0) | 2024.07.19 |
[10일차] ABC부트캠프 : 데이터 집계 & 처리 미니프로젝트 (0) | 2024.07.17 |
[9일차] ABC부트캠프 : 파이썬 프로그래밍 데이터 집계 및 처리 심화 (0) | 2024.07.16 |
[8일차] ABC부트캠프 : 파이썬 프로그래밍 데이터 전처리 & 시각화 (2) | 2024.07.16 |