MattermostでTwitterの更新情報を自動で呟いてくれるbot的なのを作った
はじめに
最近、職場でOSSの環境を乱立しまくっているのですが、そのひとつとしてMattermostも立ち上げました。
釈迦に説法かとは思いますが、Mattermostは早い話が、オンプレミスで動くSlackです。うちもセキュリティが厳しいので、流行りのツールを使おうにもオンプレで動くものでないと使えなかったりします。
で、立ち上げたのはいいのですが、早速言われたのが、「これ何に使うの?」。うちは独自のチャットツールがあるので、わざわざMattermostを使わなくてもいいじゃんという考えの人が多いようです。そこで、よくチェックする記事を自動で呟いてくれるbotを作れば需要があるんじゃなかろうかということで作りました。
使っている環境はMattermostとJenkinsとpythonです。構成としてはこんな感じになっています。
即興で作ったのでガバガバなのはご愛嬌ということで。Jenkinsはただのスケジューラーとして使っているので、こうして見返してみるとJenkinsである必要ないですね。まあ、Mattermostと繋げてビルドに失敗したときに通知が飛ばせるから(震え声)。あったので使った程度です。
今回はこちらのTwitterアカウントの更新情報を取得させていただくことにします。機械学習などの最新の論文情報をまとめてくださっているサイトです。アブストの日本語訳などもあるので、さっとチェックしたいときにとても参考になります。
実装
では、早速実装していきます。まずは、Twitterの更新情報を取得します。色々方法はあると思うのですが、今回はこちらを利用しました。
TwitRSS.me - rss of twitter user feeds by screenscraping with perl
次に、Mattermost側で情報を受け取るURLを用意しておきます。今回は情報を受け取るだけなので、内向きウェブフックを使います。
いよいよ、RSSをpythonで取得、データベースに保存、json形式に変換、Mattermostに送信部分を組んでいきます。最後に最終的に実装したコードを載せていますので、説明はいいという方は飛ばしてください。長いので処理内容毎に分けて説明します。といっても浅い理解なので間違っていたらすみません。コードもとりあえず、動くことだけしか考えてないので出来の悪い実装は適宜修正してください。ベースはこちらのサイトを参考にさせていただきました。
requestsで指定したURLからRSSの情報を取得します。requestライブラリについては、こちらをどうぞ。
プロキシの設定が必要な場合はこちらが参考になります。
無事、RSSの情報が取ってこれたら、feedparserでcontentから必要な情報を取ってきます。(これなんでfeedparserだけじゃだめなんだっけ?プロキシのせいだったかな。忘れてしまった。)この中のentryの中に欲しいデータがあるみたいなのでpandas形式に変換しておきます。そこまでできたらこちらを参考にTwitRSSでとってきた中から必要情報を抜き出します。
今回必要なのは、idとtitleとlinkです。idはツイート毎のユニークな値です。これを使って既出か否かの判別をします。titleはツイートの内容にあたるのでこれをMattermostのメッセージにします。linkはそのツイートへのリンクです。今回はMattermostでメッセージをクリックするとTwitterにリンクするようにしておきます。
取ってきたRSSの情報をデータベースに格納します。今回はそこら辺はサボってCSVに保存しています。(ちなみにこの安易な考えで後に少々面倒なことになりました)
全体通して書くとこうなります。
import requests,feedparser
import pandas as pd
import os
import json
def getNewFeed(rss_data,already_printed_feeds,filepath):
feeds_info = []
feed = feedparser.parse(rss_data.content)
entries = pd.DataFrame(feed.entries)
if already_printed_feeds.empty:
#全て新着Feed
new_entries = entries
else:
#既出のFeedは除く
new_entries = entries[~entries['id'].isin(already_printed_feeds['id'])]
if not new_entries.empty: #新着Feedがあれば
for key, row in new_entries.iterrows():
title = row['title'].split('http')[0]
#Mattermostに投稿されるメッセージ.ここではmarkdown形式でリンクになるように書いている.
feedinfo = '[**%s**](%s)' % (title, row['link'])
feeds_info.append(feedinfo)
#新着データがあれば既存のリストに追加する
already_printed_feeds = already_printed_feeds.append(new_entries)
#データベース(csv)に保存
if os.path.exists(filepath):
new_entries.to_csv(filepath,encoding='utf-8',mode='a',header=False)
else:
new_entries.to_csv(filepath,encoding='utf-8')
else: #新着Feedが無ければ
print('not found new entries')
return feeds_info
#既出のfeed情報の取得
def getAlreadyPrintedFeeds(filepath):
if os.path.exists(filepath):
already_printed_feeds = pd.read_csv(filepath)
else:
already_printed_feeds = pd.Series()
return already_printed_feeds
def setPostMessage(feedinfo,username):
payload = {
'text':feedinfo,
'username':username,
}
return payload
#データベース(csv)へのパス
filepath = 'entries.csv'
#proxyの設定
proxies = {
'http':'http://id:passward@proxyadress:port',
'https':'http://id:passward@proxyadress:port'
}
#RSSFeed取得先のURL
url = 'http://twitrss.me/twitter_user_to_rss/?user=arxivtimes'
rss_data = requests.get(url,proxies=proxies)
already_printed_feeds = getAlreadyPrintedFeeds(filepath)
feedinfo = getNewFeed(rss_data,already_printed_feeds,filepath)
#Mattermostの内向きウェブフック
mattermosturl = 'http://localhost/hooks/***'
#Mattermostのつぶやき時に表示される名前(好きな名前をつける)
username = 'FeedBot'
header = {'content-Type':'application/json'}
#新着Feedを順にMattermostに投げる
for i in range(len(feedinfo)):
payload = setPostMessage(feedinfo[i])
resp = request.post(mattermosturl,
header=header,
data=json.dumps(payload))
最後にこれを定期的に実行するためにJenkinsで実行スケジュールを組みます。と言ってもやることは単純で1時間に1回ジョブを実行させるだけです。確か今回採用しているRSSの取得方法が1時間単位に更新された気がするのでこんなもんでいいでしょう。こちらを参考にさせていただきながら、スケジュールを設定していきます。
ということで最低限飛ばせるようになりました。
ただ、ここで2つ問題が起きました。
- 一つはこのままじゃ複数ページから取ってこれないのでもうちょっとだけいじります。といってもurlをリストにして順々に実行するだけです。なので大したことないです。
- もう一つは取ってきたRSS情報のcolumn数が揃ってないだと…どういう条件かまでは見てないのですが、ツイートによっては特定のcolumnのデータがあったりなかったりするみたいです。NaNになるならいいのですが、CSVへの書き出し部分がいけないのかcolumnそのものが違って保存されてしまっているようです。そのせいで既出情報を読み出す際にcolumn数が合わないとエラーを食らいました。ダサいですが、`pandas`で読み込む際に`names`を指定して読み込むことにします。
import requests,feedparser
import pandas as pd
import os
def getNewFeed(rss_data,already_printed_feeds,filepath):
feeds_info = []
feed = feedparser.parse(rss_data.content)
entries = pd.DataFrame(feed.entries)
if already_printed_feeds.empty:
#全て新着Feed
new_entries = entries
else:
#既出のFeedは除く
new_entries = entries[~entries['id'].isin(already_printed_feeds['id'])]
if not new_entries.empty: #新着Feedがあれば
for key, row in new_entries.iterrows():
title = row['title'].split('http')[0]
#Mattermostに投稿されるメッセージ.ここではmarkdown形式でリンクになるように書いている.
feedinfo = '[**%s**](%s)' % (title, row['link'])
feeds_info.append(feedinfo)
#新着データがあれば既存のリストに追加する
already_printed_feeds = already_printed_feeds.append(new_entries)
#データベース(csv)に保存
if os.path.exists(filepath):
new_entries.to_csv(filepath,encoding='utf-8',mode='a',header=False)
else:
new_entries.to_csv(filepath,encoding='utf-8')
else: #新着Feedが無ければ
print('not found new entries')
return feeds_info
#既出のfeed情報の取得
def getAlreadyPrintedFeeds(filepath):
if os.path.exists(filepath):
col_names = ['no',
'author',
'author_detail',
'authors',
'guidislink',
'id',
'link',
'links',
'published',
'published_parsed',
'summary',
'summary_detail',
'title',
'title_detail',
'twitter_place',
'twitter_source']
already_printed_feeds = pd.read_csv(filepath)
else:
already_printed_feeds = pd.Series()
return already_printed_feeds
import json
def setPostMessage(feedinfo,username):
payload = {
'text':feedinfo,
'username':username,
}
return payload
def postForMattermost(feedinfo):
#Mattermostの内向きウェブフック
mattermosturl = 'http://localhost/hooks/***'
#Mattermostのつぶやき時に表示される名前(好きな名前をつける)
username = 'FeedBot'
header = {'content-Type':'application/json'}
#新着Feedを順にMattermostに投げる
for i in range(len(feedinfo)):
payload = setPostMessage(feedinfo[i])
resp = request.post(mattermosturl,
header=header,
data=json.dumps(payload))
def main():
#proxyの設定
proxies = {
'http':'http://id:passward@proxyadress:port',
'https':'http://id:passward@proxyadress:port'
}
#データベース(csv)へのパス
filepath = 'entries.csv'
already_printed_feeds = getAlreadyPrintedFeeds(filepath)
#RSSFeed取得先のURL
urls = ['http://twitrss.me/twitter_user_to_rss/?user=arxivtimes',
'http://twitrss.me/twitter_user_to_rss/?user=a_i_news',
'http://twitrss.me/twitter_user_to_rss/?user=ai_m_lab'
]
for i in range(len(urls)):
#RSS情報の取得
rss_data = requests.get(urls[i],proxies=proxies)
#データベースへの登録
feedinfo = getNewFeed(rss_data,already_printed_feeds,filepath)
#Mattermostへの送信
postForMattermost(feedinfo)
if __name__ == "__main__":
main()
多少整理もしたのでさっきよりは見やすくなったのではないでしょうか。Cの頃の癖なのか気づくと思考停止でインデックスでforループ回してますね。これはひどい。
長いわりに稚拙な内容でしたが参考になれば幸いです。