PythonでTwitterの1日分の投稿を取得するサンプル

bonlifeです。ノリで誰かさんのTwitterの1日分の発言を取得するスクリプトを書いてみました。Twitter APIだと20件までしか取得できないという噂を聞いたので、確かめもせずにゴリゴリとスクレイピングしてみましたよ。lxmlとdateutilを使ってますので、事前にインストールしておいてくださいませ。
使い方は以下のような感じです。

>python twitterscraper.py
Enter Twitter user id        : takets
Enter target date (YYYYMMDD) : 20080311
2008-03-11 23:28:33 : 数理生態学者の話を聞きたかった。
2008-03-11 22:58:15 : ニッポンの教養を見る
(後略)
>python twitterscraper.py -u takets -d 20080311
2008-03-11 23:28:33 : 数理生態学者の話を聞きたかった。
2008-03-11 22:58:15 : ニッポンの教養を見る
(後略)
In [1]: from twitterscraper import TwitterScraper

In [2]: import datetime

In [3]: ts = TwitterScraper('takets')

In [4]: ts.get_entries(datetime.date(2008,3,11))

In [6]: for i in ts.entries:
   ...:     print i.get('permalink')
   ...:
   ...:
http://twitter.com/takets/statuses/769854615
http://twitter.com/takets/statuses/769840277
(後略)

この3つ目のやり方を活用すれば他のスクリプトとの連携も楽なはず。
(結構汚い)ソースコードは以下の通り。

  • twitterscraper.py
import sys
import re
import datetime
from lxml import etree
from dateutil import parser as dateparser

class TwitterScraper(object):
    def __init__(self,user):
        self.user        = user
        self.entries     = list()
        self._xpath_dict_for_latest = { 'top'       : '//div[@class="desc hentry"]',
                                        'permalink' : 'p[@class="meta entry-meta"]/a',
                                        'datetime'  : 'p[@class="meta entry-meta"]/a/abbr',
                                        'comments'  : 'p[@class="entry-title entry-content"]' }
        self._xpath_dict_for_others = { 'top'       : '//tr[@class="hentry"]',
                                        'permalink' : 'td/span[@class="meta entry-meta"]/a',
                                        'datetime'  : 'td/span[@class="meta entry-meta"]/a/abbr',
                                        'comments'  : 'td/span[@class="entry-title entry-content"]'}
    def latest_check(self, target_date):
        url = 'http://twitter.com/%s' % (self.user)
        try:
            self.check(url, target_date, self._xpath_dict_for_latest)
        except DateOutOfRangeException:
            return
    def others_check(self, target_date):
        # try 100 pages to find entries posted on target date
        for page in range(1,101):
            url = 'http://twitter.com/%s?page=%d' % (self.user, page)
            try:
                self.check(url, target_date, self._xpath_dict_for_others)
            except DateOutOfRangeException:
                return
    def check(self, url, target_date, xpath_dict):
        try :
            et = etree.parse(url,etree.HTMLParser())
        except IOError, e:
            print "ERR : %s " % e
            sys.exit(1)
        ts = et.xpath(xpath_dict.get('top'))
        for i in ts:
            # get post datetime from abbr tag's title attribute
            # add 9 hours (for 'JST')
            permalink = i.xpath(xpath_dict.get('permalink'))[0].get('href')
            entry_datetime = dateparser.parse(i.xpath(xpath_dict.get('datetime'))[0].get('title')) + datetime.timedelta(hours=9)
            entry_date = entry_datetime.date()
            if entry_date < target_date:
                raise DateOutOfRangeException()
            elif entry_date == target_date:
                e = i.xpath(xpath_dict.get('comments'))[0]
                self.entries.append({ 'permalink'       : permalink,
                                      'posted_datetime' : entry_datetime,
#                                      'comments'        : "".join([ x for x in e.itertext() ]).strip() })
                                      'comments'        : self.format_comments(e) })
    def format_comments(self, e):
        comments = list()
        for i in e.iter():
            # if the hyperlink starts with 'http', get full url
            if i.tag == 'a' and i.get('href').startswith('http'):
                comments.append(i.get('href'))
                if i.tail.strip() != "":
                    comments.append(i.tail)
            else:
                comments.append(i.text)
                if i.tail.strip() != "":
                    comments.append(i.tail)
        comments[0]  = comments[0].lstrip()
        comments[-1] = comments[-1].rstrip()
        return "".join([ re.sub("\s+"," ", x) for x in comments ])
    def get_entries(self,target_date=None):
        if target_date == None:
            target_date = (datetime.datetime.today() - datetime.timedelta(days=1)).date()
        self.latest_check(target_date)
        self.others_check(target_date)

class DateOutOfRangeException(Exception):
    pass

if __name__ == "__main__":
# simple usage is like below:
#
#    tw = TwitterScraper('bonlife')
#    tw.get_entries() # get Twitter entries posted on yesterday
#    for i in tw.entries:
#        print "%s : %s " % (i.get('posted_datetime').strftime('%Y-%m-%d %H:%M:%S'),
#                            i.get('comments'))

    from optparse import OptionParser

    usage   = "usage: %prog [options] (no arguments needed)"
    version = "%prog 0.1"
    optionparser  = OptionParser(usage=usage,version=version)
    optionparser.add_option("-u","--user",action="store",type="string",
                      dest="user_id",help="twitter's user id")
    optionparser.add_option("-d","--date",action="store",type="string",
                      dest="date_str",help="target date (YYYYMMDD)")
    (options, args) = optionparser.parse_args()

    if args:
        optionparser.print_help()
        sys.exit(1)

    if options.user_id == None and options.date_str == None:
        user_id  = raw_input("Enter Twitter user id        : ")
        date_str = raw_input("Enter target date (YYYYMMDD) : ")
    else:
        user_id  = options.user_id
        date_str = options.date_str
    try:
        target_date = dateparser.parse(date_str).date()
    except:
        target_date = None

    tw = TwitterScraper(user_id)
    tw.get_entries(target_date)
    # just print Twitter entries
    for i in tw.entries:
        print "%s : %s " % (i.get('posted_datetime').strftime('%Y-%m-%d %H:%M:%S'),
                            i.get('comments'))

反省点をいくつか。

  • 最新の投稿だけが上の方で「オレサマ別格!」って感じでのけぞってるので、そこを取得をするためにちょっとややこしいことに
  • 時差の考慮が適当(マジメにやろうとすると結構面倒っぽい)
  • 投稿本文の文字列をそのまま取得するとハイパーリンクが途中で切れてしまうので、無理矢理ハイパーリンクを取得する処理が相当苦しい

でも、なんだかんだで動いたので満足だよ、ママン!