読者です 読者をやめる 読者になる 読者になる

ぽよメモ

ファッション情報学徒の備忘録.

TweepyでStreamingAPIを叩いたときのstatusオブジェクト

久しぶりにTwitterのStreamingAPIについて触れ始めたのですが, これがかなり面倒で大変です.

基本として「StreamListenerを継承してListnerクラスを作り,Queueを渡す→Streamを受信し受け取ったstatusオブジェクトをQueueに入れるスレッド」 と,「スレッドからstatusオブジェクトを取り出し処理するスレッド」の2つを作っています.(python 3.5.0で書いてます)

import tweepy
from threading import Thread
from queue import Queue

auth = tweepy.OAuthHandler('Consumer Key', 'Consumer Secret')
auth.set_access_token('Access Token', 'Access Secret')
api = tweepy.API(auth_handler=auth, wait_on_rate_limit=True)
mydata = api.me()
myid = mydata.id


class Listener(tweepy.streaming.StreamListener):

    def __init__(self, queue):
        super(Listener, self).__init__()
        self.queue = queue

    def on_status(self, status):
        if status.in_reply_to_user_id == myid:
            self.queue.put(status)
        else:
            pass

    def on_error(self, status):
        raise

    def on_event(self, status):
        if status.event == 'favorite':
            self.queue.put(status)
        else:
            pass


class StreamRecieverThread(Thread):

    def __init__(self, queue):
        super(StreamRecieverThread, self).__init__()
        self.daemon = True
        self.queue = queue

    def run(self):
        l = Listener(self.queue)
        stream = tweepy.Stream(auth, l)
        while True:
            try:
                stream.userstream()
            except Exception:
                time.sleep(60)
                stream = tweepy.Stream(auth, l)


class ProcessingThread(Thread):

    def __init__(self, queue):
        super(CoreThread, self).__init__()
        self.daemon = True
        self.queue = queue

    def run(self):
        while True:
            obj = self.queue.get()
            tweetassembler(obj)


def tweetassembler(status):
    print(type(status))
    try:
        if status.in_reply_to_user_id == myid:
            from_user = status.user
            print('{0} replies to you:'.format(from_user.name))
            print(from_user.name + ' | ' + status.text)
    except AttributeError:
        if status.event == 'favorite':
            target_user = status.target
            source_user = status.source
            target = status.target_object
            print("{0} favorited {1}'s tweet:".format(
                source_user['name'], target_user['name']))
            print(target_user['name'] + ' | ' + target['text'])



def StartThreads():
    q = Queue()
    ProcessTh = ProcessingThread(q)
    StreamTh = StreamRecieverThread(q)
    ProcessTh.start()
    StreamTh.start()
    while True:
        time.sleep(1)

if __name__ == "__main__":
    StartThreads()

実行し,リプライとふぁぼしてみた結果は以下

$ python main.py
情弱 replies to you:
情弱 | @pudding_helper テスト
情弱 favorited PuddingHelper's tweet:
PuddingHelper | てすと

on_eventとon_statusメソッドで得られるオブジェクトはどちらもtweepy.models.Statusのオブジェクトですが,中身が全然違います.

Status(
       entities={
            'hashtags': [], 
            'user_mentions': [
                                {
                                 'name': 'PuddingHelper', 
                                 'screen_name': 'pudding_helper', 
                                 'id_str': '731841914387861504', 
                                 'indices': [0, 15], 
                                 'id': 731841914387861504
                                 }
                                ], 
            'urls': [], 
            'symbols': []
            }, 
       user=User(
            follow_request_sent=None, 
            profile_background_image_url='http://abs.twimg.com/images/themes/theme1/bg.png', 
            profile_background_tile=False, 
            verified=False, 
            favourites_count=733, 
            url='https://www.poyo.info', 
            _api=<tweepy.api.API object at 0x10cd5d668>, 
            id_str='3110338100', 
            default_profile_image=False, 
            followers_count=179, 
            utc_offset=28800, 
            profile_text_color='333333', 
            profile_sidebar_fill_color='DDEEF6', 
            listed_count=11, 
            id=3110338100, 
            profile_sidebar_border_color='C0DEED', 
            is_translator=False, 
            created_at=datetime.datetime(2015, 3, 27, 15, 33, 31), 
            screen_name='pudding_info', 
            friends_count=186, 
            statuses_count=7717, 
            notifications=None, 
            profile_use_background_image=True, 
            contributors_enabled=False, 
            profile_link_color='0084B4', 
            profile_image_url_https='https://pbs.twimg.com/profile_images/592556534170660865/KRTqs6DY_normal.jpg', 
            time_zone='Irkutsk', 
            protected=False, 
            profile_banner_url='https://pbs.twimg.com/profile_banners/3110338100/1457490444', 
            profile_background_image_url_https='https://abs.twimg.com/images/themes/theme1/bg.png', 
            geo_enabled=False, 
            profile_image_url='http://pbs.twimg.com/profile_images/592556534170660865/KRTqs6DY_normal.jpg', 
            lang='ja', 
            description='雑魚だから優しくしてください/kit 応用生物ファッション情報工学課程 3回/html/css/jquery/python/RasPi/KMC こういうのを作りました→@Qkou_kit',  
            profile_background_color='C0DEED', 
            following=False, 
            default_profile=True, 
            name='情弱', 
            location='ぽよカフェ'
            ), 
       coordinates=None, 
       in_reply_to_screen_name='pudding_helper', 
       place=None,
       truncated=False, 
       id_str='731850923195043840', 
       in_reply_to_status_id=None, 
       in_reply_to_user_id_str='731841914387861504', 
       retweeted=False, 
       favorite_count=0, 
       in_reply_to_status_id_str=None, 
       text='@pudding_helper 今度こそ', 
       contributors=None, 
       in_reply_to_user_id=731841914387861504, 
       favorited=False, source='TweetDeck', 
       filter_level='low', 
       author=User(
            follow_request_sent=None, 
            profile_background_image_url='http://abs.twimg.com/images/themes/theme1/bg.png', 
            profile_background_tile=False, 
            verified=False, 
            favourites_count=733, 
            url='https://www.poyo.info', 
            _api=<tweepy.api.API object at 0x10cd5d668>, 
            id_str='3110338100', 
            default_profile_image=False, 
            followers_count=179, 
            utc_offset=28800, 
            profile_text_color='333333', 
            profile_sidebar_fill_color='DDEEF6', 
            listed_count=11, 
            id=3110338100, 
            profile_sidebar_border_color='C0DEED', 
            is_translator=False, 
            created_at=datetime.datetime(2015, 3, 27, 15, 33, 31), 
            screen_name='pudding_info', 
            friends_count=186, 
            statuses_count=7717, 
            notifications=None, 
            profile_use_background_image=True, 
            contributors_enabled=False, 
            profile_link_color='0084B4', 
            profile_image_url_https='https://pbs.twimg.com/profile_images/592556534170660865/KRTqs6DY_normal.jpg', 
            time_zone='Irkutsk', 
            protected=False, 
            profile_banner_url='https://pbs.twimg.com/profile_banners/3110338100/1457490444', 
            profile_background_image_url_https='https://abs.twimg.com/images/themes/theme1/bg.png', 
            geo_enabled=False, 
            profile_image_url='http://pbs.twimg.com/profile_images/592556534170660865/KRTqs6DY_normal.jpg', 
            lang='ja', 
            description='雑魚だから優しくしてください/kit 応用生物ファッション情報工学課程 3回/html/css/jquery/python/RasPi/KMC こういうのを作りました→@Qkou_kit', 
            profile_background_color='C0DEED', 
            following=False, 
            default_profile=True, 
            name='情弱', 
            location='ぽよカフェ'
            ), 
       id=731850923195043840, 
       is_quote_status=False, 
       timestamp_ms='1463321831879', 
       created_at=datetime.datetime(2016, 5, 15, 14, 17, 11), 
       geo=None, 
       lang='ja', 
       source_url='https://about.twitter.com/products/tweetdeck', 
       retweet_count=0
       )

ちょっとコピペしてる最中にトラブルが起きたので一部異なるかもしれませんが,上記がリプライを送られたときのオブジェクトです.今回は直接@つけてリプライを送ったため,in_reply_to_status_idはNoneになっています. _json要素はほとんど中身一緒なので割愛.

Status(
       created_at=datetime.datetime(2016, 5, 15, 15, 56, 31), 
       target={
               'profile_background_color': 'F5F8FA', 
               'profile_image_url': 'http://pbs.twimg.com/profile_images/731843487448977408/93m-tNmX_normal.jpg', 
               'profile_background_image_url_https': None, 
               'profile_link_color': '2B7BB9', 
               'profile_background_image_url': None, 
               'profile_background_tile': False, 
               'following': None, 
               'is_translator': False, 
               'utc_offset': -25200, 
               'name': 'PuddingHelper', 
               'follow_request_sent': None, 
               'listed_count': 0, 
               'is_translation_enabled': False, 
               'id_str': '731841914387861504', 
               'profile_image_url_https': 
               'https://pbs.twimg.com/profile_images/731843487448977408/93m-tNmX_normal.jpg', 
               'followers_count': 2, 'notifications': None, 
               'profile_banner_url': 'https://pbs.twimg.com/profile_banners/731841914387861504/1463320240', 
               'default_profile_image': False, 
               'protected': False, 
               'id': 731841914387861504, 
               'created_at': 'Sun May 15 13:41:24 +0000 2016', 
               'profile_sidebar_border_color': 'C0DEED', 
               'friends_count': 2, 
               'contributors_enabled': False, 
               'description': 'pudding_infoのbot', 
               'statuses_count': 2, 
               'screen_name': 'pudding_helper', 
               'location': 'Poyo Server', 
               'geo_enabled': False, 
               'url': 'https://www.poyo.info', 
               'default_profile': True, 
               'profile_text_color': '333333', 
               'verified': False, 
               'lang': 'ja', 
               'profile_sidebar_fill_color': 'DDEEF6', 
               'time_zone': 'Pacific Time (US & Canada)', 
               'favourites_count': 0, 
               'profile_use_background_image': True
               }, 
       event='favorite', 
       source={
               'profile_background_color': 'C0DEED', 
               'profile_image_url': 'http://pbs.twimg.com/profile_images/714314656593158144/4HJEF6B6_normal.jpg', 
               'profile_background_image_url_https': 'https://abs.twimg.com/images/themes/theme1/bg.png', 
               'profile_link_color': '0084B4', 
               'profile_background_image_url': 'http://abs.twimg.com/images/themes/theme1/bg.png', 
               'profile_background_tile': False, 
               'following': None, 
               'is_translator': False, 
               'utc_offset': 28800, 
               'name': '特定の色をした生命体', 
               'follow_request_sent': None, 
               'listed_count': 17, 
               'is_translation_enabled': False, 
               'id_str': '1623113876', 
               'profile_image_url_https': 'https://pbs.twimg.com/profile_images/714314656593158144/4HJEF6B6_normal.jpg', 
               'followers_count': 433, 
               'notifications': None, 
               'profile_banner_url': 'https://pbs.twimg.com/profile_banners/1623113876/1448159895', 
               'default_profile_image': False, 
               'protected': True, 
               'id': 1623113876, 
               'created_at': 'Fri Jul 26 14:39:39 +0000 2013', 
               'profile_sidebar_border_color': 'C0DEED', 
               'friends_count': 503, 
               'contributors_enabled': False, 
               'description': '応用生物学課程/甘いもの好き/新入生各位のPCの相談に多少は乗ります こっちもあるよ →@pudding_info', 
               'statuses_count': 81391, 
               'screen_name': '_pudding_1', 
               'location': '布団の中', 
               'geo_enabled': False, 
               'url': 'http://www.poyo.info', 
               'default_profile': True, 
               'profile_text_color': '333333', 
               'verified': False, 
               'lang': 'ja', 
               'profile_sidebar_fill_color': 'DDEEF6', 
               'time_zone': 'Irkutsk', 
               'favourites_count': 28303, 
               'profile_use_background_image': True
               },  
       _api=<tweepy.api.API object at 0x106707668>, 
       source_url=None, 
       target_object={
                      'entities': {
                                   'user_mentions': [
                                                     {
                                                      'id': 731841914387861504, 
                                                      'id_str': '731841914387861504', 
                                                      'indices': [0, 15], 
                                                      'screen_name': 'pudding_helper', 
                                                      'name': 'PuddingHelper'
                                                      }
                                                     ], 
                                   'symbols': [], 
                                   'urls': [], 
                                   'hashtags': []
                                   }, 
                      'in_reply_to_status_id_str': None, 
                      'favorite_count': 1, 
                      'is_quote_status': False, 
                      'contributors': None, 
                      'coordinates': None, 
                      'id_str': '731849464470589442', 
                      'place': None, 
                      'text': '@pudding_helper どう?', 
                      'truncated': False, 
                      'geo': None, 
                      'in_reply_to_user_id_str': '731841914387861504', 
                      'lang': 'ja', 
                      'id': 731849464470589442, 
                      'created_at': 'Sun May 15 14:11:24 +0000 2016', 
                      'retweeted': False, 
                      'source': '<a href="http://twitter.com/download/android" rel="nofollow">Twitter for Android</a>', 
                      'in_reply_to_status_id': None, 
                      'in_reply_to_screen_name': 'pudding_helper', 
                      'in_reply_to_user_id': 731841914387861504, 
                      'favorited': False, 
                      'user': {
                               'profile_background_color': 'F5F8FA', 
                               'profile_image_url': 'http://pbs.twimg.com/profile_images/731843487448977408/93m-tNmX_normal.jpg', 
                               'profile_background_image_url_https': None, 
                               'profile_link_color': '2B7BB9', 
                               'profile_background_image_url': None, 
                               'profile_background_tile': False, 
                               'following': None, 
                               'is_translator': False, 
                               'utc_offset': -25200, 
                               'name': 'PuddingHelper', 
                               'follow_request_sent': None, 
                               'listed_count': 0, 
                               'is_translation_enabled': False, 
                               'id_str': '731841914387861504', 
                               'profile_image_url_https': 'https://pbs.twimg.com/profile_images/731843487448977408/93m-tNmX_normal.jpg', 
                               'followers_count': 2, 
                               'notifications': None, 
                               'profile_banner_url': 'https://pbs.twimg.com/profile_banners/731841914387861504/1463320240', 
                               'default_profile_image': False, 
                               'protected': False, 
                               'id': 731841914387861504, 
                               'created_at': 'Sun May 15 13:41:24 +0000 2016', 
                               'profile_sidebar_border_color': 'C0DEED', 
                               'friends_count': 2, 
                               'contributors_enabled': False, 
                               'description': 'pudding_infoのbot', 
                               'statuses_count': 2, 
                               'screen_name': 'pudding_helper', 
                               'location': 'Poyo Server', 
                               'geo_enabled': False, 
                               'url': 'https://www.poyo.info', 
                               'default_profile': True, 
                               'profile_text_color': '333333', 
                               'verified': False, 
                               'lang': 'ja', 
                               'profile_sidebar_fill_color': 'DDEEF6', 
                               'time_zone': 'Pacific Time (US & Canada)', 
                               'favourites_count': 0, 
                               'profile_use_background_image': True
                               }, 
                      'retweet_count': 0
                      }
       )

on_eventで得られるstatusの全体像です.本当は_jsonという要素を持っているのですが,ほとんどstatusをもう一度ディクショナリにしただけの中身なので割愛.
これを見るとわかりますが,mentionとfavoriteでは要素へのアクセスの仕方が大きく異なっています.

Twitter公式のStreamingAPIのドキュメントは少しわかりにくいのですが,ストリームイベントのjsonの雛形は以下のようになっています.

{
  "event":"EVENT_NAME",
  "created_at": "Sat Sep 4 16:10:54 +0000 2010",
  "target": TARGET_USER,
  "source": SOURCE_USER,
  "target_object": TARGET_OBJECT
}

"target"と"source"はそれぞれユーザを指します. on_eventで得られるオブジェクトではではそれぞれに対してディクショナリ型でユーザの情報が格納されます.

これに対し,リプライを送ったとき≒ストリーミング中に受信したツイートのオブジェクトでは,ユーザの情報はtweepy.models.Userクラスのオブジェクトになっています.

つまり得られるstatusオブジェクトをstatusとすると,

# TLに流れてきたツイートのユーザ情報を知りたい
u = status.user
u.name # アカウント名
u.id # id
u.screen_name # スクリーンネーム
# favoriteされたとき,ふぁぼしたユーザ情報を知りたい
user = status.source
user['name'] # アカウント名
user['id'] # id
user['screen_name'] # スクリーンネーム

このように,情報へのアクセスの仕方が違う,という話です.

公式のAPIを読む限り,ユーザーストリームのイベントはStreaming message typesでわかります.
イベントであれば要素はおそらく同じなので,status.eventの中身がfavoriteなのかfollowなのかを読み取ればそのイベントが何なのかがわかるはずです.

APIを見るとわかるのですが,リツイートはイベントではありません.
リツイートかそうでないかは,on_statusで得られるオブジェクトのretweeted要素を見るとわかるはずです(リツイートならばTrue,そうでないならFalse).
このときuser要素はRTしたユーザ,author要素はそのツイートをした人になっているはずです.

ここまで書いたのはいいけど,かえってややこしくなってしまった感じが否めない.