각 사이트 뉴스 크롤링 with python + 뉴스룸 카톡 전송 자동화

2020. 6. 9. 18:54취미로 하는 개발

 

이 글은 마크다운으로 작성되었습니다. 


목차

  1. 뉴스룸이란 무엇인가
  2. 어떻게 자동화 하려고 했는가
  3. 어떻게 삽질했는가
  4. 어떻게 해결했는가
  5. 결과

1. 뉴스룸이란 무엇인가 📰

뉴스룸이란 무엇인가. 그것은 필자가 속해있는 오픈채팅방을 일컫는 말이다.

 

대충 이런 방

 

기본적으로 채팅방의 관리자가 매일 오전에 전날 혹은 당일의 IT뉴스, 시사뉴스, 각 신문사 헤드라인 등의 정보를 제공해주며 참가자들도 언제든 공유하고 싶은 뉴스를 올려 공유할 수 있다.

 

감사하게도 매일 뉴스를 올려주시는 기존 관리자님 덕분에 뉴스를 1분도 안 보는 내가 완전한 시사 무식쟁이가 되지 않을 수 있었으니 그 은혜가 참으로 크다 하겠다.

 

마침 기존 관리자분이 새 관리자를 구한다고 하셔서 그간의 은혜를 보은하고자 관리자를 이어받으려 하였으나, 아뿔싸! 매일 뉴스를 올리는 일은 나에게 너무 귀찮은 일이었다.

 

다행히 나같은 귀차니스트들이 사용할 수 있는 툴이 몇가지 떠올랐고, 매일매일 뉴스를 올리는 일을 자동화 하여 하루에 5분 더 뒹굴거리기로 결심하였다.

나는 하루 정도 투자하면 될 줄 알았다...

시작은 그랬다...😵

2. 어떻게 자동화 하려고 했는가 🛠

처음 생각한 방법은 다음과 같다

 

1.기존에 수동으로 가져오던 뉴스 소스 확인

 

2.AWS 에서 파이썬으로 뉴스 크롤링

 

3.크롤링한 뉴스 가져오는 코드는 PHP로 작성

(PHP에서 Python코드 실행 후 결과 가져오는건 몇 번 해봐서 이렇게 하면 되겠다 싶었다.)

 

4.매일 업데이트 된 뉴스를 편하게(?) 가져와서 채팅창에 올림(?)

 

??

 

나는 분명히 자동화를 원했는데 이건 반쪽짜리 자동화 같았다.

어차피 매일 가져올거면... 일일히 긁어오느냐 긁어온걸 복-붙 하느냐의 차이일 뿐.

 

서버에서 뉴스 크롤링 후, 바로 카톡방으로 쏴주면 📲 얼마나 편할까?

근데 그게 가능한가? 일단 찾아보기로 했다.

...

..

.

 

Android용 메신저봇R (카카오 봇/페메 봇/라인 봇) - APK 다운로드

Gboard 9.4.11.312687073-release-x86_64 Google LLC

apkpure.com

찾아보니 이런게 나왔다.

 

대충 흝어본 결과, 알림에 접근하는 권한을 얻고, 카톡 알림 Notification을 이용, 상단 알림창으로 메시지를 답장 하는 기능을 통해 정해진 스크립트를 사용하여 일종의 챗봇처럼 구동하게 만들어 주는 어플이었다.

 

언어는 다양하게 지원하는데, 자바스크립트자바를 모두 이용 할 수 있었다 ㄷㄷㄷ

 

라이노 엔진을 사용해서라고 한다.

 

 

라이노 (자바스크립트 엔진) - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전.

ko.wikipedia.org

라이노엔진은 자바로 만든 자바스크립트 엔진이라고 한다.

 

재미있는게 많구나...

 

 

메신저봇 가이드 - API(레거시)

연관 문서 메신저봇 가이드 - 소개 메신저봇 가이드 - API(레거시) 메신저봇 가이드 - 이벤트 리스너(레거시) 레거시 API 본 문서에서 설명하는 API는 앞으로 지원이 중단될 구형 API입니다. (신형 API�

violet.develope.kr

 

API 레퍼런스도 있었다.

//웹사이트의 HTML을 get하여 org.jsoup.nodes.Document로 반환합니다. (동기적)
org.jsoup.nodes parse(String url)

크.... 편하다. 머릿속에선 이미 개발이 끝난 기분이었다.

 

이 방법이 아니면 어떻게 구현할지 별 생각을 다 했다.

 

FCM으로 서버에서 뉴스 업데이트 알림을 받아서 안드에서 카톡 공유 인텐트를 띄워줄까도 생각했는데...

카톡 내에서 해결되면 제일 편하지!

 

이제 구현만 하면 된다.

 

3. 어떻게 삽질했는가 ⛑

먼저 기존에 뉴스를 가져오는 소스부터 살펴보았다.

 

[지난 밤 JTBC 뉴스룸 헤드라인 ]

[지난 밤 SBS 뉴스룸 헤드라인 ]

기타 KBS 등등...

 

SBS 뉴스

시청자와 함께 만들어 가는 뉴스, SBS 뉴스의 공식 유튜브 채널입니다.

www.youtube.com

 

JTBC News

Welcome to the official JTBC News Channel. Easy and Fun news channel 15! You will find the faster and more accurate news on JTBC.

www.youtube.com

연습겸 일단 긁어보았다.

 

셀레니움은 기존에 windows 환경에서 돌려본 적 있고 BeautifulSoup은 처음 써보는데,

 

 

Scrapy Vs Selenium Vs Beautiful Soup for Web Scraping.

A Complete Explanation about Scrapy, Selenium and Beautiful soup scraping tools.

medium.com

 

요로코롬 재밌는 글이 있어서 호기심이 생겨 한 번 써봤다. 다음엔 Scrapy를 써봐야겠다.

 

 

KBS.py

import requests
import sys
from bs4 import BeautifulSoup
import re

web_url = "https://www.youtube.com/user/NewsKBS"

# ...
#  ~~ 대충 중략 ~~
# ...

for title in titles:
    # print("뉴스룸 타이틀만 가져옴")
    # print(title.text)
    title_result = title.text
    if newsReview in title.text:
        if today in title.text:
            # print("뉴스룸 타이틀에 들어있는 html코드")
            # print(title)
            found = True
            review_news = title
            #리뷰 뉴스로 옮김
            break

# 톱뉴스 키워드 발견하면 바로 반환함
if found:
    found = False
    #발견 다시 초기화
    #print(today_topNews.a.text)
    newsTitle = review_news.text
    # print(newsTitle)
    #이게 제목
    # 2020년 6월 7일 (일) JTBC 뉴스룸 다시보기 - 곳곳서 터진다…이틀째 50명대 확진
    #href 안에 있는 텍스트만 추출 가능
    html = review_news
    # a 태그가 나온다
    #result set은 똑같음 bs 객체랑
    # print(review_news['href'])
    link = "https://www.youtube.com"+review_news['href']

    if len(link) > 25:
        found = True
    '''
    구동 원리는, 결과값 안에서 a 태그를 다시 찾아서, 그 안에 있는 href 요소값을 가져오는 것임.
    # 태그를 찾는건 find 나 select를 쓰고, attr를 가져오는 부분은 []를 사용해서 가져옴
    # ResultSet(bs4 결과값 양식. bs4 객체랑 같다) 에 태그를 붙여서 해당 태그 안의 내용만 가져올 수 있음.
    # ex = result.text // result.a
    # 결과값 보기
    # print("found")
    '''
    if found:#다시 한 번 체크

        req2 = requests.get(link)
        ## HTML 소스 가져오기
        html = req2.text
        soup = BeautifulSoup(html, 'html.parser')

        titles = soup.select(
            "#watch-description-text > p"
            )
        # print(titles[0].text)
        try:
            result = titles[0].text
        except:
            print("해당 날짜의 링크까지 접속했으나, 뉴스 내용을 받아오지 못했습니다. 다시 시도해보세요")
            exit(5)
        # print(result)

        result = re.sub('[·]+\s', lambda m: str("\n<br>"+m.group()), result)
        result = result[0:result.find("▣")]

        if(len(result)<50):
            print("뉴스 불러오기에 오류가 있는 것 같습니다. 확인이 필요합니다.")
            exit(2)

        print(title_result)
        print(result)

    else:
        print("KBS 뉴스 링크 내에서 타이틀 발견 실패")
else:
    print("KBS 오늘자 뉴스 타이틀 발견 실패")

 

아주 누더기 같은 코드를 짜서 돌렸다.

 

개발자 도구 - Copy Selecter 로 가져온 부분을 soup.select 안에 넣었는데 계속 못 가져왔다.

뭔가 이상해서 가져온 html코드부터 보니 형식이 달랐다.

 

셀레니움은 편했는데 bs4는 처음 써보니까 뭔가 어색하기도 하고 그렇다....

 

아무튼 그렇게 결과를 가져오는데, 중간 중간 제대로 못 받아오는 경우가 있었다.

결과값을 까보니 javascript만 가득... html 코드는 하나도 없는 경우가 있었다.

 

뭐가 문제인지 찾으면서 몇 번 더 테스트 해보던 중, 너무 잦은 빈도로 테스트를 해버린 것인지 바로 차단당했다 ^^

 

Youtube - Robots.txt

robots.txt file for YouTube

Created in the distant future (the year 2000) after

the robotic uprising of the mid 90's which wiped out all humans.

User-agent: Mediapartners-Google*
Disallow:

User-agent: *
Disallow: /channel/*/community
Disallow: /comment
Disallow: /get_video
Disallow: /get_video_info
Disallow: /live_chat
Disallow: /login
Disallow: /results
Disallow: /signup
Disallow: /t/terms
Disallow: /timedtext_video
Disallow: /user/*/community
Disallow: /verify_age
Disallow: /watch_ajax
Disallow: /watch_fragments_ajax
Disallow: /watch_popup
Disallow: /watch_queue_ajax

Sitemap: https://www.youtube.com/sitemaps/sitemap.xml

애초에 로봇 정책적으로도 안 되기도 했고...

방향을 선회하여 각 뉴스 홈페이지에서 직접 접근하기로 했다.


또한 크롤링 툴을 셀레니움으로 변경하였다

 

각 언론사 사이트의 경우, AJAX를 통한 값을 받아오는 동적 웹으로 구성되어 있어서 이 부분에 확실히 강점을 가지고 있으며 이미 한 번 윈도우에서 사용해본 셀레니움으로 변경하였다.

 

ubuntu에서의 설정은 여기서 참고했다.

 

파이썬 Selenium linux 환경 구축하기 (ubuntu)

크롬 설치하기 https://linuxize.com/post/how-to-install-google-chrome-web-browser-on-ubuntu-18-04/ 크롬 버전 체크하기 google-chrome --version 크롬드라이버 설치하기 wget -N http://chromedriver.storage..

ducj.tistory.com

 

설치 완료

 

참고로 내가 설치한 크롬 버전은 83.0.4103.97 였는데, 해당 드라이버가 잡히지 않았다

http://chromedriver.storage.googleapis.com/ 로 접속하여 살펴보니 

 

대충 비슷한 버전은 있는 것 같다

제일 비슷한 버전은 저거인 것 같으니 버전명을 바꿔 설치했다.

 


크롤링에 앞서 긁으려는 페이지의 robots.txt를 전부 확인했다.

오... 매우 관대한 정책이 나온다.

 

 

KBS - Robots.txt

User-agent: *
Disallow:

국영방송국 KBS의 위엄

바로 긁어본다.

 

KBSn.py

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from time import sleep

newsText = ""

# 셀레니움은 크롬 세션을 통해서 실행되기 때문에 드라이버 설정을 먼저 잡아준다
options = Options()
options.add_argument('--headless')
options.add_argument('--no-sandbox')
driver = webdriver.Chrome(chrome_options=options, executable_path="/usr/bin/chromedriver")

driver.implicitly_wait(3)
driver.maximize_window()

# 어디로 갈지 말해준다
driver.get("http://news.kbs.co.kr/vod/program.do?bcd=0001")
# ...
#  ~~ 대충 중략 (selector로 찾는 과정) ~~
# ...
for item in search:
    title = item.find_element_by_css_selector("a > span.desc > em")
    # print(title.text)
    if title.text == "[뉴스9 헤드라인]":# 뉴스 9 헤드라인을 찾았다.
        newsText += "<br> KBS " + title.text
        url = item.find_element_by_tag_name("a").get_attribute("href")
        # 찾은 링크로 들어간다
        driver.get(url)
        sleep(0.5)
        content = driver.find_element_by_id("cont_newstext").text
        # 본문만 딱 긁으면 내가 원하는 부분
        newsText+="<br>"+content
        # print(content)
        break
    #     print(i.text)

driver.close()
#다 쓴 크롬은 닫아주자
print(newsText)

 

코드는 아직 엉망이지만 확실히 구현이 짧으니 편하긴 하다.


이제 원하는 만큼 뉴스는 다 긁어왔고 출력도 잘 된다!

 

 

PHP에서 실행하고 결과값만 출력해주면 되겠지?ㅎㅎㅎ

 

즐거운 마음으로 PHP코드를 작성했다.

 

exec("cd /home/ubuntu/NewsRoom && sudo python3 KBSn.py", $jb_array, $status);
//echo $status;

$news_total = "";
foreach ($jb_array as $line) {
    $line .= '<br>';
    $news_total .= $line;
}

echo $news_total;

 

????????

 

결과가 안 나온다...? 뭐지???


그리고 이 문제로 약 6시간을 씨름하게 되었다...😱

 

4. 어떻게 해결했는가💡

 

이전에 PHP에서 Python 파일을 실행 후 결과를 받아오는 코드를 짰던 경험이 있었다.

그때 가장 고생했던 부분은 권한 문제였기 때문에 권한 부분을 살펴보았다.

 

권한을 주는 코드를 추가하기도 하고, root에서 파일에 권한을 직접 줘보기도 하고, chown으로 사용자도 변경해보려다가 저번에 한 번 aws날린 트라우마가 생각나서 접었다.

 

가장 큰 어려움은 디버깅이 힘들다는 부분이었는데, stackoverflow를 전전하던 중,

실행 코드 뒤에 2>&1 를 붙이면 뭐가 원인인지 알 수 있다는 내용을 알았다.

 

바로 확인해보니 뭐가 문제인지 잘 나온다!

UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)

근데 이게 뭔데... 처음 보는 에러다.

 

대충 보니 유니코드 에러고, ascii 형식은 다른 형식으로 인코딩 되는데에 문제가 있다 뭐 이런 얘기 같다.

인코딩 형식 에러라고 생각하고 그쪽 방향으로 찾기 시작했다.

 

특이한 부분은 해당 에러 검색시 거의 모든 글에서 파이썬 2.7 버전의 에러로 간주하고 있었으며, 내가 사용하는 3.6.9 버전에서는 생길 수 없는 에러였다는 것이다.

(기본 인코딩이 변경되면서 해결된 문제)

 

가정 1 : python3 으로 실행하게 했음에도 python2.x로 실행중?

 

ubuntu 18.04 LTS 버전을 사용중인데, 해당 버전에 기본으로 깔려있는 python 버전이 2.x 였던게 기억이 났다.

 

바로 버전 확인.

 

파이썬 내에서 버전 확인하는 코드를 넣어 PHP에서 실행해 보았다.

import sys

print(sys.version)

결과는? 3.6.9 맞게 나온다.... 첫 번째 가정 실패.

 

가정 2 : apache2 혹은 php 의 인코딩이 잘못되었는가?

 

1. php.ini 내의 기본 인코딩 설정 UTF-8 확인

 

2. apache2 기본 인코딩 UTF-8 로 설정

 

결과는? 아직도 같은 에러 메시지가 나온다... 두 번째 가정도 실패.

 

가정 3 : 가정이고 뭐고 그냥 계속 구글링

 

 

계속 구글링 해 본 결과,

https://stackoverflow.com/questions/7362576/php-system-python-and-utf-8

 

UnicodeEncodeError: 'ascii' codec can't encode characters in position 360-362: ordinal not in range(128)

I tried running python via PHP I kept getting UnicodeEncodeError: 'ascii' codec can't encode characters in position 360-362: ordinal not in range(128) I tried php $command = "python ".publi...

stackoverflow.com

위 두 개의 답변에서 힌트를 얻었다.


나와 같은 상황(PHP 에서 Python 코드 실행)에서 에러가 발생한 사람들이었다.

답변을 요약하자면 다음과 같다.

 

  • 파이썬 출력 인코딩은 sys.stdout.encoding 에 명시된 인코딩을 따른다.
  • 그런데 파이썬이 인코딩을 감지하지 못하면, 해당 속성을 None으로 간주하고
    print문은 ascii 코덱을 자동으로 하여 출력된다.
  • 이 문제를 해결하기 위해 print문에 명시적으로 인코딩을 설정해주거나,
    실행 변수로 PYTHONIOENCODING=utf-8를 추가해라

그리하여 PYTHONIOENCODING=UTF-8 가 추가되었고,

exec("cd /home/ubuntu/NewsRoom && sudo PYTHONIOENCODING=UTF-8 python3 KBSn.py 2>&1", $jb_array, $status);
//PYTHONIOENCODING=UTF-8 이것때문에 개고생함.

결과는? 두근두근

“위안부 운동 대의 지켜야…기부금 투명성 높일 것”

문재인 대통령이 정의기억연대를 둘러싼 논란과 관련해 위안부 운동의 대의는 지켜져야 한다고 강조했습니다. 또 기부금 모금의 투명성을 높이기 위해 기부금 통합관리시스템을 구축하겠다고 말했습니다.  
...

 

잘 나온다! 휴...

 

JTBC, SBS, 중부일보, ZDnet을 마찬가지로 작업해준다.


추가적으로 해외 영문 뉴스에 대한 수요가 있어서, Techcrunch의 뉴스도 가져왔다.

 

근데 타이틀이 다 영어로 나오다보니(당연히) 영어 울렁증이 생겼다.

 

울렁증 극복을 위해 뉴스 타이틀을 구글 번역 후 원문 아래에 붙여주었다.

 

참고로 구글 번역api 는 2가지가 있다

  • googletrans(무료)
  • Google Cloud Translation API(유료)

돌려보니 googletrans의 번역 퀄리티가 너무 쓰레기같았다...ㅠㅠ (거의 왈도체 수준...)


아예 다른 뜻으로 해석하는 바람에 어쩔 수 없이 Google Cloud Translation API를 사용해서 구현했다.


다행히 프로젝트 키는 저번에 사용했던 키가 있어서 그대로 사용 할 수 있었다.


Python에서 Google Cloud Translation API 사용하는건 찾아보면 바로 나오니까 바로 결과로 넘어가자.

 

 

TechCrunch 오늘의 뉴스

Lilium adds $35M from Baillie Gifford at a $1B+ valuation for its electric aircraft taxi service

\[Lilium, Baillie Gifford로부터 전기 항공기 택시 서비스에 대한 $ 1B 이상의 평가로 $ 35M 추가\]

Silverfin wants to modernize accounting software with its cloud service

\[Silverfin, 클라우드 서비스로 회계 소프트웨어 현대화\]

Startup dilution done right: Lemonade IPO edition

\[시작 희석 완료 : 레모네이드 IPO 에디션\]

ClassTag raises $5M for parent-teacher communication

\[ClassTag, 학부모-교사 커뮤니케이션을 위해 5 백만 달러 모금\]


 

잘 나오고 번역 퀄리티도 훨씬 올라갔다.

 

이제 위에서 나온 어플에 들어갈 스크립트를 손봐준다.

 

Utils.getKbsNews = function (month , date) {  
	try {  
		var topNews = org.jsoup.Jsoup.connect("http://주소/Kbs.php?month="+month+"&date="+date).get();  
		topNews = topNews.select("body").toString();  
		topNews = topNews.replace(/(<(\[^>\]+)>)/ig,"");//정규식으로 태그 제거  
		return topNews;  
		} catch (e) {  
		return false;  
	}  
};


​ 해당 어플의 사용법은 api 레퍼런스나 다른 사용자들이 올린 게시글을 참고하였다.


jsoup으로 찾고자 하는 월, 일 데이터를 Get으로 보낸 후,

동기적으로 가져온 데이터에서 태그만 제거하여 가져오는 코드이다.

 

tils.getNews = function(mm, dd){  
var top = "";  
var sbs = "";  
var kbs = "";  
var jtbc = "";  
var zdn = "";  
var tech = "";  
top = Utils.getTopNews(mm,dd);  
sbs = Utils.getSbsNews(mm,dd);  
kbs = Utils.getKbsNews(mm,dd);  
jtbc = Utils.getJtbcNews(mm,dd);  
zdn = Utils.getZdnNews(mm,dd);  
tech = Utils.getTechNews(mm,dd);  
return "\[어젯밤 주요뉴스\]\\r\\n"+ sbs + kbs + jtbc  
"\[해외 영문 뉴스\]\\r\\n"+ tech + zdn + top;  
};

 

이런 식으로 각 데이터를 모아모아 뉴스 String으로 모아준다.

 

 

대충 봐도 된다.

 

명청이 공기계에 구축한 챗봇이자 뉴스 트리거 역할을 한다.

 

1. while문으로 전송 타이밍을 intervalTime마다 계속 확인하고

 

2. 전송 타이밍이 되면 전송 후 다음날까지 다시 타이밍을 기다린다

* 전역 변수인 Loop를 변경하므로써 On/Off가 가능하다.

 

불행하게도 이 어플엔 답장은 가능하지만 스스로 채팅을 통해 실행시키는 기능이 없어서, 명청이한테 채팅하면 2초 후 똑같은 내용을 답장하게 만들어야 했다...

5. 결과🖨

 

테스트 채팅방에서 테스트 해 본 결과, 데이터를 잘 가져온다.


다만 PHP 파일 내에서 exec로 파이썬 크롤링을 실행하면서 시간이 걸리는 편이라,

대략 1분 내외의 소요시간이 있었다.

 

이 부분은 하루 한 번 업데이트인 만큼 감안 할 수 있는 부분이라고 생각된다.(현재는 수정됨)

 

또한 화면 꺼짐 상태에서 CPU가 느려지면서 Thread.sleep이 규칙적으로 일어나지 않았는데,

Device의 wakelock 때문이었다.

 

다행히 해당 문제를 찾아보니 어플 내 스크립트 코드 내에서 해결 할 수 있는 문제여서 해결했다.

 

 

20분 간격으로 실행 여부를 판단하는 중...

 

 

테스트는 끝났고 내일부터 실제 뉴스룸에서 구동시키면서 수정해 나갈 예정이다.

 

구현 끝!

 

하루 반. 대략 11~14시간 걸렸다.

 

 

 

P.S. 잦은 호출을 연속으로 하는 경우, AWS EC2 micro 무료 인스턴스가 과부하가 걸리는 현상이 있었다.

 

너무 잦은 호출은 삼가하도록 하자

 

 

 

P.S. 2020. 08. 기준

webdriver chrome -> firefox

aws -> home server

php -> python + django

형태로 변경되었음.

 

참고 ▽

 

Ubuntu 20.04 LTS 홈 서버 구축기 (2)

저번 편에서는 ubuntu20.04 LTS 버전을 내 서버에 설치하는 부분까지 진행했다. 사실 이 시리즈는 친절하게 설치 방법을 알려준다는 목적보다는, 내 스스로의 기록과 회고 측면에 가깝기 때문에 중��

nookpi.tistory.com