안수찬 블로그

BeautifulSoup를 이용한 언론보도 결과보고서 자동화 스크립트

Introduction

안수찬 @dobestan

안수찬 @dobestan

서울대학교에서 컴퓨터공학을 전공하고, 오랜 기간 서비스 기획 및 개발을 해 왔습니다. 이러한 전문성을 인정받아 미래부 소프트웨어 마에스트로에 선정된 바 있습니다. 현재는 모바일 방송국, 퍼스트캔버스에서 컨텐츠로 새로운 가치를 그리고 있습니다. 나는 안수찬이다. 그러므로 나는 할 수 있다. me@ansuchan.com


BeautifulSoup를 이용한 언론보도 결과보고서 자동화 스크립트

Posted by 안수찬 @dobestan on .
Featured

BeautifulSoup를 이용한 언론보도 결과보고서 자동화 스크립트

Posted by 안수찬 @dobestan on .

이 프로젝트에 대해서는 dobestan/memberbook_article 프로젝트에서 더욱 자세히 확인해보실 수 있습니다. 이 포스트에서는 간단하게 프로젝트 진행 배경, 처음이라 조금은 헤멧던 기술적인 이슈들을 공유하고자 합니다. 감사합니다.

프로젝트 진행 배경

2014년 말에 인수한 멤버북 서비스를 신규 개발 중에 있습니다. 현재 안드로이드, 아이폰 어플리케이션이 개발이 완료되었고 실 서비스를 위해서 2주 정도의 테스트만 거치면 바로 배포가 가능할 것 같습니다. 이에 따라 마케팅 업체를 통하여 언론보도를 진행하였습니다. 동창회 전문앱 ‘멤버북’, 출시 전부터 연이은 이용계약 체결로 주목 - 현대경제 에서 배포된 기사 예시를 확인하실 수 있습니다.

처음 진행한 언론보도라 부족한 점도 많았지만 11개 언론사에 배포가 되었고, 이 중 5개 언론사에는 메인페이지에 배포되었습니다. 언론보도가 완료된 이후 마케팅 업체로부터 언론보도 결과보고서 라는 이름의 엑셀 파일을 수령하였습니다. 배포된 언론사 리스트, 각각의 기사 링크, 스크린샷 등에 대한 정보가 포함되어 있던 문서였고, 각각의 정보를 수작업으로 입력한 것 같아 보였습니다.

최근에 소프트웨어 마에스트로 과정에서 학습하고 있는 크롤링을 이용하면 이 부분을 자동화할 수 있다고 판단하여, 개인적으로 프로젝트를 진행해보았습니다. 현재는 스크립트로 동작하지만 추후에 네이버, 네이트, 다음 기사 섹션을 포함하여 웹 서비스로 배포할 생각이 있습니다.

기술 개요

가장 기초적인 크롤링, 파싱 라이브러리를 이용해서 다음 기사 섹션에 멤버북과 관련된 정보를 저장합니다. 최대한 업계에서 사용하는 언론보도 결과보고서 양식과 유사하게 나오도록 진행하였습니다.

  • Requests 라이브러리를 이용한 크롤링
  • BeautifulSoup 라이브러리를 이용한 파싱 ( 구조화된 데이터 : 이 경우에는 html 부분을 의미합니다. )
  • 정규표현식 ( re )을 이용한 파싱 ( 덜 구조화된 데이터 : 이 경우에는 BeautifulSoup를 이용해서 파싱하지 못하는 javascript 부분을 의미합니다. )
  • webkit2png를 이용하여 커맨드 라인에서 웹 페이지 스크린샷 저장하기

결과물

1. *.json 형식으로 기사 정보 저장하기

[
  {
    "date": "2014.12.29",
    "media": "일간연예스포츠",
    "link": "http://www.esportsi.com/xe/108270",
    "title": "동창회 전문앱 ‘멤버북’, 출시 전부터 연이은 이용계약 체결로 주목"
  },
  {
    "date": "2014.12.24",
    "media": "데이타넷",
    "link": "http://www.datanet.co.kr/news/articleView.html?idxno=78237",
    "title": "동창회 전문앱 ‘멤버북’, 출시 전부터 연이은 이용계약 체결로 주목"
  },
  ...
]

2. 각각의 기사들에 대한 스크린샷

images/  
├── Screenshot_2014.12.26_캐드앤그래픽스_동창회_전문_앱_‘멤버북’,_활용도_및_가치_인정받아.png
├── Screenshot_2014.12.24_이데이뉴스닷컴_동창회_전문앱_‘멤버북’,_출시.png
├── Screenshot_2014.12.24_중앙통신뉴스_연말이면_생각나는_학창시절_동창들_"동창회_전문앱_‘멤버북'"_으로_해결.png
├── Screenshot_2014.12.24_시사타임즈_동창회_전문앱_‘멤버북’,_성균관대_등_11개_대학원과_계약_체결.png
├── Screenshot_2014.12.24_뉴스에이_동창회_전문앱_‘멤버북’,_출시_전부터_주목.png
...

개발 이슈

#01. BeautifulSoup를 이용한 구조화된 데이터 파싱하기

beautifulSoup는 파싱을 위한 파이썬 라이브러리 입니다. 다음 기사 페이지에서 각각의 기사 섹션을 파싱해서 다시 각각의 정보를 파싱하도록 하겠습니다.

data = requests.get(TARGET_URL)  
data = bs4.BeautifulSoup(data.text)

articles = data.findAll("div", attrs={'class': 'cont_inner'})

for article in articles:  
    title_and_link = article.findAll("a")[0]
    title = title_and_link.text.encode('utf-8')
    link = title_and_link["href"]

    date_and_media = str(article.findAll("span", attrs={'class': 'date'})[0])
    date = date_and_media.split("\n")[1]
    media = date_and_media.split("\n")[2].split("</span> ")[1]

이렇게 2번에 걸쳐서 파싱을 하게 됩니다.

  1. 페이지에 대해서 각각의 기사를 파싱 : articles = ...
  2. 기사에 대해서 각각의 정보를 파싱 : for article in articles: { ... }

#02. 정규표현식(re)를 이용한 구조화되지 않은 데이터 파싱하기

사실 딱 "#01. BeautifulSoup를 이용한 구조화된 데이터 파싱하기" 까지 진행하면 모든 기사에 대한 정보를 가져올 수 없습니다. 기사가 10개가 넘어가는 경우 각각의 페이지에 대해서 크롤링을 진행하고, 크롤링된 각각의 페이지에 대해서 기사를 파싱해야 합니다. 사실 이 부분은 #01번 이슈와 크게 다르지는 않을거라고 예상했습니다.

하지만, 다음 기사 섹션에서 pagination의 구현이 서버단에서 처리되는것이 아니라 자바스크릡트를 이용해서 클라이언트단에서 처리되게 해두어 문제가 발생했습니다. 즉, 웹 크롤러인 Requests.get(...)를 이용해서 데이터를 가져와도 실제로 자바스크립트를 실행하지는 않기에 페이지 부분이 비어있는 문제가 발생하였습니다.

In  : data.find("div", attrs={'class': 'paging_comm'})  
Out : <div class="paging_comm"></div>  

따라서, 이러한 경우에 해결방법은 크게 2가지가 있을 것 같습니다.

  1. Selenium 등을 이용하여 실제로 웹 브라우저 처럼 동작하게 한 후, 다시 페이지를 표시하는 부분을 파싱한다.
  2. 자바스크립트의 특정 부분을 파싱하여, 역으로 페이지를 계산한다.

다음 기사 섹션의 경우에는 쉽게(?) 페이지를 계산할 수 있도록 되어있어서, 2번 자바스크립트를 파싱하는 방법으로 진행하였습니다. 크롤링된 데이터를 살펴보면 다음과 같은 부분이 있습니다 :

<script type="text/javascript">  
(function(){
var pagingInstance = new SF.M.paging({  
lpp: 10,  
totalCount: 11,  
currentPage: 1,  
wrapperEl: daum.$$(".paging_comm")[0],  
endType: false,  
sCode: "NS",  
aCode: "PGDA",  
pageName: "p",  
baseUrl: location.search,  
additionalParam: {DA: "PGD"}  
});
pagingInstance.render();  
})();
</script>  

이 부분에서 totalCount: 11을 파싱하면 역으로 페이지수를 계산할 수 있겠다는 생각을 했습니다. BS4를 이용한 방법으로는 도저히 내부 변수까지 파싱할 수 없어서 정규표현식("totalCount: [0-9]+")을 이용하여 파싱하고, 데이터를 가져왔습니다.

data = bs4.BeautifulSoup(data.text)  
match = re.search("totalCount: [0-9]+", data.text)  
total_count = int(match.group(0).split("totalCount: ")[1])  
pages = total_count / 10 + 1  

이렇게 파싱된 페이지수 값을 가지고 다시 각각의 페이지에 대해서 크롤링을 진행하여 모든 기사에 대한 정보를 얻을 수 있었습니다.

for page in range(1, pages+1):  
    TARGET_URL = BASE_URL + "&p=" + str(page)
    data = requests.get(TARGET_URL)
    data = bs4.BeautifulSoup(data.text)
    ...

#03. 웹페이지 스크린샷 찍기

웹페이지 스크린샷은 webkit2png라는 라이브러리를 사용했습니다. 원래는 파이썬 모듈로 import해서 사용할 계획이였으나 문서화가 잘 되어있지 않아 subprocess를 이용하여 진행하였습니다.

subprocess.call([  
            "webkit2png",
            "-F",   # only create fullsize screenshot
            "--filename=temp",
            "--dir=images",
            link
        ])

원래 목표는 스크린샷 이름을 Screenshot_2014.12.26_캐드앤그래픽스_동창회_전문_앱_‘멤버북’,_활용도_및_가치_인정받아.png 이렇게 저장하려고 하였으나 webkit2png의 인코딩 문제로 --filename 옵션에 한글이 들어가면 제대로 결과물을 만들어내지 못하는 문제가 있었습니다. 나중에 풀리퀘스트 날려야겠다. 그래서 파일이름을 변경하는 부분을 별도로 개발하였습니다.

for filename in os.listdir("./images/"):  
    if filename.startswith("temp"):
            os.rename(
                os.path.join(os.getcwd(), "images", filename),
                os.path.join(os.getcwd(), "images",
                            "Screenshot_" + date + "_" + media + "_" + title.replace(" ", "_") + ".png")
                )

#04. 파이썬 한글 인코딩 문제

생각해보면 파이썬에서 한글을 처리하는 부분이 항상 문제가 되는 부분인 것 같습니다. 다만, 특이하게도(?) 이 프로젝트를 진행하면서 인코딩을 크게 신경쓰지 않아도 자동으로 해결이 되었습니다. 그래서 이 부분만 간단하게 살펴보고 포스팅을 마치겠습니다.

$ curl -I http://www.daum.net
Content-Type: text/html;charset=UTF-8  

다음(daum)의 경우에는 UTF-8 인코딩을 사용합니다. Requests, BeautifulSoup를 이용해서 크롤링, 파싱을 해보고 어디에서 인코딩이 자동으로 변경되는지 살펴보겠습니다.

import bs4  
import requests

data = requests.get("http://daum.net")  
bs4_data = bs4.BeautifulSoup(data.text)

print(data.encoding)          # UTF-8  
print(type(data.text))        # <type 'unicode'>  
print(type(bs4_data.text))    # <type 'unicode'>  

즉, Request 라이브러리의 경우에는 데이터를 받아오는 순간에는 리퀘스트 헤더에 있는 Content-Type의 인코딩을 그대로 따릅니다. 다만, 데이터를 가져오려고 하는 순간 내부적으로 유니코드로 변경해서 데이터를 전달합니다. 또한, BeautifulSoup의 경우에도 완전히 동일합니다. ( 물론 여기에서는 인풋도 유니코드, 아웃풋도 유니코드라 살펴볼 수 없습니다. ) 인풋의 인코딩이 어떻던 간에 데이터를 가져오는 순간 유니코드로 데이터를 전달합니다.

따라서, 두 라이브러리를 사용하면서는 크게 인코딩 문제를 신경쓰지 않고 진행을 해도 괜찮을 것 같습니다.

아마 1월 말까지는 지속적으로 파싱, 크롤링 관련해서 프로젝트를 진행할 것 같습니다. 개인적으로 해보고 싶은 프로젝트는 "네이버 카페 중고나라 크롤링하기" 프로젝트인데, 소프트웨어 마에스트로 프로젝트를 진행해나가면서 계속적으로 공유하겠습니다. 감사합니다.

나는 안수찬이다. 그러므로 나는 할 수 있다.

안수찬 @dobestan

안수찬 @dobestan

https://ansuchan.com/

서울대학교에서 컴퓨터공학을 전공하고, 오랜 기간 서비스 기획 및 개발을 해 왔습니다. 이러한 전문성을 인정받아 미래부 소프트웨어 마에스트로에 선정된 바 있습니다. 현재는 모바일 방송국, 퍼스트캔버스에서 컨텐츠로 새로운 가치를 그리고 있습니다. 나는 안수찬이다. 그러므로 나는 할 수 있다. me@ansuchan.com

View Comments...