들어가기 앞서..
- Python의 BeautifulSoup 라이브러리를 활용해서 내 맘대로 내가 원하는 뉴스의 일부분을 크롤링하고, 색인/검색하는 간단한 시스템을 만들 예정이다. 사실 크롤링을 할 수 있는 방법은 너무 다양하고, 관련 라이브러리도 상당히 많다. 그리고 이 부분에 있어서 예전에도 생각했던 거지만, 최근 특정 블로그에서 공감가는 글을 읽었다. 읽어보면 좋을듯 . . (그렇다고 해답이 주어지진 않음) [링크]
- 우리가 보는 웹은 html로 구성되어 있고, 해당 html을 가져와서 입 맛에 맞게 파싱하여 원하는 부분의 데이터를 가져오는 것을 웹 스크래핑, 크롤링이라고 부른다.
- 아래 이미지와 같이 구글 크롬에서 네이버 뉴스 "삼성전자"라는 키워드로 검색 시에 나오는 화면에서, F12버튼을 누르면 해당 웹페이지의 html 소스를 확인 할 수 있다.
BeautifulSoup 라이브러리란?
- HTML 및 XML 파일에서 데이터를 가져 오기위한 Python 라이브러리이다. 즉 웹상의 HTML, XML 데이터를 가져오고 "특정 파서"를 통해 구문 분석 트리를 탐색, 검색 및 수정하는 방법을 제공하는 라이브러리라고 볼 수 있다. 여기서 제공하는 함수들을 활용하면 일반적으로 프로그래머의 작업 시간 몇 일을 절약할 수 있다고 한다.
(www.crummy.com/software/BeautifulSoup/bs4/doc/)
네이버 뉴스 크롤링을 위해 사용한 BeautifulSoup 클래스의 함수
: 네이버 뉴스에 해당하는 html text데이터를 가져와서 내 입 맛에 맞는 파싱을 해서 사용할 것이기 때문에 아래 함수들만 활용해서 데이터를 가져오고 파싱을 진행했다.
- select_one('tag') : 'tag'에 매칭되는 첫 번째 html tag 객체를 가져옴.
soup.select_one(".sister")
# <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>
- select('tag') : 'tag'에 매칭되는 html tag 객체들을 가져옴.
soup.select('a[href]')
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]
- find('pattern') : 'pattern'에 매칭되는 첫 번째 html tag 객체를 가져옴.
soup.find(id="link3")
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>
- find_all('pattern') : 'pattern'에 매칭되는 html tag 객체들을 가져옴.
soup.find_all('a')
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]
find와 select 함수는 사용법이 약간 다르지만 비슷한 기능을 한다고 볼 수 있다. 두 함수 모두 태그 이름, 속성, 속성값을 특정하는 방식은 같지만, select가 여러 요소를 조합하여 태그를 특정하기 쉬운 장점이 있다.
#특정 html 태그에서 div아래 div아래 section내의 데이터가 필요한 경우
#find
soup.find('div').find('div').find('section')
#select
soup.select_ond('div > div > p')
따라서 일반적으로 select함수를 쓰는게 편해 보이며, select를 써서 원하는 tag에 대한 규칙을 따로 config로 관리해도 좋을 것 같다.
네이버 뉴스 html parser 코드
import requests
import re
import json
from bs4 import BeautifulSoup
class naver():
def __init__(self, query):
super()
config = configparser.ConfigParser() #향후 html 네비게이터 config화 예정
self.query = query
self.url = 'https://search.naver.com/search.naver?where=news&sm=tab_jum&query={}'.format(self.query)
self.id = 0 # 색인되어 있는 뉴스 id의 최댓값 가져오기
self.res = requests.get(self.url)
self.soup = BeautifulSoup(self.res.content, 'html.parser')
init에 네이버 뉴스에 검색하고자 하는 키워드를 초기화하고, 해당 url을 requests 라이브러리를 통해 html데이터를 가져와서 BeautifulSoup 객체를 만든다.
위 이미지를 보면 "1)기본 뉴스" 마다 3) 페이징 처리가 되어 있어, 오른쪽에 확인할 수 있고, "1)기본 뉴스" 마다 "2)클러스터 뉴스"가 있어 "2)클러스터 뉴스"를 "1)기본 뉴스"로 순환하여 해당 정보를 가져오게끔 구성하였다.
def get_news_html(self, soup):
news_info_list = soup.select('#main_pack > section > div.api_subject_bx > div.group_news > ul > li')
return news_info_list
def get_page_info(self, soup=None):
if soup == None:
page_info_list = self.soup.select_one('#main_pack > div.api_sc_page_wrap > div.sc_page > div.sc_page_inner')
page_list = ['https://search.naver.com/search.naver'+data['href'] for data in page_info_list.find_all('a')]
return page_list
else:
page_info_list = soup.select_one('#main_pack > div.api_sc_page_wrap > div.sc_page > div.sc_page_inner')
page_list = ['https://search.naver.com/search.naver'+data['href'] for data in page_info_list.find_all('a')]
return page_list
def get_cluster_news_info(self, url):
res = requests.get(url)
soup = BeautifulSoup(res.content, 'html.parser')
page_list = self.get_page_info(soup)
results = []
first_news_flag = True
for page_url in page_list:
res = requests.get(page_url)
soup = BeautifulSoup(res.content, 'html.parser')
news_info_list = self.get_news_html(soup)
for news_info in news_info_list:
if first_news_flag and first_page_flag:
first_news_flag = False
continue
title = news_info.select_one('div.news_wrap.api_ani_send > div.news_area > a').text
desc = news_info.select_one('div.news_wrap.api_ani_send > div.news_area > div.news_dsc > div.dsc_wrap > a').text
news_url = news_info.find("a")["href"]
results.append({"title": title, "desc":desc, "url":news_url})
return results
def get_news_info(self):
page_list = self.get_page_info()
results_list = []
for page_url in page_list:
res = requests.get(page_url)
soup = BeautifulSoup(res.content, 'html.parser')
news_info_list = self.get_news_html(soup)
results = {}
for news_info in news_info_list:
self.id += 1
title = news_info.select_one('div.news_wrap.api_ani_send > div.news_area > a').text
desc = news_info.select_one('div.news_wrap.api_ani_send > div.news_area > div.news_dsc > div.dsc_wrap > a').text
news_url = news_info.find("a")["href"]
cluster = news_info.select_one('a.news_more') # 숫자만 빼내야댐
if cluster != None:
cluster_str = str(cluster)
idx = cluster_str.find("news_submit_related_option('") + len("news_submit_related_option('")
num = cluster_str[idx:idx+13]
cluster_news_url = 'https://search.naver.com/search.naver?where=news&query={}&sm=tab_opt&sort=0&photo=0&field=0&reporter_article=&pd=0&ds=&de=&docid={}&nso=&mynews=0&refresh_start=0&related=1'.format(self.query, num)
cluster_news_results = self.get_cluster_news_info(cluster_news_url)
results[self.id] = {"title":title, "desc":desc, "url":news_url, "cluster_news_list":cluster_news_results}
results_list.append(results)
return results_list
최종적으로 특정 키워드에 해당하는 네이버 뉴스의 요약본을 json형식으로 저장할 수 있다.
(github 소스 확인 : [링크])
if __name__ == '__main__':
parser = naver('삼성전자')
news = parser.get_news_info()
with open("news.json", "w", encoding='utf8') as json_file:
json.dump(news, json_file)
마무리
2021년 2월 현재 네이버 html구조에 맞는 파싱구조를 통해 입맛대로 데이터를 가져왔고, elastic search를 활용해 색인/검색에 활용할 예정이다.