Data Crawling - 네이버 뉴스 데이터 수집하기

네이버 뉴스와 관련된 데이터로 연구실에서 일하다 보니, 여러가지 관점에서 데이터를 수집하는 경우가 생긴다.

네이버 뉴스에서 오른쪽 위쪽을 잘 살펴보면 기사배열 이력 이라는 코너가 있다.

2019년 4월 4일 이후부터는 메인에 뜨는 뉴스가 개인마다 다르게 적용되도록 서비스 하고 있는 것 같은데, 그 전에는 네이버가 자신들의 기준으로 메인에 기사를 걸어놓은 것 같다.

원하는 기간에 네이버 뉴스 메인에 게시되었던 기사들만 골라서 제목 및 링크를 수집하여 보자.

차근차근 따라올 수 있도록 코드를 최대한 구성해 보도록 노력했다.

코드는 python 3.7 환경에서 작성했다.

수집 기간 정의하기

먼저, 원하는 수집 기간을 정의해 보자. 파이썬의 기본 패키지 중 하나인 datetime을 사용할 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 가져올 범위를 정의
# 2015-02-25 ~ 2015-02-28 // 2015-03-01 ~ 2015-03-30

import datetime

days_range = []

start = datetime.datetime.strptime("2015-02-25", "%Y-%m-%d")
end = datetime.datetime.strptime("2015-03-31", "%Y-%m-%d") # 범위 + 1
date_generated = [start + datetime.timedelta(days=x) for x in range(0, (end-start).days)]

for date in date_generated:
days_range.append(date.strftime("%Y-%m-%d"))

print(days_range)

>> ['2015-02-25', '2015-02-26', '2015-02-27', '2015-02-28', '2015-03-01', ... , '2015-03-30']

코드를 읽어보면 별로 어렵지 않다. startend로 날짜 범위를 지정한 다음, striptime 함수를 활용하여 원하는 형식인 %Y-%m-%d 으로 뽑아내어 days_range 라는 리스트에 저장하였다.

html parser 정의하기

이제 원하는 페이지의 html에 접근하기 위해, parser을 정의할 것이다.

다행히도 간편하게 html parsing을 지원하는 패키지인 BeautifulSoup4가 존재한다.

혹시나 이 패키지를 다운받지 않으신 분들은 주피터 노트북 상에서 아래와 같은 코드를 입력하면 된다.

1
!pip install bs4

다운로드가 완료되었다면, 이제 원래의 목적으로 돌아가보자. 아래의 코드를 작성한다.

1
2
3
4
5
6
7
8
import requests
from bs4 import BeautifulSoup

def get_bs_obj(url):
result = requests.get(url)
bs_obj = BeautifulSoup(result.content, "html.parser")

return bs_obj

get_bs_obj() 라는, url을 입력하면 bs_obj을 반환하는 함수를 작성하였다.

뉴스 페이지 수 구하기

이제 우리가 수집할 페이지에 접속하여, 페이지가 동작하는 방식에 대해서 파악해 보자.

네이버 주요뉴스 배열 이력 (리스트형)

우리가 원하는 정보는 위의 페이지에서 우리 눈으로 보이는 정보들 (기사 제목, 링크, 날짜 등) 이다.

빨간색 네모 부분을 누르면 다음 페이지로 넘어가며, 사진과 같이 메인 뉴스로 게시되었던 기사 및 텍스트만 메인 뉴스로 게시된 기사 모두를 수집해야 한다.

날짜가 바뀔때 마다 메인 뉴스에 걸렸던 기사의 갯수가 달라질 것인데, 그러면 당연하게도 페이지 수를 알아야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from tqdm import tqdm_notebook

test = ["2015-03-01"] # 테스트를 위한 데이터 수집 구간

for date in tqdm_notebook(test):

news_arrange_url = "https://news.naver.com/main/history/mainnews/list.nhn"
news_list_date_page_url = news_arrange_url + "?date=" + date

# get bs_obj
bs_obj = get_bs_obj(news_list_date_page_url)

# 포토 뉴스 페이지 수 구하기
photo_news_count = bs_obj.find("div", {"class": "eh_page"}).text.split('/')[1]
photo_news_count = int(photo_news_count)

print(photo_news_count)

# 리스트 뉴스 페이지 수 구하기
text_news_count = bs_obj.find("div", {"class": "mtype_list_wide"}).find("div", {"class": "eh_page"}).text.split('/')[1]
text_news_count = int(text_news_count)

print(text_news_count)

>> 15
>> 7

위 코드에 사용된 tqdm은 for문의 진행 정도를 시각적으로 보여주는 좋은 패키지이다. 사용법은 위와 같이 import한 후, for문의 범위에 감싸주면 된다.

웹페이지의 html에서 값을 가져오는 데에는 bs4find() 함수를 사용하였다.

사용법이 궁금하다면, 예전 포스트들을 참고하자.

전체적인 흐름은, 페이지 수 주변의 html 코드를 가져온 후, Python의 기본 함수인 split() 으로 쪼개어, 원하는 부분만 출력한 것이다.

뉴스 정보 수집하기

이제 진짜 우리가 원하는 정보를 수집할 준비가 완료되었다.

예시로 각 페이지에서 기사 제목, 발행 언론사, 카테고리, 기사 링크, 기사 고유번호 (10자리) 를 수집할 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
from tqdm import tqdm_notebook
from pprint import pprint

test = ["2015-03-01"]
main_news_list = []

for date in tqdm_notebook(test):

news_arrange_url = "https://news.naver.com/main/history/mainnews/list.nhn"
news_list_date_page_url = news_arrange_url + "?date=" + date

# get bs_obj
bs_obj = get_bs_obj(news_list_date_page_url)

# 포토 뉴스 페이지 수 구하기
photo_news_count = bs_obj.find("div", {"class": "eh_page"}).text.split('/')[1]
photo_news_count = int(photo_news_count)

# 리스트 뉴스 페이지 수 구하기
text_news_count = bs_obj.find("div", {"class": "mtype_list_wide"}).find("div", {"class": "eh_page"}).text.split('/')[1]
text_news_count = int(text_news_count)

# 포토 뉴스 부분 링크 크롤링
for page in tqdm_notebook(range(1,photo_news_count+1)):

# 포토 뉴스 링크
news_list_photo_url = 'http://news.naver.com/main/history/mainnews/photoTv.nhn'
date_str = "?date="
page_str = "&page="
news_list_photo_full_url = news_list_photo_url + "?date=" + date + "&page=" + str(page)

# get bs obj
photo_bs_obj = get_bs_obj(news_list_photo_full_url)

# 링크 내 정보 수집
ul = photo_bs_obj.find("ul", {"class": "edit_history_lst"})
lis = ul.find_all("li")
for item in lis:
title = item.find("a")["title"]
press = item.find("span", {"class" : "eh_by"}).text

# link
link = item.find("a")["href"]

sid1 = link.split('&')[-3].split('=')[1]
oid = link.split('&')[-2].split('=')[1]
aid = link.split('&')[-1].split('=')[1]

# 연예 TV 기사 제외
if sid1 == "shm":
continue

article_type = "pic"

pic_list = [date, article_type, title, press, sid1, link, aid]

main_news_list.append(pic_list)

# 텍스트 뉴스 부분 링크 크롤링
for page in tqdm_notebook(range(1, text_news_count+1)):

# 텍스트 뉴스 링크
news_list_text_url = 'http://news.naver.com/main/history/mainnews/text.nhn'
date_str = "?date="
page_str = "&page="
news_list_text_full_url = news_list_text_url + "?date=" + date + "&page=" + str(page)

# get bs obj
text_bs_obj = get_bs_obj(news_list_text_full_url)

# 링크 내 정보 수집
uls = text_bs_obj.find_all("ul")
for ul in uls:
lis = ul.find_all("li")
for item in lis:
title = item.find("a").text
press = item.find("span", {"class" : "writing"}).text

# link
link = item.find("a")["href"]

sid1 = link.split('&')[-3].split('=')[1]
oid = link.split('&')[-2].split('=')[1]
aid = link.split('&')[-1].split('=')[1]

# 연예 TV 기사 제외
if sid1 == "shm":
continue

article_type = "text"

text_list = [date, article_type, title, press, sid1, link, aid]

main_news_list.append(text_list)

pprint(main_news_list, width = 20)

코드가 크게 두 부분으로 나뉘어져 있다.

하나는 포토 기사에 대한 데이터 수집. 앞선 step에서 photo_news_count 에 페이지 수를 저장했던 것을 기억할 것이다.

for문으로 페이지 수만큼 반복하여 여러가지 정보를 수집하도록 코드를 짰다.

그리고, 수집하다보면 네이버 기사 메인에 게시되긴 하였지만 뉴스가 아닌(?) 링크들이 있어서 제외하였다.

두 번째는 text_news_count 으로 페이지 수를 저장하였던 텍스트 기사에 대한 데이터 수집이다.

또한, 수집된 기사가 포토 기사인지 텍스트 기사인지 알고 싶어서 중간에 article_type을 추가해 놓았다.

모든 수집된 데이터는 main_news_list 라는 이름의 리스트에 차례대로 append 되도록 설정했다.

마지막 줄의 pprint()는 단지 리스트를 깔끔하게 출력하고 싶어서 추가한 코드이니, 크롤러랑은 무관하다.

test 기간에 대해 네이버 메인 뉴스 기사배열이력 수집 결과

CSV 파일로 저장하기

수집을 했으면 관리하기 쉽도록 적절한 형태로 저장하는 것이 필수!

가장 보편적인 형태인 .csv 파일로 저장해 보자.

1
2
3
4
5
6
import pandas as pd

# make .csv file
naver_news_df = pd.DataFrame(main_news_list,
columns = ["date", "type", "title", "press", "category", "link", "aid"])
naver_news_df.to_csv("naver_main_news.csv", index=False)

실행하면, working directory 내에 naver_main_news.csv 파일이 생성되어 있음을 확인할 수 있다.

csv file을 만들기 위해, 데이터를 다루는 데에 매우 편리한 패키지인 pandas를 사용하였다.

참고로, 주피터 노트북에서 작업하는 사용자의 경우 위에 bs4 패키지를 설치할 때와 유사하게 코드를 입력하면 쉽게 패키지를 다운로드 받을 수 있다.

제대로 수집되었는지 csv file을 열어서 확인해 보자. 아래와 같은 코드를 입력한다.

1
2
3
4
# open .csv file

df_naver_news = pd.read_csv('naver_main_news.csv', dtype = {"aid" : "str"})
df_naver_news.head(10)

위 코드에서 read_csv()으로 파일을 읽을 때 dtype 을 추가한 이유는, 저장했었던 aid가 뉴스 고유 번호 10자리인데 경우에 따라 0000003455와 같이 앞에 0이 있어서, 이를 그대로 출력하고 싶어서 이다.

만약 저 옵션을 추가하지 않으면 방금 전 예로 든 뉴스 고유 번호는 10자리가 아니라 0을 다 빼버린 3455 라고 출력된다.. (ㅠㅠ)

전체 코드

수집을 원하는 기간을 가장 위의 start, end에 입력하면 그 결과가 csv file 로 저장되는 코드이다.

본 코드는 정적 데이터를 수집하기 위해 작성되었으며, 동적 데이터가 존재하는 경우 다른 패키지를 사용해야 한다. (참고!)

그리 깔끔한 코드는 아니니까 공부용으로 사용하시길..

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# 가져올 범위를 정의
# 2015-02-25 ~ 2015-02-28 // 2015-03-01 ~ 2015-03-31

import datetime

days_range = []

start = datetime.datetime.strptime("2015-02-25", "%Y-%m-%d") # 수집 시작 날짜
end = datetime.datetime.strptime("2015-03-31", "%Y-%m-%d") # 수집 종료 날짜 + 1
date_generated = [start + datetime.timedelta(days=x) for x in range(0, (end-start).days)]

for date in date_generated:
days_range.append(date.strftime("%Y-%m-%d"))

# 크롤러 작성

import requests
import pandas as pd
from bs4 import BeautifulSoup
from tqdm import tqdm_notebook

main_news_list = []

# html parser 정의
def get_bs_obj(url):
result = requests.get(url)
bs_obj = BeautifulSoup(result.content, "html.parser")

return bs_obj

for date in tqdm_notebook(days_range):

news_arrange_url = "https://news.naver.com/main/history/mainnews/list.nhn"
news_list_date_page_url = news_arrange_url + "?date=" + date

# get bs_obj
bs_obj = get_bs_obj(news_list_date_page_url)

# 포토 뉴스 페이지 수 구하기
photo_news_count = bs_obj.find("div", {"class": "eh_page"}).text.split('/')[1]
photo_news_count = int(photo_news_count)

# 리스트 뉴스 페이지 수 구하기
text_news_count = bs_obj.find("div", {"class": "mtype_list_wide"}).find("div", {"class": "eh_page"}).text.split('/')[1]
text_news_count = int(text_news_count)

# 포토 뉴스 부분 링크 크롤링
for page in tqdm_notebook(range(1,photo_news_count+1)):

# 포토 뉴스 링크
news_list_photo_url = 'http://news.naver.com/main/history/mainnews/photoTv.nhn'
date_str = "?date="
page_str = "&page="
news_list_photo_full_url = news_list_photo_url + "?date=" + date + "&page=" + str(page)

# get bs obj
photo_bs_obj = get_bs_obj(news_list_photo_full_url)

# 링크 내 정보 수집
ul = photo_bs_obj.find("ul", {"class": "edit_history_lst"})
lis = ul.find_all("li")
for item in lis:
title = item.find("a")["title"]
press = item.find("span", {"class" : "eh_by"}).text

# link
link = item.find("a")["href"]

sid1 = link.split('&')[-3].split('=')[1]
oid = link.split('&')[-2].split('=')[1]
aid = link.split('&')[-1].split('=')[1]

# 연예 TV 기사 제외
if sid1 == "shm":
continue

article_type = "pic"

pic_list = [date, article_type, title, press, sid1, link, aid]

main_news_list.append(pic_list)

# 텍스트 뉴스 부분 링크 크롤링
for page in tqdm_notebook(range(1, text_news_count+1)):

# 텍스트 뉴스 링크
news_list_text_url = 'http://news.naver.com/main/history/mainnews/text.nhn'
date_str = "?date="
page_str = "&page="
news_list_text_full_url = news_list_text_url + "?date=" + date + "&page=" + str(page)

# get bs obj
text_bs_obj = get_bs_obj(news_list_text_full_url)

# 링크 내 정보 수집
uls = text_bs_obj.find_all("ul")
for ul in uls:
lis = ul.find_all("li")
for item in lis:
title = item.find("a").text
press = item.find("span", {"class" : "writing"}).text

# link
link = item.find("a")["href"]

sid1 = link.split('&')[-3].split('=')[1]
oid = link.split('&')[-2].split('=')[1]
aid = link.split('&')[-1].split('=')[1]

# 연예 TV 기사 제외
if sid1 == "shm":
continue

article_type = "text"

text_list = [date, article_type, title, press, sid1, link, aid]

main_news_list.append(text_list)


# make .csv file
naver_news_main_df = pd.DataFrame(main_news_list,
columns = ["date", "type", "title", "press", "category", "link", "aid"])
naver_news_main_df.to_csv("naver_main_news_{}_to_{}.csv".format(days_range[0], days_range[-1]), index=False)

print("=== total # of articles is {} ===".format(len(main_news_list)))