Plan9/Inferno Twitter bot
最近、ずいぶん周回遅れでTwitterを使い始めた。ちょっと雰囲気がわかってきたので、Plan9とInferno関連のblog更新をポストするボットをPythonで書いてみた。興味のある人はp9botをfollowしてやってください。今のところ、自分の日記以外に更新をチェックしているのは、次のページ。
気が向いたら、Planet 9あたりを参考に増やしていこうと思う。
以下、実装などについて。スクリプト自体はできるだけ汎用に作ったつもり。Twitter APIまわりはpython-twitterを参考にし、RSSの解析にはfeedparserを使った。あと、設定ファイルはYAML形式にしたので、その解析にPyYAMLを使っている。レンタルサーバなどルート権限がない環境で、モジュールをインストールしなければならない場合は、Virtual Pythonを使うとよい。
設定ファイルはこんな感じで、Twitterのユーザ名とパスワード、そしてチェックしたいRSS/ATOMを列挙する。最初はPlanet 9やはてなアンテナからまとめて取得しようと思ったけど、どうも結果がしっくりこなかったので、自前で全エントリを取得して、更新時刻でソートすることにした。
twitter: username: XXXX password: YYYY feed: - http://d.hatena.ne.jp/oraccha/rss2 - http://ninetimes.cat-v.org/index.atom - http://graverobbers.blogspot.com/feeds/posts/default - http://d.hatena.ne.jp/m-shiba/rss2 - http://inferno-os.blogspot.com/feeds/posts/default
p9bot.pyとp9bot.yamlを適当なディレクトリにおいて、p9bot.yamlを書き換え、cronで定期的に実行する。今は1時間ごとに見に行っているけど、もっと間隔開けても大丈夫かな。
TODO
- If-Modified-SinceやIf-None-Matchを使ってサーバの負荷を軽くする
- RSS/ATOMを調べるのはひと手間かかるので、トップページから自動取得する
- 更新時間のタイムゾーンの扱いにバグがあるような。
追記:githubにアカウントを作って、そこにコードを置くことにした。こりゃ簡単だな。すごい。
(追記:2009-06-21)TwitterFeedとかFriendFeedを使えばよかったのか。
#!/usr/bin/python # -*- coding: utf-8 -*- import sys import urllib import urllib2 import urlparse import time import feedparser import yaml rootdir = os.path.dirname(os.path.abspath(__file__)) LASTFILE = rootdir + '/p9bot.timestamp' CONFFILE = rootdir + '/p9bot.yaml' TWITTER_REALM = 'Twitter API' TWITTER_URL = 'http://twitter.com/statuses/update.xml' class TwitterBot: now = last = 0.0 def __init__(self): self.read_last() self.now = time.mktime(time.gmtime()) y = yaml.load(open(CONFFILE)) self.target = y['feed'] self.config = y['twitter'] user = self.config['username'] passwd = self.config['password'] parsed_url = urlparse.urlparse(TWITTER_URL) handler = urllib2.HTTPBasicAuthHandler() handler.add_password(TWITTER_REALM, parsed_url.hostname, user, passwd) self.opener = urllib2.build_opener(handler) def read_last(self): try: f = open(LASTFILE, 'r') try: for line in f: self.last = float(line) finally: f.close() except IOError: pass def update_last(self): try: f = open(LASTFILE, 'w') try: f.write(str(self.now)) finally: f.close() except IOError: pass def open(self, url): self.feed = feedparser.parse(url) return self.feed['feed'].get('title', '-'), \ self.feed['feed'].get('author', '-'), \ self.feed['feed'].get('link', '-') def fetch(self, max=None): for e in self.feed['entries'][:max]: title = link = '' try: date = time.mktime(e['updated_parsed']) if self.last > date: break link = e['link'] title = e['title'] except (AttributeError, KeyError), e: pass yield date, title, link def submit(self, post): data = urllib.urlencode({'status': post.encode('utf-8')}) self.opener.open(TWITTER_URL, data) def main(): limit = 10 if len(sys.argv) == 2: limit = int(sys.argv[1]) entries = [] p9bot = TwitterBot() for target in p9bot.target: if target[0] == '#': continue title, author, url = p9bot.open(target) for date, etitle, link in p9bot.fetch(): fdate = time.strftime("%b %d, %H:%M:%S", time.localtime(date)) msg = "%s (%s), %s, %s" % (etitle, title, fdate, link) entries.append({'date' : date, 'msg' : msg}) entries.sort(key=lambda h : h['date'], reverse=False) elen = len(entries) for i in range(max(0, elen - limit), elen): post = entries[i]['msg'] p9bot.submit(post) print "posted", post.encode('utf-8') p9bot.update_last() if __name__ == '__main__': main()