프로그래밍/웹크롤링 & 텍스트 데이터 분석

파이썬을 이용한 네이버 뉴스 스크래핑 (1)

못난명서 2023. 2. 5. 15:42

안녕하세요?

오늘은 오랜만에 파이썬 웹스크래핑을 해보려고 합니다.

https://audqjawns.tistory.com/7

 

파이썬 웹크롤링 기초 복습 (1)

안녕하세요? 오늘은 구름 인공지능 교육에서 배운 파이썬 웹크롤링 & 텍스트 데이터 분석 활동을 복습해보는 시간을 갖도록 하겠습니다. 시작하기에 앞서, 먼저 단어의 뜻을 명확히 하고 가겠습

audqjawns.tistory.com

 

저번시간에 배웠던 기초적인 웹스크래핑 방법들을 적용하여 네이버에 '인공지능'을 검색했을 때 나오는 뉴스들을 스크래핑 해보도록 하겠습니다.

오늘 최종적으로 사용할 코드는 다음과 같습니다.

#!pip install beautifulsoup4==4.9.3
import requests 
from bs4 import BeautifulSoup
import pandas as pd
from datetime import datetime 
import time 

titles = []
dates = []
articles = []
article_urls = []
press_companies = []

query = '인공지능'
url = "https://search.naver.com/search.naver?where=news&query=" + query
web = requests.get(url).content 
source = BeautifulSoup(web, 'html.parser')

# 1) 네이버 뉴스만 추려내기
urls_list = []
for urls in source.find_all('a', {'class' : "info"}):
    if urls["href"].startswith("https://n.news.naver.com"):
        urls_list.append(urls["href"])

for url in urls_list:
    try:
        headers = {'User-Agent':'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36'}
        web_news = requests.get(url, headers=headers).content
        source_news = BeautifulSoup(web_news, 'html.parser')

        # 2) 기사 제목 
        title = source_news.find('h2', {'class' : 'media_end_head_headline'}).get_text()
        print('Processing article : {}'.format(title))

        # 3) 기사 날짜
        date = source_news.find('span', {'class' : 'media_end_head_info_datestamp_time'}).get_text()

        # 4) 기사 본문
        article = source_news.find('div', {'id' : 'dic_area'}).get_text()
        article = article.replace("\n", "")
        article = article.replace("// flash 오류를 우회하기 위한 함수 추가function _flash_removeCallback() {}", "")
        article = article.replace("동영상 뉴스       ", "")
        article = article.replace("동영상 뉴스", "")
        article = article.strip()
        
        # 5) 기사 발행 언론사
        press_company = source_news.find('em', {'class':'media_end_linked_more_point'}).get_text()
        
        titles.append(title)
        dates.append(date)
        articles.append(article)
        press_companies.append(press_company)
        article_urls.append(url) # 6) 기사 URL 
    
    except:
        print('*** 다음 링크의 뉴스를 크롤링하는 중 에러가 발생했습니다 : {}'.format(url))
        

article_df = pd.DataFrame({'Title':titles, 
                           'Date':dates, 
                           'Article':articles, 
                           'URL':article_urls, 
                           'PressCompany':press_companies})

article_df.to_excel('result_{}.xlsx'.format(datetime.now().strftime('%y%m%d_%H%M')), index=False, encoding='utf-8')
article_df.head()

 

이제 코드를 하나하나 자세히 살펴보도록 할까요?


첫번째로 살펴볼 것은 request입니다. 

저번시간까지는 저희가 웹스크래핑을 할때는 스크래핑을 하고자하는 url을 따와 HTTP의 response를 얻기 위해 urlopen()함수를 사용했었습니다. 

그런데 이 urlopen은 적용하려는 url에 한글이 있으면 이 한글에 대한 아스키코드를 못받아 들여 오류가 발생하게 됩니다. (오늘 우리가 처음 활용할 url 또한  네이버에 '인공지능'을 검색했을 때 나오는 뉴스 페이지이기 때문에 url 끝에 query부분에 한글이 나옵니다.) 

그래서 저희는 앞으로 url에 한글이 들어가도 HTTP 응답을 잘 받아와주는 request.get(url).content 를 자주 사용하게 될 것입니다.

import requests
query = '인공지능'
url = "https://search.naver.com/search.naver?where=news&query=" + query
web = requests.get(url).content
source = BeautifulSoup(web, 'html.parser')

# 원래
# import urllib.request import urlopen
# web = urlopen(url) -> url 마지막에 들어있는 query 부분에 한글 '인공지능'이 들어가있어 해석X

# print(web)을 하시면 http형식으로 바뀐 url을 확인할 수 있고
# print(source)를 하시면 해당 페이지의 html을 전부 확인 할 수 있는데, 담고있는 텍스트가 너무 많아 버벅일 수 도 있습니다.
 

다음으로 해볼 것은 네이버에 '인공지능'을 검색한 뒤 나오는 뉴스들의 제목만 뽑아내어 리스트로 저장해보고, 해당 뉴스의 url을 뽑아와 보겠습니다.

# 뉴스 제목만 꺼내와 리스트로 만들기 
news_subjects = source.find_all('a', {'class' : 'news_tit'}) # ResultSet (리스트와 유사한 형태)

subject_list = []

for subject in news_subjects:
    subject_list.append(subject.get_text())

print(subject_list)
["서울시교육청-KT, '인공지능 전문인재 양성' 위해 맞손", "챗GPT 돌풍, 손안에 들어온 인공지능…'사람같은 AI'는 30년뒤에", "지구 밖 나간 첫 인공지능…두달 뒤 '여기서' 움직인다", "책 '인공지능 메타버스 시대 미래전략', 명품도서 인증 대상 수상", '인공지능 설명하는 MIT-IBM 왓슨 AI 연구소 소속 연구원', '무엇이든 답하는 챗GPT? 인공지능 시대, 무엇을 대비해야 할까?', '美 유명 잡지 기사 알고보니 AI(인공지능)가 작성…주가도 급등', '초거대 인공지능 답변에 세계가 열광…“대혁명 시작됐다” [AI 챗봇 신드롬]', '‘빈틈’ 찾기 소용 없는 인공지능 심판 전성시대 [광화문에서/황규인]', '인공지능·모빌리티 양 날개...혁신과 변화로 시민 행복 증진']

 

news_subjects[0] # 첫번째 뉴스의 'news_tit'class가 들어간 a태그 전부임을 확인
urls = news_subjects
first_article = urls[0]
first_article.attrs # tag's attributes (== attrs)
{'href': 'https://www.lawtimes.co.kr/Legal-News/Legal-News-View?serial=185062',
 'class': ['news_tit'],
 'target': '_blank',
 'onclick': "return goOtherCR(this, 'a=nws*f.tit&r=1&i=880000CD_000000000000000000073642&g=5094.0000073642&u='+urlencode(this.href));",
 'title': '데이터 계약의 유형'}

 

# 각각의 a태그-new_tit에 접근해서 attrs를 꺼내고 그중 href를 꺼낸다 
for urls in source.find_all('a', {'class' : "news_tit"}):
    print(urls.attrs['href'])
https://www.news1.kr/articles/4942668
https://www.yna.co.kr/view/AKR20230204005100072?input=1195m
https://www.khan.co.kr/science/aerospace/article/202302050800001
https://www.aitimes.com/news/articleView.html?idxno=149273
https://www.yna.co.kr/view/PYH20230205004800072?input=1196m
https://www.mhns.co.kr/news/articleView.html?idxno=547360
https://nownews.seoul.co.kr/news/newsView.php?id=20230204601009&wlog_tag3=naver
...

이렇게 네이버에 '인공지능'을 검색하고 해당 뉴스들의 url을 따오고 보았더니 한가지 문제점이 보입니다.

바로 상위 5개의 뉴스 기사들만 들어가봐도 다 각각 다른 신문사의 기사들인 것입니다.

여기서 생기는 문제점이 무엇이냐면, 우리가 궁극적으로 해보려는 것은 '인공지능'을 검색했을 때 나오는 뉴스들을 스크래핑하려는 것인데 이 뉴스들이 각각 다른 신문사의 기사이다보니 기사들이 제각각 다른 html 구조를 가지고 있고, 때문에 언론사별로 웹스크래핑 코드를 따로 만들어서 프로그램을 돌려야 하는 상황에 놓여지게 된 것입니다. (언론사별로 만든다음 if 연합뉴스~ 면 연합뉴스 코드로 넘어가는~~)

결국 모든 언론사들의 웹스크래핑 코드를 만들어서 돌리는 것은 불가능하기에 메이저 언론사들의 코드들만 만들어서 돌려야하는데 그러면 몇몇 마이너한 언론사의 글들은 어쩔 수 없이 포기를 해야하는 아쉬운 상황이 오게 됩니다. (웹스크래핑을 완벽하게 하는 것은 거의 불가능하기에 항상 선택과 집중이 필요합니다.)

그런데 네이버에는 이 상황을 해결할 해결책이 존재합니다. 바로 네이버에 등장하는 거의 대부분의 뉴스들이 네이버 뉴스에 따로 옮겨져 있기 때문에 이 네이버뉴스를 활용하는 것입니다.

저기를 크롬개발자도구로 html을 확인해보면 'info' class 임을 확인할 수 있는데 

for urls in source.find_all('a', {'class' : 'info'}):
    print(urls.attrs['href'])
http://news1.kr/
https://n.news.naver.com/mnews/article/421/0006612246?sid=102
https://www.yna.co.kr/
https://n.news.naver.com/mnews/article/001/0013737516?sid=105
http://www.khan.co.kr/
https://n.news.naver.com/mnews/article/032/0003202955?sid=105
http://aitimes.co.kr
https://www.yna.co.kr/
https://n.news.naver.com/mnews/article/001/0013737784?sid=105
http://www.munhwanews.com
...

실제로 코드를 확인해보면 네이버뉴스 말고 다른 홈페이지도 딸려오는 것을 확인할 수 있습니다.

이런 상황에 놓이게되면 왜그런지하고 직접 html에 ctrl F 돌려서 info나 다른홈페이지 눌러서 찾아봐야 하는데

실제로 네이버뉴스가 아닌 언론사 이름을 선택해서 보면 'info press'라는 class가 숨어있는 것을 확인해볼 수 있습니다.

for urls in source.find_all('a', {'class' : 'info'}):
    print(urls.attrs['class'])
['info', 'press']
['info']
['info', 'press']
['info']
['info', 'press']
['info']
['info', 'press']
...

이렇게 되면 네이버뉴스 만을 추려볼 해결방법이 2가지가 존재하는데

한가지는 info가 들어가는 class중 원소가 1개인 것만 출력하는 방법, 그리고 한가지는 url이 'https://n.news.naver.com'으로 시작하는 것만 골라서 출력하는 방법입니다.

전자같이 class의 개수에만 의존하는 경우에는 정확하지 않을 수 있으니 후자를 이용하도록 하겠습니다.

# for urls in source.find_all('a', {'class' : 'info'}):
#     if len(urls.attrs['class']) == 1 :
#         print(urls.attrs['href'])

urls_list = []
for urls in source.find_all('a', {'class' : "info"}):
    if urls.attrs["href"].startswith("https://n.news.naver.com"):  # starts with ~~~
        urls_list.append(urls.attrs["href"])
urls_list
['https://n.news.naver.com/mnews/article/421/0006612246?sid=102',
 'https://n.news.naver.com/mnews/article/001/0013737516?sid=105',
 'https://n.news.naver.com/mnews/article/032/0003202955?sid=105',
 'https://n.news.naver.com/mnews/article/001/0013737784?sid=105',
 'https://n.news.naver.com/mnews/article/081/0003336940?sid=104',
 'https://n.news.naver.com/mnews/article/022/0003780184?sid=101',
 'https://n.news.naver.com/mnews/article/020/0003477589?sid=110',
 'https://n.news.naver.com/mnews/article/052/0001845526?sid=102']

이제 동일한 html 구조를 가진 네이버 뉴스들의 url을 모아봤으니 이제 저번시간에 배운 내용들을 바탕으로 각 뉴스 페이지에 들어가 뉴스마다 제목, 본문, 언론사 정보, url등을 스크래핑 할 수 있게 되었습니다.

그런데 이때 모아둔 url을 그냥 바로 requests 함수를 실행하여 해당 뉴스에 접근하게 되면 오류가 생깁니다.

HTTP를 주고 받는 작업에는 요청하는쪽의 header정보를 주는쪽의 header라는게 존재하는데, 네이버는 파이썬코드로 직접 url을 접근하면 우리를 Bot으로 인식하여 접속을 차단시켜버리기 때문입니다. 따라서 아래와 같이 코드를 수정하여 우리 코드가 크롬브라우저에서 보내는 요청으로 인식하도록 HTTP Request에 Header 정보를 추가해주면 됩니다.

(headers값은 크롬 개발자 도구 -> Network -> request headers -> user agent에서 볼 수 있음)

# 아래코드는 오류 발생!!
# web_news = requests.get(urls_list[0]).content
# source_news = BeautifulSoup(web_news, 'html.parser')

headers = {'User-Agent':'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36'}

web_news = requests.get(urls_list[0], headers=headers).content 
source_news = BeautifulSoup(web_news, 'html.parser')

이제 뉴스들의 데이터를 한번 모아볼까요?

기사의 데이터들을 스크래핑 해오는 과정들은 크롬 개발자도구로 html의 위치를 빠르게 확인할 수도 있고,  전시간에 자세히 했었기 때문에 자세한 내용은 생략하도록 하겠습니다.

# 각 기사들의 데이터를 종류별로 나눠담을 리스트를 생성합니다. (추후 DataFrame으로 모을 예정)
titles = []
dates = []
articles = []
article_urls = []
press_companies = []

query = '인공지능'
url = "https://search.naver.com/search.naver?where=news&query=" + query
web = requests.get(url).content
source = BeautifulSoup(web, 'html.parser')

# 1) 네이버 뉴스만 추려내기
urls_list = []
for urls in source.find_all('a', {'class' : "info"}):
    if urls["href"].startswith("https://n.news.naver.com"):
        urls_list.append(urls["href"])

for url in urls_list:
    headers = {'User-Agent':'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36'}
    web_news = requests.get(url, headers=headers).content
    source_news = BeautifulSoup(web_news, 'html.parser')

    # 2) 기사 제목 
    title = source_news.find('h2', {'class' : 'media_end_head_headline'}).get_text()
    titles.append(title)
    print('Processing article : {}'.format(title))
    
    # 3) 기사 날짜
    date = source_news.find('span', {'class' : 'media_end_head_info_datestamp_time'}).get_text()
    dates.append(date)

    # 4) 기사 본문
    article = source_news.find('div', {'id' : 'dic_area'}).get_text()
    article = article.replace("\n", "")
    article = article.replace("// flash 오류를 우회하기 위한 함수 추가function _flash_removeCallback() {}", "")
    article = article.replace("동영상 뉴스       ", "")
    article = article.replace("동영상 뉴스", "")
    article = article.strip()
    articles.append(article)
    
    # 5) 기사 URL 
    article_urls.append(url)
    
    # 6) 기사 발행 언론사
    press_company = source_news.find('em', {'class':'media_end_linked_more_point'}).get_text()
    press_companies.append(press_company)

# 데이터프레임으로 데이터 모아주기
article_df = pd.DataFrame({'Title':titles, 
                           'Date':dates, 
                           'Article':articles, 
                           'URL':article_urls, 
                           'PressCompany':press_companies})

# 데이터프레임 엑셀 파일로 저장
article_df.to_excel('result_{}.xlsx'.format(datetime.now().strftime('%y%m%d_%H%M')), index=False, encoding='utf-8')
article_df.head()

 

마지막에 리시트로 데이터프레임을 만들때는 각 열의 길이가 전부 동일해야합니다!!! 하나라도 안맞으면 에러가 터집니다.

그리고 맨 마지막에 datetime 함수를 이용하여 파일명을 result_연도월일_시분.xlsx 로 지정해줍니다. (코드를 실행한 시간을 기준으로 파일 저장)

datetime 함수는 날짜, 시간과 관련된 연산에 도움을 주는데 

print(datetime.now())
print(datetime.now().strftime('%y%m%d_%H%M')) # string format time

을 실행하면 코드를 실행한 현재 시간을 출력할 수 있습니다.

2023-02-05 16:31:57.117643
230205_1631

이런 식으로 파일명을 저장시켜주는 이유는 나중에 코드를 자동화시켜 1시간마다 업데이트 시키면 파일명을 같게 해 놓았을때 계속 파일이 덮어씌우기가 되어 이전 파일에 대한 데이터들이 다 사라지게 되기 때문입니다.


이제 마지막으로 코드를 정리해보고 마무리하는 시간을 갖도록 하겠습니다.

지금까지 해온 코드에 try, except 함수를 추가해주어 웹스크래핑을 하다가 에러가 발생했을 때에도 멈추지 않고 그 부분은 건너뛰고 계속 스크래핑을 할 수 있게끔 해주도록 하겠습니다.

감사합니다.

#!pip install beautifulsoup4==4.9.3
import requests # urlopen을 대신하는 requests 
from bs4 import BeautifulSoup
import pandas as pd
from datetime import datetime # 날짜, 시간과 관련된 연산에 도움
import time # 코드를 중간중간에 멈추는 것과 관련된 라이브러리
import re

titles = []
dates = []
articles = []
article_urls = []
press_companies = []

query = '인공지능'
url = "https://search.naver.com/search.naver?where=news&query=" + query
web = requests.get(url).content # 원래쓰던 urlopen은 오류가 터짐(아스키코드에서) -> 한글을 못받아 들여서
source = BeautifulSoup(web, 'html.parser')

# 1) 네이버 뉴스만 추려내기
urls_list = []
for urls in source.find_all('a', {'class' : "info"}):
    if urls["href"].startswith("https://n.news.naver.com"):
        urls_list.append(urls["href"])

for url in urls_list:
    try:
        headers = {'User-Agent':'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36'}
        web_news = requests.get(url, headers=headers).content
        source_news = BeautifulSoup(web_news, 'html.parser')

        # 2) 기사 제목 
        title = source_news.find('h2', {'class' : 'media_end_head_headline'}).get_text()
        print('Processing article : {}'.format(title))

        # 3) 기사 날짜
        date = source_news.find('span', {'class' : 'media_end_head_info_datestamp_time'}).get_text()

        # 4) 기사 본문
        article = source_news.find('div', {'id' : 'dic_area'}).get_text()
        article = article.replace("\n", "")
        article = article.replace("// flash 오류를 우회하기 위한 함수 추가function _flash_removeCallback() {}", "")
        article = article.replace("동영상 뉴스       ", "")
        article = article.replace("동영상 뉴스", "")
        article = article.strip()
        
        # 5) 기사 발행 언론사
        press_company = source_news.find('em', {'class':'media_end_linked_more_point'}).get_text()
        
        # 위 2~5를 통해 성공적으로 제목/날짜/본문/언론사 정보가 모두 추출되었을 때에만 리스트에 추가해 길이를 동일하게 유지해줍니다.
        # append를 여기서 해줘야 나중에 에러떠서 처음으로 되돌아갔을 때 그 이전단계 추가가 안되서 각 리스트의 길이가 동일함
        titles.append(title)
        dates.append(date)
        articles.append(article)
        press_companies.append(press_company)
        article_urls.append(url) # 6) 기사 URL 
    
    except:
        print('*** 다음 링크의 뉴스를 크롤링하는 중 에러가 발생했습니다 : {}'.format(url))
        

# 각 데이터 종류별 list에 담아둔 전체 데이터를 DataFrame에 모아둡니다.
article_df = pd.DataFrame({'Title':titles, 
                           'Date':dates, 
                           'Article':articles, 
                           'URL':article_urls, 
                           'PressCompany':press_companies})

# 파일명을 result_연도월일_시분.xlsx 로 지정하여 저장합니다.
article_df.to_excel('result_{}.xlsx'.format(datetime.now().strftime('%y%m%d_%H%M')), index=False, encoding='utf-8')
article_df.head()