동아일보, 한겨레 신문에서 '사드'관련 신문기사 크롤링하기
""" 동아일보 특정 키워드를 포함하는, 특정 날짜 이전 기사 내용 크롤러(정확도순 검색)
python [모듈 이름] [키워드] [가져올 페이지 숫자] [결과 파일명]
한 페이지에 기사 15개
"""
import sys
from bs4 import BeautifulSoup
import urllib.request
from urllib.parse import quote
TARGET_URL_BEFORE_PAGE_NUM = "http://news.donga.com/search?p="
TARGET_URL_BEFORE_KEWORD = '&query='
TARGET_URL_REST = '&check_news=1&more=1&sorting=3&search_date=1&v1=&v2=&range=3'
# 기사 검색 페이지에서 기사 제목에 링크된 기사 본문 주소 받아오기
def get_link_from_news_title(page_num, URL, output_file):
for i in range(page_num):
current_page_num = 1 + i*15
position = URL.index('=')
URL_with_page_num = URL[: position+1] + str(current_page_num) \
+ URL[position+1 :]
source_code_from_URL = urllib.request.urlopen(URL_with_page_num)
soup = BeautifulSoup(source_code_from_URL, 'lxml',
from_encoding='utf-8')
for title in soup.find_all('p', 'tit'):
title_link = title.select('a')
article_URL = title_link[0]['href']
get_text(article_URL, output_file)
# 기사 본문 내용 긁어오기 (위 함수 내부에서 기사 본문 주소 받아 사용되는 함수)
def get_text(URL, output_file):
source_code_from_url = urllib.request.urlopen(URL)
soup = BeautifulSoup(source_code_from_url, 'lxml', from_encoding='utf-8')
content_of_article = soup.select('div.article_txt')
for item in content_of_article:
string_item = str(item.find_all(text=True))
output_file.write(string_item)
# 메인함수
def main(argv):
if len(argv) != 4:
print("python [모듈이름] [키워드] [가져올 페이지 숫자] [결과 파일명]")
return
keyword = argv[1]
page_num = int(argv[2])
output_file_name = argv[3]
target_URL = TARGET_URL_BEFORE_PAGE_NUM + TARGET_URL_BEFORE_KEWORD \
+ quote(keyword) + TARGET_URL_REST
output_file = open(output_file_name, 'w')
get_link_from_news_title(page_num, target_URL, output_file)
output_file.close()
if __name__ == '__main__':
main(sys.argv)
위 코드는 동아일보에서 특정 키워드를 가지는 신문기사를 대량으로 크롤링하기 위한 크롤러 모듈입니다.
전체적인 모듈의 동작 과정은 다음과 같습니다.
첫째, 특정 키워드를 통해 신문 기사를 검색해 관련된 기사 목록을 얻어 옴
둘째, 신문사의 기사 목록 페이지의 URL 주소 패턴을 분석해, 반복문으로 여러 목록 페이지를 돌며 올라와 있는 기사 제목에 연결된 모든 링크 주소(기사 본문 URL 주소)를 얻어 옴
셋째, 얻어낸 기사의 링크 주소 하나하나에 접근해, 기사 본문 내용이 담긴 HTML 요소를 찾아 해당 요소만 긁어와 본문 내용을 하나의 파일에 저장
그럼 아래에서 한줄한줄 코드를 살펴보며 자세히 설명하도록 하겠습니다.
import sys
from bs4 import BeautifulSoup
import urllib.request
from urllib.parse import quote
해당 모듈에서 크롤링을 위해 사용할 라이브러리를 임포트 한 부분입니다.
해당 모듈을 실행할 때, 터미널에서 사용자로부터 인자를 받기 위해 'sys.argv'를 사용하려고 'sys'를 임포트 했고, 타겟 URL에 요청을 보내고 응답을 받기 위해 'urllib'을, 받은 응답(HTML 코드)을 파싱 하기 위해 'BeautifulSoup'을 임포트 했습니다.
이전 예제와는 다르게 'urllib.parse'에서 'quote'함수를 임포트 했는데, 'quote'는 'urlopen'에서 인자로 사용되는 URL 주소(이하 타겟 주소)에 한글(UTF_8)이 포함되었을 때, 이를 아스키(ASCII) 형식으로 바꿔주기 위한 함수입니다.
이 내용은 해당 함수를 사용할 때, 다시 한번 자세히 설명하겠습니다.
TARGET_URL_BEFORE_PAGE_NUM = "http://news.donga.com/search?p="
TARGET_URL_BEFORE_KEWORD = '&query='
TARGET_URL_REST = '&check_news=1&more=1&sorting=3&search_date=1&v1=&v2=&range=3'
이 부분은 'urlopen'으로 요청을 보낼 타겟 주소를 세 부분으로 나누어 상수에 할당한 부분입니다.
타겟 주소를 왜 세 부분으로 나누어놓았는지 알기 위해선 'urlopen'으로 처음 접근할 동아일보의 기사 검색 페이지(이하 타겟 페이지)를 찾아 해당 페이지의 URL 패턴을 찾아내야 합니다.
위는 동아일보 메인 페이지입니다.
URL 패턴을 찾기 위해 검색 키워드로 '사드'를 사용해 우측 상단 검색 공간에서 검색해보겠습니다.
사드로 검색한 결과 페이지입니다. 우리는 정확히 사드와 관련된 신문 기사만 필요하기 때문에 본 화면에서 주황색으로 칠해진 카테고리 탭을 뉴스로 한정 짓고, 카테고리 탭 바로 밑의 검색 필터에서 정렬은 정확도순으로 범위는 동아일보로 기간은 전체로 다시 한번 검색을 설정하도록 하겠습니다.
위의 설정을 마친 화면입니다.
웹 동아일보에서 총 823건이 검색되었으나 첫 화면에 보이는 기사의 개수는 총 5개입니다. 따라서 본 페이지는 아직 크롤링의 타겟 페이지로는 부족합니다.
더 많은 기사를 보기 위해 아래의 주황색으로 링크된 더보기를 클릭하도록 하겠습니다.
위 화면은 더보기를 누른 화면입니다.
이전과는 다르게 많은 기사들이 한 페이지당 15개씩 게시되어 나타나는 것을 알 수 있습니다.
(한 사진에 해당 웹페이지 화면은 다 담을 수 없어 하단 부분만 사진으로 담았습니다.)
우리는 이 화면뿐 아니라 원하는 개수만큼의 기사 목록 페이지(2페이지, 3페이지 ...)에 있는 각각의 기사에 접근해 본문 내용을 수집해야 합니다.
기사 내용을 수집하기 위해선 실제 기사 페이지로 접근해야 하기 때문에 파란색으로 링크 처리가 된 기사의 제목에서 연결된 기사 본문 URL 주소를 뽑아와야 합니다.
그럼 먼저 이 페이지의 URL주소를 한번 살펴보겠습니다.
http://news.donga.com/search?check_news=1&more=1&sorting=3&range=3&query=사드
사진 속 URL주소창을 보게 되면 위 화면의 URL 주소는 위와 같습니다.
'check_news=1'은 뉴스로 한정지은 것을, 'more=1'은 더보기를 누른 상태임을, 'sorting=3'과 'range=3'은 각각 정확도순 정렬이며, 전체 기간 검색임을, 마지막으로 'query=사드'는 우리가 검색한 키워드임을 지레짐작할 수 있습니다.
하지만 아직도 이 페이지가 우리가 크롤링할 타겟 페이지가 되기엔 부족합니다.
우리는 1번 페이지의 15개의 기사만 가져올 것이 아니라 2번 페이지, 3번 페이지 등등 원하는 페이지 수만큼 기사를 크롤링해야 하기 때문입니다.
그러나 이상하게도 위 URL 주소에는 페이지에 해당하는 내용이 있어 보이진 않습니다.
사실 페이지에 관한 내용은 숨겨져 있습니다.
하단의 페이지 숫자를 눌러 2번째 페이지로 들어가 보겠습니다.
위 화면은 두 번째 페이지로 넘어온 화면입니다.
첫 번째 화면의 URL 주소와는 달리 두 번째 페이지의 URL 주소가 달라진 것을 확인할 수 있습니다.
http://news.donga.com/search?p=16&query=사드&check_news=1&more=1&sorting=3&search_date=1&v1=&v2=&range=3
기존의 'query=사드'위치가 앞쪽으로 이동해 왔으며 페이지 숫자로 추정되는 'p=16'이 나타났습니다.
16의 숫자가 페이지를 어떤 식으로 정의하는지 알아보기 위해 다시 여러 페이지를 눌러 URL 주소를 찾아본 결과 아래와 같이 변했습니다.
1번 페이지: http://news.donga.com/search?p=1&query=사드&check_news=1&more=1&sorting=3&search_date=1&v1=&v2=&range=3
2번 페이지: http://news.donga.com/search?p=16&query=사드&check_news=1&more=1&sorting=3&search_date=1&v1=&v2=&range=3
3번 페이지: http://news.donga.com/search?p=31&query=사드&check_news=1&more=1&sorting=3&search_date=1&v1=&v2=&range=3
4번 페이지: http://news.donga.com/search?p=46&query=사드&check_news=1&more=1&sorting=3&search_date=1&v1=&v2=&range=3
URL 주소가 변화하는 것을 살펴보면 페이지가 증가할 때마다 'p'의 값이 15씩 더해지는 것을 알 수 있습니다.
한 페이지에 나타나는 기사의 수가 15개인 것으로 보아 한 페이지가 증가할 때마다 p의 값을 15씩 늘려 페이지를 구분한 것을 알아낼 수 있습니다.
그럼 다시 한번 타겟 주소를 상수화한 부분을 살펴보겠습니다.
TARGET_URL_BEFORE_PAGE_NUM = "http://news.donga.com/search?p="
TARGET_URL_BEFORE_KEWORD = '&query='
TARGET_URL_REST = '&check_news=1&more=1&sorting=3&search_date=' \
'1&v1=&v2=&range=1'
'사드'를 통해 검색된 기사 목록 전체를 둘러보기 위해선 'query=사드'로 설정되어야 하며 페이지 번호를 'p=1', 'p=16', 'p=31'... 처럼 원하는 페이지를 나타내는 수만큼변경시켜가며 전부 둘러봐야 하는 것을 알 수 있습니다.
따라서 위처럼 페이지수를 나타내는 'p='부분과 검색 키워드를 나타내는 '&query='부분의 정보를 사용자로부터 입력받아 하나의 타겟 주소로 결합하기 위해서 타겟 주소를 세 부분으로 나눈 것입니다.
그럼 타겟 주소가 어떻게 결합되어 사용되는지 알아보기 위해 크롤러 모듈에서 메인 함수를 먼저 살펴보겠습니다.
# 메인함수
def main(argv):
if len(argv) != 4:
print("python [모듈이름] [키워드] [가져올 페이지 숫자] [결과 파일명]")
return
keyword = argv[1]
page_num = int(argv[2])
output_file_name = argv[3]
target_URL = TARGET_URL_BEFORE_PAGE_NUM + TARGET_URL_BEFORE_KEWORD \
+ quote(keyword) + TARGET_URL_REST
output_file = open(output_file_name, 'w')
get_link_from_news_title(page_num, target_URL, output_file)
output_file.close()
if __name__ == '__main__':
main(sys.argv)
위 코드는 메인함수 부분입니다.
'sys.argv'를 통해 사용자로부터 검색 키워드, 페이지 숫자, 결과 파일명을 각각 'keyword', 'page_num', 'output_file_name'에 입력받습니다.
'page_num'은 정수형으로 쓰여야 하므로 int형으로 변환해서 사용합니다.
이후, target_URL 변수에 모듈 위에서 정의해놓은 URL상수들을 사용자로부터 입력받은 정보와 결합시킵니다.
이때, 'TARGET_URL_BEFORE_PAGE_NUM'뒤에 사용자로부터 입력받은 페이지 숫자를 결합하지는 않고 검색 키워드인 'keword'만 'TARGET_URL_BEFORE_KEWORD'뒤에 'quote'(urllib.parse의 메서드) 메서드만 사용해 결합합니다.
(페이지 숫자를 결합하지 않는 이유는 아래에서 설명하겠습니다.)
'quote' 메서드를 'keword'에 사용하는 이유는 우리가 검색할 때 사용하는 언어는 한글('UTF-8')이기 때문입니다.
URL 주소에는 'ASCII' 표현 방식 이외의 문자 표기법은 사용될 수 없기 때문에 '사드'라는 'UTF-8' 방식의 문자를 'ASCII' 방식으로 변환해야 하기 위해 'quute'메서드를 'keword'에 사용했습니다.
그렇게 완성된 타겟 주소(사실 페이지 숫자가 아직 포함되지 않았으므로 완전한 형태의 타겟 주소는 아닙니다.)를 'get_link_from_news_title'함수를 이용해 기사를 크롤링해와 파일로 저장합니다.
# 기사 검색 페이지에서 기사 제목에 링크된 기사 본문 주소 받아오기
def get_link_from_news_title(page_num, URL, output_file):
for i in range(page_num):
current_page_num = 1 + i*15
position = URL.index('=')
URL_with_page_num = URL[: position+1] + str(current_page_num) \
+ URL[position+1 :]
source_code_from_URL = urllib.request.urlopen(URL_with_page_num)
soup = BeautifulSoup(source_code_from_URL, 'lxml',
from_encoding='utf-8')
for title in soup.find_all('p', 'tit'):
title_link = title.select('a')
article_URL = title_link[0]['href']
get_text(article_URL, output_file)
위 함수는 메인 함수에서 크롤링하기 위한 함수인 'get_link_from_news_title'함수입니다.
인자로는 크롤링해 올 기사 목록 수인 page_num과, 타겟 주소인 URL, 그리고 본문 내용을 저장할 결과 파일명인 'output_file'을 인자로 받습니다.
함수 안 첫 번째 반복문은 여러 기사 목록을 돌며 기사 제목에 링크된 기사 본문 주소(URL 주소)를 얻어 오는 부분입니다.
이 반복문에서는 기사를 긁어올 페이지 수만큼 코드를 반복합니다.
'current_page_num'에 'i'를 이용해서 긁어올 페이지 수만큼의 현재 페이지를 만듭니다.
(i=0 일 때 current_page_num=1, i=1 일때 current_page_num=16, i=2 일때 current_page_num=31 ...)
그 후, 인자로 받은 URL에서 첫 번째로 '='문자가 나오는 위치를 'position'에 할당한 후, 해당 'position'뒤에 'current_page_num'을 스트링으로 변환한 후 삽입합니다.
(21~22번째 줄 코드가 이해가 가지 않는다면 문자열 슬라이스를 검색해 살펴보시길 바랍니다.)
URL에서 처음으로 '='문자가 나오는 위치는 아까 위에서 URL 주소에 마저 결합하지 않았던 페이지 숫자를 나타내는 부분이기 때문에 위와 같은 방법으로 완전한 타겟 주소를 만들 수 있으며 반복문을 통해 1번 페이지부터, 원하는 페이지(예를 들어 원하는 페이지 수가 10이면 i의 최댓값은 9가 되고, 그때의 'current_page_num'은 136이 됩니다.)까지의 모든 URL 주소를 만들어 낼 수 있습니다.
이렇게 만들어진 'URL_with_page_num'을 이용해 'urlopen'으로 요청을 하고 응답을 받아 'BeaurifulSoup'객체인 'soup'을 만듭니다.
이제부터는 모든 기사 목록에 올라온 기사 제목에서 기사 본문 주소(URL 주소)를 추출해올 수가 있습니다.
하단의 26번째 줄부터 시작되는 반복문은 기사 제목에 링크된 기사 본문 주소(URL 주소)를 얻어오기 위한 부분입니다.
이를 설명하기 앞서 다시 한번 동아일보 기사 검색 페이지 화면을 보도록 하겠습니다.
'사드'로 검색한 기사 목록 첫 번째 페이지입니다.
크롬 개발자 도구를 통해 첫 번째 기사의 제목(링크)부분이 어딘지 찾아봤습니다.
class가 'tit'인 p태그 안의 첫번째 a태그에 연결된 URL 주소가 해당 기사 본문 URL가 포함된 것을 알 수 있습니다.
직접 더 살펴본 결과, 2번째 기사, 3번째 기사 .... 15번째 기사 제목 모두 연결된 기사 본문 URL 주소가 위와 같은 형식이었습니다.
따라서 우리는 위에서 만든 'soup'객체에서 class='tit인 p태그를 모두 뽑아와 그 안에 있는 첫 번째 a태그의 'href'의 내용을 가져온다면 모든 기사의 내용을 크롤링할 수 있습니다.
그럼 'get_link_from_news_title'함수의 하단 반복문을 다시 보도록 하겠습니다.
for title in soup.find_all('p', 'tit'):
title_link = title.select('a')
article_URL = title_link[0]['href']
get_text(article_URL, output_file)
위에서 생성한 'soup'객체에서 'find_all' 메서드를 통해 class가 'tit'인 p태그를 모두 가져와 반복문을 통해 'title'에 하나씩 할당했으며 'select'메서드를 통해 모든 a태그를 'title_link'에 저장했습니다.
('select' 메서드는 인자로 주어지는 태그를 모두 가지고 오며, 해당 객체는 인덱스로 태그 하나하나에 접근할 수 있습니다. 자세한 사항은 BeautifulSoup 문서를 참조하시기 바랍니다.)
기사 본문 URL 주소는 p태그 안 첫 번째 a태그에 저장되어 있으므로 'title_link'의 0번 인덱스로 첫번째 a태그에 접근했으며 키 값으로 'href'를 이용해 'href'로 연결된 URL 주소를 'article_URL'에 저장했습니다.
(역시나 BeutifulSoup 객체의 자세한 사용법은 BeautifulSoup 문서를 참조하시기 바랍니다.)
2개의 반복문을 통해 최종적으로 얻어진 기사 본문 URL주소를 'get_text'함수에 결과 파일명과 함께 전달합니다.
'get_text'메서드는 실제적으로 각 기사의 본문 내용을 크롤링해오는 함수입니다.
# 기사 본문 내용 긁어오기 (위 함수 내부에서 기사 본문 주소 받아 사용되는 함수)
def get_text(URL, output_file):
source_code_from_url = urllib.request.urlopen(URL)
soup = BeautifulSoup(source_code_from_url, 'lxml', from_encoding='utf-8')
content_of_article = soup.select('div.article_txt')
for item in content_of_article:
string_item = str(item.find_all(text=True))
output_file.write(string_item)
위는 기사 본문 주소를 받아 본문 내용을 긁어오는 'get_text'함수입니다.get_text'함수 입니다.
이전 글에서 네이버 뉴스를 긁어오기 위해 사용했던 함수와 큰 차이는 없습니다.
이 함수는 기사 URL 주소를 통해 응답을 받아 'BeautifulSoup'객체를 만들고, 본문을 추출해 결과 파일에 쓰는 동작을 합니다.
다만 이 함수에서 다른 점이 있다면, 동아일보에서는 기사의 본문 내용이 class가 'article_txt'인 div태그 안에 포함되어 있었기 때문에 'select'메소드를 통해 class가 'article_txt'인 div태그안 텍스트 요소만 뽑아온 점입니다.
다시 한번 두 함수를 정리해보면 다음과 같습니다.
'get_link_from_title'함수를 통해 특정 키워드로 검색한 기사 목록의 모든 기사 주소들을 추출해오며
해당 함수 내부에서 추출한 기사 주소들을 이용해 'get_text'함수를 호출에 결과 파일에 기사 본문 내용을 누적해 저장하는 방식으로 두 함수는 동작하게 됩니다.
터미널을 통해 크롤러 모듈을 실행시켜 사드 관련 기사를 10페이지(150개)를 크롤링해 결과 파일을 'result_articles.txt'에 저장했습니다.
그 저장 결과는 다음과 같습니다.
기사가 잘 크롤링된 것을 알 수 있습니다.
텍스트 중간 자바스크립트 코드도 포함된 것이 보이는데, 아마 본문 내용이 포함된 태그에 같이 포함된 코드인 것으로 추정됩니다.
해당 코드뿐 아니라 불필요한 문자는 이전 글에서 설명한 텍스트 정제 모듈로 제거해서 분석에 사용하시면 됩니다.
한겨레신문에서 기사를 크롤링해 오는 방법 또한 동아일보에서 크롤링해 온 방법과 동일합니다.
다만 한겨레신문에서 사용하는 별도의 URL 패턴이 있을 것이며 기사 본문 내용을 구성하는 HTML 코드 또한 동아일보와 다를 것입니다.
위 크롤러 모듈에서 해당 사항만 한겨레신문에 맞게 변경하여 사용한다면 동일하게 '사드'와 관련된 신문 기사를 크롤링해 오실 수 있을 겁니다.
한겨레신문을 위한 크롤러 모듈은 아래의 코드를 참조하시기 바랍니다.
""" 한겨레 신문 특정 키워드를 포함하는, 특정 날짜 이전 기사 내용 크롤러(정확도순 검색)
python [모듈이름] [키워드] [가져올 페이지 숫자] [가져올 기사의 최근 날짜]
[결과 파일명.txt]
한페이지에 10개
"""
import sys
from bs4 import BeautifulSoup
import urllib.request
from urllib.parse import quote
TARGET_URL_BEFORE_KEWORD = 'http://search.hani.co.kr/Search?command=query&' \
'keyword='
TARGET_URL_BEFORE_UNTIL_DATE = '&media=news&sort=s&period=all&datefrom=' \
'2000.01.01&dateto='
TARGET_URL_REST = '&pageseq='
def get_link_from_news_title(page_num, URL, output_file):
for i in range(page_num):
URL_with_page_num = URL + str(i)
source_code_from_URL = urllib.request.urlopen(URL_with_page_num)
soup = BeautifulSoup(source_code_from_URL, 'lxml',
from_encoding='utf-8')
for item in soup.select('dt > a'):
article_URL = item['href']
get_text(article_URL, output_file)
def get_text(URL, output_file):
source_code_from_url = urllib.request.urlopen(URL)
soup = BeautifulSoup(source_code_from_url, 'lxml', from_encoding='utf-8')
content_of_article = soup.select('div.text')
for item in content_of_article:
string_item = str(item.find_all(text=True))
output_file.write(string_item)
def main(argv):
if len(sys.argv) != 5:
print("python [모듈이름] [키워드] [가져올 페이지 숫자] "
"[가져올 기사의 최근 날짜] [결과 파일명.txt]")
return
keyword = argv[1]
page_num = int(argv[2])
until_date = argv[3]
output_file_name = argv[4]
target_URL = TARGET_URL_BEFORE_KEWORD + quote(keyword) \
+ TARGET_URL_BEFORE_UNTIL_DATE + until_date + TARGET_URL_REST
output_file = open(output_file_name, 'w')
get_link_from_news_title(page_num, target_URL, output_file)
output_file.close()
if __name__ == '__main__':
main(sys.argv)
다음 글에선 크롤링한 기사 본문 내용을 바탕으로 단어 사용 빈도를 체크하는 프로그램을 만들어보도록 하겠습니다.