본문 바로가기

Programming/python

내 맘대로 네이버 뉴스 스크래핑

들어가기 앞서..

- Python의 BeautifulSoup 라이브러리를 활용해서 내 맘대로 내가 원하는 뉴스의 일부분을 크롤링하고, 색인/검색하는 간단한 시스템을 만들 예정이다. 사실 크롤링을 할 수 있는 방법은 너무 다양하고, 관련 라이브러리도 상당히 많다. 그리고 이 부분에 있어서 예전에도 생각했던 거지만, 최근 특정 블로그에서 공감가는 글을 읽었다. 읽어보면 좋을듯 . . (그렇다고 해답이 주어지진 않음) [링크]

 

- 우리가 보는 웹은 html로 구성되어 있고, 해당 html을 가져와서 입 맛에 맞게 파싱하여 원하는 부분의 데이터를 가져오는 것을 웹 스크래핑, 크롤링이라고 부른다.

 

- 아래 이미지와 같이 구글 크롬에서 네이버 뉴스 "삼성전자"라는 키워드로 검색 시에 나오는 화면에서, F12버튼을 누르면 해당 웹페이지의 html 소스를 확인 할 수 있다.

네이버에서 "삼성전자" 키워드로 뉴스 검색 시 나오는 웹에 대한 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를 활용해 색인/검색에 활용할 예정이다.