スクレイピングして更新された記事をslackに通知する【python】【BeautifulSoup】

# 目標

[ぷろたん筋肉研究所](https://kintore.site)

このサイトのToday's pick upの更新された記事を抜き取って、slackに通知します。



# 手順

1. slackへのプログラムからの通知
2. 情報を抜き取る
3. プログラムを定期的に回す(cron)



# 1. slackへのプログラムからの通知

まずは、incoming webhookに登録します。

[この記事を参考にすると良いです](https://qiita.com/vmmhypervisor/items/18c99624a84df8b31008)

私の場合configファイルにslackのincoming webhookの情報を示しているので、下記のようなコードになっていますが、urlの部分は自分で取得したurlにしてください。

~~~python:slack_notification.py
import slackweb
from config import config

slack = slackweb.Slack(url=config['slack_info']['in_webhook_url'])
slack.notify(text="test")
~~~

このプログラムを回すと、登録したチャットにtestと通知がくると思います。



# 2. 情報を抜き取る

筋トレに興味があったので[このサイト](https://kintore.site)を抜き取ることにします。かなり綺麗なサイト構造をしていたので取得しやすかったです。

抜き取る情報としてとりあえずpick upに並んでる記事とします。



## まずはサイトを見てる

[サイト](https://kintore.site)に行って、リソースを見てみます。リソースの見方は調べればすぐに出てくると思うので、自分が使っているブラウザで調べてみてください。

調べてみると...

どうやら、

<img width="687" alt="スクリーンショット 2018-07-05 17.37.32.png" src="https://qiita-image-store.s3.amazonaws.com/0/187535/ce5b8fc3-5008-b326-db04-42bf95f24479.png">


ここらへんが最新記事を表示しているコードっぽいです。



## コード

プログラムの流れとして、

1. トップページに表示されているToday's pick upの記事を取得する
2. 取得したものから必要な情報を抜き取る(今回の場合url)
   - 最新の記事を設定ファイルに保持しているのでそれを参考に必要な記事を取得
   - 最新の記事を更新
3. 取得した情報をslackに通知する

情報を抜き取るためのライブラリとしてBeautifulSoupを使用しました。使い方等は[このサイト](https://qiita.com/itkr/items/513318a9b5b92bd56185)を参考にしました。

更新記事がある場合
<img width="558" alt="スクリーンショット 2018-08-22 10.43.22.png" src="https://qiita-image-store.s3.amazonaws.com/0/187535/0b42d669-56ee-0b48-c81d-20ee8e01ea12.png">


更新記事が複数ある場合
<img width="559" alt="スクリーンショット 2018-08-22 10.43.37.png" src="https://qiita-image-store.s3.amazonaws.com/0/187535/2f1289e1-c0ca-8c0c-3574-f30cd497c5ee.png">


更新記事がない場合
<img width="230" alt="スクリーンショット 2018-08-22 10.43.08.png" src="https://qiita-image-store.s3.amazonaws.com/0/187535/4f120937-91fe-4c1d-6123-ee5fadf6232b.png">


のように通知が来ます。

### スクレイピングプログラム

~~~python:extract_web_info.py
import urllib.request as url_req
from bs4 import BeautifulSoup
from config import config, update_recent_article
from slack_notification import slack_notify

first_view = url_req.urlopen(config['web_info']['url']).read()
soup = BeautifulSoup(first_view, "lxml")

def extract_pick_up(soup=soup):
    """
    指定されたページのhtmlを読み込み、最新記事を抜粋してくる
    """
    columns = soup.find_all("article", class_="post-list-item")
    return columns[0:13]


def extract_url(column):
    """
    columns @params: コラム

    コラムのurlを取得する
    """
    column_html = BeautifulSoup(str(column), "lxml")
    url = column_html.find("a").get("href")
    return url


def extract_title(column):
    """
    columns @params: コラム

    コラムのタイトルを取得する
    """
    column_html = BeautifulSoup(str(column), "lxml")
    title = column_html.find("h2").string
    return title

def extract_update_article(columns):
    """
    更新された記事を抽出
    更新前の最新の記事のindexはconfig.iniで管理
    """
    # 最新の記事番号を取得
    recent_article = config['web_info']['recent_article']
    # 更新された記事のurlを取得
    article_list = []
    update_start = False
    # 逆順に取得していき、最新記事の次からの記事のurlを取得
    for column in reversed(columns):
        url = extract_url(column)
        # 記事番号取得
        article_num = url.replace(config['web_info']['url']+"/", "")

        # タイトル取得
        title = extract_title(column)
        # 取得すべきurl
        if update_start:
            article_list.append([title, url])
            recent_article = article_num
            continue
        # 前回取得した最新の記事かどうか判定
        if recent_article == article_num:
            update_start = True

    # config更新
    update_recent_article(recent_article)
    if article_list == []:
        return [["更新記事はありません", "https://kintore.site"]]
    return article_list


if __name__ == "__main__":
    columns = extract_pick_up()
    url = extract_url(columns[0])
    article_list = extract_update_article(columns)
    slack_notify(text_="---プロたんの記事---", list_=article_list)

~~~
このプログラムを回すことで情報を取得して、slackに通知するまでを完了することができます。
前回取得した記事がトップページにある前提でプログラムは書かれています。

### slack通知プログラム

~~~python:slack_notification.py
import slackweb
from config import config

slack = slackweb.Slack(url=config['slack_info']['in_webhook_url'])

def slack_notify(text_="", list_=None):
    slack.notify(text=text_)
    for element in list_:
        # タイトル
        slack.notify(text=element[0])
        # url
        slack.notify(text=element[1])
~~~
1.で説明したプログラムを少し改良して、リストで渡されたものを順番に通知するようにしています。

### 設定プログラム

~~~python:config.py
import configparser
import re

config = configparser.ConfigParser()
config.read('config.ini')

# recent_articleの記事番号を更新
def update_recent_article(article_num):
    with open('config.ini', 'r') as f:
        lines = f.readlines()
    with open('config.ini', 'w') as f:
        for line in lines:
            if re.match(r'(recent_article = )', line):
                f.write("recent_article = {}".format(article_num))
                continue
            f.write(line)
~~~

~~~ini:config.ini
[slack_info]
# 取得したincoming webhookのurl
in_webhook_url = https://hooks.slack.com/services/***/***/***

[web_info]
url = https://kintore.site
# 前回取得した記事番号
recent_article = 5915
~~~
configparsesrには、configを設定するメソッドがあるようですが、更新するものはなかったっぽい(自分の調査した範囲で)ので、かなり強引ではありますが、recent_articleの数値部分のみを書き換えるようなプログラムにして、最新の記事を保持するような形にしました。

recent_articleは`https://kintore.site/***`の***に記述している記事番号です。この記事番号は順番になっているというわけではないようなので、更新するときに最新の記事番号を引数として受け取って、その値になるように変更しています。


# 3. プログラムを定期的に回す(cron)

cronを使います。

~~~
crontab -l
~~~

で、cronが設定されてるかみてみる。

もし、設定されてなければ、下記で設定します。操作はvimで行います。

~~~
crontab -e
~~~

詳しくは[このサイト](https://saba.omnioo.com/note/1233/crontabの使い方/)がわかりやすいです。
私の場合は下記のように設定しました。

~~~
PATH=/usr/local/bin:/bin:/Users/hoge/.pyenv/versions/anaconda3-4.3.1/bin/

0 0 * * * cd /Users/hoge/Documents/ ; python -B extract_web_info.py 2>> error.txt
~~~
2行目は以下のような構成になっています。なので、このコードだとカレントディレクトリをプログラムのあるディレクトリまで移動して、0時0分に定期的にプログラムを回すようになっています。
~~~
[分] [時] [日] [月] [曜日] [コマンド]
~~~


## cronを設定するときの注意点
- PATHを設定しないとpythonやcdコマンドがうまく回らないことがあります。whichコマンド等を使って正しいパスを設定する必要があります。
    - 特にpythonの実行パスは、注意しないとデフォルトで入っている(macの場合)python2系で回ってしまったりします。
- カレントディレクトリは**実行ユーザーのホームディレクトリ**となっているので、ファイルの場所によっては移動する必要があります。
- 頻繁にこのプログラム(スクレイピングするようなプログラム)を回すとサーバーに負荷を与えることになるので注意が必要です


# 拡張

拡張として色々なことが考えられます。

拡張ができ次第記事を追加していきたいと思います。

## 拡張例
- ~~取得する情報をurlだけではなく、タイトル等も取得する~~
- slackのoutgoingwebhookを使ってこちらからの指示通りの記事を取ってくる
  - 例:「カテゴリ 筋トレ」とbotに発言→カテゴリが筋トレの記事を取ってくる