들어가며
오늘은 유큐브 댓글을 크롤링해서 시각화까지 해보는 시간이다. 그 전에 11일차 막바지에 주어진 과제를 리뷰하는 시간부터 가져보자.
네이버 연예 공감별 랭킹 뉴스 크롤링 및 시각화
- 순위, 공감종류, 기사제목, 기사링크, 기사내용, 공감수, 수집일자 데이터 수집
- 공감 6가지에서 30위까지 = 180건의 기사 정보 수집
저번시간과 같이 필요한 라이브러리를 불러온다.
from urllib.request import urlopen # 웹페이지를 여는데 사용
from bs4 import BeautifulSoup # HTML 및 XML 문서를 파싱하는 데 사용
import re # 정규표현식 사용
import pandas as pd # 데이터프레임 및 데이터 조작 및 분석 기능 활용
import datetime # 현재 시간을 가져오는데 사용
from pytz import timezone # 우리나라 기준 서버시간을 가져오는데 사용
import warnings # 경고메시지를 발행하는 방법 제공
warnings.filterwarnings('ignore') # 콘솔에 출력할 모든 경고를 '무시'
총 6가지의 공감유형을 `url_list`에 집어넣었다. 그리고 이 리스트의 길이(공감유형 개수)만큼 반복해서 유형별로 크롤링을 진행한다. 중간중간 `print()`를 통해 코드가 문제없이 작성이 됐는지 확인해본다.
# 1) 데이터 프레임 생성
data = pd.DataFrame(columns=['순위', '공감종류', '기사제목', '기사링크', '기사내용', '공감수', '수집일자'])
# 2) 네이버 연예 공감별 랭킹 뉴스 URL 준비
# https://entertain.naver.com/ranking/sympathy 좋아요
# https://entertain.naver.com/ranking/sympathy/cheer 응원해요
# https://entertain.naver.com/ranking/sympathy/congrats 축하해요
# https://entertain.naver.com/ranking/sympathy/expect 기대해요
# https://entertain.naver.com/ranking/sympathy/surprise 놀랐어요
# https://entertain.naver.com/ranking/sympathy/sad 슬퍼요
url_list = ['', '/cheer', '/congrats', '/expect', '/surprise', '/sad']
# 공감별 url 주소 조합하여 크롤링 시작
for n in range(len(url_list)):
# 기본 url
url = 'https://entertain.naver.com/ranking/sympathy'
url += url_list[n] # 기본url에 해당하는 공감유형 이어붙이기
# 공감유형 추출
sympathy = 'like' # 기본값 '좋아요'
if url_list[n] != '': # 해당 공감유형에 값이 존재하면
sympathy = url_list[n].replace('/', '') # '/'를 없앰(단어만 추출)
print(' 수집 중 ...', url, 'sympathy: ', sympathy)
# 3) url에서 html 가져오기
html = urlopen(url)
# 4) html 파싱할 수 있도록 변환
soup = BeautifulSoup(html, 'html.parser')
# 5) 네이버 공감별 랭킹 뉴스 정보가 있는 태그 가져오기 -> li class='_inc_news_lst3_rank_reply'
li = soup.find_all('li', {'class':'_inc_news_lst3_rank_reply'})
for index_l in range(len(li)):
# 순위
rank = li[index_l].find('em', {'class':'blind'}).text
# 뉴스 제목
title = li[index_l].find('a', {'class':'tit'}).text
# 뉴스 내용
summary = li[index_l].find('p', {'class':'summary'}).text
# 뉴스 링크
link = li[index_l].find('a', {'class':'tit'}).attrs['href']
# 공감수
temp_cnt = li[index_l].find('a', {'class':'likeitnews_item_likeit'}).text
# 숫자만 추출 (숫자가 아닌 값은 제거) -> "공감수1,789" -> 1789
# '^' : not을 의미
# [A-Za-z] : 대소문자 모든 알파벳
# [ㄱ-ㅎ] : 모든 한글
# [A-Za-zㄱ-ㅎ] : 연결해서 사용 가능
cnt = re.sub('[^0-9]', '', temp_cnt) # 숫자가 아닌 것들을 ''으로 대체
# 6) 데이터 프레임이 저장
temp_df = pd.DataFrame({'순위': rank,
'공감종류': sympathy,
'기사제목':title,
'기사링크':link,
'기사내용':summary,
'공감수':cnt,
'수집일자':datetime.datetime.now(timezone('Asia/Seoul'))},
index=['순위'])
data = pd.concat([data, temp_df], ignore_index=True)
print('Completes of', rank, ':', title)
print('-' * 50)
print(data.info())
수집된 데이터 프레임을 csv파일로 저장한다.
data.to_csv('네이버 연예 공감별 랭킹 뉴스 크롤링_20240722.csv', encoding='utf-8-sig', index=True)
네이버 연예 공감별 랭킹 뉴스 워드 클라우드 시각화
공감이 총 6가지이기 때문에 사용자로부터 하나의 입력을 받고 그 값에 따른 워드클라우드를 보여주도록 할 것이다.
# 입력할 공감종류 확인
data['공감종류'].unique()
`input_symphaty`에 입력을 받고 입력값에 해당하는 공감유형의 `기사내용`들을 텍스트뭉치로 만든다. 그리고 이 텍스트뭉치들을 워드클라우드로 시각화해준다.
import matplotlib.pyplot as plt
from wordcloud import WordCloud
# 사용자로부터 워드클라우드를 확인하고 싶은 공감종류를 입력받기
input_symphaty = input('보고 싶은 공감 생킹 뉴스 입력하세요 ' + str(data['공감종류'].unique()) + ':')
# 선택한 공감 뉴스 제목만 테스트로 만들기
text = ' '.join(li for li in data[data['공감종류']==input_symphaty].기사내용.astype(str))
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')
plt.show()
해리포터 유튜브 영상 댓글 크롤링 및 시각화
유튜브의 댓글은 스크롤을 내리면서 수집을 해야되기 때문에 정적사이트에서의 크롤링으로는 수집이 어렵다. 따라서 동적 사이트 방식에 맞는 크롤링을 `spyder` IDE환경에서 진행할 것이다.
동적 크롤링
`spyder`실행 후 console창에는 다음과 같이 필요한 패키지 설치하고 에디터에는 라이브러리를 불러온다.
pip install selenium # 웹애플리케이션 자동화 및 테스트 프레임워크
pip install webdriver-manager # 크롬브라우저 버전 자동 확인
pip install beautifulsoup4 # HTML 및 XML 문서를 파싱하는 데 사용
pip install wordcloud # 워드클라우드
from selenium import webdriver # 웹애플리케이션 테스트 자동화 도구
# Selenium을 사용하여 크롬브라우저를 자동화할 때 필요한 크롬 드라이버를 관리하는 데 사용
from webdriver_manager.chrome import ChromeDriverManager
# 크롬브라우저 서비스를 제어하기 위해 필요한 클래스
from selenium.webdriver.chrome.service import Service as ChromeService
from bs4 import BeautifulSoup as BS # HTML 및 XML 문서를 파싱하는 데 사용
import time # 시간 지연에 사용
import pandas as pd # 데이터프레임
import warnings # 경고메시지를 발행하는 방법 제공
warnings.filterwarnings('ignore') # 콘솔에 출력할 모든 경고를 '무시'
크롬브라우저의 각종 세팅값을 설정한다.
options = webdriver.ChromeOptions() # 크롬브라우저의 옵션을 설정하기 위한 객체 생성
options.add_argument('--no-sandbox') # 보안 기능인 샌드박스 비활성화
# dev/shm 디렉토리 사용 안함
# 메모리 공유 비활성화함으로써 컨테이너환경에서 문제를 피하기 위함
options.add_argument('--disable-dev-shm-usage')
# 크롬 드라이버 서비스 설정 및 설치
service = ChromeService(executable_path=ChromeDriverManager().install())
# WebDriver 객체 생성 및 Chrome 브라우저 실행
driver = webdriver.Chrome(service=service, options=options)
# 브라우저 창 크기 설정
driver.set_window_size(800, 800)
유튜브 댓글 페이지를 들어가보면 아래와 같이 스크롤을 내리는 과정에서 렌더링이 존재한다. 댓글을 모두 불러온 상태에서 최종적으로 수집을 하는 것이 목표이기 때문에 코드를 통해 스크롤을 끝까지 내려줄 것이다. 다만 여기서 주의할 점은 매크로처럼 짧은 간격으로 스크롤을 내리는 행위를 보일 경우 IP 접속차단을 야기할 수 있기 때문에 일정한 시간 간격을 두고 진행해야한다.
처음 화면 진입 시 유튜브 페이지의 모든 요소가 렌더링 될 때까지 10초정도 대기한다.
# 유튜브 영상 접속 https://youtu.be/Ww9FVHIWolk
driver.get('https://youtu.be/Ww9FVHIWolk')
driver.implicitly_wait(10) # 화면 렌더링 대기
# 사람인 척 하기 동적 이벤트 주기 -> 스크롤 내리기(js 명령어)
driver.execute_script('window.scrollTo(0,800)')
time.sleep(10)
# 댓글 수집을 위한 스크롤 내리기
last_height = driver.execute_script('return document.documentElement.scrollHeight')
이후 `while`문을 통해 스크롤이 모두 내려갈 때까지 6초 간격으로 내려준다.
while True:
print('스크롤 중...')
# 스크롤을 최대로 내리기
driver.execute_script('window.scrollTo(0,document.documentElement.scrollHeight)')
time.sleep(3)
# 스크롤 높이 갱신
new_height = driver.execute_script('return document.documentElement.scrollHeight')
if new_height == last_height: # 스크롤이 모두 내려갔을 경우 종료
break
last_height = new_height
time.sleep(3)
스크롤이 모두 내려갔으니 이제 모든 댓글을 수집할 차례이다. 태그 리스트를 가져올 때 `find`, `find_all` 말고 이번엔 `select`를 사용했다. 또한 댓글에 존재하는 `\n`, `\t`, `' '`문자들을 모두 제거해줬다. 데이터프레임에 저장하고, csv파일까지 내보내기가 완료되면 테스트에 사용된 브라우저는 종료된다.
# 댓글 크롤링
html_source = driver.page_source
soup = BS(html_source, 'html.parser')
# 댓글 태그 리스트 가져오기 '#'은 id, '-'는 class
# select : 모두, select_one : 하나
comment_list = soup.select('yt-attributed-string#content-text')
comment_final = []
# 대댓글은 제외
print('댓글 수', str(len(comment_list)))
# 댓글 텍스트 추출
for i in range(len(comment_list)):
temp_comment = comment_list[i].text
# '\n', '\t' 지우고 ' ' 지우기
temp_comment = temp_comment.replace('\n', '').replace('\t', '').strip()
print(temp_comment)
comment_final.append(temp_comment)
# 데이터프레임 만들고 저장
youtube_dic = {'댓글내용': comment_final}
youtube_df = pd.DataFrame(youtube_dic)
print('=='*30)
print('크롤링 종료...')
print('=='*30)
# 수집된 데이터 확인하기
print(youtube_df.info())
youtube_df.to_csv('03 유튜브 댓글 크롤링_20240722.csv', encoding='utf-8-sig', index=False)
print('=='*30)
print('파일 저장 완료...')
# 브라우저 닫기
driver.close()
csv파일을 열었을 때 댓글이 잘 수집된 것을 확인할 수 있다.
시각화
이제 오늘 수업 주제인 유튜브 영상 댓글 시각화를 시작해보도록 하자. 저번과 마찬가지로 `konlpy`와 `koreanize-matplotlib`를 설치하고 필요한 라이브러리도 불러온다.
import pandas as pd # 데이터프레임 및 데이터 조작 및 분석 기능 활용
import numpy as np # 이미지 마스킹에 사용
import konlpy # 단어 정보 처리에 사용
import matplotlib.pyplot as plt # 그래프 그리기1
import plotly.express as px # 완성형 보편적인 그래프 그리기
import koreanize_matplotlib # 그래프에서 한글지원
from PIL import Image # 이미지 읽기
# 워드클라우드, 컬러 이미지배열 값의 컬러값 가져오는데 사용
from wordcloud import WordCloud, ImageColorGenerator
1. 데이터 불러오기
192개의 댓글이 수집되었다. 대댓글은 포함이 되지 않은 수치이다.
youtube_df = pd.read_csv('/content/drive/MyDrive/ABC부트캠프/유튜브 댓글 크롤링_20240722.csv')
youtube_df
2. 단어 분석
`join`을 이용하여 공백기준으로 댓글내용들을 합친 후 `nouns`메서드를 이용하여 텍스트 뭉치들에서 명사를 분리해준다.
okt = konlpy.tag.Okt()
word_df = pd.DataFrame({'word':okt.nouns(' '.join(w for w in youtube_df['댓글내용'].astype(str)))})
그리고 만들어진 `word_df`에 `count`변수를 추가해서 댓글 내용마다 존재하는 각 단어 수를 집계한다.
word_df['count'] = word_df['word'].str.len()
word_df
빈도표 만들기
위에서는 댓글마다 동일한 단어 수를 집계했으므로 `브금`과 같이 같은 단어이지만 별개로 집계된 것들이 보인다. 우리는 모든 댓글을 기준으로 단어 수를 집계하고 싶기 때문에 `groupby`를 사용해서 각 단어별 집계값을 내림차순으로 정렬해줄 것이다. 이미지처럼 단어 별로 중복되지 않게 각 수치를 잘 보여주고 있는 것을 확인할 수 있다.
group_df = word_df.groupby('word', as_index=False).agg(n=('word', 'count')).sort_values('n', ascending=False)
group_df
단어 집계 결과 시각화
위에서 집계한 결과에서 상위 20개를 바탕으로 막대형 그래프를 그려보자. `text_auto`는 각 막대그래프 상단에 수치값을 표시하는 옵션이다.
px.bar(group_df.head(20), x='word', y='n', text_auto=True)
명사 빈도표를 활용한 워드 클라우드 시각화
group_df를 딕셔너리로 변환하여 word 열을 키로, n 열을 값으로 하는 딕셔너리 word_dic을 생성한다.
word_dic = group_df.set_index('word').to_dict()['n']
word_dic
워드 클라우드를 생성하는데 그 방법이 전과는 조금 다르다. `generate`는 텍스트 데이터를 입력으로 받고 각 단어의 빈도를 계산했지만, `generate_from_frequencies`는 딕셔너리로 입력을 받고 이미 계산된 단어 빈도를 기반으로 워드클라우드를 생성한다.
font_path = '/content/drive/MyDrive/ABC부트캠프/BMDOHYEON_ttf.ttf'
wc = WordCloud(width=1000, height=700, font_path=font_path).generate_from_frequencies(word_dic)
plt.axis('off')
plt.imshow(wc, interpolation='bilinear')
plt.show()
마스킹을 활용한 워드 클라우드 시각화
마스킹을 활용하여 워드클라우드를 만들어보자. 마스킹을 위해서 사용할 이미지를 불러오고 이 이미지를 배열 형태로 만들어주기 위해 `np.array()`를 적용한다. 그리고 이 배열 형태로 만들어진 `harry_mask`를 워드클라우드 `mask`옵션에 지정해주고, 시인성을 위해 배경색을 흰색으로 지정했다. 또한 `ImageColorGenerator`를 이용하여 이미지의 색상을 가져온 후, `recolor`메서드로 적용 시켜준다. 결과적으로 아래와 같이 마스킹된 워드클라우드를 확인할 수 있다.
icon = Image.open('/content/drive/MyDrive/ABC부트캠프/harry.png')
harry_mask = np.array(icon) # 배열 형태로 만들어 줌
plt.subplots(figsize=(10,10), dpi=200)
wc = WordCloud(width=1000, height=700, background_color='white', font_path=font_path, mask=harry_mask).generate_from_frequencies(word_dic)
plt.axis('off')
img_colors = ImageColorGenerator(harry_mask, default_color=(255,255,255))
wc = wc.recolor(color_func=img_colors)
plt.imshow(wc, interpolation='bilinear')
plt.show()
마무리
댓글 수집을 통해 그 영상에 대한 사람들의 생각을 분석할 수 있어서 신기했고, 웹브라우저 자동제어는 처음 써봤는데 이것도 신기했다. 그리고 교수님께서 말씀하신 것을 참고해서 반복된 요청으로 IP차단을 당하지 않게 심혈을 기울여서 코드를 작성해야겠다.
'ABC부트캠프 테크노트' 카테고리의 다른 글
[15일차] ABC부트캠프 : 구글 이미지 크롤링 및 데이터 분석 팀 프로젝트(1/2) (0) | 2024.07.24 |
---|---|
[14일차] ABC부트캠프 : 음악 정보 수집 및 시각화 (0) | 2024.07.23 |
[12일차] ABC부트캠프 : ESG포럼 & 세미나2 (0) | 2024.07.19 |
[11일차] ABC부트캠프 : 랭킹뉴스 크롤링 및 데이터 시각화 (0) | 2024.07.18 |
[10일차] ABC부트캠프 : 데이터 집계 & 처리 미니프로젝트 (0) | 2024.07.17 |