日記とか、工作記録とか

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

Raspberry pi + Twitter : 謎の例外が飛んできても動揺せずにしゃべり続ける

動作を安定させるために

しばらく動かしたままにしておいた @RaspiParrot ですが、いつの間にか止まっていました。

twitter.com

何か動作不具合が起こっているに違いないのですが、ログではTwitterにアクセスしたところで突然死んでいることしかわかりません。Python-TwitterモジュールはTwitter APIのエラーが発生した場合にTwitterError Exceptionをraiseするようになっており、私のプログラムも例外処理をしているのですが、今回は突然停止していることからそれ以外の例外が発生しているとみられます。このままでは原因追求ができないので、標準エラー出力をファイルに取得しておきました。

pi@pi3:~/twitter$ more boot.sh 
#!/bin/bash

./RaspiParrot.py 1>> log/stdout.log 2>> log/stderr.log &

pi@pi3:~/twitter$

で、今回停止した時に取得された標準エラー出力がこれ。実際にはもっと長いのですが要点を抜粋です。

Traceback (most recent call last):
  File "./RaspiParrot.py", line 201, in CheckMentions
    for state in api.GetMentions():
  File "/usr/local/lib/python3.4/dist-packages/twitter/api.py", line 3430, in GetMentions
    resp = self._RequestUrl(url, 'GET', data=parameters)
  File "/usr/local/lib/python3.4/dist-packages/twitter/api.py", line 4772, in _RequestUrl
    resp = requests.get(url, auth=self.__auth, timeout=self._timeout)
  File "/usr/lib/python3/dist-packages/requests/api.py", line 60, in get
    return request('get', url, **kwargs)
  File "/usr/lib/python3/dist-packages/requests/api.py", line 49, in request
    return session.request(method=method, url=url, **kwargs)
  File "/usr/lib/python3/dist-packages/requests/sessions.py", line 457, in request
    resp = self.send(prep, **send_kwargs)
  File "/usr/lib/python3/dist-packages/requests/sessions.py", line 569, in send
    r = adapter.send(request, **kwargs)
  File "/usr/lib/python3/dist-packages/requests/adapters.py", line 407, in send
    raise ConnectionError(err, request=request)
requests.exceptions.ConnectionError: ('Connection aborted.', gaierror(-2, 'Name or service not known'))

つまり接続エラーな訳ですね。今のところ、DHCPIPアドレスの解放と再取得の動作が怪しいと感じていますが、原因追求のためにもう少し細かいエラーを取得することにします。他の問題も起こるかもしれませんし。こんな感じで未知のExceptionも最低限ログが残るようにします。

    try:
        api = twitter.Api(consumer_key=consumer_key,
                       consumer_secret=consumer_secret,
                       access_token_key=access_key,
                       access_token_secret=access_secret,
                       input_encoding=encoding)
        logger.info(u"GetAPI:正常終了")
    except TwitterError as e:
        logger.warning(u"twitter.Api error: %s" % ( e.message ))
    except Exception as e:
        logger.warning(u"Exception[0]%s [1]%s [2]%s" % (sys.exc_info()[0], sys.exc_info()[1], sys.exc_info()[2]))

これで再始動して様子見です。少なくとも、エラーが発生してもそのまま死なずにリトライすることを期待しています。さて、何が釣れますやら……

オウムらしからぬ適当なことを喋る

目次 [Raspberry pi 3 + Python = Twitter Bot]

  1. Twitterの開発者登録からツイートをポストするまで
  2. Python-Twitterモジュールの情報まとめ
  3. メンションをもらったら返答するようにした
  4. 異常終了の原因を追及できるように例外の処理を追加
  5. マルコフ連鎖モジュールのPython3対応作業

前回までに、マルコフ連鎖に関連するモジュールをいくつか改造しまして、動作するようにしました。これは、ツイートを自動生成する処理の基板にあたるものです。今回はようやく独自のツイートを生成して投稿するところを作ろうと思います。

つくっていて急に心配になったのですが、オウムも場合によっては人語を喋りますよね。九官鳥じゃなかったかと心配になりました。他所様の動画ですが、これすごいですね。オウムが笑うと子供がつられて笑うので、ますますオウムが笑うようになるという…… 恐ろしい連鎖です。

youtu.be

ツイート生成の方針

これまでに、トレンドワードを取得してリストにするところまではできていました。これをツイートにまとめて投稿することもできます。

f:id:WindVoice:20160724212621j:plain

トレンドを取得すると、キーワードだけでなく、関連する情報もついてきます。キーワードは trend.name という名称で取り出せるのですが、他にも trend.url という名前で、関連ツイートを検索するURLが取得できます。このURLをSearchして、他のツイートをまとめ、それをマルコフ連鎖モジュールで「学習」したあと、自分なりのツイートを生成することにします。できあがりはこんな感じです。

f:id:WindVoice:20160724212910j:plain

ただ、取得したツイートを学習させるだけだと、他の方のツイートをそのままパクツイしたようなものも生成されてしまいます。それに他人を中傷するツイートも少なからずあるので、安易にマネしてしまうのは危ないこともあります。そんなわけで、カタカナに展開することでインパクトを和らげることにしました(まぁ、オウムですし……(逃避))。ただ、全部カタカナだと何を言っているのかわからないので、トレンドワードそのものは残しておくことにしています。

twitter.com

技術的なところ

マルコフ連鎖は、前回Python3対応の作業をした python-markovchains と、その前提モジュールとなる py-extractword を使用します。

具体的には、こんな流れになりますね。あとはひとつひとつ地道に作っていくだけです。

  • 地域の指定を「日本」にしてトレンドワードを取得(twitter.GetTrendWoeid)
  • trend.url を使用してトレンドワード関連ツイートを取得(twitter.GetSearch)
  • ツイート群からテキストを取り出し(status.text)、マルコフ連鎖の邪魔になるゴミを取り除く
  • テキストを全部連結してmarkovchains.analyze_sentence()で学習させる
  • markovchains.make_sentence()で文章を作成する。ほどよいのができるまでリトライ
  • MeCabを-Oyomiオプションで起動して、読み仮名を取得する
  • できあがったツイートを投稿

今回のハマリポイント

markovchains.make_sentence()が、無限に長い文章を生成して応答を返さなくなることがあります。プログラムがCPUを100%使用する状態のまま止まってしまい原因を探るのに苦労しました。そういうときはpython -m pdbを使用してデバッガから動作を確認するのが早道かとおもいます。

この問題は、python-markovchainsのmake_sentence()関数の中に処理を追加して対処しました。ツイッターではどうせ140文字を超えたらツイートできないのですから、漢字を含めて100文字くらいで終わってくれないと、使えません。ある程度長くなったところで諦めて結果を返すように動作変更しました。GitHUBからforkしたのは今回が初めてですが、やってみるものですね。

次回予告?

気の向くままに作業していますが、それでもフォローしてくれる人がいるので、フォローが届いたらフォロー返しをしようかと思います。多くのBotはこちらから積極的にフォローしていきますが、そこまでやっても得るものがあるかどうか…… しばらくはやめておきましょう。

Raspberry Pi 3 Model B (Element14)

Raspberry Pi 3 Model B (Element14)

Raspberry pi 3 + Twitter [RaspiParrot] 他のプログラムのPython3対応作業

目次

  1. Raspberry pi 3 + Python = Twitter Botの起動(とりあえず投稿するところまで)
  2. Python Twitterのメモ
  3. Raspberry pi 3 + Python = Twitter Bot(Mentionをとりあえず返せるところまで)
  4. 例外の処理を追加

現在の作業目標は、Twitterのトレンドワードに関連したツイートを多数集めて、それらに使われている単語を合成し、何か面白そうなツイートができないかな、というものです。

トレンドワードに関連するツイートを収集

Twitterにはトレンドという情報があります。多くの人がつぶやいているキーワード(ニュースとかテレビ番組とかの話題が多い)がリストになったものです。この情報の入手は簡単で、Twitter APIとしてすでに実装されていますので、割合スムーズに収集することができました。部分的なソースを引用しますが、GetTrendsWoeid()という関数が、Twitter APIの下のマニュアルの機能を呼び出しています。WOEIDというのは、地球上の特定の場所を示す郵便番号的なもののようです。私のソースでは23424856を使っており、これは日本を示すWOEIDです。都道府県単位でも変更できるようです。

dev.twitter.com

def CheckTrendWord(api):
    '''
    時々トレンドを入手してコメントしたりする
    '''
    global trendticker
    try:
        logger.debug(u"trendticker:%s" % ( trendticker ))
        if trendticker != 0:
            trendticker -= 1
        else:
            trendticker = int(inifile.get("trend","frequency"))
            # woeid = 23424856は、日本のトレンドワード
            trend = api.GetTrendsWoeid(woeid=23424856)
            # トレンドに関連したツイートを調査
            AnalyzeTrendTweet(api, trend)
            # トレンドワード一覧をツイート
            PostTrendWord(api, trend)
    except TwitterError as e:
        logger.warning(u"CheckTrendWord Error:%s" %( e.message ))

で、とりあえず @RaspiParrot はトレンドワードを並べて定期的にツイートする機能を実装しています。Botにありがちな機能ですね。昨日はポケモンGO関係のキーワードばかりになっていてテストとしてはイマイチな感じでした。

f:id:WindVoice:20160723225643p:plain

しかし、正直これだけだと、フォローしたいアカウントとは言えないですね。もっと面白くしなければ……

独自のツイートをつぶやかせたい

マルコフ連鎖、という技術があります。たくさんの既存の文章から、単語と単語のつながりに関する情報を学習して、ある単語の後には別のある単語が来ることがある…… といった情報を集積することで、今度はその情報をもとになんとなく意味が通りそうな文章を生成できるというものです。単語と単語のつながりしか見ていないので、文章や文脈の全体としては、まとまった内容になりません。何も知らずに読んでいると、酔っているか、頭のおかしい人が支離滅裂なことを言っているように見えます。でも、今作っているのはBotですし、名前もオウム(Parrot)ですので、その辺りはご愛嬌ということにしまして、ともかく独自の文章を生成するプログラムの作成を進めることにします。

実は何年も前に書いたマルコフ連鎖のプログラムが手元にあるのですが、Perlで書かれているので、Pythonに書き換えるのも手間がかかります。どうしたものかと思っていたらすでに作成している方がいたので、GitHubにあったソースコードをforkさせてもらうことにしました。

github.com

すぐ使えるかと思ったのですが、Python 2系列で使うことを目的にしていて、そのままでは動作しませんでした。また、MySQLPostgreSQLPythonモジュールがインストールされていないと、これらのデータベースを使わない場合でも動かない、というちょっと困った依存についても排除したかった(だってそれらをインストールするにはまたPython3の壁に突き当たるので……)ので、forkして修正させてもらいました。その私のレポジトリがこちら。

github.com

まだ広汎なテストはできていませんが、少なくともサンプルレベルであれば動きます。前提パッケージになっているpy-extractwordの方もPython3向けに修正が必要でしたのでforkしました。情報を集積するデータベースとしてMySQLPostgreSQL は使いたいのであれば使える(はず)ですが、テストはしていません。私としては、使わずに済ますつもりでいます。

github.com

さらに前提としてMeCabpython-mecabも必要なのですが、こちらもPython3で動かすのに苦労したものの、なんとかなりました。ネットを検索すると他の方の足跡とともに多数の情報が見つかりますので、ここには書きません。必要でしたら探してみてください。

準備完了……? のつもりで次回予告

これで作りたいプログラムの前提になるモジュール類は揃ったと思いますので、ようやくトレンドツイートを収集して、学習して、なんとなく今はやりのツイートっぽい発言をするオウムの作成を目指そうと思います。

Raspberry pi 3でTwitter [RaspiParrot]

#おそるべしPokemon Go…… ってのはさておきまして。

安普請だったのでもう少し堅牢に修正

昨日動かしたままにしておいたRaspiParrotなのですが、謎のエラーで停止していました。

2016-07-21 18:40:40,130 INFO ----- OneCycle開始 -----
2016-07-21 18:40:40,131 INFO Twitter APIの使用を開始します。
2016-07-21 18:40:40,146 INFO Starting new HTTPS connection (1): api.twitter.com
2016-07-21 18:40:40,842 INFO 返答済みのMention:[2016-07-21 17:52:19] WindVoice_en
2016-07-21 18:40:40,843 INFO 返答済みのMention:[2016-07-21 16:21:50] WindVoice_en
2016-07-21 18:40:40,843 INFO 返答済みのMention:[2016-07-21 15:06:49] WindVoice_jp
2016-07-21 18:40:40,844 INFO 返答済みのMention:[2016-07-21 10:32:28] WindVoice_jp
2016-07-21 18:40:40,845 INFO 返答済みのMention:[2016-07-21 09:19:22] WindVoice_jp
2016-07-21 18:40:40,845 INFO ----- OneCycle終了 -----
2016-07-21 18:43:40,945 INFO ----- OneCycle開始 -----
2016-07-21 18:43:40,946 INFO Twitter APIの使用を開始します。
2016-07-21 18:43:40,962 INFO Starting new HTTPS connection (1): api.twitter.com
pi@pi3:~/twitter$

なぜ謎のエラーなのかといいますと、記録が残っていないためです。上のとおり、ログは突然途切れています。サドンデスですね。これでは原因が突き止められないので、まずは情報採取をしっかりやりましょうということで改造します。

例外を処理

Pythonには「例外」という機能があります。何かまずいことが発生したときに、本来のプログラムの段取りとは別に、「突然ですがここで緊急ニュースです」とでもいった調子で始まる処理のことです。どんな「例外」が発生しうるのかはそのときやっている処理によるのですが、Python-Twitterの場合は、"TwitterError"という名称の「例外」が定義されており、これが発生(raise)します。下のリンクはソースコードです。ところどころにraiseという命令が置いてあります。

python-twitter/api.py at master · bear/python-twitter · GitHub

さて、例えばtwitter.api()を呼び出してkeyからアクセス権を取得するとき「アクセスしすぎでしょエラー」が発生することがあります。15分で15回以上アクセスすると、Twitter側でそれ以上の処理を拒否するようです。この場合、しばらく待ってリトライするのが正しい処理ということになりますね。

下のようにtry~catchを使用しました。しばらく待ってリトライするので、ログに残すだけでよいはずです。なお、発生した例外に対応する処理が作られていない場合、プログラムは強制的に終了させられます。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'
    try:
        api = twitter.Api(consumer_key=consumer_key,
                       consumer_secret=consumer_secret,
                       access_token_key=access_key,
                       access_token_secret=access_secret,
                       input_encoding=encoding)
    except TwitterError as e:
        logging.warning(u"twitter.Api error: %s" % ( e.message ))
    except Exception as e:
        logging.warning(u"不明なエラー")

GitHub更新しました

更新したソースコードGitHubに上げておきました。
github.com

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

今後

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

Python Twitterのメモ

Python-Twitterの細かい説明が無くソースコードを読んだりTwitter APIの規格を読む必要があります。
ちょっとメモのため残しておきます。
Modules Documentation — python-twitter 3.1 documentation

Status

Tilelineを取得したときなど、ツイートのひとつひとつはStatusと呼ばれる単位で管理されています。これに含まれる情報の一覧です。

AsDict このオブジェクトの辞書形式表現
AsJsonString このオブジェクトのJSON文字列形式
NewFromJsonDict  
contributors このアカウントをcontributorsとして運営するユーザの一覧
coordinates ユーザやアプリによって報告された緯度経度など
created_at ツイートされた時間。世界標準時(UTC)で表記
created_at_in_seconds ツイートされた時間を秒数で表記
favorite_count likeされたおおよその回数
favorited このユーザによってlikeされたかどうか(true/false)
geo 使わない。corrdinatesを代わりに使用する
hashtags  
id このツイートにつけられた固有のID
id_str idの文字列表現
in_reply_to_screen_name このツイートがリプライの場合、元ツイートを書いた人のスクリーン名
in_reply_to_status_id このツイートがリプライの場合、元ツイートのID
in_reply_to_user_id このツイートがリプライの場合、元ツイートを書いた人のユーザID
lang 機械的に検知されたこのツイートの言語(BCP47形式)
location  
media  
param_defaults  
place このツイートが関連づけられた場所
possibly_sensitive このツイートが(sensitive)と判断されたかどうか(true/false)
retweet_count リツイートされた回数
retweeted このユーザによってリツイートされたかどうか
retweeted_status  
scopes プロモーションツイートで使われる模様
source  
text このツイートのテキスト(UTF-8形式)
truncated ツイートは140文字で短縮されたかどうか
urls  
user このツイートをポストしたユーザについての情報
user_mentions このツイートでメンションされているユーザのIDとScreenName
withheld_copyright DCMAの申し立てで保留されているかどうか(true/false)
withheld_in_countries 保留されている国を表す2文字のコードのリスト
withheld_scope 保留の範囲(userまたはstatus)

Twitter公式の説明を参考にしました。
dev.twitter.com

User

ついでにuserの内容についても書いておきます。

created_at ユーザアカウントの作成日時
description ユーザの自己紹介文
favourites_count likeの数
followers_count フォロワー数
friends_count フォロー数
id アカウントのID番号
lang 言語
listed_count このユーザがリストされている数
location 場所
name スクリーンに表示される名前
profile_background_color  
profile_background_image_url  
profile_image_url ユーザのアイコン
profile_link_color  
profile_sidebar_fill_color  
profile_text_color プロファイルのテキスト
screen_name ログインID
statuses_count ツイート数
time_zone タイムゾーン
utc_offset 世界標準時からの時差(日本は32400秒(+9時間))

Raspberry pi 3 + Python = Twitter Botの起動(とりあえず投稿するところまで)

Raspberry piの良いところは常時起動しても電気代があまりかからないところです。Raspi 3になって消費電力はやや増えましたけれども、それでもスマホより少し少ないくらいなので、24時間稼働していても心が痛みません。

常時起動の良さを活かしてTwitterBotにしてみようということで、今回は起動部分を作ってみます。今回の作業はとっかかりのところだけですが、このくらいなら1時間ほどあればできるのでお手軽です。

Python Twitterモジュールのインストール

Twitterはユーザが凄く多いですから、自分で作らなくても配布されているものがたくさん見つかります。GitHubを探したらたくさんForkされているモジュールがあったので、これをお借りすることにしました。顔写真は作者さんなのでしょうか…… 熊……

github.com

インストールはsudo pipを使えば簡単です。

pi@pi3:~/twitter $ sudo pip install python-twitter
Collecting python-twitter
  Downloading python-twitter-3.1.tar.gz (80kB)
    100% |????????????????????????????????| 81kB 1.2MB/s
Collecting future (from python-twitter)
  Downloading future-0.15.2.tar.gz (1.6MB)
    100% |????????????????????????????????| 1.6MB 154kB/s
Requirement already satisfied (use --upgrade to upgrade): requests in /usr/lib/python2.7/dist-packages (from python-twitter)
Collecting requests-oauthlib (from python-twitter)
  Downloading requests_oauthlib-0.6.2-py2.py3-none-any.whl
Collecting oauthlib>=0.6.2 (from requests-oauthlib->python-twitter)
  Downloading oauthlib-1.1.2.tar.gz (111kB)
    100% |????????????????????????????????| 112kB 1.2MB/s
Building wheels for collected packages: python-twitter, future, oauthlib
  Running setup.py bdist_wheel for python-twitter ... done
  Stored in directory: /root/.cache/pip/wheels/22/1e/2e/506871fa7dc610616948e70812d5e2518cd89c13f757b98f6c
  Running setup.py bdist_wheel for future ... done
  Stored in directory: /root/.cache/pip/wheels/11/c5/d2/ad287de27d0f0d646f119dcffb921f4e63df128f28ab0a1bda
  Running setup.py bdist_wheel for oauthlib ... done
  Stored in directory: /root/.cache/pip/wheels/e6/be/43/e4a2ca8cb9c78fbd9b5b14b96cb7a5cc43f36bc11af5dfac5b
Successfully built python-twitter future oauthlib
Installing collected packages: future, oauthlib, requests-oauthlib, python-twitter
Successfully installed future-0.15.2 oauthlib-1.1.2 python-twitter-3.1 requests-oauthlib-0.6.2
You are using pip version 8.1.1, however version 8.1.2 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.
pi@pi3:~/twitter $ pip install --upgrade pip
Collecting pip
  Downloading pip-8.1.2-py2.py3-none-any.whl (1.2MB)
    100% |????????????????????????????????| 1.2MB 201kB/s
Installing collected packages: pip
  Found existing installation: pip 8.1.1
    Uninstalling pip-8.1.1:
Exception:
Traceback (most recent call last):
  File "/usr/local/lib/python2.7/dist-packages/pip/basecommand.py", line 209, in main
    status = self.run(options, args)
  File "/usr/local/lib/python2.7/dist-packages/pip/commands/install.py", line 317, in run
    prefix=options.prefix_path,
  File "/usr/local/lib/python2.7/dist-packages/pip/req/req_set.py", line 726, in install
    requirement.uninstall(auto_confirm=True)
  File "/usr/local/lib/python2.7/dist-packages/pip/req/req_install.py", line 746, in uninstall
    paths_to_remove.remove(auto_confirm)
  File "/usr/local/lib/python2.7/dist-packages/pip/req/req_uninstall.py", line 115, in remove
    renames(path, new_path)
  File "/usr/local/lib/python2.7/dist-packages/pip/utils/__init__.py", line 267, in renames
    shutil.move(old, new)
  File "/usr/lib/python2.7/shutil.py", line 303, in move
    os.unlink(src)
OSError: [Errno 13] 許可がありません: '/usr/bin/pip'
You are using pip version 8.1.1, however version 8.1.2 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.
pi@pi3:~/twitter $ sudo pip install --upgrade pip
Collecting pip
  Downloading pip-8.1.2-py2.py3-none-any.whl (1.2MB)
    100% |????????????????????????????????| 1.2MB 201kB/s
Installing collected packages: pip
  Found existing installation: pip 8.1.1
    Uninstalling pip-8.1.1:
      Successfully uninstalled pip-8.1.1
Successfully installed pip-8.1.2
pi@pi3:~/twitter $

Exampleを入手するためにソースコードもclone

pipでインストールすると、exampleは含まれていません。exampleの入手や、今後の開発の目的でソースも入手しておきます。

pi@pi3:~/twitter $ git clone https://github.com/bear/python-twitter/
Cloning into 'python-twitter'...
remote: Counting objects: 2954, done.
remote: Total 2954 (delta 0), reused 0 (delta 0), pack-reused 2954
Receiving objects: 100% (2954/2954), 7.24 MiB | 1.36 MiB/s, done.
Resolving deltas: 100% (1873/1873), done.
Checking connectivity... done.
pi@pi3:~/twitter $

Twitterのアカウントに開発者登録する

さて、いまやろうとしていることは、既存のTwitter Botを利用者として使うということではなくて、自作の(とはいえソースのほとんどは借り物ですが)Botを動かそうということです。こういったときには、Twitter社に自分のプログラムを登録して、アクセス権をもらう必要があります。このため自分のTwitterアカウントを登録します。

dev.twitter.com

apps.twitter.com

私はこれまでTwitterアカウントに携帯電話の番号を登録していなかったのですが、開発者登録するときには必須となっていました。私のアカウントのうち、ほとんど使われていなかったWindVoice_enのほうにプログラムを登録しました。

twitter.com

開発者登録の方法は、上でソースをもらったbearさんのページに記載があるのでそれを参考にすればOKです。自分で作ろうとしているアプリケーションの名称、簡単な説明、アプリを登録しているURL(作成中の場合はとりあえず何でも埋めておけばいい(spaceholder)ようです)を記載すれば良いことになっています。

登録をすると、コンシューマーキー、コンシューマーシークレット、アクセストークン、アクセストークンシークレット、という4種類の鍵文字列がもらえます。これらは大事な情報なので他の人に知られてはいけません。パスワードに相当するものですね。以下の説明ではxで情報をつぶしてあります。

テストツイート

cloneしたソースコードのexample/tweet.pyがつぶやきを投稿するためのサンプルプログラムです。投稿したい文字列と一緒にアクセスキー4個を入力する必要があるのですが、長くて面倒なので、環境変数に登録してしまうことにしました。それができる機能がすでに織り込まれています。

pi@pi3:~/twitter/python-twitter/examples $ tail ~/.bashrc
fi

eval $(thefuck --alias)

# Env for Raspi3Bot
export TWEETUSERNAME=xxxxxxxxxxxxxxxxxxxxxxxxx
export TWEETPASSWORD=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
export TWEETACCESSKEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
export TWEETACCESSSECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

pi@pi3:~/twitter/python-twitter/examples $
pi@pi3:~/twitter/python-twitter/examples $ . ~/.bashrc

ホームディレクトリの.bascrcの末尾のところにこんな感じで4つの鍵を書いておきます(.bashrcはworld readableなので、アクセス権的に不安ですけど、このラズパイを使うのは私だけということで……)。これを記載したら、.bashrcを再読込させて、いよいよ投稿を試してみます。

pi@pi3:~/twitter/python-twitter/examples $ ./tweet.py "もう一度コメントに挑戦……"
WindVoice_en just posted: もう一度コメントに挑戦……
pi@pi3:~/twitter/python-twitter/examples $

念のため自分のタイムラインを確認すると確かに投稿されていました。

f:id:WindVoice:20160720115411p:plain

まとめ

とりあえず投稿するところまでいけましたが、目的はコマンドラインからつぶやくことではなくて、これで常時起動のBotを作ることでした。場当たり的ですがここからがアイディアの出し所ですね。過去の投稿と合わせればセンサーからの読み取り値をつぶやくなどはすぐできると思いますが、しかし公に知らせて楽しい物でもないかも…… どうしたものか少し考えます。

誰でもできるTwitter Botの作り方―人気キャラにつぶやかせる

誰でもできるTwitter Botの作り方―人気キャラにつぶやかせる