RaspberryPiで自分のQiita投稿データを自動収集して定期的にSlackで通知してみた

# はじめに

最近いくつか記事を投稿してきましたが、これまでの合計閲覧数などのデータを見てみたいと思い、ラズパイで環境を作ってみました。今回は、ラズパイで定期的にデータを取得し、1日に1回グラフを作ってSlackに自動送信することをゴールとします。

# 環境

主に開発を行うMacの環境は次の通りです。

```
$sw_vers
ProductName:	Mac OS X
ProductVersion:	10.13.6
BuildVersion:	17G9016
```

使用するラズパイはRaspberry Pi 3 Model Bを使用しています。OSは次の通りです。

```
$ cat /etc/os-release 
PRETTY_NAME="Raspbian GNU/Linux 10 (buster)"
NAME="Raspbian GNU/Linux"
VERSION_ID="10"
VERSION="10 (buster)"
VERSION_CODENAME=buster
ID=raspbian
ID_LIKE=debian
HOME_URL="http://www.raspbian.org/"
SUPPORT_URL="http://www.raspbian.org/RaspbianForums"
BUG_REPORT_URL="http://www.raspbian.org/RaspbianBugs"
```

# 構築

ラズパイはしばらく使っていなかったのですが、再度使おうとしたらログインできず、パスワード変更しようとしたらOSが起動しなくなってしまったので、OSを再インストールするところから始めます。
その後必要なライブラリやツールをインストールして、Qiitaのデータを格納するMySQL(MariaDB)を構築します。
最後にSlackに定期送信するような処理を作っていきます。

## Raspberry Pi OS再インストール

OS再インストールはこちら[^1]の記載通りに実行してOSを起動できるようになりました。手順については全く記載通りなので、省略します。

``apt update``をエラーで実行できない問題が発生しました。試行錯誤していたところ、こちら[^2]に記載している通り、

```
sudo dhclient eth0
```

これで問題解決しました。その後、次のコマンドを実行しておきます。

```
sudo apt-get update
sudo apt-get upgrade
```


## DBセットアップ

次にQiitaの閲覧数などのデータを格納するデータベースを構築します。今回はMySQLを使用します。
しかし、MySQLをインストールしようとしたところ、


```
$ sudo apt-get install mysql-server
パッケージリストを読み込んでいます... 完了
依存関係ツリーを作成しています                
状態情報を読み取っています... 完了
パッケージ mysql-server は使用できませんが、別のパッケージから参照されます。
これは、パッケージが欠落しているか、廃止されたか、または別のソース
からのみ利用可能であることを意味します。
しかし、以下のパッケージが置き換えます:
  mariadb-server-10.0

E: パッケージ 'mysql-server' にはインストール候補がありません
```
とエラーが表示され、インストールできませんでした。
ただエラーメッセージの中でMariaDBが置き換えると表示されているので試したところ、
次のコマンドでインストールできました。

```
sudo apt-get install mariadb-client mariadb-server
```

今回はデータ中に日本語を入れようと思っているので、言語の設定をします。
こちら[^3]を参考にして、``/etc/mysql/conf.d/mysql.cnf``を次のように修正しました。

```/etc/mysql/conf.d/mysql.cnf
[mysql]
default-character-set=utf8mb4
```

MySQLを再起動して、ユーザとデータベースの設定をします。

``` 
$ sudo service mysql restart
$ sudo mysql -u root -p
Enter password: 
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 60
Server version: 10.3.17-MariaDB-0+deb10u1 Raspbian 10

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [(none)]> CREATE DATABASE myqiita;
MariaDB [(none)]> CREATE USER 'pi'@'localhost' IDENTIFIED BY '<パスワード>';
MariaDB [(none)]> GRANT ALL PRIVILEGES ON myqiita.* to 'pi'@'localhost';
MariaDB [(none)]> FLUSH PRIVILEGES;
MariaDB [(none)]> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| myqiita            |
+--------------------+
2 rows in set (0.002 sec)

MariaDB [(none)]> quit
Bye
```

これでユーザpiでログインして、データベースmyqiitaにデータを格納する準備ができました。



## データ取得スクリプト作成

先ほど設定したデータベースへの接続関係のコードを書いておきます。

```myconnection.py 
import MySQLdb
import pandas.io.sql as psql

class myDB():

    def __init__(self):
        self.connection = MySQLdb.connect(
            host='localhost',
            user='pi',
            passwd='<パスワード>',
            charset='utf8',
            db='myqiita')
        self.cursor = self.connection.cursor()

    def exe(self, sql):
        self.cursor.execute(sql)
        return self.cursor.fetchall()

    def exedf(self, sql):
        return psql.read_sql(sql, self.connection)

    def commit(self):
        self.connection.commit()

    def close(self):
        self.connection.close()
```

``<パスワード>``と記載した部分はMySQLでユーザpiを作成したときのパスワードとなります。

次のプログラムを実行して、データを格納するテーブルmytableを作成しておきます。なお、各列の型指定についてはテキトーな部分もあります。

```create.py 
import MySQLdb
from myconnection import myDB

if __name__ == '__main__':
    
    mydb = myDB()

    sql = """
    create table mytable( 
    id int(11) AUTO_INCREMENT NOT NULL,
    create_datetime datetime not null default current_timestamp,
    tot_num_articles int(11) not null,
    tot_num_views int(11) not null,
    tot_num_likes int(11) not null,
    max_num_views int(11) not null,
    max_num_views_title varchar(100) not null,
    max_num_likes int(11) not null,
    max_num_likes_title varchar(100) not null,
    PRIMARY KEY (id))
    """
    print( sql )
    mydb.exe( sql )
```

次にQiitaからデータを取得、データベースへ格納するコードを作成します。

``` insert.py 
import requests
import json
import datetime
import MySQLdb
from myconnection import myDB

def collection():

    dt_now = datetime.datetime.now()

    url = 'https://qiita.com/api/v2/authenticated_user/items'
    headers = {"content-type": "application/json",
               "Authorization": <アクセストークン>}
    
    res = requests.get(url, headers=headers)
    article_list = res.json()

    tot_num_articles = len(article_list)
    tot_num_views = 0
    tot_num_likes = 0
    max_num_views = 0
    max_num_likes = 0
    max_num_views_title = ''
    max_num_likes_title = ''

    
    for item in article_list:
        item_id = item['id']
        title = item['title']
        likes_count = item['likes_count']
    
        url = 'https://qiita.com/api/v2/items/' + item_id
        res = requests.get(url, headers=headers)
        json = res.json()
        page_views_count = json['page_views_count']
    
        tot_num_views += page_views_count
        tot_num_likes += likes_count

        if max_num_views < page_views_count:
            max_num_views = page_views_count
            max_num_views_title = title

        if max_num_likes < likes_count:
            max_num_likes = likes_count
            max_num_likes_title = title

    data={'dt':dt_now, 
            'tot_num_articles':tot_num_articles, 
            'tot_num_views':tot_num_views, 
            'tot_num_likes':tot_num_likes, 
            'max_num_views':max_num_views, 
            'max_num_views_title':max_num_views_title, 
            'max_num_likes':max_num_likes, 
            'max_num_likes_title':max_num_likes_title}
    return data
    

if __name__ == '__main__':
    
    mydb = myDB()

    data = collection()

    sql = """
    insert into mytable 
    (tot_num_articles,tot_num_views,tot_num_likes,max_num_views,max_num_views_title,max_num_likes,max_num_likes_title)
    values({},{},{},{},"{}",{},"{}")
    """.format(data['tot_num_articles'],
            data['tot_num_views'],
            data['tot_num_likes'], 
            data['max_num_views'], 
            data['max_num_views_title'], 
            data['max_num_likes'], 
            data['max_num_likes_title'])
    print(sql)
    mydb.exe(sql)
    mydb.commit()
    mydb.close()
```

``collection()``の中でQiitaのAPIを叩いて、データを取得しています。
アクセストークンの取得やデータの取得方法については、こちら[^4]を参考にしています。

main関数の中で、``collection()``で取得したデータテーブルmytableにinsertし、commitしています。

## cronで定期実行

先ほど作成したinsert.pyを定期実行するためにcronに登録しておきます。

```
sudo vim /etc/cron.d/cron_test 
```

cron_testというファイルを作成して、次のように記載することで毎時00分にinsert.pyが実行されるようになります。

```
00 *	* * *	pi	/usr/bin/python3 /home/pi/work/insert.py   
```

cronを再起動しておきます。

```
sudo service cron restart
```

以上で、データを定期的に取得するところまではできました。

## Slackへの定期送信

最後に1日に1回データをグラフにしてSlackに送信するように設定します。
プログラムは次の通りです。

```slack_report.py 
from slacker import Slacker
import monitor

def main():
    monitor.report()
    
    slack = Slacker("<トークン>")
    slack.files.upload(file_='out_monitor.png', channels='myqiita')

if __name__ == "__main__":
    main()
```

最初に``monitor.report()``としていますが、別のmonitor.pyというプログラムの中でデータベースのデータを取得して、グラフを作成し、out_monitor.pngという名前で保存しています。
このPNGファイルをSlackの自分のチャンネルに送信しています。送信部分についてはこちら[^5]を参考にしました。

monitor.pyのプログラム内容については次の通りです。

```monitor.py 
import MySQLdb
from myconnection import myDB
import pandas as pd
import matplotlib.pyplot as plt

def report():
    mydb = myDB()

    sql = """
    select * from mytable
    """
    df = mydb.exedf( sql )

    # Preprocess
    df['create_datetime'] = pd.to_datetime(df['create_datetime'])
    df = df.set_index('create_datetime')

    # Plot
    fig = plt.figure()
    ax1 = fig.add_subplot(311)
    df['tot_num_articles'].plot(ax=ax1)
    ax1.set_xticklabels('')
    ax1.set_xlabel('')
    ax1.set_ylabel('tot_num_articles')
    ax1.grid()
    ax2 = fig.add_subplot(312)
    df['tot_num_views'].plot(ax=ax2)
    ax2.set_xticklabels('')
    ax2.set_xlabel('')
    ax2.set_ylabel('tot_num_views')
    ax2.grid()
    ax3 = fig.add_subplot(313)
    df['tot_num_likes'].plot(ax=ax3)
    ax3.set_ylabel('tot_num_likes')
    ax3.grid()
    fig.savefig('out_monitor.png')

if __name__ == '__main__':
    report()
```

テーブルmyqiitaに格納されているデータを全て取得、pandasのDataFrameにしたあと、グラフにしています。DataFrameへの変換についてはこちら[^6]を参考にしています。
グラフにしている項目は、自分の記事数、総閲覧数、総いいね数としています。

slack_report.pyを定期実行するようにcronにプログラムを登録しておきます。
以下にcron_testの中身を全て表示しています。

```/etc/cron.d/cron_test 
# /etc/crontab: system-wide crontab
# Unlike any other crontab you don't have to run the `crontab'
# command to install the new version when you edit this file
# and files in /etc/cron.d. These files also have username fields,
# that none of the other crontabs do.

SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

# Example of job definition:
# .---------------- minute (0 - 59)
# |  .------------- hour (0 - 23)
# |  |  .---------- day of month (1 - 31)
# |  |  |  .------- month (1 - 12) OR jan,feb,mar,apr ...
# |  |  |  |  .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# |  |  |  |  |
# *  *  *  *  * user-name command to be executed
17 *	* * *	root    cd / && run-parts --report /etc/cron.hourly
25 6	* * *	root	test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily )
47 6	* * 7	root	test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.weekly )
52 6	1 * *	root	test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.monthly )
00 *	* * *	pi	/usr/bin/python3 /home/pi/work/insert.py   
10 8	* * *	pi	/usr/bin/python3 /home/pi/work/slack_report.py 
```

最後にcronを再起動しておきます。

```
sudo service cron restart
```

# 結果

こちらがスマホのSlackに送られたスクリーンショットです。

<img width="375" alt="7F3D2229-9000-4AEC-A4E7-AF27B797E7D5.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329888/f20920da-5cf7-bfa7-d150-b7304a6c338e.png">

毎日朝8:10にグラフが送られてくるようになりました。よし。

# さいごに 

ラズパイで定期的にQiitaの閲覧データをSlackに送信するようにできました。
データを見れるようになると、1日の中でも時間帯によって閲覧数の増加量が違っているなーというように気がつく点がいくつか出てくるので、面白いですね。
まだまだ閲覧数やいいね数が少ないですが...


[^1]: https://www.no-title.com/raspberrypi/raspberry-pi-3-model-b-os-reinstall
[^2]: https://www.1ft-seabass.jp/memo/2015/05/15/raspberry-pi-dhclient-refresh-wlan/
[^3]: https://obel.hatenablog.jp/entry/20170127/1485482514
[^4]: https://qiita.com/Naoto9282/items/252c4b386aeafc0052ba
[^5]: https://se.miyabikno-jobs.com/python-slacker-upload/
[^6]: https://www.python.ambitious-engineer.com/archives/1179