ブログbot

「ねこぜ」と申します。

twitterの @x21_blog_bot について、仕様や実装の話をします。X21のメンバーの最新のブログをツイートするbotです。

僕が技術的な話をくどくどしたいだけなので、後半は読み飛ばしてくださって構いません。

0. 動機づけ
毎日、ほのぴょんのブログの更新をチェックして コメントを書くので、自分が使いたいと思ったこと!
まわりの方で コメントをすぐ書いている方に話を伺ったら、手動で何度も更新して調べていて大変そうだったので、少しでも苦労が減ることを祈って。
beamieの公式アプリで 新着をチェックできるけど、「通知が入れられない」仕様なのが理不尽だと思ったから。
ースマホアプリで通知機能がないのはどうなのかー

1. 仕様
2017/06 時点でのX21メンバー19名の、beamieへのブログ記事投稿の新着を調べます。
見つかればそのブログ記事のURLを、投稿したメンバー名のハッシュタグとともに @x21_blog_bot からツイートします。
新着記事のチェックのみなら マイアミー登録し個人ページを見ればよいだけなので、このBOTのメリットは 通知を入れられることかなと思っています。
通知は 当アカウントをフォローすれば入れられるので、記憶の片隅に置いておいてくださいね。
実装で詳しく述べますが、ブログ記事の新着記事検索は、手動の場合と同じく、各メンバーのページに定期的にアクセスするだけです。今の設定だと1分に一回、19名分で行なっています。新着記事発見から 投稿までのタイムラグは1分ほど。
運用していく中で気づいたのですが、毎時00分 30分まわりの投稿が多く、これが予約投稿機能かな、と思っています。経験的にそのことを気づいている方も多いかと思いますが、毎時01分 31分のチェックは 効率が良いです。
しかし、それとは別に 手動で投稿することができるので、半端な時間に投稿されるケースもあります。その場合は 当botを有効活用していただければ、と思います。

2.実装
最近やっと動作が安定したので、ドキュメント的な感じで。(自分以外に保守する人間が出てくるとは思いませんが)オープンソースにしようかなとは思ってます。今はクローズドです。

実行環境はPython3.x 必要な外部モジュールはtweepy, lxml, crontabです。ブログのクロールと ツイッターへの投稿に必要な機能をまとめて、beamie_botというモジュールにまとめてます。
今は、スクリプトごとに beamie_botの機能を組み合わせて、目的のbotを作ってます。

2.1 ブログのクロール
まず、beamieのURLについて少し話します。適当な記事のURLをご覧ください。
ttps://beamie.jp/?m=user&a=blog&k=detail&target_c_diary_id=$(ダイアリーID)
こうなっているはずです。
ダイアリーIDはその名の通り 記事を識別するためのIDです。
つまり、新着記事を見つけるというのは 新しいダイアリーIDを見つけることと同じです。
では、どうやって指定した人物のみのダイアリーIDを得るのでしょうか?
適当な人物の個人ページのURLを見てみましょう。
ttps://beamie.jp/d/$(名)_$(姓).html
こんな風になっていると思います。姓, 名はローマ字表記です。他の人物の姓名に置き換えてみれば、その人物のページに移動することがわかると思います。(例: miyu_yoshimoto)
このページに新着が上となる順番に記事へのリンクが並んでいます。
今月は6月ですが、5月の記事やもっと過去の記事のダイアリーIDを得たい場合は?ページ下部の
「次の16件」をクリックしてみましょう。URLが
ttps://beamie.jp/d/$(名)_$(姓).html?page=$(ページ数)
と変化したはずです。ページ数は1以上の整数で、増えるほど過去に遡ります。
1ページの記事数は16で、最後のページは16以下になります。
ちなみに、ページ数を1とした場合、個人ページと同じところにアクセスし(デフォルト値)、ありえない数字(0とか8931とか)にすると、記事へのリンクが何もないページにアクセスします。
各ページからのダイアリーIDの取得は、htmlを解析すればよく、
今回の場合、xpathでいえば//div[@class="post"]/a
つまり、「クラス属性がpostであるdivタグの子要素a」となります。
このaタグのhref属性のURLで直接記事に行けます。当然 ここにあるtarget_c_diary_id=に続いた整数値を抽出すれば、ダイアリーIDも取得できます。

以上まとめると、
調べたいメンバーの姓名をローマ字表記にして、ページ数=1から 必要な分までページ数を増やして辿って行き、そのhtmlの特定の場所から URLを抽出すればよいということになります。

2.2 ツイッターへの投稿
これは tweepyを基本通りに使えば、問題なくできるはずです。tweepyで検索して 検索上位だったページを読んだ程度の知識で ツイートは簡単にできます。
TwitterAPIへのアクセス権のために、トークンやキーの取得が必要になります。 なりますけど その方法については かなり多くのサイトで紹介されているので、僕からは特にありません。
自分は、重複した投稿でエラーになるのがちょっと嫌だったので、投稿をtry: except: で包んで、重複だった場合は raiseせずNoneを返すメソッドpost_ignore_duplicateを定義しました。
あと、API認証に必要な4つのトークンやキーを記述したjson形式のファイルからtweepy.APIインスタンスを作るメソッドも作りました。
よく見るtweepyのサンプルコードだと、こういう情報をハードコーディングしている上に、グローバル変数だったりするので、僕はそれは嫌だからそうしました。セキュリティ性も再利用性もよくないので。

ちなみに、投稿時、URLはツイッターによって短縮URL化されます。

2.3 並列処理(threading)
仕様で述べた通り、このbotは19人分のブログ記事をクロールします。こういうのってまさに IOバインドな状態の訳で、並列処理の例題にもってこいだなぁと思ったので、以下のようなスレッドを用意しています。
①BlogCrawler タイミング合わせ用のeventオブジェクトがsetされるごとに 指定された個人ページにアクセスして、新着記事のダイアリーIDが見つかるごとにqueueにputするスレッド
②Clock ①で使用するタイミング合わせ用のeventオブジェクトをsetするスレッド
③メインスレッド 他のスレッドを生成・管理して、queueに入ってきたダイアリーIDを取り出してツイート内容を作成し、ツイートする。あるいは、他スレッドのエラーなどを監視する。

人数分のBlogCrawler 加えてClock メインスレッドの計21スレッドが稼働します。
この時、メインスレッド以外のスレッドは実行中は止まらずに動き続けて欲しいスレッドです。
しかし、ネットワークがらみのことをしている以上は、GATEWAY TIMEOUTなどの エラーを出してしまいます。この時、メインスレッド以外で起きた例外がraiseされないPythonの仕様のために、そのスレッドは無言で止まってしまいます。
すると、生存しているクローラーが一人、また一人と減ってしまうという 悲しい現象が発生します。
そこで今回は、errorpoolというモジュールを用意して、例外オブジェクトを保存し、取り出すことができるようにしました。
各スレッドの実行文をtry: except: で包んで、例外をerrorpoolに保存するようにして、
メインスレッドでは取り出してraiseするようにします。
例外の補足をするために、errorpool.registerというデコレーターを用意して、それで各スレッドのrunメソッドをラップしてやるだけでOKです。
勿論、errorpoolではqueueを用いて例外を保存します。スレッドセーフは重要。

これで、どこかのスレッドで例外が発生したことを メインスレッドは知れるようになりました。
今回のプログラムでは、エラーが起きた場合、一旦他のスレッドを全部止めて、また1からやり直すという方法にしてあります。
そこで、スレッドに対して停止させるメソッドを用意しました。その名もシンプルにkillです。
threading.threadを継承したKillableというクラスを作り、
killメソッドでフラグを立てて、
runメソッドの適当なところに
if self.isKilled():return
を挟んで停止できる、というような感じです。
並列実行についてまとめるとこんな感じ。
try:
# スレッドの初期化とstartをする
while True:
#他スレッドのエラーを監視
beamie_bot.errorpool.check()

#処理を書く

finally:
beamie_bot.threads.killAll()
beamie_bot.threads.joinAll()

こういった感じです。

2.4 ログ機構
長期間動くプログラムにログ機構は不可欠です。中がどうなっているのか気になりますし。
今回は デバッグ用のログに加えて、実行中のエラーを 管理者に報告するようなロガーを作りました。
なぜなら、このプログラムは 例外が起きたら スレッドを停止後、再起動するような無限ループだからです。HTTPErrorなら もう一回やって貰えばいいので 無視してもいいですが、それ以外のエラーが起きたら おそらくバグですし、それでも再起動してします...
管理者に報告するようなロガーは、以下のように作っています。

・flush()された時に、管理者に連絡するような Streamを実装する。
・そのStreamを使って、logging.StreamHandlerを作成する
・対象とするロガーにaddHandlerメソッドで登録する。

ストリームの実装については、いろいろ考慮した結果、管理者=私のアカウントにDMを送ってもらえるのがいいかなと考え、DirectMessageStreamと名付けました。文字列を使うので、io.StringIOを継承しました
flushされた時に、StringIOの値を用いてtweepyでDMを送り、
内部バッファをクリアする、という感じです。


↑エラー発生時に送られてくるDMの例。これは GATEWAY_TIMEOUTなので、大丈夫なエラー。しかし、Pythonのエラーをスマホで見るのは大変(最後の行だけ読めばいい) DMは字数制限がないから この用途に適していると思います。

2.5 運用および保守
プログラムの仕様上、24時間稼働が必要です。初めは シェルスクリプトなんかを使っていたのですが、完成形ではPythonしか使わないので、OSの依存性はないです。
いろいろ考えたのですが、現状ではAmazonWebService=AWSのEC2でインスタンスを作って 運用してます。Linuxです。初めの12ヶ月は無料だそうです。
なので 来年の今頃になったら 必死に他の無料のサービスを探すんじゃないでしょうか? ぜひおすすめを紹介してください。
ちなみに、候補としてはRaspberry Piを使う、なんていうのもありました。それはそれで面白そうだと思います。
EC2に関しては、sshがわかれば 苦もなく使えます。そこで思ったのが、なんとかして出先でbotの動作を確認できないか!? ということで、今は iPhone用のsshクライアントiTerminalから 直接覗いております。 これが本当に手軽で素晴らしい。
logファイルは ディスプレイでの閲覧をするように作っており、縦長画面のiPhoneには見えにくいので logファイルの文字を省略するシェルスクリプトを仕込んで 読みやすくしてます。


↑起動直後の画面 キー入力がiPhoneです


↑動作チェックのスクリプトを実行したところ tickはClockがブログのクロールの開始を指示したことを示している。現在時刻と最後のtickの時刻が一致しており、全19人分の名前が並んでいることから 正常動作とわかる。

だいたいこんな感じです。
これくらいのことができたなら、今度はこういうのはどうか?
あるいは こういう需要がある! みたいな アイディアなどお待ちしています!

コメント

不適切なコメントを通報する

最新ブログ

近況
君と 僕と
いいね企画、感謝の気持ち!!!
皆様にお願いですっ!!!