@sashimimochiの技術ブログ

2018/12/26追記

Facebookページでも同様のことができることがわかりました。
FEED.EXILEED.COM
ここにRSSを取得したいFacebookページのURLを入力してGenerate RSS linkのボタンを押せばRSSのリンクが取得できました。フォーマットはtwitrssと同じっぽいので、新規にロジックを組む必要もなさそうです。

2018/10/31追記

運用しているとTwitRSSからRSSの取得に失敗しているときがあるようなので、失敗したときはQueryFeedからも取得できるようにしました。個人的に苦戦したのはRSSの取得先によって返ってくる情報が微妙に合ってないので、そこを合わせてあげるところです(QueryFeedに一本化すればいいのでは...?)。特にQueryFeedの場合、Mattermostに飛ばしたい情報がSummaryの中にHTMLタグ付きで返ってくるので、そこからHTMLタグを削除してあげる必要がありました。

あと、ついでにその他で修繕したかったところをちまちま直しています。

  • Mattermostに飛ばす部分に含まれていた改行を削除できるようにしました。
  • デバッグしやすいように細かい単位で関数に分けました。
  • RSS情報の取得に失敗したときに404エラーだよとprintしてくれるようにしました。
  • あまり意味はないですが何となくクラス化しました。

HTMLタグの削除はこちらを利用させていただきました。
Qiita:python3でhtmlタグを除去

無駄に長くなったので、最新版のコードはこちらに晒しておきます。独学なのでコードの読みにくさは仕様です。
Github: mmfeedbot sashimimochi

はじめに

最近、職場でOSSの環境を乱立しまくっているのですが、そのひとつとしてMattermostも立ち上げました。釈迦に説法かとは思いますが、Mattermostは早い話が、オンプレミスで動くSlackです。うちもセキュリティが厳しいので、流行りのツールを使おうにもオンプレで動くものでないと使えなかったりします。
SlackクローンのMattermostを使ってみる - 導入、初期設定編

で、立ち上げたのはいいのですが、早速言われたのが、「これ何に使うの?」。うちは独自のチャットツールがあるので、わざわざMattermostを使わなくてもいいじゃんという考えの人が多いようです。そこで、よくチェックする記事を自動で呟いてくれるbotを作れば需要があるんじゃなかろうかということで作りました。※botではなく、単にスクリプトを組んだだけです。
使っている環境はMattermostとJenkinsとpythonです。構成としてはこんな感じになっています。



即興で作ったのでガバガバなのはご愛嬌ということで。Jenkinsはただのスケジューラーとして使っているので、こうして見返してみるとcronで十分だと思います(使ったことがないので確証はありませんが)のでJenkinsである必要ないですね。すでにJenkinsの環境は構築してあったし、Mattermostと繋げてジョブに失敗したときに通知が飛ばせるから多少なりともメリットはあるはず(震え声)。
今回はこちらのTwitterアカウントの更新情報を取得させていただくことにします。機械学習などの最新の論文情報をまとめてくださっているサイトです。アブストの日本語訳などもあるので、さっとチェックしたいときにとても参考になります。
arXivTimes (@arxivtimes) | Twitter

実装

RSS情報の取得

では、早速実装していきます。まずは、Twitterの更新情報を取得します。色々方法はあると思うのですが、今回はこちらを利用しました。
TwitRSS.me - rss of twitter user feeds by screenscraping with perl

Mattermostの設定

次に、Mattermost側で情報を受け取るURLを用意しておきます。今回は情報を受け取るだけなので、内向きウェブフックを使います。
SlackクローンのMattermostを使ってみる - 外部連携編 -(WebHooks、Hubot) - Qiita

pythonでの加工/Mattermostへの送信

いよいよ、RSSをpythonで取得、データベースに保存、json形式に変換、Mattermostに送信部分を組んでいきます。長いので処理内容毎に分けて説明します。とりあえず、動くことだけしか考えてないので出来の悪い実装は適宜修正してください。ベースはこちらのサイトを参考にさせていただきました。
Python+pandasを使ってRSSフィードを取得→Mattermostに投稿&DBに保存 - Qiita
こちらを参考に取得したのRSSの仕様を読み解いて、必要情報を抜き出します。
【データサイエンスの基礎】pythonでRSSからデータ収集 - Qiita

今回必要なのは、タイトルとリンクURLと(各ツイートに対してユニークな情報)です。プロキシの設定が必要な場合はこちらが参考になります。
PythonのWebスクレイピングでproxy経由でのhttpsアクセスがrequestsを使ったら簡単だった - Qiita

requestライブラリについては、こちらをどうぞ
Requests の使い方 (Python Library) - Qiita

取ってきたRSSの情報をデータベースに格納します。今回はそこら辺はサボってCSVに保存しています。DBを使う場合は先ほども上げたこちらのサイトに書かれています。
Python+pandasを使ってRSSフィードを取得→Mattermostに投稿&DBに保存 - Qiita

全体通して書くとこうなります。

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の設定

最後にこれを定期的に実行するためにJenkinsで実行スケジュールを組みます。と言ってもやることは単純で1時間に1回ジョブを実行させるだけです。確か今回採用しているRSSの取得方法が1時間単位に更新された気がするのでこんなもんでいいでしょう。こちらを参考にさせていただきながら、スケジュールを設定していきます。
[Jenkins] ビルドトリガ(定期的に実行)設定についてのまとめ - Qiita

ということで最低限飛ばせるようになりました。

複数ページのRSSに向けて改良

ただ、このままじゃ複数ページから取ってこれないのでもうちょっとだけいじります。といってもurlをリストにして順々に実行するだけです。

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):  
        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ループ回してますね。これはひどい。
長いわりに稚拙な内容でしたが参考になれば幸いです。

後半で追加したサイト

この記事へのコメント

まだコメントはありません