Qiitaのトレンド情報を保存しておくDocker環境を作ろう！

Qiitaのトレンド情報を保存しておく環境をDockerで作成しました。
基本的にコンテナを立ち上げていれば、毎日勝手にスクレイピング処理が走り、JSON化したトレンド情報を保存してくれます。この記事は以下のような方におすすめです。

- Qiitaのトレンドを分析しておきたいな
- Pythonの勉強を少しやってみたいな
- Dockerちょっと触ってみたい

## ※保存しておくQiitaのJSONフォーマットについて

- author（トレンド入りした著者の一覧
- list（トレンド記事の一覧
- tag（トレンドの記事に付けられたタグ一覧

実際に保存しておくJSONの中身は以下のようになっています。
### `author`： Qiitaにトレンド入りした著者を取得する
著者のユーザーネームを一覧化。

```json
[
    "uhyo",
    "suin",
    "Yz_4230",
    "atskimura",
    "pineappledreams",
    "Amanokawa",
    "k_shibusawa",
    "minakawa-daiki",
    "morry_48",
    "c60evaporator",
    "takuya_tsurumi",
    "TomoEndo",
    "yhatt",
    "CEML",
    "moritalous",
    "svfreerider",
    "daisukeoda",
    "karaage0703",
    "tommy19970714",
    "tyru",
    "galileo15640215",
    "keitah",
    "mocapapa",
    "akeome",
    "ssssssssok1",
    "yuno_miyako",
    "katzueno",
    "cometscome_phys",
    "mpyw",
    "akane_kato"
]
```

### `list`: Qiitaにトレンド入りした記事の一覧を取得する
以下の情報を出力します。

- 記事のUUID(記事のID）
- 記事のタイトル
- 記事のURL
- 記事の著者名
- LGTM数
- 記事に付けたれたタグ, タグURL

```json
[
    {
        "article_id":"e66cbca2f582e81d5b16",
        "article_title":"Let's Encryptを使用しているウェブページをブロックするプロキシサーバー",
        "article_url":"https://qiita.com/uhyo/items/e66cbca2f582e81d5b16",
        "author_name":"uhyo",
        "likes":66,
        "tag_list":[
            {
                "tag_link":"/tags/javascript",
                "tag_name":"JavaScript"
            },
            {
                "tag_link":"/tags/node.js",
                "tag_name":"Node.js"
            },
            {
                "tag_link":"/tags/proxy",
                "tag_name":"proxy"
            },
            {
                "tag_link":"/tags/https",
                "tag_name":"HTTPS"
            },
            {
                "tag_link":"/tags/letsencrypt",
                "tag_name":"letsencrypt"
            }
        ]
    },
    {
        "article_id":"83ebaf96caa2c13c8b2f",
        "article_title":"macOSのスクリーンセーバーをHTML・CSS・JSで作る (Swiftスキル不要)",
        "article_url":"https://qiita.com/suin/items/83ebaf96caa2c13c8b2f",
        "author_name":"suin",
        "likes":60,
        "tag_list":[
            {
                "tag_link":"/tags/html",
                "tag_name":"HTML"
            },
            {
                "tag_link":"/tags/css",
                "tag_name":"CSS"
            },
            {
                "tag_link":"/tags/javascript",
                "tag_name":"JavaScript"
            },
            {
                "tag_link":"/tags/macos",
                "tag_name":"macos"
            }
        ]
    }
]
```


Qiitaのトレンドは1日に2回、毎日5時/17時に更新更新されていますが、そこまで記事の入れ替わりはないので1日1回だけの実行にしておこうと思います。

### `tag`: Qiitaにトレンド入りした記事に付けられたタグを取得する

```json
[
    {
        "tag_link":"/tags/python",
        "tag_name":"Python"
    },
    {
        "tag_link":"/tags/r",
        "tag_name":"R"
    },
    {
        "tag_link":"/tags/%e6%a9%9f%e6%a2%b0%e5%ad%a6%e7%bf%92",
        "tag_name":"機械学習"
    }
]
```

上記の記事の一覧でもタグは取得していますが、一個の記事に紐付けられたタグなので、異なる記事で同じタグが付いていた場合重複してしまいます。そのため、重複したタグを省いてトレンド入りしたタグだけ一覧で保存しておく処理にしました。


## DockerでPythonが実行できる環境を作成しよう

簡易的なDocker環境を作成していきます。
ディレクトリ構成は以下のような感じ。

```bash
├── batch
│   └── py
│       └── article.py
├── docker
│   └── python
│       ├── Dockerfile
│       ├── etc
│       │    └── cron.d
│       │        └── qiita
│       └── requirements.txt
├── docker-compose.yml
└── mnt
    └── json
        ├── author
        ├── list
        └── tag
```
- `batch` directory
  pythonファイルを置いています。
このファイルがスクレイピングを行う実ファイルです。

- `docker`directory
  コンテナの内部で必要なものだったり、実際のcron設定はここで置いています

- `mnt`directory
  host上のディレクトリをマウントしていて、ここにスクレイピングの結果JSONファイルが生成されます


## Qiitaのトレンドをスクレイピングで取得しよう　（`batch directory `）

batchディレクトリの中にある実ファイル`article.py`の中身です。
過去にこんな記事を書いていたので、詳しいやり方とかはそっちで解説しています。
[≫ Qiitaのトレンド（ランキング）を取得してSlackに送信する](https://qiita.com/Daara_y/items/c4b01107bc6191b9fbff)
この記事ではあくまでプログラムだけにします。

上記の記事のプログラムとの相違点は以下の2点です。
記事の一覧だけ欲しいねん！って人は上記の記事だけで事足りると思います。

1. トレンド入りした記事のタグと著者を取得
2. 取得した内容をJSON化して保存しておく

```python
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import requests
from bs4 import BeautifulSoup
import json
import datetime
import os

def get_article_tags(detail_url):
    tag_list = []

    res = requests.get(detail_url, headers=headers)

    # htmlをBeautifulSoupで扱う
    soup = BeautifulSoup(res.text, "html.parser")

    tags = soup.find_all(class_="it-Tags_item")
    for tag in tags:
        tag_name = tag.get_text()
        tag_link = tag.get('href')

        tag_list.append({
            'tag_name' : tag_name,
            'tag_link': tag_link
        })

    return tag_list

def write_json(json_list, path):
    with open(path, 'w') as f:
        f.write(json.dumps(json_list, ensure_ascii=False, indent=4, sort_keys=True, separators=(',', ':')))

def mkdir(path):
    os.makedirs(path, exist_ok=True)

def get_unique_list(seq):
    seen = []
    return [x for x in seq if x not in seen and not seen.append(x)]

def get_unique_tag(tag_lists):
    tags = []
    for v in tag_lists:
        for i in v:
            tags.append(i)
    return tags        

try:
    # Root URL
    url = "https://qiita.com/"
    headers = {
        "User-Agent" : "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1"
    }
    today_date = datetime.datetime.now().date()

    items = []
    item_json = []
    result = []

    res = requests.get(url, headers=headers)

    # htmlをBeautifulSoupで扱う
    soup = BeautifulSoup(res.text, "html.parser")

    try:
        main_items = soup.find(class_="p-home_main") 

        for main_items in soup.find_all():
            if "data-hyperapp-props" in main_items.attrs:
                item_json.append(main_items["data-hyperapp-props"])
        items = json.loads(item_json[1])
    except:
        raise Exception("Not Found Json Dom Info")

    if 'edges' not in items['trend']:
        raise Exception("The expected list does not exist")

    try:
        item_detail_list = []
        tags_list = []
        author_list = []

        for edges in items['trend']['edges']:
            uuid = edges['node']['uuid']
            title = edges['node']['title']
            likes = edges['node']['likesCount']
            article_url =  url + edges['node']['author']['urlName'] + '/items/' + uuid
            author_name = edges['node']['author']['urlName']
            create_at = datetime.datetime.now().date()
            tag_list = get_article_tags(article_url)

            item = {
                'article_title' : title,
                'article_url' : article_url,
                'article_id' : edges['node']['uuid'],
                'likes' : likes,
                'uuid' : uuid,
                'author_name' : author_name,
                'tag_list' : tag_list,
            }

            item_detail_list.append(item)
            tags_list.append(tag_list)
            author_list.append(author_name)

        mkdir('/mnt/json/list/')
        mkdir('/mnt/json/tag/')
        mkdir('/mnt/json/author/')

        # タグをuniqu化
        tags_list = get_unique_tag(tags_list)

        # jsonファイルを書き出し
        write_json(item_detail_list, f"/mnt/json/list/{today_date}.json")
        write_json(tags_list, f"/mnt/json/tag/{today_date}.json")
        write_json(author_list, f"/mnt/json/author/{today_date}.json")
    except:
        raise Exception("Can't Create Json")
    
except Exception as e:
    # jsonファイル作成失敗
    mkdir('/mnt/log/')
    with open(f'/mnt/log/{today_date}', 'w') as f:
        f.write(e)
```
次に上記のファイルを実行する環境を作成します。

### Pythonを動かすためのDocker部分を作成（`docker directory`）

#### docker-compose.ymlの作成
ここは大したことしてない。
volumesでPC上のディレクトリとマウント。

```yml
version: "3"

qiita_batch:
  container_name: "qiita_batch"
  build: 
    context: ./docker/python
  tty: true
  volumes:
    - ./batch:/usr/src/app
    - ./mnt:/mnt
```

#### Dockerfileの作成

Dockerfile汚いのは許して...軽く説明だけ↓

- コンテナ内のタイムゾーンの設定（cron設定のため
- cronを反映
- requirement.txtで必要なモジュールをインストール

cronをちゃんと指定した日本時間で実行したいのであれば、タイムゾーンの設定は必須ですね。
なんかごちゃごちゃやって、ようやく日本時間に変えられたんですが、もっとうまいやり方あるはず...。

cronの設定はetc/cron.d/qiitaにまとめておいて、後々crontabに書き込むような処理にしてます。管理が楽になるのでこっちの方がいいかなと。間違っても`crontab -r`というコマンドは呼び出してはいけない...！！


```Dockerfile
FROM python:3

ARG project_dir=/usr/src/app
WORKDIR $project_dir

ADD requirements.txt $project_dir/py/
ADD /etc/cron.d/qiita /etc/cron.d/

ENV TZ=Asia/Tokyo

RUN apt-get update && \
    apt-get install -y cron less vim tzdata && \
    rm -rf /var/lib/apt/lists/* && \
    echo "${TZ}" > /etc/timezone && \
    rm /etc/localtime && \
    ln -s /usr/share/zoneinfo/Asia/Tokyo /etc/localtime && \
    dpkg-reconfigure -f noninteractive tzdata && \
    chmod 0744 /etc/cron.d/* && \
    touch /var/log/cron.log && \
    crontab /etc/cron.d/qiita && \
    pip install --upgrade pip　&& \
    pip install -r $project_dir/py/requirements.txt

CMD ["cron", "-f"]
```

### Pythonの実行に必要なパッケージをまとめたrequirement.txtを作成
requirement.txtは自分のMacbookProで使用していたものを出力しただけなので、かなり適当に色々なものが入っています。いらないものは適宜削ってくださいまし。`beautifulsoup4`と`requests`と`json`だけあれば事足ります。なんか足らん！って人は動かしながら足らないやつpip install！！

```python
appdirs==1.4.3
beautifulsoup4==4.8.1
bs4==0.0.1
certifi==2019.9.11
chardet==3.0.4
Click==7.0
filelock==3.0.12
get==2019.4.13
gunicorn==20.0.4
idna==2.8
importlib-metadata==1.5.0
importlib-resources==1.0.2
itsdangerous==1.1.0
Jinja2==2.11.1
MarkupSafe==1.1.1
post==2019.4.13
public==2019.4.13
query-string==2019.4.13
request==2019.4.13
requests==2.22.0
six==1.14.0
soupsieve==1.9.5
urllib3==1.25.7
virtualenv==20.0.1
Werkzeug==1.0.0
zipp==2.2.0
```
#### cron設定

`/etc/cron.d/qiita`の中身です

```
PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

LANG=ja_JP.UTF-8

# Create Qiita JSON (every day AM:10:00)
0 10 * * * python /usr/src/app/py/article.py >> /var/log/cron.log 2>&1
```

こんな感じ！
あとは`docker-compose up -d`でやれば立ち上がるので、放置しておけば勝手にQiitaにスクレイピングしに行ってjsonファイルを作成してくれます。簡易的なDocker環境で出来るのでおすすめ！