오늘 오소진 교수님께 ZOOM으로 크롤링 및 워드클라우드 시각화를 배웠다✌
항상 대면으로 수업하다가 온라인으로 수업을 해보니...
수업 중 코드가 돌아가지 않아 질문을 하고 싶어도 세종 반도 같이 딜레이가 된다는 생각에 도저히 질문🙋♀️을 할 수 없었다. 결국 우리 교육생들끼리 문제를 해결하려다 보니 전보다 더 많은 시간이 걸렸다😭
어찌저찌 수업을 따라갔고, 타슈 정류장 지도 시각화 때 배웠던 크롤링과 url 오픈으로 크롤링이 어려울 때 시도할 만한 크롤링 방식 그리고 워드 클라우드 시각화를 구현했다. 기술 블로그를 쓰면서 또 한 번 느끼지만 확실히 대면 수업 때보다 온라인 수업 내용이 복습하는 데 더 어려움이 있었다.
교육이 진행될수록 임포트하는 라이브러리가 켜켜이 쌓이는 느낌으로 늘어간다😆
from urllib.request import urlopen
from bs4 import BeautifulSoup
import pandas as pd
import datetime # 크롤링 기준 시간을 저장! 현재 내 시간을 가져옴
from pytz import 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")
# 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 수집 '헤럴드 경제'
#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)
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)
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()
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)
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 값으로 찾을 수 없다.
개발자 모드로 클릭할 때만 페이지 소스를 확인할 수 있도록 만든 사이트
#새로운 라이브러리를 사용해야 함
그럼 어떤 라이브러리를 사용해서 파싱해와야 할까🤔
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)
참고.
#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)
복습 중 한 가지 실수를 범했는데, 특수문자를 공백으로 치환하는 대신 아예 없애버리면 그 기사제목이 한 문자열이 되어버려서 워드클라우드 때 문장을 한 단위로 그리게 된다.
수업 때 그 부분을 정정하셨었는데 그렇게 하면 안 되는 이유를 복습을 통해 알게 될 줄이야🤗 역시 실수를 통해 더 정확히 배우게 된다는 걸 또 한 번 깨달았다. 아래는 마스크를 변경한 워드클라우드이다.