ブログ記事の更新を自動投稿するTwitter BOTを作る
twitterfeed という外部サービスを利用していましたが,つぶやくタイミングとか調整できませんし,シンプルなものを自分で実装したくなりました.
そこで,ブログ記事の更新があったときに Twitter に自動で投稿してくれるプログラムを組んでみたいと思います.
希望する仕様
記事を公開した後に feed か何かを拾って投稿すれば OK なんじゃないかと思いましたが,どうやって拾うのかが一番の問題です.
一定時間ごとに BOT を動かして最新の記事 1 件をつぶやくようにすれば,行けそうな気がします.
言語は Python で,Twitter を利用するために Tweepy, サーバには PaaS の Heroku と自動実行のために Heroku のアドオン Heroku Scheduler を使います.
feed を取得するためのモジュール
自力で取得プログラムから書くのは費用対効果が小さすぎるので,モジュールを使いたいと思います.
いろいろ調べてみたところ,Python で feed を取得するパッケージに feedparser というものがあるらしいです.
随分前からあるみたいで今も使っている人がいるようなので,これを利用することにします.
feedparser の導入
例によって pip によるインストール.
terminal
$ pip install feedparser
インストール後,feedparser を少し使ってみてどのように動くのかテストします.(リファレンスを読もう)
import feedparser
url = 'http://diary.aita.info/feed'
res = feedparser.parse(url)
print(res.entries[0].title)
print(res.entries[0].link)
これで最新の記事のタイトルと URL を取得できることがわかりました!
が,ここで一つの問題にぶつかってしまいました.毎回最新記事を取得しても,ブログ更新が行われていなければ同じ記事を投稿してしまいます.
post.txt みたいなものを用意しておいて,そこに前回つぶやいた URL を保存し,次に実行したときにマッチするかどうかで判別すればいいと思ったんですが...
import feedparser
url = 'http://blog.mktia.com/feed'
res = feedparser.parse(url)
title = res.entries[0].title
link = res.entries[0].link
f = open('post.txt', 'r')
l = f.read()
f.close()
if l != link:
print(title)
print(link)
f = open('post.txt', 'w')
f.write(link)
f.close()
else:
print('You have already posted.')
二回目以降は You have already posted. と表示されます.(これが推奨される方法かは知りません)
残念ながら,Heroku 上ではこうはいきません.(理由は後述)
前回投稿した URL を記録する
データベースは使わない
Heroku は PostgreSQL を使えるようですが,単に前回つぶやいた URL を記録するためだけに使うのも...というのは建前.使い方を知らないのでスルーです.
他にも Amazon S3 などがよく使われているようですが,アドオンの Filestack などを利用するのがこれまた面倒で,無料期間が 1 年だったりするのでこれもパス.
データベースを使わないでもっと簡単にやりたい!
一時ファイルも使えない
Heroku 上では原則としてファイルを保存することができませんが,tmp フォルダ内にのみ一時ファイルを保存できるそうです!
しかし,あくまで一時的なものなので 10 ~ 30 分で消えてしまいます.数時間おきにしか動かない BOT には使えません.
Twitter 内に保存すればいいんじゃないか
紆余曲折あって最終的に辿り着いた結論は,Twitter 内のどこかに保存しておくという手段です.Twitter の内部に保存していれば API で呼び出せます.
ツイートして記録するとか自己紹介文に記録するとか色々考えましたが,思いついた中で恐らく最良の手段は,リストの説明文に保存する方法です.リストは非公開にしておけば自分にしか見えず,リスト名で説明文を取得して処理すればうまくいきそうです.
twitter のリストの取得
リスト名は固定し,リストの説明文に URL を保存する方向で実装します.
api.get_list(owner_screen_name='USER', slug='LIST_NAME')
このメソッドを使えばリストの情報を取ってくることができます.
エラーメッセージ
このメソッドを使ってたら何度もエラーに遭遇することになってしまいました...
You must specify either a list ID or a slug and owner. code: 112
(前にもやった気がしますが)このようなエラーが出た場合は,引数の指定の仕方が間違っています.それは明白なのでまだいいのですが,問題は次のエラーメッセージです.
Sorry, that page does not exist. code:
ページ,この場合はリストのことだと思いますが,存在しないとは....以前,リストに追加されたユーザーを取得するときもそうでしたが,リスト関連の API は扱いがめんどくさい.
このエラーの原因を探るのに数時間も費やしてしまいましたが,どうやらリスト名にアンダースコア ( _ ) が含まれているとダメらしいです.アンダースコアを使っていいか悩んだ時点で避けておけば...
もちろん,リスト名が日本語とかめんどくささの極みです.Unicode 辺りで送信すれば返ってくるかと思いますが.
全体像
tweepy で作る BOT の中で feedparser を利用して feed を取得し,その URL をつぶやきます.ただし,リスト機能を使って前回つぶやいた URL を記録しておき,重複してなければつぶやくようにします.
さらに,複数の URL を利用できるようにします.
ちなみに,リストはプログラム内で生成するのではなく,予め作っておきます.説明文もテキトーに入力しておかないと取得できず,エラーで動かない可能性があります.
import tweepy, feedparser
#認証
consumer_key = 'consumer keyの値'
consumer_secret = 'consumer secretの値'
access_token = 'access tokenの値'
access_token_secret = 'access token secretの値'
auth = tweepy.OAuthHandler(consumer_key, consumer_secret)
auth.set_access_token(access_token, access_token_secret)
api = tweepy.API(auth)
#feed URLとリスト名のリスト
feed_url = [['URL1', 'LISTNAME1'], ['URL2', 'LISTNAME2']]
for data in feed_url:
#ブログの最新記事のtitleとURLを取得
res = feedparser.parser(data[0])
title = res.entries[0].title
link = res.entries[0].link
#リストの説明文を取得
list = api.get_list(owner_screen_name='自分', slug=data[1])
#説明文に記録したURLとfeedで取得したURLを照合し,一致しなければ動く
if link != list.description:
#ツイート
api.update_status(title + ' ' + link)
#リストの説明文のURLを更新
api.update_list(slug=data[i], description=link, owner_screen_name='自分')
あとは Heroku にデプロイするだけです.requirements.txt に feedparser を追加することをお忘れなく.(引っかかった顔)
デプロイした後にログを見てわかったことですが,title をツイートしようとすると文字コード関連でエラーが発生するようなので,とりあえず URL だけつぶやくといいかもしれません.
# -*- coding: utf-8 -*-
一行目にこれを書いておけばたぶん OK です.Twitter card に登録していると,URL だけでタイトルと短い説明文,画像なんかを自動で表示してくれるので便利ですね.
(2017/4/5 追記)
feed_url のところもリストの説明文に書いて取得する方式にしたので,新しく feed 取得したいサイトを簡単に追加できるようになりました.
最後に
無事完成できました.
ファイルの保存ができないという Heroku の特性...なのか PaaS では概ねそうなのかは知りませんが,Twitter のリスト機能がこんなことに応用できるなんて.おかしいけど.
以前 MySQL 辺りを少し勉強した気もしますが,データベースの勉強もそのうちしないといけないですね.