Python+pandasを使ってRSSフィードを取得→Mattermostに投稿&DBに保存


やったこと

Mattermostを導入してなにかやってみたかったので、RSSフィードを投稿するプログラムを作りました。(あとで、公式のプロジェクトがあったことに気づいたのは秘密)

いろいろと応用が利くかなと思い、取得したfeedをpandasで処理して
DBに格納することとしました。

※Mattermostってなんぞやって言う人はこちら

環境

  • CentOS7
  • Python2.7
    • pandas
    • feedparser
    • sqlalchemy
    • requests
  • PostgreSQL9.5
  • MatterMost

PostgreSQLについては、dockerイメージから導入しました。

docker pull postgres:9.5
docker run -p 5432:5432 --name postgres-server -v /var/lib/postgresql:/var/lib/postgresql:rw  postgres:9.5
firewall-cmd --permanent --add-port=5432/tcp
firewall-cmd --reload

これでとりあえずリモート接続可能なPostgreSQLコンテナが立ちあがるはず。
docker便利だなぁ。。。

そのほかの環境構築は省略、Pythonはpyenv環境で実施しています。

1. RSSフィードを取得する。

feedparserというPythonライブラリを使います。
ここら辺を参考にして、pipでインストールしました。

import feedparser

RSS_URL = "http://b.hatena.ne.jp/hotentry/it.rss"
print("Start get feed from %s" % (RSS_URL))
feed = feedparser.parse(RSS_URL)

これでフィードが取得できます。
(ちなみに↑ははてなのテクノロジーカテゴリのホットエントリを取得しています。)

2. 取得したフィードをpandas.DataFrameに展開

今後の処理のしやすさなどを考えて、pandas.DataFrameにマッピングします。

import pandas as pd
entries = pd.DataFrame(feed.entries)

...終わり。
pandasさん優秀ですね。

はてなのRSSフィードの場合、以下の12列の要素が取得されていました。

  • content
  • hatena_bookmarkcount
  • id
  • link
  • links
  • summary
  • summary_detail
  • tags
  • title
  • title_detail
  • updated
  • updated_parsed

ここまでくれば、あとはpandasの機能で自由にデータをいじれます。

3. 新着フィードのチェック

feedparserはとても便利ですが、アクセスした時点のフィードを取得するので過去に取得したフィードと重複がでます。

ここでDataFrameに展開した意味が出てくるのです!
以下はDataFrameの操作で、新着のフィードのみを取り出して表示する例です。

already_print_feeds = pd.Series()

while True:
        time.sleep(300)
        feed = feedparser.parse(RSS_URL)
        entries = pd.DataFrame(feed.entries)
        new_entries = entries[~entries['id'].isin(already_print_feeds)]
        if not new_entries.empty:
            for key, row in new_entries.iterrows():
                feedinfo = "[**%s**](%s)\n\n>%s" % (row['title'],  row['link'], tag_re.sub('', row['summary']))
                print(feedinfo)
        already_print_feeds = already_print_feeds.append(new_entries['id'])

ちょっと解説

new_entries = entries[~entries['id'].isin(already_print_feeds)]

これは、取得したRSSフィードから新着のみをぬきだしています。

already_print_feedsにはこれまで取得したRSSフィードのidが入っている前提です。

そうすると、entriesに格納されたフィードのうち、
新着の行のみにTrueが設定されたSeriresが返却されるので、
これをentriesのインデックスに指定すれば新着だけ抜き出せます。

~entries['id'].isin(already_print_feeds)
# =>
0     False
1     True # => ★New!
2     False
3     False
4     False
5     False
6     False
7     False
8     False
9     False
10    False
11    False
12    False
13    False
14    False
15    False
16    False
17    False
18    False
19    True # => ★New!
20    False
21    False
22    False
23    False
24    False
25    False
26    False
27    False
28    False
29    False
Name: id, dtype: bool

already_print_feedsにはこれまで表示した新着フィードのIDを追記していけばいいです。

already_print_feeds = already_print_feeds.append(new_entries['id'])

ただし上記のコードだとalready_print_feedsに無限にデータが貯まっていくので、いつか破綻します(メモリが)
1日1回フラッシュしたり、DBから読み取るようにしたりしましょう

4. DB(PostgreSQL)に保存

取得した、RSSフィードをPostgreSQLに保存します。
ただし、列は下記に絞りました。

  • id
  • link
  • title
  • summary
  • updated

まずDBにテーブルを作ります。

create table feed ( id text primary key , link text, title text, summary text, updated timestamp );

一応、idには主キー制約を入れて、かつupdatedはtimestamp型としておきました。
(はてなのフィードのupdatedはそのままtimestamp型としてINSERTできる形式のようです。)

from sqlalchemy import create_engine

DATABASE_CONN = "postgresql://xxxx:xxxx@xxxxx:xxx/xxxx"
DATABASE_TABLE = "feed"
# connect database
engine = create_engine(DATABASE_CONN)

# Store database
stored_entries = new_entries.ix[:, [
                "id", "link", "title", "summary", "updated"]]
stored_entries.to_sql(DATABASE_TABLE, engine, index=False, if_exists='append')

DataFrameのto_sqlメソッドを使います。

  • index=False

とすることで、格納時に勝手にインデックス列を付与しなくなり、

  • if_exists='append'

で、すでに存在するテーブルにデータを追記する挙動となります。

5. MatterMostに投稿する

HTTPリクエストを送るrequestというPythonライブラリで非常に簡単に投稿できます。

import requests
import json

mattermosturl = "MatterMostのincomming webhook URL"
username = "好きな名前"
header = {'Content-Type': 'application/json'}
payload = {
        "text": feedinfo,
        "username": username,
        }

resp = requests.post(mattermosturl,
                     headers=header, data=json.dumps(payload))

というわけで

せっかくpandasにマッピングしたので、
機械学習的なこともやっていきたいと思います。