日記とか、工作記録とか

自分に書けることを何でも書いてゆきます。作った物、買ったもの、コンピュータ系の話題が多くなるかもしれません。

Raspberry pi 3 + Python = Twitter Bot(Mentionをとりあえず返せるところまで)

アカウントを独立する

さて、前回までにとりあえずPython-Twitterモジュールの力を借りて起動するようになったTwitterクライアントですが、BotBotらしくしたほうがいいのではないかということで、独立したアカウントを作成することにしました。

twitter.com

現在は、こちらのアカウントにRaspiParrotという名前でアプリケーションを登録しています。決まったことしかしゃべれないオウムのようなヤツなのでParrotという名前にしています。

ソースコードをひたすら書く

実装したい機能は、処理順に並べて大きく4つあります。

  1. 常時起動のプログラムで、定期的に動作を開始する
  2. 自分に届いたメンション(*1)を取得する
  3. 取得したStatesのそれぞれでループする。すでに返答を返したツイートも取得されるので、返答済みかどうかは判断する必要がある。
  4. まだ返答していないメンションを見つけたら何か応答を返す

(*1) 自分のアカウント名 @RaspiParrot という文字列を含んだツイートのことです

何からやるか?

下調べをした結果をもとに計画を立てました。実際には作りながら計画を立てている感じですが。

まず、自分に届いたメンションを取得するのはPython-TwitterのGetMentions()を使用すれば得られることがわかりました。

得られるのはメンションのリストなので、for文を使ってひとつひとつ確認することにします。メンションには、それが投稿された日時を示すcreated_atやcreated_at_in_secondsという要素を持っているので、いつのメンションまで返答済みなのかを記録しておいて、その時刻よりあとに届いたメンションに返答を返すことにします。

また、このプログラムは相当な回数再起動をすることになるので、どこまで返答済みかは、プログラムの外部に保存しておく必要があります(そうでないと何度も返答がきてうっとうしいことになるので)。コンフィグファイルを作成してそこに記録を残すことにしましょう。

返答を返すのはPostUpdate()というPython-TwitterAPIに任せればよい…… と思ったのですがこれが大変でした(後述)。返答内容は、まずは簡易的にごあいさつを返すだけにしましょう。 @(相手のスクリーン名) + あいさつひとこと、でよいのですが、Twitterは同じ内容のメッセージを連投することはできない制限があるので、場当たり的ですが日時文字列を追加して重複メッセージを避けることにします。

今回のハマリポイント

なんでもない作業と思っていても、毎回何らかのポイントではまります。プログラムを書く力というのは結局はまったときに対処する方法をどれだけ持っているかですね。今回はPython-Twitterのバグでした。api.pyの中でstr()関数の変換のエラーがでています。モジュール内のエラーだと対処に困ります。ということで本家を探してみたところ、既知の問題のようです。

pi@pi3:~/twitter $ ./RaspiParrot.py
Traceback (most recent call last):
  File "./RaspiParrot.py", line 86, in <module>
    main()
  File "./RaspiParrot.py", line 82, in main
    OneCycle()
  File "./RaspiParrot.py", line 62, in OneCycle
    ReplyMention(api, state)
  File "./RaspiParrot.py", line 77, in ReplyMention
    newstates = api.PostUpdate(u"@%s メンションありがとう" % ( state.user.screen_name ))
  File "/usr/local/lib/python2.7/dist-packages/twitter/api.py", line 949, in PostUpdate
    u_status = str(status, self._input_encoding)
TypeError: str() takes at most 1 argument (2 given)

Error in UploadMediaChunked · Issue #345 · bear/python-twitter · GitHub

ここでは、対処済みということになっているのですが、私の手元では問題が再発します。該当のファイルが古いのだろうか、など調べてみたのですがそうでもなく…… 原因がよくわからなかったのですがPython 2.7の互換性の問題だという説明があったので、やむをえずPython3を使って動かすことにしました。

Raspberry piにはPython3がインストール済みでしたが、Python-Twitterは2.7にインストールされていたので、インストールし直しになりました。

  1. /usr/bin/pythonがpython27へのシンボリックリンクになっていたのでいったん削除
  2. python3へのシンボリックリンクを/usr/bin/pythonにリンクしなおし
  3. Python-Twitterをインストール

これでなんとか動くようになりました。

ソースコード

というわけで、できたのがこちらのソース。
同じ物をGitHubにも置いておきます。

github.com

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import sys
import twitter
import time
import logging
import configparser

CONFIGFILE="config.ini"

def init():
    '''
    最初に1回だけ処理する部分
    '''
    # 設定ファイルの準備
    global inifile
    inifile = configparser.ConfigParser()
    inifile.read(CONFIGFILE)

    # ログファイルの準備
    global logger
    logging.basicConfig(filename=inifile.get("log","filename"),
                        level=logging.INFO,
                        format='%(asctime)-15s %(levelname)s %(message)s')
    logger = logging.getLogger("RaspiParrot")

def GetAPI():
    '''
    Twitter APIを準備する部分
    '''
    global inifile
    logger.info("Twitter APIの使用を要求")
    consumer_key    = inifile.get("keys", "TWEETUSERNAME")
    consumer_secret = inifile.get("keys", "TWEETPASSWORD")
    access_key      = inifile.get("keys", "TWEETACCESSKEY")
    access_secret   = inifile.get("keys", "TWEETACCESSSECRET")
    encoding        = 'utf-8'
    return twitter.Api(consumer_key=consumer_key,
                       consumer_secret=consumer_secret,
                       access_token_key=access_key,
                       access_token_secret=access_secret,
                       input_encoding=encoding)

def OneCycle():
    '''
    定期的に、繰り返し処理する内容
    '''
    global inifile
    logger.info(u"----- OneCycle開始 -----")
    LastMentionSeconds = int(inifile.get("records", "LastMentionSeconds"))

    api = GetAPI()

    try:
        MaxMentionSeconds = 0
        for state in api.GetMentions():
            logging.debug(u"LastMentionSeconds:%s" % ( LastMentionSeconds ))
            logging.debug(u"created_at_in_seconds:%s" % ( state.created_at_in_seconds ))
            t = time.strftime(u"%Y-%m-%d %H:%M:%S", time.localtime(state.created_at_in_seconds))
            if LastMentionSeconds < state.created_at_in_seconds:
                 logger.info(u"新しいMentionが到着:[%s] %s" % (t, state.user.screen_name) )
                 # このMentionは未処理なので応答を返す
                 ReplyMention(api, state)
                 if MaxMentionSeconds < state.created_at_in_seconds:
                     MaxMentionSeconds = state.created_at_in_seconds
            else:
                 logger.info(u"返答済みのMention:[%s] %s" % (t, state.user.screen_name) )
        # メンション応答があった場合はLastMentionSecondsを更新
        if MaxMentionSeconds != 0:
            inifile.set("records", "LastMentionSeconds", str(MaxMentionSeconds))
            inifile.write(open(CONFIGFILE, 'w'))
    except UnicodeDecodeError:
        print("Your message could not be encoded.  Perhaps it contains non-ASCII characters? ")
        print("Try explicitly specifying the encoding with the --encoding flag")
        sys.exit(2)
    logger.info(u"----- OneCycle終了 -----")

def ReplyMention(api, state):
    '''
    Mentionが届いている。何か気の利いた返答をしよう。
    ToDo: ちっとも気が利いていないので何か考える
    '''
    logger.info(u"Mention返しを開始")
    t = time.strftime(u"%Y-%m-%d %H:%M:%S", time.localtime())
    message = u"@%s メンションありがとう[%s]" % ( state.user.screen_name, t )
    newstates = api.PostUpdate(message, in_reply_to_status_id=state.id)

def main():
    '''
    メインルーチン
    '''
    init()
    while True:
        OneCycle()
        # Twitter API Rate Limitにより、15回/15分の制限がある
        time.sleep(180)

# おまじない
if __name__ == "__main__":
    main()

動作します

今のところごく簡単なありがとうメッセージを返すだけです。フォローする必要はありません(しても、何も起こりません)。@RaspiParrotにメンションが届くと、こんな簡単なメッセージが返ってきます。開発中でなければ、動き続けているはずです。

f:id:WindVoice:20160721173135p:plain

今後

あとはなんといっても気の利いた返答ですね。ここがアイディアの絞りどころなわけですが…… できそうなことで何かあるかなぁ。検討中です。