네이버 뉴스 댓글 크롤링 대시보드 만들기 with Heroku

Page content

강의 홍보

1. 개요

  • 기존 웹크롤링은 주로 코드에 기반한 소개가 주를 이루었음
  • 본 장에서는 가급적 사용자 기준에 맞춰서 뉴스 URL만 입력하면 댓글 수집할 수 있는 기능 소개함

2. 라이브러리

  • 크롤링 및 대시보드 작업을 위한 필수 라이브러리는 다음과 같음 (requirements.txt)
colorama==0.4.4
dash==1.21.0
gunicorn==20.1.0
numpy==1.19.4
pandas==1.2.0
beautifulsoup4==4.9.3
openpyxl==3.0.7
requests==2.26.0
  • 위 파일을 프로젝트의 가장 최상단에 위치시켜 놓는다.
  • 설치 진행 시에는 pip install -r requirements.txt 해도 좋고, 아니면 개별적으로 설치를 해도 좋다.

3. 코드 설명

  • 본장에서는 디테일한 코드 설명은 넘어가도록 한다.

(1) 크롤링 코드

  • 먼저 크롤링 코드는 다음과 같다.
# 크롤링 라이브러리
from bs4 import BeautifulSoup
import requests
import re

# 데이터프레임
import pandas as pd

# 샘플 URL을 적용한다. 
url = "https://news.naver.com/main/read.naver?mode=LSD&mid=shm&sid1=100&oid=022&aid=0003609357" 

def get_df(url):
    # 댓글을 달 빈 리스트를 생성합니다.
    List = []
    url = url
    oid = url.split("oid=")[1].split("&")[0]
    aid = url.split("aid=")[1]
    page = 1
    header = {
        "User-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36",
        "referer": url,

    }
    while True:
        c_url = "https://apis.naver.com/commentBox/cbox/web_neo_list_jsonp.json?ticket=news&templateId=default_society&pool=cbox5&_callback=jQuery1707138182064460843_1523512042464&lang=ko&country=&objectId=news" + oid + "%2C" + aid + "&categoryId=&pageSize=20&indexSize=10&groupId=&listType=OBJECT&pageType=more&page=" + str(
            page) + "&refresh=false&sort=FAVORITE"
    # 파싱하는 단계입니다.
        r = requests.get(c_url, headers=header)
        cont = BeautifulSoup(r.content, "html.parser")
        total_comm = str(cont).split('comment":')[1].split(",")[0]

        match = re.findall('"contents":([^\*]*),"userIdNo"', str(cont))
        # 댓글을 리스트에 중첩합니다.
        List.append(match)

        # 한번에 댓글이 20개씩 보이기 때문에 한 페이지씩 몽땅 댓글을 긁어 옵니다.
        if int(total_comm) <= ((page) * 20):
            break
        else:
            page += 1

		# 
    def flatten(l):
        flatList = []
        for elem in l:
        # if an element of a list is a list
        # iterate over this list and add elements to flatList
            if type(elem) == list:
                for e in elem:
                    flatList.append(e)
            else:
               flatList.append(elem)
        return flatList

    # 리스트 결과입니다.
    # print(flatten(List))

    # convert dataframe
    data = pd.DataFrame(flatten(List), columns=["기사댓글"])
    data = data.rename_axis("index").reset_index()

    # write_excel
    # data.to_excel("news_comments.xlsx", sheet_name="Sheet1")
    return data

# data = get_df(url) # URL 테스트 시, 실행 
data = pd.DataFrame({"index": [0], "기사댓글": ["댓글"]}) # 앱 배포시 실행
# print(data.head())
  • 중간에 주석처리 한 것을 풀면 된다.
  • 해당 코드는 app.py 또는 일반적인 주피터 노트북, 구글 코랩에서 실행해도 된다.
  • 수집된 댓글을 확인해보니, 중간에 삭제된 글은 댓글 수집 시, 제외되는 것을 확인할 수 있다.

dash-crawling-04.png

  • 이제 저 코드를 Dash에 포함시키도록 한다.

(2) 대시 코드

  • 이번에는 대시 코드를 적용해본다.
  • 기존 코드에 이어서 작성을 해본다. (app.py)
# 크롤링 라이브러리
from bs4 import BeautifulSoup
import requests
import re
import pandas as pd

# dash 라이브러리
import dash
import dash_core_components as dcc
import dash_html_components as html
import dash_table
from dash.dependencies import Input, Output, State

# 댓글을 달 빈 리스트를 생성합니다.
url = "https://news.naver.com/main/read.naver?mode=LSD&mid=shm&sid1=100&oid=022&aid=0003609357"

def get_df(url):
    # 댓글을 달 빈 리스트를 생성한다.
    List = []
    url = url
    oid = url.split("oid=")[1].split("&")[0]
    aid = url.split("aid=")[1]
    page = 1
    header = {
        "User-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36",
        "referer": url,

    }
    while True:
        c_url = "https://apis.naver.com/commentBox/cbox/web_neo_list_jsonp.json?ticket=news&templateId=default_society&pool=cbox5&_callback=jQuery1707138182064460843_1523512042464&lang=ko&country=&objectId=news" + oid + "%2C" + aid + "&categoryId=&pageSize=20&indexSize=10&groupId=&listType=OBJECT&pageType=more&page=" + str(
            page) + "&refresh=false&sort=FAVORITE"
    # 파싱하는 단계입니다.
        r = requests.get(c_url, headers=header)
        cont = BeautifulSoup(r.content, "html.parser")
        total_comm = str(cont).split('comment":')[1].split(",")[0]

        match = re.findall('"contents":([^\*]*),"userIdNo"', str(cont))
        # 댓글을 리스트에 중첩합니다.
        List.append(match)

        # 한번에 댓글이 20개씩 보이기 때문에 한 페이지씩 몽땅 댓글을 긁어 옵니다.
        if int(total_comm) <= ((page) * 20):
            break
        else:
            page += 1

    def flatten(l):
        flatList = []
        for elem in l:
        # if an element of a list is a list
        # iterate over this list and add elements to flatList
            if type(elem) == list:
                for e in elem:
                    flatList.append(e)
            else:
               flatList.append(elem)
        return flatList

    # 리스트 결과입니다.
    # print(flatten(List))

    # convert dataframe
    data = pd.DataFrame(flatten(List), columns=["기사댓글"])
    data = data.rename_axis("index").reset_index()

    # write_excel
    # data.to_excel("news_comments.xlsx", sheet_name="Sheet1")
    return data

# data = get_df(url)
data = pd.DataFrame({"index": [0],
                     "기사댓글": ["댓글"]})
# print(data.head())

external_stylesheets = [
    {
        "href": "https://fonts.googleapis.com/css2?"
                "family=Lato:wght@400;700&display=swap",
        "rel": "stylesheet",
    },
]
app = dash.Dash(__name__, external_stylesheets=external_stylesheets,
                suppress_callback_exceptions=True,
                prevent_initial_callbacks=True)

app.title = "네이버 크롤링"
server = app.server # 해당 코드를 새롭게 추가한다.

app.layout = html.Div(
    children = [
        html.Div(
            children = [
                html.H1(children="네이버 뉴스 댓글 크롤링 싸이트", className="header_title", ),
                html.P(children="temp ~~~ ", className="header_description", ),
            ],
            className='header',
        ),
        # URL html.Div
        html.Div(
            children=[
                html.Div(
                    children=[
                        html.Div(children="네이버 뉴스 주소를 입력하여 주세요", className="menu-title"),
                        dcc.Input(id="naver_news_url",
                                  placeholder="URL을 입력하여 주세요",
                                  className="naver_news_url"),
                        html.P("예: https://news.naver.com/main/read.naver?mode=LSD&mid=shm&sid1=100&oid=586&aid=0000027892",
                               className="url_sample"),
                        html.Button('크롤링 시작', id='submit-val', n_clicks=0),
                    ]
                ), # html.Div
            ], # children
            className="menu"
        ), # URL html.Div
        html.Div(
            children=[
                html.Div(
                    children=dash_table.DataTable(
                        id = "data_id",
                        columns=[{"id":c, "name":c} for c in data.columns],
                        data = [],
                        style_cell={'textAlign': 'left',
                                    'whiteSpace': 'normal',
                                    'fontWeight': 'normal',
                                    'height': 'auto'
                                    },
                        style_header={
                            'backgroundColor': 'black',
                            'fontWeight': 'bold',
                            'color': 'white'
                        },
                        export_format="xlsx",
                    ) # children
                ) # Table
            ],
            className="wrapper",
        ), # html.Div
    ]
)

# URL 텍스트
@app.callback(
    Output(component_id="data_id", component_property="data"),
    [Input(component_id="submit-val", component_property="n_clicks")],
    [State(component_id="naver_news_url", component_property='value')]

)

def update_output_url(n_clicks, input_url):
    global data
    if n_clicks > 0:
        data = get_df(input_url)
    else:
        print("None")
    return data.to_dict('records')

if __name__ == "__main__":
	app.run_server(debug=True)
  • 본 코드에서의 핵심은 @app.callbackupdate_output_url 함수 영역이다.
  • 또한, global data 는 기 저장된 data 객체를 함수 내에서 쓰기 위함이다.
    • 코드 용어로는 globaldata 를 전역변수로 쓰겠다는 것을 의미한다.

(3) CSS 소스 적용

  • CSS 적용 파일은 아래와 같이 적용했다.
body {
    font-family: "Lato", sans-serif;
    margin: 0;
    background-color: #F7F7F7;
}

.header {
    background-color: #222222;
    height: 288px;
    padding: 16px 0 0 0;
}

.header_title {
    color: #FFFFFF;
    font-size: 48px;
    font-weight: bold;
    text-align: center;
    margin: 0 auto;
}

.header_description {
    color: #CFCFCF;
    margin: 4px auto;
    text-align: center;
    max-width: 384px;
}

.menu {
    height: 130px;
    width: 912px;
    display: flex;
    justify-content: space-evenly;
    padding-top: 24px;
    margin: -80px auto 0 auto;
    background-color: #FFFFFF;
    box-shadow: 0 4px 6px 0 rgba(0, 0, 0, 0.18);
}

.naver_news_url {
    width: 100%;
}

.menu-title {
    margin-bottom: 6px;
    font-weight: normal;
    font-style: bold;
}

.url_sample {
    font-weight: normal;
    font-style: italic;
    font-size: 80%;
    color: #079A82;
}

.card {
    margin-bottom: 24px;
    box-shadow: 0 4px 6px 0 rgba(0, 0, 0, 0.18);
}

.wrapper {
    margin-right: auto;
    margin-left: auto;
    max-width: 1024px;
    padding-right: 10px;
    padding-left: 10px;
    margin-top: 32px;
}

(4) 로컬호스트 앱 실행

  • 이제 파일의 구조를 확인해본다.
C:.
│  app.py
│  requirements.txt
├─assets
│      favicon.ico
│      style.css
  • 이제 app.py를 실행한다.
$python app.py
Dash is running on http://127.0.0.1:8050/

 * Serving Flask app "app" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: on
None
  • 기존에 입력한 URL과 동일하게 나타나는 것을 확인할 수 있다.

dash-crawling-05.png

  • 그리고, Export 버튼을 누르면, excel 파일이 출력될 것이다.

dash-crawling-06.png

  • 이 때, 처음 댓글의 개수가 1개 정도 차이가 나는 것을 확인할 수 있었는데, 이 글을 작성하는 중간에 누군가가 또 삭제 하였기 때문에 차이가 발생했을 뿐이다.
  • 즉 로컬호스트에서는 이제 정상적으로 배포가 되었고, 마지막으로 Heroku를 통해서 배포를 진행하도록 한다.

4. 웹 배포하기 with Heroku & Git

  • 웹 배포를 위해서는 Heroku & Git 설치를 해야 한다.
  • 각 버전에 맞는 것을 설치 진행한다.
    • Heroku & git 정상적으로 설치 후 버전 확인을 화면 다음과 같이 확인이 가능하다.
(venv) $ echo 'export PATH="/usr/local/homebrew/opt/heroku-node/bin:$PATH"' >> /Users/evan/.bash_profile
(venv) $ git --version
git version 2.30.0
(venv) $ heroku --version
 ›   Warning: Our terms of service have changed: https://dashboard.heroku.com/terms-of-service
heroku/7.56.1 darwin-x64 node-v12.21.0
  • 이번에는 runtime.txt 파일을 프로젝트 폴더 내 상단 위치에 생성한 후, 다음과 같이 입력한다.
    • 해당 버전은 독자의 버전과 다를 수 있으니 확인 후 입력한다.
python-3.8.7
  • 이번에는 Procfile 을 생성한 후, 아래 텍스트를 추가한다.
    • Heroku app에서 gunicorn 서버로 대시보드를 운영한다는 뜻이다.
web: gunicorn app:server
  • 이번에는 .gitignore 파일을 생성하여 불필요한 파일들을 추적하지 않도록 한다.
    • 해당 파일만 git commit 을 진행한다.
venv
*.pyc
.DS_Store # 맥 사용자만 추가
  • 전체적인 프로젝트의 파일 구조는 아래와 같다.
C:.
│  app.py
│  Procfile
│  README.md
│  requirements.txt
│  runtime.txt
│
├─.idea
│  │  .gitignore
│  │  dash-crawling.iml
│  │  dbnavigator.xml
│  │  misc.xml
│  │  modules.xml
│  │  vcs.xml
│  │  workspace.xml
│  │
│  └─inspectionProfiles
│          profiles_settings.xml
│
├─assets
       favicon.ico
       style.css
  • 이제 마지막으로 heroku에 앱 배포를 시작한다.
    • 사전에 회원가입 등 진행이 되어 있어야 한다.
    • 이 때, 중요한 것은 프로젝트 폴더명과 Heroku App 이름이 동일해야 한다. your-project
$ heroku create your-project # 각자의 이름을 추가한다. 
  • 이제 heroku login을 진행한다.
$ (venv) heroku login
heroku: Press any key to open up the browser to login or q to exit: 
Opening browser to https://cli-auth.heroku.com/auth/cli/browser/9320abcd-b8c6-406d-9198-ca14d1e59a26?requestor=SFMyNTY.g2gDbQAAAA4yMjEuMTU3LjM3LjIxNm4GAGgtTBB7AWIAAVGA.GlyVc8jbyiW6NG0MVzCS0bOjtzBWvYRfjB9-gnkQaoQ
Logging in... done
Logged in as your_email_address
  • 웹 화면에 로그인 한 후, 본인의 프로젝트를 확인한다.

dash-crawling-07.png

  • 이제 배포를 진행한다. 소스코드는 Deploy 메뉴를 클릭하면 확인할 수 있다.

dash-crawling-08.png

  • 다만 필자는 아래와 같이 명령어를 살짝 바꿔서 진행하고 있다.
$ git add .
$ git commit -am "make it better"
$ git push 
$ git push heroku main
  • 이제 완성된 heroku-app을 확인한다.
  • 웹 크롤링은 완전한 합법은 아닙니다. 따라서, 샘플용으로 제작했을 뿐입니다.
  • [Settings]-[Maintenance Mode] 에서 쉽게 온오프를 할 수 있다.

dash-crawling-09.png

dash-crawling-10.png

5. Reference