"Twitter Friends" つくりました. [プログラム三昧]
Twitter でフォローしてきた ID .突如として他の人のつぶやきに現れた ID .「この方は,どのようなクラスタの方なのかしら.」という疑問の解決に少しでも役立てようと, "Twitter Friends" なる CGI を作成しました.
ID という個人に近い情報を表示するプログラムなので,使用例は掲載していません.ご自由にお試しください.
二つのアカウントのつながりを列挙したい
この CGI は,「自分とアノ人」などのように,二つの ID をつないでくれる ID を列挙してくれます.二つの ID の間に共通のフォロー関係が存在していれば,どのようなクラスタに属する人なのかをある程度は判断することができます.使い方は,簡単です.二つの Textbox のそれぞれに ID を入力して, Submit ボタンをクリックします.しばらくすると, follow/follower 関係ごとに二つの ID ともにつながりのある ID が表示されます.
参考文献
フォローおよびフォロワー情報を引き出す方法
Twitter で,フォローおよびフォロワーの情報を引き出すためには, API があると便利です.この CGI を記述しているプログラム言語は, Python です.そのため, Python から情報をアクセスするための python-twitter のような API をインストールしてやれば,簡単です.
ところが,私が使用している「さくらのレンタルサーバ」では,ライブラリをインストールすることができません.さらに,「ライトプラン」では,独自に Python をインストールすることもできません.そのため, API の下のレベルでの記述を行うこととしました.
# # getnodetext() : # Returns a text string enclosed by the specified tag # from the user DOM node. # # @param # user - a DOM node with "user" tag # tag - a tag name to be found from the user # def getnodetext(user, tag): elements = user.getElementsByTagName(tag) if len(elements) > 0: nodes = elements[0].childNodes if len(nodes) > 0: if nodes[0].nodeType == xml.dom.Node.TEXT_NODE: return cgi.escape(nodes[0].data) return "-" # # getusers() : # Return a databse of users # # @param # category - a category name of the users to be collected # name - a screen_name related to the users to be collected. # # @return # a database with the screen_name as a key and the # associated database of a user. # def getusers(category, name): # Establish a connection connection=httplib.HTTPConnection("twitter.com") connection.request("GET", "/statuses/%s/%s.xml" % (category, name)) response = connection.getresponse() # Construct as a DOM object dom = xml.dom.minidom.parseString(response.read()) r = {} for user in dom.getElementsByTagName("user"): d = {} key = getnodetext(user, 'screen_name') d['screen_name'] = key d['name'] = getnodetext(user, 'name') d['profile_image_url'] = getnodetext(user, 'profile_image_url') r[key] = d connection.close() return r
"getusers()" メソッドは, "twitter.com" にアクセスして,フォローおよびフォロワー情報を引き出し,データベースとして返します.与える引数のうち, category には, "friends" (フォロー情報)または "followers" (フォロワー情報)を指定します.また, name には, Twitter の ID を指定します.これらの情報を元に, twitter.com のしかるべきパスを求め, XML 形式で情報を引き出します.
引き出された情報は, XML 形式の文字列です. XML の解析プログラムまでは作りたくなかったので, xml.dom.minidom.parseString() メソッドに渡して, DOM ツリーに変換してもらい,そこから,必要な情報だけを取り出します.
一つの ID は, Python の databse として,表現されます.この database には, 'screen_name' 'name' 'profile_image_url' の三つのキーと対応する値が登録されています.
{'screen_name':'noritan_org' 'name':'noritan.org' 'profile_image_url':'http://.....png'}
この ID ごとの database をさらに 'screen_name' をキーとした database にまとめて,フォロワー情報またはフォロー情報として,構成しています.
{'noritan_org':{'screen_name':'noritan_org' 'name':'noritan.org' 'profile_image_url':'http://.....png'}}
フォロー・フォロワー関係から ID を三つに分類する
フォロー情報とフォロワー情報から,それぞれの ID との関係を三つに分類します.
- inout
- フォローし,かつ,フォローされている ID
- int
- フォローされているが,フォローしていない ID
- out
- フォローしているが,フォローされてはいない ID
# # getrelation() : # Create a cross-reference database # Returns a database with three databases with a category # as follows. # inout : users included in both friends and followers # in : users included in followers but friends # out : users included in friends but followers # # @param # name - a screen_name whose relation is to be created. # def getrelation(name): friends = getusers("friends", name) followers = getusers("followers", name) db_inout = {} db_out = {} db_in = {} for key in friends: if followers.has_key(key): db_inout[key] = friends[key] else: db_out[key] = friends[key] for key in followers: if not friends.has_key(key): db_in[key] = followers[key] db = {} db['inout'] = db_inout db['in'] = db_in db['out'] = db_out return db
ひたすら,地道に database に含まれているかどうかを検査しています.
二つの database での共通 ID を求める
分類された三つの database に対して共通の ID を求めるのが,次のステップです.共通の ID に対して, TABLE 記述の断片を出力します.
# # getuniondatabase() : # Get union database of two databases # # @param # db1 - a database of users # db2 - another database of users # # @return # a database of users included in both databases. # def getuniondatabase(db1, db2): r = {} for key in db1: if db2.has_key(key): r[key] = db1[key] return r # # showcell() : # Show one table cell with td tag # # @param # user - a database corresponding to a user # def showcell(user): writer.write("""<td> <img src="%(profile_image_url)s" width="48" height="48" title="%(name)s" alt="%(name)s" /> <a href="http://twitter.com/%(screen_name)s">%(screen_name)s</a> </td>""" % user) # # showrelation() : # Show relation content as table rows # # @param # lhs_rel - relation code for LHS user # rhs_rel - relation code for RHS user # def showrelation(lhs_rel, rhs_rel): db = getuniondatabase(lhs_db[lhs_rel], rhs_db[rhs_rel]) if len(db) <= 0: return writer.write(""" <tr> <td style="vertical-align:top;" rowspan="%d">%s</td> <td></td> <td style="vertical-align:top;" rowspan="%d">%s</td> </tr>""" % (len(db)+1, getlhsarrow(lhs_rel), len(db)+1, getrhsarrow(rhs_rel))) for user in db.values(): writer.write(""" <tr>""") showcell(user) writer.write("""</tr>""")
キーになるポイントは,こんなところでしょうか.
プログラム全景
#!/usr/local/bin/python # # Make a list of Friends on Twitter. # # $Id: twitter-friends.cgi,v 1.2 2010/04/07 10:20:57 noritan Exp $ import cgi import cgitb import sys import codecs import httplib import xml.dom.minidom # Enable debug output cgitb.enable() # Create UTF writer as BROWSER expected writer = codecs.getwriter('utf-8')(sys.stdout) # # Get a POST data. # form : FieldStorage object of the posted form # form = cgi.FieldStorage() #============================================================== # Method declaration #============================================================== # # getformvalue() : # Get the value of a specified element from the form # # @param # key - the ID of a element # defvalue - a default value when no value specified. # def getformvalue(key, defvalue): if key in form: return form.getvalue(key) else: return defvalue # # getnodetext() : # Returns a text string enclosed by the specified tag # from the user DOM node. # # @param # user - a DOM node with "user" tag # tag - a tag name to be found from the user # def getnodetext(user, tag): elements = user.getElementsByTagName(tag) if len(elements) > 0: nodes = elements[0].childNodes if len(nodes) > 0: if nodes[0].nodeType == xml.dom.Node.TEXT_NODE: return cgi.escape(nodes[0].data) return "-" # # getusers() : # Return a databse of users # # @param # category - a category name of the users to be collected # name - a screen_name related to the users to be collected. # # @return # a database with the screen_name as a key and the # associated database of a user. # def getusers(category, name): # Establish a connection connection=httplib.HTTPConnection("twitter.com") connection.request("GET", "/statuses/%s/%s.xml" % (category, name)) response = connection.getresponse() # Construct as a DOM object dom = xml.dom.minidom.parseString(response.read()) r = {} for user in dom.getElementsByTagName("user"): d = {} key = getnodetext(user, 'screen_name') d['screen_name'] = key d['name'] = getnodetext(user, 'name') d['profile_image_url'] = getnodetext(user, 'profile_image_url') r[key] = d connection.close() return r # # getrelation() : # Create a cross-reference database # Returns a database with three databases with a category # as follows. # inout : users included in both friends and followers # in : users included in followers but friends # out : users included in friends but followers # # @param # name - a screen_name whose relation is to be created. # def getrelation(name): friends = getusers("friends", name) followers = getusers("followers", name) db_inout = {} db_out = {} db_in = {} for key in friends: if followers.has_key(key): db_inout[key] = friends[key] else: db_out[key] = friends[key] for key in followers: if not friends.has_key(key): db_in[key] = followers[key] db = {} db['inout'] = db_inout db['in'] = db_in db['out'] = db_out return db # # showcell() : # Show one table cell with td tag # # @param # user - a database corresponding to a user # def showcell(user): writer.write("""<td> <img src="%(profile_image_url)s" width="48" height="48" title="%(name)s" alt="%(name)s" /> <a href="http://twitter.com/%(screen_name)s">%(screen_name)s</a> </td>""" % user) # # getuniondatabase() : # Get union database of two databases # # @param # db1 - a database of users # db2 - another database of users # # @return # a database of users included in both databases. # def getuniondatabase(db1, db2): r = {} for key in db1: if db2.has_key(key): r[key] = db1[key] return r # # getlhsarrow() : # Get a relation arrow character for LHS # # @param # relation - relation code # def getlhsarrow(relation): if relation == "inout": return "↔" elif relation == "in": return "←" elif relation == "out": return "→" return "?" # # getrhsarrow() : # Get a relation arrow character for RHS # # @param # relation - relation code # def getrhsarrow(relation): if relation == "inout": return "↔" elif relation == "in": return "→" elif relation == "out": return "←" return "?" # showrelation() : # Show relation content as table rows # # @param # lhs_rel - relation code for LHS user # rhs_rel - relation code for RHS user # def showrelation(lhs_rel, rhs_rel): db = getuniondatabase(lhs_db[lhs_rel], rhs_db[rhs_rel]) if len(db) <= 0: return writer.write(""" <tr> <td style="vertical-align:top;" rowspan="%d">%s</td> <td></td> <td style="vertical-align:top;" rowspan="%d">%s</td> </tr>""" % (len(db)+1, getlhsarrow(lhs_rel), len(db)+1, getrhsarrow(rhs_rel))) for user in db.values(): writer.write(""" <tr>""") showcell(user) writer.write("""</tr>""") #============================================================== # Main procedure #============================================================== # # Get parameters from the form # lhs_name : the screen_name of the Left Hand Side user # rhs_name : the screen_name of the Right Hand Side user # lhs_name = getformvalue("lhs", "-") rhs_name = getformvalue("rhs", "-") # # Create a database structure for LHS and RHS # lhs_db : a database for the LHS user # rhs_db : a database for the RHS user # lhs_db = getrelation(lhs_name) rhs_db = getrelation(rhs_name) # Show HTML header writer.write("""Content-type: text/html <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" > <head profile="http://www.w3.org/2005/10/profile"> <link rel="icon" href="/favicon.png" type="image/png" /> <title>Twitter Friends</title> </head> <body>""") # Show title line and a form writer.write(""" <h1>Twitter Friends</h1> <form action="./twitter-friends.cgi" method="post"> <p>Analyze connection between two Twitter IDs.</p> <div style="padding:1em;"> <textarea name="lhs" rows="1" cols="16">%s</textarea> <input type="submit" value="Submit" /> <textarea name="rhs" rows="1" cols="16">%s</textarea> </div> </form>""" % (cgi.escape(lhs_name), cgi.escape(rhs_name))) # Show a table header writer.write(""" <table border="1" cellpadding="5" cellspacing="1"> <tr><th>%s</th><th>connection</th><th>%s</th></tr>""" % (cgi.escape(lhs_name), cgi.escape(rhs_name))) # Show a comment for debug. writer.write(""" <!-- LHS (%d,%d,%d) RHS(%d,%d,%d) -->""" % (len(lhs_db['inout']), len(lhs_db['in']), len(lhs_db['out']), len(rhs_db['inout']), len(rhs_db['in']), len(rhs_db['out']))) # Show relation descriptions showrelation('inout', 'inout') showrelation('inout', 'in') showrelation('in', 'inout') showrelation('inout', 'out') showrelation('out', 'inout') showrelation('in', 'out') showrelation('out', 'in') showrelation('in', 'in') showrelation('out', 'out') # Show a table footer writer.write(""" </table>""") # Show HTML footer writer.write(""" </body> </html> """) # Close the output file writer.close()
使ってみました。便利です。ありがとうございます。
by もり (2010-04-08 21:44)
一部ユーザ様のご要望により,二つのアカウントの直接の関係が表示されるように修正しました.
というか,固定ユーザーさんが存在したことに驚きを隠せません.
by noritan (2010-06-05 15:13)
どうもうまく動いていないというレポートがありました.ちょいと調べて,原因を突き止めました.
API を一回呼び出しただけでは,最大50件の関係情報しか得らず,それ以上の数の friends/followers が居る場合には,すべての関係が表示されません.
解決するためには, HTTP にパラメータを追加して,複数回 API を呼び出し,それらをつなぎ合わせる必要がありそうです.
作ったときにどちらも 50 未満だったので,気が付きませんでした.
また,考えます.
by noritan (2010-06-20 23:28)
自分のための覚書 : API ドキュメントのありか.
http://dev.twitter.com/doc/get/statuses/friends
http://dev.twitter.com/doc/get/statuses/followers
by noritan (2010-06-20 23:31)