AI/정책 댓글 반응 NLP

[1] 네이버 뉴스 댓글 크롤러_ver2

eraser 2020. 4. 6. 03:46
반응형

 네이버 뉴스 기사량이 많아 팀원과 작업을 분배하여 2019년 기사를 수집했다. ver1.5의 크롤러를 수정했다. 작업을 통해 얻고자 하는 결과물은 네이버에서 '주52시간'으로 검색했을 때 나오는 2019년의 모든 기사들 중 네이버 뉴스 플랫폼에 등록된 기사들의 기사 제목, 언론사, 기사 작성 시간, 댓글 수, 댓글 작성자, 댓글 내용, 댓글 공감/비공감 수이다.

 


# 변수 설정

 

 1년치 기사량이 많아 한 번에 크롤링할 수 없다. 한 달씩 나누어 작업을 진행했다. 월별로 기간을 지정해 기사를 검색하고, 기사 양에 맞게 최대 페이지를 지정해 준다. 다행히 한 페이지당 10개씩 기사가 보여지기 때문에, 페이지를 지정하는 일이 쉬웠다.

 URL이 조금 달라졌는데, 시작 날짜를 나타내는 ds, 종료 날짜를 나타내는 de와 시작 날짜와 종료 날짜를 이용해 조건을 주는 p 부분이 추가되었다. urlencode를 사용해 URL 구조를 바꿔 주었다.

 

 이에 따라 달라진 변수 설정은 다음과 같다.


# 변수 설정
QUERY = "주52시간"
START_DATE = "2019.12.01"
END_DATE = "2019.12.31"

search_QUERY = urllib.parse.urlencode({'query': QUERY}, encoding='utf-8')
start_QUERY = urllib.parse.urlencode({'ds': START_DATE}, encoding='utf-8')
end_QUERY = urllib.parse.urlencode({'de': END_DATE}, encoding='utf-8')
p_QUERY = urllib.parse.urlencode({'p': f"from{START_DATE.replace('.', '')}to{END_DATE.replace('.', '')}"}, encoding='utf-8')

URL = f"https://search.naver.com/search.naver?&where=news&{search_QUERY}&sm=tab_pge&sort=2&photo=0&field=0&reporter_article=&pd=3&{start_QUERY}&{end_QUERY}&docid=&nso=so:da,{p_QUERY},a:all&mynews=0"

LINK_PAT = "https:\/\/news\.naver\.com\/main\/read\.nhn\?"
search_PAGE = 326

 


 

# 기사 페이지 정보 스크레이핑

 

 마지막 댓글이 나올 때까지 '더보기' 버튼을 클릭하는 것은 동일하다. 달라진 것은 다음과 같다.

 첫째, 댓글 부분의 정보를 풀어서 저장헀다.

 둘째, 각 기사의 경우를 나누어 정보를 다르게 저장하도록 했다. 연예 뉴스의 경우 댓글이 없고, '주52시간' 정책과 관련이 없을 가능성이 크기 때문에, 아예 None을 저장하도록 바꿨다. 댓글이 없는 경우는 나중에 기사 부분만 저장하도록 하고, 댓글 관련 컨텐츠는 모두 None을 저장하도록 했다.

 셋째, 연예 분야 뉴스는 저장하지 않는다.

 


 위의 모든 사항을 반영한 함수는 다음과 같다. 

 

# 한 페이지 별로 필요한 정보 스크레이핑
def extract_info(url, wait_time=2, delay_time=0.5):
    driver.implicitly_wait(wait_time)
    driver.get(url)

    # 댓글 창 있으면 다 내리기
    while True:
        try:
            more_comments = driver.find_element_by_css_selector('a.u_cbox_btn_more')
            more_comments.click()
            time.sleep(delay_time)
        except:
            break

    # html 페이지 읽어오기
    html = driver.page_source
    soup = BeautifulSoup(html, 'lxml')

    result = []

    try: # 연예 분야 뉴스 제외

        site = soup.find('h1').find("span").get_text(strip=True)  # 출처
        title = soup.find('h3', {'id': 'articleTitle'}).get_text(strip=True)  # 기사 제목
        article_time = soup.find('span', {'class': 't11'}).get_text(strip=True)  # 작성 시간

        press = soup.find('div', {'class': "press_logo"}).find('a').find('img')['title']  # 언론사

        total_com = soup.find("span", {"class": "u_cbox_info_txt"}).get_text(strip=True)  # 댓글 수
        total_com = int(total_com.replace('\n', '').replace('\t', '').replace('\r', '').replace(',', ''))

        if total_com == 0: # 댓글 없는 경우
            result = [{'site': site,
                       'title': title,
                       'article_time': article_time,
                       'press': press,
                       'total_comments': total_com,
                       'nickname': None,
                       'date': None,
                       'contents': None,
                       'recomm': None,
                       'unrecomm': None}]
        else:
            nicks = soup.find_all("span", {"class": "u_cbox_nick"})  # 댓글 작성자
            nicks = [nick.text for nick in nicks]

            dates = soup.find_all("span", {"class": "u_cbox_date"})  # 댓글 날짜
            dates = [date.text for date in dates]

            contents = soup.find_all("span", {"class": "u_cbox_contents"})  # 댓글 내용
            contents = [content.text for content in contents]

            recomms = soup.find_all("em", {"class": "u_cbox_cnt_recomm"})  # 공감 수
            recomms = [recomm.text for recomm in recomms]

            unrecomms = soup.find_all("em", {"class": "u_cbox_cnt_unrecomm"})  # 비공감수
            unrecomms = [unrecomm.text for unrecomm in unrecomms]

            for i in range(len(contents)):
                result.append({'site': site,
                               'title': title,
                               'article_time': article_time,
                               'press': press,
                               'total_comments': total_com,
                               'nickname': nicks[i],
                               'date': dates[i],
                               'contents': contents[i].replace('\r','').replace('\t','').replace('\n',''),
                               'recomm': recomms[i],
                               'unrecomm': unrecomms[i]})

    except: # 연예 분야 뉴스인 경우 AttributeError.
        pass

    return result

 

# 파일 저장

 

 MLBPARK에서와 동일하게 처음 파일을 쓰고, 이후 해당 파일을 열어서 한 줄 한 줄씩 데이터를 저장하도록 했다. 다만 링크만 저장하는 코드는 따로 작성하지 않았다. 

 해당 파일을 열 때, 이미 똑같은 파일이 존재하면 NameError를 일으키도록 코드를 바꿨다. 혹시 검색 시작 날짜와 종료 날짜를 변경하지 않아 똑같은 달의 기사를 중복 검색하는 경우를 방지하기 위함이다.

 


 

 위의 모든 사항을 반영하여 작성한 함수는 다음과 같다.

 

# 파일 만드는 함수
def make_file():

    if os.path.exists(f"C:/Users/user/PycharmProjects/Scraping/news_comments_NAVER_{START_DATE}_{END_DATE}.csv"):
        raise NameError("동일한 파일이 존재합니다.")

    file = open(f"news_comments_NAVER_{START_DATE}_{END_DATE}.csv", mode="w", encoding="UTF-8")
    writer = csv.writer(file)
    writer.writerow(['site', 'title', 'article_time', 'press', 'total_comments', 'nickname', 'date', 'contents', 'recomm', 'unrecomm'])
    file.close()
    return


# 파일에 한 줄씩 덮어 쓰는 함수
def append_to_file(lst):
    global START_DATE
    global END_DATE
    file = open(f"news_comments_NAVER_{START_DATE}_{END_DATE}.csv", mode="a", encoding="UTF-8")
    writer = csv.writer(file)
    for result in lst:
        writer.writerow(list(result.values()))
    file.close()
    return

# 데이터 저장 및 확인

 위의 모든 과정을 종합하여 만든 크롤러 ver2는 다음과 같다.

 

더보기
# module import
import requests
import urllib.parse
from bs4 import BeautifulSoup
import re
from selenium import webdriver
import time
import csv
import os

# 변수 설정
QUERY = "주52시간"
START_DATE = "2019.12.01"
END_DATE = "2019.12.31"

search_QUERY = urllib.parse.urlencode({'query': QUERY}, encoding='utf-8')
start_QUERY = urllib.parse.urlencode({'ds': START_DATE}, encoding='utf-8')
end_QUERY = urllib.parse.urlencode({'de': END_DATE}, encoding='utf-8')
p_QUERY = urllib.parse.urlencode({'p': f"from{START_DATE.replace('.', '')}to{END_DATE.replace('.', '')}"}, encoding='utf-8')

URL = f"https://search.naver.com/search.naver?&where=news&{search_QUERY}&sm=tab_pge&sort=2&photo=0&field=0&reporter_article=&pd=3&{start_QUERY}&{end_QUERY}&docid=&nso=so:da,{p_QUERY},a:all&mynews=0"

LINK_PAT = "https:\/\/news\.naver\.com\/main\/read\.nhn\?"
search_PAGE = 326

# driver 설정
driver = webdriver.Chrome("C:/Users/user/PycharmProjects/Scraping/chromedriver.exe")


# 검색결과 내 링크 찾기 : news.naver.com으로 시작하는 모든 링크 반환
def get_news_links(page_num, link_pattern):
    links = []
    for page in range(page_num):
        print(f"Scrapping page : {page + 1}")  # 확인용
        req = requests.get(f"{URL}&start={10 * page + 1}")
        print(req.status_code)  # 확인용
        soup = BeautifulSoup(req.text, 'lxml')
        results = soup.find_all('a', {'href': re.compile(link_pattern)})
        for result in results:
            links.append(result['href'])
    print(f"총 {len(links)}개의 뉴스 링크를 찾았습니다.")  # 확인용
    return links


# 한 페이지 별로 필요한 정보 스크레이핑
def extract_info(url, wait_time=2, delay_time=0.5):
    driver.implicitly_wait(wait_time)
    driver.get(url)

    # 댓글 창 있으면 다 내리기
    while True:
        try:
            more_comments = driver.find_element_by_css_selector('a.u_cbox_btn_more')
            more_comments.click()
            time.sleep(delay_time)
        except:
            break

    # html 페이지 읽어오기
    html = driver.page_source
    soup = BeautifulSoup(html, 'lxml')

    result = []

    try: # 연예 분야 뉴스 제외

        site = soup.find('h1').find("span").get_text(strip=True)  # 출처
        title = soup.find('h3', {'id': 'articleTitle'}).get_text(strip=True)  # 기사 제목
        article_time = soup.find('span', {'class': 't11'}).get_text(strip=True)  # 작성 시간

        press = soup.find('div', {'class': "press_logo"}).find('a').find('img')['title']  # 언론사

        total_com = soup.find("span", {"class": "u_cbox_info_txt"}).get_text(strip=True)  # 댓글 수
        total_com = int(total_com.replace('\n', '').replace('\t', '').replace('\r', '').replace(',', ''))

        if total_com == 0: # 댓글 없는 경우
            result = [{'site': site,
                       'title': title,
                       'article_time': article_time,
                       'press': press,
                       'total_comments': total_com,
                       'nickname': None,
                       'date': None,
                       'contents': None,
                       'recomm': None,
                       'unrecomm': None}]
        else:
            nicks = soup.find_all("span", {"class": "u_cbox_nick"})  # 댓글 작성자
            nicks = [nick.text for nick in nicks]

            dates = soup.find_all("span", {"class": "u_cbox_date"})  # 댓글 날짜
            dates = [date.text for date in dates]

            contents = soup.find_all("span", {"class": "u_cbox_contents"})  # 댓글 내용
            contents = [content.text for content in contents]

            recomms = soup.find_all("em", {"class": "u_cbox_cnt_recomm"})  # 공감 수
            recomms = [recomm.text for recomm in recomms]

            unrecomms = soup.find_all("em", {"class": "u_cbox_cnt_unrecomm"})  # 비공감수
            unrecomms = [unrecomm.text for unrecomm in unrecomms]

            for i in range(len(contents)):
                result.append({'site': site,
                               'title': title,
                               'article_time': article_time,
                               'press': press,
                               'total_comments': total_com,
                               'nickname': nicks[i],
                               'date': dates[i],
                               'contents': contents[i].replace('\r','').replace('\t','').replace('\n',''),
                               'recomm': recomms[i],
                               'unrecomm': unrecomms[i]})

    except: # 연예 분야 뉴스인 경우 AttributeError.
        pass

    return result


# 각 페이지 돌면서 스크레이핑
def extract_contents(links):
    for link in links:
        print(f"{link}&m_view=1")
        content = extract_info(f"{link}&m_view=1")
        append_to_file(content)
    return print("모든 작업이 완료되었습니다.")


# 파일 만드는 함수
def make_file():

    if os.path.exists(f"C:/Users/user/PycharmProjects/Scraping/news_comments_NAVER_{START_DATE}_{END_DATE}.csv"):
        raise NameError("동일한 파일이 존재합니다.")

    file = open(f"news_comments_NAVER_{START_DATE}_{END_DATE}.csv", mode="w", encoding="UTF-8")
    writer = csv.writer(file)
    writer.writerow(['site', 'title', 'article_time', 'press', 'total_comments', 'nickname', 'date', 'contents', 'recomm', 'unrecomm'])
    file.close()
    return


# 파일에 한 줄씩 덮어 쓰는 함수
def append_to_file(lst):
    global START_DATE
    global END_DATE
    file = open(f"news_comments_NAVER_{START_DATE}_{END_DATE}.csv", mode="a", encoding="UTF-8")
    writer = csv.writer(file)
    for result in lst:
        writer.writerow(list(result.values()))
    file.close()
    return


# main 함수
def main():
    global search_PAGE
    make_file()
    news_links = get_news_links(search_PAGE, LINK_PAT)
    result = extract_contents(news_links)
    driver.quit()
    return


# 함수 실행
main()

 

 

반응형