SSブログ

"Twitter Friends" つくりました. [プログラム三昧]このエントリーを含むはてなブックマーク#

WS000963.png

Twitter でフォローしてきた ID .突如として他の人のつぶやきに現れた ID .「この方は,どのようなクラスタの方なのかしら.」という疑問の解決に少しでも役立てようと, "Twitter Friends" なる CGI を作成しました.


ID という個人に近い情報を表示するプログラムなので,使用例は掲載していません.ご自由にお試しください.

二つのアカウントのつながりを列挙したい

この CGI は,「自分とアノ人」などのように,二つの ID をつないでくれる ID を列挙してくれます.二つの ID の間に共通のフォロー関係が存在していれば,どのようなクラスタに属する人なのかをある程度は判断することができます.

使い方は,簡単です.二つの Textbox のそれぞれに ID を入力して, Submit ボタンをクリックします.しばらくすると, follow/follower 関係ごとに二つの ID ともにつながりのある ID が表示されます.

参考文献

Python Cookbook

Python Cookbook

  • 作者:
  • 出版社/メーカー: Oreilly & Associates Inc
  • 発売日: 2005/05/05
  • メディア: ペーパーバック

フォローおよびフォロワー情報を引き出す方法

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 "&harr;"
  elif relation == "in":
    return "&larr;"
  elif relation == "out":
    return "&rarr;"
  return "?"

#
# getrhsarrow() : 
#   Get a relation arrow character for RHS
#
# @param
#   relation - relation code
#
def getrhsarrow(relation):
  if relation == "inout":
    return "&harr;"
  elif relation == "in":
    return "&rarr;"
  elif relation == "out":
    return "&larr;"
  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()


nice!(0)  コメント(4)  トラックバック(0)  このエントリーを含むはてなブックマーク#

nice! 0

コメント 4

もり

使ってみました。便利です。ありがとうございます。
by もり (2010-04-08 21:44) 

noritan

一部ユーザ様のご要望により,二つのアカウントの直接の関係が表示されるように修正しました.

というか,固定ユーザーさんが存在したことに驚きを隠せません.
by noritan (2010-06-05 15:13) 

noritan

どうもうまく動いていないというレポートがありました.ちょいと調べて,原因を突き止めました.

API を一回呼び出しただけでは,最大50件の関係情報しか得らず,それ以上の数の friends/followers が居る場合には,すべての関係が表示されません.
解決するためには, HTTP にパラメータを追加して,複数回 API を呼び出し,それらをつなぎ合わせる必要がありそうです.

作ったときにどちらも 50 未満だったので,気が付きませんでした.
また,考えます.
by noritan (2010-06-20 23:28) 

noritan

自分のための覚書 : API ドキュメントのありか.

http://dev.twitter.com/doc/get/statuses/friends
http://dev.twitter.com/doc/get/statuses/followers

by noritan (2010-06-20 23:31) 

コメントを書く

お名前:
URL:
コメント:
画像認証:
下の画像に表示されている文字を入力してください。

トラックバック 0

トラックバックの受付は締め切りました

この広告は前回の更新から一定期間経過したブログに表示されています。更新すると自動で解除されます。