SQLiteが本気を出せるPythonのライブラリのAPSWを使用してみる

背景

SQLiteをPythonから使用する場合、Python2.5以上ではデフォルトでsqlite3が使用できます。

これは、DB-API 2.0 仕様に準拠した SQL インタフェースを提供するもので、pysqliteという名前で開発されています。

sqlite3
http://docs.python.jp/2/library/sqlite3.html

しかしながら、このライブラリはSQLiteの全ての機能を使えるわけではありません。これは、DB-APIの仕様に準拠するためです。

APSWは、SQLite APIをPythonで完全に使用できるようなライブラリになっています。APSWはPython2.xとPython3.xで動作します。

https://github.com/rogerbinns/apsw
http://rogerbinns.github.io/apsw/

インストール方法

pipやeasy_installではインストールが行えません。

2019.08.03 更新
現在は以下でもインストール可能です。

pip install --user https://github.com/rogerbinns/apsw/releases/download/3.28.0-r1/apsw-3.28.0-r1.zip --global-option=fetch --global-option=--version --global-option=3.28.0 --global-option=--all --global-option=build --global-option=--enable-all-extensions

最新情報は下記を参照してください。
https://rogerbinns.github.io/apsw/download.html#easy-install-pip-pypi

Windowsの場合

以下から該当するバイナリをダウンロードしてインストーラーを実行します。

http://rogerbinns.github.io/apsw/download.html#source-and-binaries

VisualStudioを持っている場合、UNIXと同様にソースからのビルドが可能です。
(mingw32だと、ビルドはできても実行時にDLLが足りなくて動作しませんでした。)

Unixの場合

ソースコードをダウンロードして、下記のコマンドを実行してください。

python setup.py fetch --all build --enable-all-extensions install

ビルドについての詳細は下記のページに記述してあります。
http://rogerbinns.github.io/apsw/build.html

APSWとpysqliteの違い

APSWとpysqliteは根本的に異なる方向からSQLiteへのアクセス方法を提供します。

APSWはSQLiteのバージョン3のみをラップしており、全てのAPIにアクセスできる方法を提供します。

pysqliteはDBAPI準拠のラッパーを提供するために、他のデータベースと同じような振る舞いを行うようにします。そのため、いくつかのSQLiteの特徴を隠ぺいします。

以下に、APSWの利点や拡張された機能を紹介します。

最新のSQLiteの機能が使える

APSWはSQLiteの最新が使用できるようにしています。もし、SQLiteに機能の追加や変更が行われたらAPSWもそれらの機能を追いかけます。

Virtual Tableが使用可能である。

Virtual Table はSQLite 3.3.7から導入された機能です。

Virtual TableはSQL文の観点からは他のテーブルやビューと変わりありません。しかし、舞台裏ではVirtual Tableへのクエリや書き込みは、データベースファイルの読み書きの代わりにコールバックメソッドを引き起こします。

以下に、二次元配列のデータをSQLで操作する例を示します。

# -*- coding: utf-8 -*- 
import os, sys, time
import apsw
connection=apsw.Connection(":memory:")
cursor=connection.cursor()

###
### Virtual tables
### 

#
data = [
    [1, 'test1', 'categoryA'],
    [2, 'test2', 'categoryA'],
    [3, 'test3', 'categoryA'],
    [4, 'test4', 'categoryB'],
    [5, 'test5', 'categoryB'],
    [6, 'test6', 'categoryB'],
    [7, 'test7', 'categoryB'],
    [8, 'test8', 'categoryC'],
    [9, 'test9', 'categoryC'],
    [10, 'test10', 'categoryC']
]
counter = len(data)

# This gets registered with the Connection
class Source:
    def Create(self, db, modulename, dbname, tablename, *args):
        columns = ['rowid', 'name', 'category']
        schema="create table foo("+','.join(["'%s'" % (x,) for x in columns[1:]])+")"
        return schema,Table(columns,data)
    Connect=Create

# Represents a table
class Table:
    def __init__(self, columns, data):
        self.columns=columns
        self.data=data

    def BestIndex(self, *args):
        return None

    def Open(self):
        return Cursor(self)

    def Disconnect(self):
        pass

    def UpdateChangeRow(self, row, newrowid, fields):
        for d in data:
            if(d[0] == row):
                d[0] = newrowid
                d[1] = fields[0]
                d[2] = fields[1]

    def UpdateDeleteRow(self, row):
        for i in range(len(data)):
            if(data[i][0] == row):
                del data[i]
                return

    def UpdateInsertRow(self, rowid, fields):
        global counter
        counter = counter + 1
        data.append([counter, fields[0], fields[1]])
        return counter

    Destroy=Disconnect

# Represents a cursor
class Cursor:
    def __init__(self, table):
        self.table=table

    def Filter(self, *args):
        self.pos=0

    def Eof(self):
        return self.pos>=len(self.table.data)

    def Rowid(self):
        return self.table.data[self.pos][0]

    def Column(self, col):
        return self.table.data[self.pos][1+col]

    def Next(self):
        self.pos+=1

    def Close(self):
        pass

connection.createmodule("source", Source())
cursor.execute("create virtual table test using source()")
ret = cursor.execute("select * from test where category = 'categoryB'")
for row in ret:
    print row[0], row[1]
print ('update -----------------')
cursor.execute("update test set category='categoryB' where name='test1'")
ret = cursor.execute("select * from test where category = 'categoryB'")
for row in ret:
    print row[0], row[1]

print ('delete -----------------')
cursor.execute("delete from test where name='test4'")
ret = cursor.execute("select * from test")
for row in ret:
    print row[0], row[1]

print ('insert ----------------')
cursor.execute("insert into test values('xxxx','yyyy')")
ret = cursor.execute("select * from test")
for row in ret:
    print row[0], row[1]

このように、VirutalTableを用いることで任意のデータをSQLで操作することができます。公式のサンプルでは、ディレクトリ中のファイルをSQLで選択するサンプルがあります。
http://apidoc.apsw.googlecode.com/hg/example.html

その他、VirtualTableの詳細は下記を参照してください。
http://rogerbinns.github.io/apsw/vtable.html

Virtual File System (VFS)が使用可能である

SQLiteのコアおよび基礎となるオペレーティング·システム間のインタフェースを定義するVFSが使用できます。

APSWでは、VFSの機能を利用することができ、また、既定のVFSを継承して拡張した機能を持たせることができます。たとえば、以下の例ではVFSを用いてSQLiteのファイルを難読化しています。

# -*- coding: utf-8 -*- 
import os, sys, time
import apsw

### このサンプルは以下から抜粋したもの
### http://apidoc.apsw.googlecode.com/hg/example.html
### VFSを使ってデータベースを難読化する
### スキーマのすべてのバイトを0xa5でXORする。
### この方式はMAPIとSQL SERVERで用いられる
###

def encryptme(data):
    if not data: return data
    return "".join([chr(ord(x)^0xa5) for x in data])

# ""の基底からの継承はデフォルトのVFSを意味する
class ObfuscatedVFS(apsw.VFS):
    def __init__(self, vfsname="obfu", basevfs=""):
        self.vfsname=vfsname
        self.basevfs=basevfs
        apsw.VFS.__init__(self, self.vfsname, self.basevfs)

    # 独自のファイルを実装したいが、またそれは継承させたい
    def xOpen(self, name, flags):
        # We can look at uri parameters
        if isinstance(name, apsw.URIFilename):
            print "fast is", name.uri_parameter("fast")
            print "level is", name.uri_int("level", 3)
            print "warp is", name.uri_boolean("warp", False)
            print "notpresent is", name.uri_parameter("notpresent")
        return ObfuscatedVFSFile(self.basevfs, name, flags)

# 暗号ルーチンを実装するためにxReadとxWriteをオーバーライドする
class ObfuscatedVFSFile(apsw.VFSFile):
    def __init__(self, inheritfromvfsname, filename, flags):
        apsw.VFSFile.__init__(self, inheritfromvfsname, filename, flags)

    def xRead(self, amount, offset):
        return encryptme(super(ObfuscatedVFSFile, self).xRead(amount, offset))

    def xWrite(self, data, offset):
        super(ObfuscatedVFSFile, self).xWrite(encryptme(data), offset)

# To register the VFS we just instantiate it
obfuvfs=ObfuscatedVFS()
# Lets see what vfs are now available?
print apsw.vfsnames()

# Make an obfuscated db, passing in some URI parameters
obfudb=apsw.Connection("file:myobfudb?fast=speed&level=7&warp=on",
                       flags=apsw.SQLITE_OPEN_READWRITE | apsw.SQLITE_OPEN_CREATE | apsw.SQLITE_OPEN_URI,
                       vfs=obfuvfs.vfsname)
# Check it works
obfudb.cursor().execute("create table foo(x,y); insert into foo values(1,2)")

# 実際のディスクの中身を確認する
print `open("myobfudb", "rb").read()[:20]`
# '\xf6\xf4\xe9\xcc\xd1\xc0\x85\xc3\xca\xd7\xc8\xc4\xd1\x85\x96\xa5\xa1\xa5\xa4\xa4'

print `encryptme(open("myobfudb", "rb").read()[:20])`
# 'SQLite format 3\x00\x04\x00\x01\x01'

# Tidy up
obfudb.close()
os.remove("myobfudb")

詳細については下記を参照してください。
http://rogerbinns.github.io/apsw/vfs.html

BLOB I/O が使用できる

Blobは、バイトのシーケンスを表すSQLiteのデータ型です。それは、サイズがゼロ以上のバイトです。

APSWを用いて、このBlobに対して読み書きが行えます。以下に、その使用例を示します。

# -*- coding: utf-8 -*- 
import os, sys, time
import apsw
import os
connection=apsw.Connection("blob.sqlite")
cursor=connection.cursor()

###
### Blob I/O
### http://apidoc.apsw.googlecode.com/hg/example.html

cursor.execute("create table blobby(x,y)")
# Add a blob we will fill in later
cursor.execute("insert into blobby values(1,zeroblob(10000))")
# Or as a binding
cursor.execute("insert into blobby values(2,?)", (apsw.zeroblob(20000),))
# Open a blob for writing.  We need to know the rowid
rowid=cursor.execute("select ROWID from blobby where x=1").next()[0]
blob=connection.blobopen("main", "blobby", "y", rowid, 1) # 1 is for read/write
blob.write("hello world")
blob.seek(100)
blob.write("hello world, again")
blob.close()

詳細については下記を参考にしてください。
http://rogerbinns.github.io/apsw/blob.html

バックアップが使用できる

APSWではbackupを用いて接続中のDBを別の接続中のDBにバックアップが可能です。

# -*- coding: utf-8 -*- 
import os, sys, time
import apsw
import os
connection=apsw.Connection("src.sqlite")
cursor=connection.cursor()

# バックアップ元を作成
cursor.execute("create table test(x,y)")
cursor.execute("insert into test values(1,'TEST1')")
cursor.execute("insert into test values(2,'TEST2')")
cursor.execute("insert into test values(3,'TEST3')")
cursor.execute("insert into test values(4,'TEST4')")
cursor.execute("insert into test values(5,'TEST5')")

# バックアップ
memcon=apsw.Connection("backup.sqlite")
with memcon.backup("main", connection, "main") as backup:
    backup.step() # copy whole database in one go

for row in memcon.cursor().execute("select * from test"):
    print row[0], row[1]
    pass

詳細については下記を参考にしてください。
http://rogerbinns.github.io/apsw/backup.html

スレッドをまたいだ操作が可能

connectionやcursorをスレッドをまたいで共有できます。
pysqliteの場合、Connectionやcursorsは同じスレッドで使用しなければいけません。

pysqliteの例

# -*- coding: utf-8 -*- 
import threading
import sqlite3
def func(t):
    return 1 + t

class TestThread(threading.Thread):
    def __init__(self, conn):
        threading.Thread.__init__(self)
        self.conn = conn

    def run(self):
        self.conn.create_function("func", 1, func)
        cur = self.conn.cursor()
        ret = cur.execute("select func(3)")
        for row in ret:
            print(row[0])

conn = sqlite3.connect(":memory:")
th = TestThread(conn)
th.start()
th.join()

pysqliteではスレッドをまたいだ操作が認められないので以下のような例外が発生します。

Exception in thread Thread-1:
Traceback (most recent call last):
  File "C:\Python27\lib\threading.py", line 810, in __bootstrap_inner
    self.run()
  File "test_thread.py", line 14, in run
    self.conn.create_function("func", 1, func)
ProgrammingError: SQLite objects created in a thread can only be used in that sa
me thread.The object was created in thread id 19540 and this is thread id 4652

APSWの場合、同様のスレッドをまたいでも例外は発生しません。

APSWでスレッドをまたぐ

# -*- coding: utf-8 -*- 
import threading
import apsw

def func(t):
    return 1 + t

class TestThread(threading.Thread):
    def __init__(self, conn):
        threading.Thread.__init__(self)
        self.conn = conn

    def run(self):
        self.conn.createscalarfunction("func", func, 1)
        cur = self.conn.cursor()
        ret = cur.execute("select func(3)")
        for row in ret:
            print(row[0])

conn = apsw.Connection(":memory:")
th = TestThread(conn)
th.start()
th.join()

ただし、排他処理を注意深くあつかわないとクラッシュやデッドロックを引き起こします。

ネストしたトランザクションの利用

APSWではConnectionのContext Managerを使用することでネストしたトランザクションを使用できます。pysqliteでは1度に1つのトランザクションしか利用できず、ネストはできません。

このネストしたトランザクションで用いるSavePointはSQLite3.6.8で追加されたものです。これはSQLiteを最新の状態で使用できるAPSWの利点の一つと言えるでしょう。

以下にネストしたトランザクションの例を示します。

# -*- coding: utf-8 -*- 
import os, sys, time
import apsw
import os
connection=apsw.Connection(":memory:")

connection.cursor().execute("create table test(x primary key,y)")
with connection: # with でトランザクションを開始。例外ならRollback、それ以外はCommit
    connection.cursor().execute("insert into test values(1,'TEST1')")
    try: # with でSAVEPOINTを開始。例外ならRollback、それ以外はCommit
        with connection:
            connection.cursor().execute("insert into test values(2,'TEST2')")
            connection.cursor().execute("insert into test values(3,'TEST3')")
    except Exception, ex:
        print (ex)
        print ('rollback 1')
    try:
        with connection: # 以下のSQLはエラーがでて記録されない
            connection.cursor().execute("insert into test values(4,'TEST4')")
            connection.cursor().execute("insert into test values(4,'Error')")
    except Exception, ex:
        print (ex)
        print ('rollback 2')
    try:
        with connection:
            connection.cursor().execute("insert into test values(5,'TEST5')")
            connection.cursor().execute("insert into test values(6,'TEST6')")
    except Exception, ex:
        print (ex)
        print ('rollback 3')

for row in connection.cursor().execute("select * from test"):
    print row[0], row[1]

ConnectionのContextについて
http://rogerbinns.github.io/apsw/connection.html#apsw.Connection.__enter__

SQLiteのネストされたトランザクションについて
https://sqlite.org/lang_savepoint.html

複数コマンドの実行

APSWではセミコロンで区切ることにより、複数のコマンドが実行可能です。

# -*- coding: utf-8 -*- 
import apsw
con=apsw.Connection(":memory:")
cur=con.cursor()
for row in cur.execute("create table foo(x,y,z);insert into foo values (?,?,?);"
                       "insert into foo values(?,?,?);select * from foo;drop table foo;"
                       "create table bar(x,y);insert into bar values(?,?);"
                       "insert into bar values(?,?);select * from bar;",
                       (1,2,3,4,5,6,7,8,9,10)):
                           print row

SELECTのようなデータを返すSQLがCursor.executemany()で使用可能

APSWではSELECTのようなデータを返すSQLがCursor.executemany()で使用可能になっています。

# -*- coding: utf-8 -*- 
import apsw
con=apsw.Connection(":memory:")
cur=con.cursor()
cur.execute("create table foo(x);")
cur.executemany("insert into foo (x) values(?)", ( [1], [2], [3] ) )

# You can also use it for statements that return data
for row in cur.executemany("select * from foo where x=?", ( [1], [2], [3] ) ):
    print row

pysqliteではSELECTを含むSQLでexecutemany()は使用できません。
http://stackoverflow.com/questions/14142554/sqlite3-python-executemany-select

コールバック中のエラーが追跡がしやすい

ユーザ定義関数内でエラーが発生したときなどAPSWは追跡が容易な例外を出力します。

以下にユーザ定義関数内で例外を発生させた場合の違いを確認してみます。

pysqliteの例外

import sqlite3
def badfunc(t):
    return 1/0

# sqlite3.enable_callback_tracebacks(True)
con = sqlite3.connect(":memory:")
con.create_function("badfunc", 1, badfunc)
cur = con.cursor()
cur.execute("select badfunc(3)")

enable_callback_tracebacksがFalse(デフォルト)では以下のようなエラーになります。

Traceback (most recent call last):
  File "test_fnc1.py", line 9, in <module>
    cur.execute("select badfunc(3)")
sqlite3.OperationalError: user-defined function raised exception

enable_callback_tracebacksがTrueでは以下のようなエラーになります。

Traceback (most recent call last):
  File "test_fnc1.py", line 3, in badfunc
    return 1/0
ZeroDivisionError: integer division or modulo by zero
Traceback (most recent call last):
  File "test_fnc1.py", line 9, in <module>
    cur.execute("select badfunc(3)")
sqlite3.OperationalError: user-defined function raised exception

enable_callback_tracebacksがFalseの場合は、ユーザー定義関数内の例外が握りつぶされており、仮に、これをTrueにした場合でもTracebackの表示のされ方が直観的なものとは言えないでしょう。

一方、APSWの例外を見てみます。

APSWでの例外

def badfunc(t):
    return 1/0

import apsw
con = apsw.Connection(":memory:")
con.createscalarfunction("badfunc", badfunc, 1)
cur = con.cursor()
cur.execute("select badfunc(3)")

APSWでは次のように直観的にわかりやすいTracebackが出力されます。

Traceback (most recent call last):
  File "test_fnc2.py", line 9, in <module>
    cur.execute("select badfunc(3)")
  File "c:\apsw\src\connection.c", line 2021, in user-defined-scalar-badfunc
  File "test_fnc2.py", line 2, in badfunc
    return 1/0
ZeroDivisionError: integer division or modulo by zero

APSW Traceによるレポートが出力可能

APSW Traceは、簡単にあなたのコードを変更せずにSQLの実行をトレースして、要約レポートを提供します。

APSW Traceは下記のソースコード中のtoolsフォルダにあります。
http://rogerbinns.github.io/apsw/download.html#source-and-binaries

実行方法

$ python /path/to/apswtrace.py [apswtrace options] yourscript.py [your options]

実行例
「ネストしたトランザクションの利用」で使用したスクリプトのレポートを求めた場合の例を以下に示します。

C:\dev\python\apsw>python apswtrace.py --sql --rows --timestamps --thread test_n
est.py
290e5c0 0.002 1734 OPEN: "" win32 READWRITE|CREATE
292aad8 0.009 1734 CURSORFROM: 290e5c0 DB: ""
292aad8 0.010 1734 SQL: create table test(x primary key,y)
290e5c0 0.012 1734 SQL: SAVEPOINT "_apsw-0"
292aad8 0.013 1734 CURSORFROM: 290e5c0 DB: ""
292aad8 0.015 1734 SQL: insert into test values(1,'TEST1')
290e5c0 0.016 1734 SQL: SAVEPOINT "_apsw-1"
292aad8 0.018 1734 CURSORFROM: 290e5c0 DB: ""
292aad8 0.019 1734 SQL: insert into test values(2,'TEST2')
292aad8 0.021 1734 CURSORFROM: 290e5c0 DB: ""
292aad8 0.022 1734 SQL: insert into test values(3,'TEST3')
290e5c0 0.023 1734 SQL: RELEASE SAVEPOINT "_apsw-1"
290e5c0 0.025 1734 SQL: SAVEPOINT "_apsw-1"
292ab10 0.026 1734 CURSORFROM: 290e5c0 DB: ""
292ab10 0.028 1734 SQL: insert into test values(4,'TEST4')
292ab10 0.029 1734 CURSORFROM: 290e5c0 DB: ""
292ab10 0.031 1734 SQL: insert into test values(4,'Error')
290e5c0 0.032 1734 SQL: ROLLBACK TO SAVEPOINT "_apsw-1"
290e5c0 0.034 1734 SQL: RELEASE SAVEPOINT "_apsw-1"
ConstraintError: UNIQUE constraint failed: test.x
rollback 2
290e5c0 0.038 1734 SQL: SAVEPOINT "_apsw-1"
292ab48 0.040 1734 CURSORFROM: 290e5c0 DB: ""
292ab48 0.041 1734 SQL: insert into test values(5,'TEST5')
292ab48 0.043 1734 CURSORFROM: 290e5c0 DB: ""
292ab48 0.044 1734 SQL: insert into test values(6,'TEST6')
290e5c0 0.046 1734 SQL: RELEASE SAVEPOINT "_apsw-1"
290e5c0 0.047 1734 SQL: RELEASE SAVEPOINT "_apsw-0"
292acd0 0.049 1734 CURSORFROM: 290e5c0 DB: ""
292acd0 0.050 1734 SQL: select * from test
292acd0 0.052 1734 ROW: (1, "TEST1")
1 TEST1
292acd0 0.056 1734 ROW: (2, "TEST2")
2 TEST2
292acd0 0.059 1734 ROW: (3, "TEST3")
3 TEST3
292acd0 0.062 1734 ROW: (5, "TEST5")
5 TEST5
292acd0 0.066 1734 ROW: (6, "TEST6")
6 TEST6
APSW TRACE SUMMARY REPORT

Program run time                    0.072 seconds
Total connections                   1
Total cursors                       9
Number of threads used for queries  1
Total queries                       18
Number of distinct queries          14
Number of rows returned             5
Time spent processing queries       0.017 seconds

MOST POPULAR QUERIES

  3 SAVEPOINT "_apsw-1"
  3 RELEASE SAVEPOINT "_apsw-1"
  1 select * from test
  1 insert into test values(6,'TEST6')
  1 insert into test values(5,'TEST5')
  1 insert into test values(4,'TEST4')
  1 insert into test values(4,'Error')
  1 insert into test values(3,'TEST3')
  1 insert into test values(2,'TEST2')
  1 insert into test values(1,'TEST1')
  1 create table test(x primary key,y)
  1 SAVEPOINT "_apsw-0"
  1 ROLLBACK TO SAVEPOINT "_apsw-1"
  1 RELEASE SAVEPOINT "_apsw-0"

LONGEST RUNNING - AGGREGATE

  1  0.017 select * from test
  3  0.000 SAVEPOINT "_apsw-1"
  3  0.000 RELEASE SAVEPOINT "_apsw-1"
  1  0.000 insert into test values(6,'TEST6')
  1  0.000 insert into test values(5,'TEST5')
  1  0.000 insert into test values(4,'TEST4')
  1  0.000 insert into test values(4,'Error')
  1  0.000 insert into test values(3,'TEST3')
  1  0.000 insert into test values(2,'TEST2')
  1  0.000 insert into test values(1,'TEST1')
  1  0.000 create table test(x primary key,y)
  1  0.000 SAVEPOINT "_apsw-0"
  1  0.000 ROLLBACK TO SAVEPOINT "_apsw-1"
  1  0.000 RELEASE SAVEPOINT "_apsw-0"

LONGEST RUNNING - INDIVIDUAL

 0.017 select * from test
 0.000 insert into test values(6,'TEST6')
 0.000 insert into test values(5,'TEST5')
 0.000 insert into test values(4,'TEST4')
 0.000 insert into test values(4,'Error')
 0.000 insert into test values(3,'TEST3')
 0.000 insert into test values(2,'TEST2')
 0.000 insert into test values(1,'TEST1')
 0.000 create table test(x primary key,y)
 0.000 SAVEPOINT "_apsw-1"
 0.000 SAVEPOINT "_apsw-1"
 0.000 SAVEPOINT "_apsw-1"
 0.000 SAVEPOINT "_apsw-0"
 0.000 ROLLBACK TO SAVEPOINT "_apsw-1"
 0.000 RELEASE SAVEPOINT "_apsw-1"

C:\dev\python\apsw>

その他、詳細は下記を参考にしてください。
http://rogerbinns.github.io/apsw/execution.html#apswtrace

pysqlite よりAPSWのほうが早い

以下のテストではpysqlite よりAPSWのほうが早い結果が出ています。
http://rogerbinns.github.io/apsw/benchmarking.html

参考

APSW 3.8.7.3-r1 documentation » pysqlite differences
http://rogerbinns.github.io/apsw/pysqlite.html#pysqlitediffs

せっかくだから俺は衆院選挙のツイートを調べるぜ

ネット選挙が解禁されて早一年、選挙終わるたびに民主主義さんや日本ちゃんの残機をスペランカーのように減らし続ける言論空間な今日この頃、皆様いかがおすごしでしょうか。

さて、今回は開票前後のツイートの内容を調べてみます。

収集内容

2014/12/14 18:00 ~ 07:00 まで下記の情報を含むツイートを収集する

# 総選挙,#衆院選,選挙

PythonでStreaming APIを使用して特定のキーワードを含んだツイートを取得しつづける
http://qiita.com/mima_ita/items/ecdf7de2fe619378beee

収集、解析に使用したコード

https://github.com/mima3/stream_twitter

Windows7 Python2.7で動作確認済み

収集結果の解析

収集したデータは下記からダウンロードできます。
http://needtec.sakura.ne.jp/doc/shuin47twitter.zip

時間別のヒストグラム

まずは、ツイート数を時間別に見てみましょう。
先のコードを利用して2014/12/14 18:00 ~ 07:00までの1時間毎のデータをみてみます。

python twitter_db_hist.py "2014/12/14 9:00" "2014/12/14 22:00" 3600

※Twitter中の時刻がUTCで取得できるため日本時間だと9時間ずれています。

この結果は次のようになります。

figure_1.png

時刻(UTC) 日本時間 件数
12/14 09:00 12/14 18:00 3149
12/14 10:00 12/14 19:00 4047
12/14 11:00 12/14 20:00 11280
12/14 12:00 12/14 21:00 9755
12/14 13:00 12/14 22:00 7199
12/14 14:00 12/14 23:00 5207
12/14 15:00 12/14 00:00 3472
12/14 16:00 12/14 01:00 3801
12/14 17:00 12/14 02:00 1545
12/14 18:00 12/14 03:00 529
12/14 19:00 12/14 04:00 292
12/14 20:00 12/14 05:00 300
12/14 21:00 12/14 06:00 477

20:00の開票のタイミングが一番もりあがります。
そして、時間経過とともにツイート数は減っていきます。
しかし、1:00代に復活し、その後はツイート数は減り、目の覚める5:00頃から多少回復します。

深夜はツイート数が減り、朝増えるのは理解できます。
しかし、夜中1:00代にツイート数が増加した理由はなんでしょうか?

このため、1:00代を1分単位でみてみましょう。

python twitter_db_hist.py "2014/12/14 16:00" "2014/12/14 17:00" 60

figure_2.png

この結果をみると1:27分あたりで急速な盛り上がりを見せています。

このタイミングでなにが発生したのでしょうか?
ここで、民主党を愛してやまない海江田研究所の方々のスレを確認してみます。

【ふっかつのじゅもんがちがいます】海江田民主党研究第802弾【とうせんのしょはきえてしまいました】
http://anago.2ch.net/test/read.cgi/asia/1418565521/

811 :日出づる処の名無し:2014/12/15(月) 01:26:44.86 ID:tG+ZZ8gB
    【当落速報】民主党の海江田万里代表が比例東京ブロックで復活せず、落選確実となった(01:19)(c)2ch.net
    http://daily.2ch.net/test/read.cgi/newsplus/1418574054/

812 :日出づる処の名無し:2014/12/15(月) 01:26:49.76 ID:4Us97nfn
    落選決定w

813 :日出づる処の名無し:2014/12/15(月) 01:26:51.11 ID:pW7uplw3
    さよなら、万里

814 :日出づる処の名無し:2014/12/15(月) 01:27:01.20 ID:yIjazH47
    うわああああNHKでも落選きたw

815 :日出づる処の名無し:2014/12/15(月) 01:27:02.02 ID:NOhUWn58
    NHKでマリ完全落選

    いやここからフェニックスするから!絶対にふぇにっくすだから____

816 :日出づる処の名無し:2014/12/15(月) 01:27:08.70 ID:4zmUGrZE
    >>802
    ㌧。何もつまみ買ってきてないから鯖缶開けるわw

どうも当時の状況をみると1:19に朝日が号外として海江田代表の比例落選を報道し、NHKも1:27に同ニュースを報道したようです。

さすが、野党第一党の党首の進退は深夜のツイッタラーの眠気眼をもふきとばすインパクトがあったとうかがい知れます。また、この結果より、新聞の号外よりテレビの方が拡散力がつよいことがわかります。

頻出単語の抽出

次は頻出単語を見てみましょう。
Mecabを用いることで、形態素解析を行い、その単語を集計しました。

これは以下のスクリプトで行うことができます。

python twitter_db_mecab.py "2014/12/14 9:00" "2014/12/14 22:00" > mecab.txt

以下にそのベスト100を表示します。

単語 出現数
選挙 70626
33315
27196
衆院 27152
投票 13740
11698
当確 8386
自民党 7403
速報 7120
7074
東京 6864
開票 6484
当選 6456
6443
NHK 6222
5866
 # 5519
落選 5504
official 5488
kyodo 5487
確実 5384
5352
5236
番外 5229
議席 5025
行く 4811
4796
BqAAr 4633
vlhS 4606
4460
比例 4419
自民 4302
ブロック 4208
4196
てる 4035
3912
候補 3811
衆議院 3782
seiji 3773
3745
3726
日本 3611
jimin 3607
koho 3603
代表 3599
民主 3592
3589
3548
nicohou 3490
JNSC 3203
blogos 3170
ld 3125
名前 3098
安倍 3068
民主党 3039
言う 3011
特番 2959
次世代 2889
2881
2844
ニコ 2817
られる 2750
出演 2727
ビートたけし 2723
れる 2721
神奈川 2690
政治 2532
2511
海江田 2504
できる 2488
2424
2371
2315
視聴 2315
沖縄 2231
復活 2176
2092
1997
獲得 1977
1954
ない 1953
現在 1926
維新 1905
首相 1898
報道 1888
報じる 1882
1831
取る 1775
共産党 1773
1769
必要 1766
nMDR 1761
YidT 1761
若者 1750
1727
用紙 1712
万里 1666
senkyost 1645
情報 1628
是非 1618

やはり党名でもっとも抽出されたのは過半数を取った「自民党」でした。次に「民主党」、つづいて「次世代」で、あとは「維新」、「共産党」になります。
次世代の党は実際の議席数とネットでの注目度にかなりの乖離があるようです。

地名で抽出されたのは「東京」と「沖縄」でした。東京に関しては「東京新聞」の記事のリツイートもあったので、多く抽出され、沖縄に関しては自民党が小選挙区全滅という形になっていたので他の地域より注目があつまったと考えられます。

人名で注目されたのは「安倍」、「ビートたけし」、「海江田」でした。首相と野党第一党党首は当然としても、「ビートたけし」が注目されたの意外でした。どうもこれはニコ生で「ビートたけし」が出演していたためのようです。

文節の係受けの関係を探る

最後に文節の係受けの関係をCabochaを用いて集計してみます。
WindowsにCabochaを入れる方法は下記を参照してください。

WindowsにCabochaをいれてPythonで係り受けを解析してみる
http://qiita.com/mima_ita/items/161cd869648edb30627b

なお、今回は0.66で解析しました。最新でも似たような結果になると思います。

これは以下のスクリプトで行うことができます。

python twitter_db_cabocha.py "2014/12/14 9:00" "2014/12/14 22:00"  > cabocha.txt

以下にそのベスト100を表示します。

文節1 文節2 出現数
落選 確実 1762
co/ 4nMDR4YidT#総選挙http://t 1557
投票率 0% 1538
【選挙】衆院選、 10代 1534
10代 投票率 1534
若者 行く 1504
名前 書く 1504
RT@whsaito:投票用紙 記入する 1502
名前 記入する 1502
是非候補者 名前 1502
方式 取る 1502
高い 日本 1502
書く 行く 1502
取る 日本 1502
記入する 方式 1502
14日 行く 1502
教育水準 高い 1502
RT@kyoho_times: 10代 1460
復活 確実 1288
当選する 1208
こういう 1208
行く-否定 当選する 1186
当確 報じる 1172
3700kei:#総選挙選挙 行く-否定 1141
RT@keisei 3700kei:#総選挙選挙 1107
比例東京ブロック 復活 1075
RT@kyodo_official:民主党 海江田万里代表 964
片山哲委員長 落選する 928
落選する 落選する 928
海江田万里代表 復活 928
野党 落選する 928
社会党 片山哲委員長 928
1949年衆院選 落選する 928
敗北 確実 914
みんな 政治http://t 885
当選 確実 815
衆院選特集ページ http://t 761
感じる #選挙 755
投票所 聞く 755
1票 格差 755
格差 感じる 754
聞く 感じる 754
女子高校 聞く 754
RT@kurosia:知り合い 投票所 752
戦後最低 前回 742
RT@ld_blogos: 【速報】 663
次世代 633
【速報】 当確 561
下回る http://t 551
RT@kyodo_official:次世代 546
衆院選 投票率 535
述べる 514
大阪10区民主・辻元清美氏 当確 475
候補者情報 衆議院選挙 2014-Yahoo 433
午後 6時現在 424
投票率 34 424
79ポイント 下回る 420
全国平均 34 420
6時現在 34 420
98% 前回 420
よる 34 420
RT@senkyost: 【獲得議席___ 386
投票 行く 379
集団的自衛権 行使容認 377
敗北 報じる 370
当確 破る 367
こと 知る 363
日本 知る 360
Jリーグ復帰 知る 359
こんなんなるなら 出馬する 359
そろそろ Jリーグ復帰 359
俺様 出馬する 359
必要 言う 357
人当選#拡散希望___#RT 人全員フォロー 356
【悲報】アニメに 必要 356
規制 必要 356
人当選 必要 356
言う #選挙http://t 355
人全員フォロー #選挙http://t 355
8bu_: 必要 352
RT@K 8bu_: 352
#選挙#NHK#衆議院選挙#池上 選挙#開票 344
石原氏 明言する 341
石原慎太郎最高顧問 341
今回衆院選 政界引退 341
石原慎太郎最高顧問 落選 341
政界引退 明言する 341
co/ 7LGbX1z 322
RT@mainichijpedit:総務省 よる 309
理解 得る 304
___http ://t 303
国民 理解 303
RT@jimin_koho: /する 301
こと 確実 278
RT@jimin_koho: / 270
行使容認 表明する 268
自民党 表明する 268
表明する 村上誠一郎氏 268
反対 表明する 268
2区 表明する 268

落選→確実が一位になっているあたり、ツイッターの関心は誰が受かるかより誰を落とすかへの注目が高いようです。

あと若者の投票率に関する言及が多いように見えます。ただ、「10代の投票率0%」というネタツイートが多いのも事実です。

「高い」→「日本」っていうのは「投票用紙に名前を記入する方式を取っているのは教育水準の高い日本だけ」というツイートが大量にリツイートされた結果のようです。

また、最初に述べたように、選挙の度に残機を減らされる民主主義と日本について調べてみました。
民主主義が死ぬといっているツイートが2件あったうち、民主主義は死なないと言っているツイートが11件なので多分、民主主義君の残機は思ったより減ってないようです。

ただし、以下のような残機を減らす言葉は数件抽出されました。

文節1 文節2 出現数
民主主義 死ぬ 2
民主主義 終わる 2
民主主義 終了 2
民主主義 崩壊 2
・・・民主主義 崩壊 1

どうように日本ちゃんの残機の減少は以下の通りです。

文節1 文節2 出現数
RT@inosan08260:日本終了確定 178kakapo:日本 7
自民 日本崩壊 4
もう 日本崩壊 4
笑える 日本崩壊 4
日本 潰す 2
日本 終わる 2

この結果より今回の選挙では民主主義の残機は9機へり、日本ちゃんの残機23機ほど減ったようです。

データから読み取れる事のまとめ

・海江田さんが無職になると夜中なのに盛り上がったり、落選→確実という文節の係受けが頻出されるのをみると、誰が受かるかより誰が落ちるかの方が注目されます。

・次世代という単語の出現数と現実の結果をみると、ネットで注目されたからって議席はとれないといえます。

・選挙のたびに残機がゴリゴリ減っているイメージのあった民主主義と日本ですが、今回はそんなこともなかったようです。

本当のまとめ

・・・ってな感じのインチキな解析が、それっぽく行えます。
時間ごとのツイートに関しては、この例のように、データの増減をみて、変化のあったところを詳細に調べていけばいいと思います。

単語の頻出については、たしかに注目を集めている事は簡単にわかります。しかしながら、この例の用に抽出数が多いことが即ポジティブな反応とは限らないことに注意する必要があります。

係受け解析ついては、単語のみの頻出を調べる弱点を克服できる可能性はあります。しかし、正直今回については、その可能性を十分に実践できていなかったでしょう。これは今後の課題になります。

ちなみに鍵垢のツイートについては、StreamingAPIのfilterでは取得できませんでした。

WindowsにCabocha 0.68をいれてPythonで係り受けを解析してみる

目的

Cabocha0.68をインストールしてPythonで係受け解析を行う

前提

Mecabをインストールしてあること
https://code.google.com/p/mecab/downloads/list

ここではmecab-0.996.exeをUTF-8でインストールしてあるものとする。

Cabochaのインストール

1.cabocha-0.68.exeのダウンロード
 http://code.google.com/p/cabocha/downloads/list

2.ダウンロードしたEXEの実行。この際、選択する文字コードはMecabの文字コードと同一にする。
※ここではUTF-8を選択

3.環境変数のパスに”C:\Program Files (x86)\CaboCha\bin”を通してcabochaを実行できるようにしておく
れはpythonがdllにアクセスするのにも必要である。

4.実行確認を行う
input.txtというUTF8のファイルを作成して解析したい文字列を入力して、コマンドプロンプトから下記を実行する。

cabocha < input.txt > out.txt

適切に解析できれば下記のようなファイルが出力される。


                ここは---D
                まりさの-D
    ゆっくりプレイスだよ!
EOS

なおここでファイルを経由しているのはコマンドプロンプトでUTF-8を扱えないためである。

ここはまりさのゆっくりプレイスだよ!
EOS

こうなってしまった場合、input.txtの文字コードがutf-8になっていない場合がある。
(メモ帳で作成した場合、デフォルトがANSIなので注意)

また、以下のようなエラーが発生する場合がある。

svm.cpp(140) [version == MODEL_VERSION] incompatible version: 101
svm.cpp(751) [size >= 2] dep.cpp(79) [!failed] no such file or directory: C:\Program Files (x86)\CaboCha\etc\..\model\dep.ipa.model

この場合は、cabochaのバージョンアップが適切に行えていないので、下記のフォルダを削除すること。

C:\Users\ユーザ名\AppData\Local\VirtualStore\Program Files(x86)\CaboCha

PythonからCabochaを使用できるようにする。

1.cabocha-0.68.tar.bzのダウンロード
 http://code.google.com/p/cabocha/downloads/list
このファイルはLhaplusなどで解凍できる

2.解凍したフォルダ中のpythonフォルダにカレントディレクトリを移動させて下記のコマンドを実行する。

python setup.py install

3.以下のエラーが発生する。

Traceback (most recent call last):
  File "setup.py", line 13, in <module>
    version = cmd1("cabocha-config --version"),
  File "setup.py", line 7, in cmd1
    return os.popen(str).readlines()[0][:-1]
IndexError: list index out of range

これは、Windowsにはcabocha-configがインストールされていないために発生する

4.setup.pyを変更する。

変更前

# !/usr/bin/env python

from distutils.core import setup,Extension,os
import string

def cmd1(str):
    return os.popen(str).readlines()[0][:-1]

def cmd2(str):
    return string.split (cmd1(str))

setup(name = "cabocha-python",
    version = cmd1("cabocha-config --version"),
    py_modules=["CaboCha"],
    ext_modules = [
        Extension("_CaboCha",
            ["CaboCha_wrap.cxx",],
            include_dirs=cmd2("cabocha-config --inc-dir"),
            library_dirs=cmd2("cabocha-config --libs-only-L"),
            libraries=cmd2("cabocha-config --libs-only-l"))
            ])

version と、ext_modulesの内容をインストールした情報に書き換える。

変更後

# !/usr/bin/env python

from distutils.core import setup,Extension,os
import string

def cmd1(str):
    return os.popen(str).readlines()[0][:-1]

def cmd2(str):
    return string.split (cmd1(str))

setup(name = "cabocha-python",
    version = "0.68",
    py_modules=["CaboCha"],
    ext_modules = [
        Extension("_CaboCha",
            ["CaboCha_wrap.cxx",],
            include_dirs=[r"C:\Program Files (x86)\CaboCha\sdk"],
            library_dirs=[r"C:\Program Files (x86)\CaboCha\sdk"],
            libraries=['libcabocha'])
])

5.再度、setup.pyの実行

python setup.py install

6.以下のサンプルプログラムを入力して試す。

# !/usr/bin/python
# -*- coding: utf-8 -*-

import CaboCha

# c = CaboCha.Parser("");
c = CaboCha.Parser("")

sentence = "帽子を返す"

# print c.parseToString(sentence)

# tree =  c.parse(sentence)
#
tree =  c.parse(sentence)
print tree.toString(CaboCha.FORMAT_TREE)
print tree.toString(CaboCha.FORMAT_LATTICE)
# print tree.toString(CaboCha.FORMAT_XML)

for i in range(tree.chunk_size()):
    chunk = tree.chunk(i)
    print 'Chunk:', i
    print ' Score:', chunk.score
    print ' Link:', chunk.link
    print ' Size:', chunk.token_size
    print ' Pos:', chunk.token_pos
    print ' Head:', chunk.head_pos # 主辞
    print ' Func:', chunk.func_pos # 機能語
    print ' Features:',
    for j in range(chunk.feature_list_size):
        print '  ' + chunk.feature_list(j) 
    print
    print 'Text' 
    for ix  in range(chunk.token_pos,chunk.token_pos + chunk.token_size):
      print ' ', tree.token(ix).surface 
    print

for i in range(tree.token_size()):
    token = tree.token(i)
    print 'Surface:', token.surface
    print ' Normalized:', token.normalized_surface
    print ' Feature:', token.feature
    print ' NE:', token.ne # 固有表現
    print ' Info:', token.additional_info
    print ' Chunk:', token.chunk
    print
帽子を-D
    返す
EOS

* 0 1D 0/1 0.000000
帽子  名詞,一般,*,*,*,*,帽子,ボウシ,ボーシ
を   助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
* 1 -1D 0/0 0.000000
返す  動詞,自立,*,*,五段・サ行,基本形,返す,カエス,カエス
EOS

Chunk: 0
 Score: 0.0
 Link: 1
 Size: 2
 Pos: 0
 Head: 0
 Func: 1
 Features:   FCASE:を
  FHS:帽子
  FHP0:名詞
  FHP1:一般
  FFS:を
  FFP0:助詞
  FFP1:格助詞
  FFP2:一般
  FLS:帽子
  FLP0:名詞
  FLP1:一般
  FRS:を
  FRP0:助詞
  FRP1:格助詞
  FRP2:一般
  LF:を
  RL:帽子
  RH:帽子
  RF:を
  FBOS:1
  GCASE:を
  A:を

Text
  帽子
  を

Chunk: 1
 Score: 0.0
 Link: -1
 Size: 1
 Pos: 2
 Head: 0
 Func: 0
 Features:   FHS:返す
  FHP0:動詞
  FHP1:自立
  FHF:基本形
  FFS:返す
  FFP0:動詞
  FFP1:自立
  FFF:基本形
  FLS:返す
  FLP0:動詞
  FLP1:自立
  FLF:基本形
  FRS:返す
  FRP0:動詞
  FRP1:自立
  FRF:基本形
  LF:返す
  RL:返す
  RH:返す
  RF:返す
  FEOS:1
  A:基本形

Text
  返す

Surface: 帽子
 Normalized: 帽子
 Feature: 名詞,一般,*,*,*,*,帽子,ボウシ,ボーシ
 NE: None
 Info: None
 Chunk: <CaboCha.Chunk; proxy of <Swig Object of type 'CaboCha::Chunk *' at 0x0274A170> >

Surface: を
 Normalized: を
 Feature: 助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
 NE: None
 Info: None
 Chunk: None

Surface: 返す
 Normalized: 返す
 Feature: 動詞,自立,*,*,五段・サ行,基本形,返す,カエス,カエス
 NE: None
 Info: None
 Chunk: <CaboCha.Chunk; proxy of <Swig Object of type 'CaboCha::Chunk *' at 0x0274A170> >

せっかくだから俺は現在地から小選挙区を検索できるようにするぜ

ネット選挙が解禁されて早一年、公式の政党関係者のネット発信が、支持者のSAN値をカツオ節のごとく削り続けるエクストリームゲームと化した今日この頃、皆様いかがおすごしでしょうか。
さて、今回は現在地から小選挙区の情報を取得する方法を考えてみます。

結果

http://needtec.sakura.ne.jp/analyze_election/page/ElectionArea/shuin_47

このページでは、県を選択するか、現在地を取得することで、小選挙区の候補が表示されます。
さらに小選挙区を選択することで、選挙区のだいたいの位置と候補者の一覧が表示されます。

ソースコード

https://github.com/mima3/analyze_election

依存しているライブラリ
lxml-3.4.0-py2.7-freebsd-9.1-RELEASE-p15-amd64.egg
rdp-0.5-py2.7.egg
numpy-1.9.1-py2.7-freebsd-9.1-RELEASE-p15-amd64.egg
sympy-0.7.5-py2.7.egg
Beaker-1.6.4-py2.7.egg

sympyは0.7.5でないと動きません

使用データ

国土数値情報 行政区域データ
http://nlftp.mlit.go.jp/ksj/gml/datalist/KsjTmplt-N03.html

衆議院小選挙区選出議員の選挙区(都道府県別)
http://www.soumu.go.jp/senkyo/senkyo_s/news/senkyo/shu_kuwari/

上記をCSVに手入力で変換したデータ
https://github.com/mima3/analyze_election/blob/master/election_area.csv

朝日新聞デジタル>2014衆院選>候補者
http://www.asahi.com/senkyo/sousenkyo47/kouho/

上記をCSVに変換したデータ
https://github.com/mima3/analyze_election/blob/master/script/candidate_shuin_47.csv

データ作成の手順

# データベースの作成
python create_db.py election.sqlite

# 国土数値情報 行政区域データのインポート 数時間で終わるよ!
python import_administrative_boundary.py election.sqlite area\N03-14_140401.xml

# 行政区域データをsympyのPolygonに変換する 24時間ぐらいで終わるよ!
python convert_poly.py election.sqlite

# 小選挙区の情報を登録
python import_election_area.py election.sqlite election_area.csv

# 小選挙区の候補者情報を登録
python import_candidate.py election.sqlite shuin_47 script\candidate_shuin_47.csv

解説

行政区域データ

国土数値情報には行政区域データがXMLで提供されています。
これを用いれば、行政区割りをGoogleMap上に表示することができます。
ただし、このデータは莫大な大きさなので、取扱いにはいくつか注意する点があります。

大きなサイズXMLを解析する

大きなサイズのXMLを解析する場合、XMLファイルを文字に変換してパースするというやりかたをすると、メモリ使用量が激増し、処理しきれなくなります。

そのため、lxml.etree.iterparseを用いて、順次処理するようにします。
実際の処理をみてみましょう。

election_db.py

    def ImportAdministrativeBoundary(self, xml):
        f = None
        contents = None
        namespaces = {
            'ksj': 'http://nlftp.mlit.go.jp/ksj/schemas/ksj-app',
            'gml': 'http://www.opengis.net/gml/3.2',
            'xlink': 'http://www.w3.org/1999/xlink',
            'xsi': 'http://www.w3.org/2001/XMLSchema-instance'
        }
        self._conn.execute('begin')

        print ('admins....')
        context = etree.iterparse(xml, events=('end',), tag='{http://nlftp.mlit.go.jp/ksj/schemas/ksj-app}AdministrativeBoundary')
        for event, admin in context:
            adminId = admin.get('{http://www.opengis.net/gml/3.2}id')
            print (adminId)
            bounds = admin.find('ksj:bounds', namespaces=namespaces).get('{http://www.w3.org/1999/xlink}href')[1:]
            prefectureName = admin.find('ksj:prefectureName', namespaces=namespaces).text
            subPrefectureName = admin.find('ksj:subPrefectureName', namespaces=namespaces).text
            countyName = admin.find('ksj:countyName', namespaces=namespaces).text
            cityName = admin.find('ksj:cityName', namespaces=namespaces).text
            areaCode = admin.find('ksj:administrativeAreaCode', namespaces=namespaces).text
            sql = '''INSERT INTO administrative_boundary
                     (gml_id, bounds, prefecture_name, sub_prefecture_name, county_name, city_name, area_code)
                     VALUES(?, ?, ?, ?, ?, ?, ?);'''
            self._conn.execute(sql, [adminId, bounds, prefectureName, subPrefectureName, countyName, cityName, areaCode ])

            admin.clear()
            # Also eliminate now-empty references from the root node to <Title> 
            while admin.getprevious() is not None:
                del admin.getparent()[0]
        del context

上記のコードは国土数値情報を解析してDBに格納している処理の一部です。
この例では、etree.iterparseを用いて、「AdministrativeBoundary」タグが検知されるたびに、処理を行っています。

詳細は、以下のページを参考にすると良いです。
lxml を使用して Python での XML 構文解析をハイパフォーマンスにする
http://www.ibm.com/developerworks/jp/xml/library/x-hiperfparse/

座標情報を間引く

国土数値データは正確な座標情報が格納されているため、サイズが大きくなります。
今回は、ざっくりした座標がわかればいいので、その情報を間引きます。

線を間引くアルゴリズムとしては,Ramer-Douglas-Peuckerというアルゴリズムがあります。
このアルゴリズムの説明は下記のページを見ると良いでしょう。

[Mathematica] 折れ線を間引く
http://www.330k.info/essay/oresenwomabiku

Pythonの Ramer-Douglas-Peucker アルゴリズムのモジュールとしては下記が利用できます。
https://pypi.python.org/pypi/rdp

小選挙区の情報登録

総務省は例によって例のごとく、小選挙区の情報をPDFでしか公開しておりません。
なので、頑張って手入力するか、別の方法をためす必要があります。

また、小選挙区の情報が意外とファジーな感じになっています。
たとえば、岩手県の第一区と第二区を見てみましょう。
http://www.soumu.go.jp/main_content/000240041.pdf

第一区 盛岡市(本庁管内、盛岡市役所青山支所管内、盛岡市役所簗川支所管内、盛岡市役所太田支所管内、盛岡市役所支所管内、盛岡市都南総合支所管内)

第二区 盛岡市(第一区に属しない区域)

とあります。
当然の権利のように、本庁管内がどこを表すかの情報は、ここで提供されていません。
そのため、今回は、盛岡市だった場合は第一区と第二区が小選挙区の候補としてあがるようにしました。

このようにデータ解析をさせないという強い意志を持った総務省に対して呪詛を吐きながら作成したCSVが以下の通りになります。
https://github.com/mima3/analyze_election/blob/master/election_area.csv

また、このあたりで心がおれたので、候補者の情報については朝日新聞から引っ張ってきています。
下記のスクリプトで、それっぽいデータが抜けるので、必要に応じて手で直し使用しています。

https://github.com/mima3/analyze_election/blob/master/script/analyze_asahi.py

現在地から小選挙区を取得する。

ブラウザで現在地を取得する。

ブラウザで現在地を使用するには、navigator.geolocationを使用します。
以下のようなコードで経度、緯度が取得できるでしょう。

if (!navigator.geolocation) {
  //Geolocation APIを利用できない環境向けの処理
  alert('GeolocateAPIが使用できません');
}
navigator.geolocation.getCurrentPosition(function(position) {
  console.log(position)
}

なお、IEの場合、現在位置が、ひどくずれます。
これは、Microsoftが使用している位置用法のデータベースがChromeやFirefoxが利用している位置情報のデータベースより精度が悪いためだと考えられます。(おれは悪くねー!おれは悪くねー!)
https://social.technet.microsoft.com/Forums/ie/en-US/aea4db4e-0720-44fe-a9b8-09917e345080/geolocation-wrong-in-ie9-but-not-other-browsers

特定の座標が行政区域に含まれているか調べる。

特定の座標が行政区域に含まれているか調べるには、多角形中に点が含まれているか調べるのと同じ意味になります。
Pythonの場合、SymPy で 点の内外判定を行えば 基本的 には良いです。

[Python]SymPy で 点の内外判定
http://d.hatena.ne.jp/factal/20121013

基本的にはと言ったのは、この処理がくっそ遅いからです。
数万ある行政区画に対して一々やっていたのでは時間がかかりすぎてしょうがないです。

このために、ここでは2つの対策を施しました。

1つ目、点の内外判定を行う前に、対象の多角形の数を絞る。
これは、多角形の頂点と指定の点の距離が、ある範囲内にある場合のみ検索対象としています。

具体的には下記のようなコードになります。

election_db.py

    def GetPos(self, lat, long):
        """
        現在の経度緯度に紐づくcurveのデータを取得
        """
        m =0.005
        while 1:
            rows = self._getCurveId(lat, long, m).fetchall()
            if rows:
              break
            m = m * 2
            if m > 0.1:
              return None

        dict = {}
        pt = Point(lat, long)

        for r in rows:
            key = r[0]
            ret = self._isCrossCurveId(pt, key)
            if ret:
                return ret
        return None

2つ目として、あらかじめPolygonのオブジェクトを作成し、データベースに登録しておくことです。
具体的には以下のような実装になります。

    def ConvertPoly(self):
        """
        curveテーブルからpolygonの作成
        """
        #gc.enable()
        #gc.set_debug(gc.DEBUG_LEAK)
        sql = '''DELETE FROM polygon'''
        self._conn.execute(sql)

        sql = '''select curve_id,lat,lng from curve order by curve_id'''
        rows = self._conn.execute(sql)
        dict = {}
        for r in rows:
            key = r[0]
            if dict.get(key) is None:
                dict[key] = []
            dict[key].append((r[1], r[2]))
        i = 0
        self.Commit()

        self._conn.execute('begin')
        for key in dict:
            print (key + ":" + str(i))
            #b = len(gc.get_objects())
            self._createPoly(key, dict[key])
            i = i + 1
            #gc.collect()
            #print str(b) + ":" + str(len(gc.get_objects()))
            if i % 100 == 0:
                clear_cache()
                self.Commit()

    def _createPoly(self, key, list):
        poly = Polygon(*list)
        obj = pickle.dumps(poly)
        sql = '''INSERT INTO polygon
                       (curve_id, object)
                     VALUES(?, ?);'''
        self._conn.execute(sql, [key, obj ])
        del poly
        del obj

オブジェクトのシリアライズはpickle.dumpsを用いて行います。
また、sympyはキャッシュ機構を使用しているので、大量にオブジェクトを作成するとメモリがすぐに数GByte消費されます。
それを避けるため、100個作成するため、clear_chacheを用いてキャッシュを削除しています。

また、最新のsympyのPolygonで"Polygon has intersecting sides."を例外を出力する条件が0.7.5と0.7.6で異なっています。
0.7.6では条件が厳しくなり、このデータでは正常に作成できないです。
そのため、sympyは0.7.5を使用してください。

PythonでStreaming APIを使用して特定のキーワードを含んだツイートを取得しつづける

背景

TwitterAPIには利用制限があり、特定の期間では指定の回数以上使用することができません。
以下の記事ではsearch/tweets APIを用いてツイートの検索をおこなっていますが、一回のAPIで取得できる上限は100件、15分で180回しか実行できません。

Pythonを用いてTwitterの検索を行う
http://qiita.com/mima_ita/items/ba59a18440790b12d97e

これは、少ないデータであれば問題ないのですが、「選挙番組中に#総選挙を含むハッシュタグを検索しつづける」という使い方には不向きです。

そこで、Twitterでは、常時データを取得しつづける方法としてStreaming APIを提供しています。

The Streaming APIs
https://dev.twitter.com/streaming/overview

Streaming API

Streaming APIは開発者に少ない遅延でツイッターの情報を取得しつづけることがあります。

Streaming APIには大きく3つの種類があります。

名前 説明
Public streams 公開されているツイッターのデータを取得できます。
キーワードや場所でフィルタすることのできるfilterを利用できます
User streams 特定の認証されたユーザのデータとイベントを取得します。
Site streams 複数のユーザーのデータを取得します。現在ベータ版です。アクセスは、ホワイトリストアカウントに制限されています。

Pythonのサンプル

目的

Pythonで特定のキーワードをDBに登録しつづけます。

前提のライブラリ

python_twitter
https://code.google.com/p/python-twitter/
PythonでTwitterを操作するライブラリです。

peewee
https://github.com/coleifer/peewee
sqlite,postgres,mysqlを使用できるORMです。

サンプルコード

# -*- coding: utf-8 -*-
# easy_install python_twitter
import twitter
import sys
import codecs
import dateutil.parser
import datetime
import time
from peewee import *

db = SqliteDatabase('twitter_stream.sqlite')

class Twitte(Model):
    createAt = DateTimeField(index=True)
    idStr = CharField(index=True)
    contents = CharField()

    class Meta:
        database = db

# easy_install の最新版でGetStreamFilterがなければ下記のコード追加
# https://github.com/bear/python-twitter/blob/master/twitter/api.py
def GetStreamFilter(api,
                    follow=None,
                    track=None,
                    locations=None,
                    delimited=None,
                    stall_warnings=None):
    '''Returns a filtered view of public statuses.

    Args:
      follow:
        A list of user IDs to track. [Optional]
      track:
        A list of expressions to track. [Optional]
      locations:
        A list of Latitude,Longitude pairs (as strings) specifying
        bounding boxes for the tweets' origin. [Optional]
      delimited:
        Specifies a message length. [Optional]
      stall_warnings:
        Set to True to have Twitter deliver stall warnings. [Optional]

    Returns:
      A twitter stream
    '''
    if all((follow is None, track is None, locations is None)):
        raise ValueError({'message': "No filter parameters specified."})
    url = '%s/statuses/filter.json' % api.stream_url
    data = {}
    if follow is not None:
        data['follow'] = ','.join(follow)
    if track is not None:
        data['track'] = ','.join(track)
    if locations is not None:
        data['locations'] = ','.join(locations)
    if delimited is not None:
        data['delimited'] = str(delimited)
    if stall_warnings is not None:
        data['stall_warnings'] = str(stall_warnings)

    json = api._RequestStream(url, 'POST', data=data)
    for line in json.iter_lines():
        if line:
            data = api._ParseAndCheckTwitter(line)
            yield data

def main(argvs, argc):
    if argc != 6:
        print ("Usage #python %s consumer_key consumer_secret access_token_key access_token_secret #tag1,#tag2 " % argvs[0])
        return 1
    consumer_key = argvs[1]
    consumer_secret = argvs[2]
    access_token_key = argvs[3]
    access_token_secret = argvs[4]
    # UNICODE変換する文字コードは対象のターミナルに合わせて
    track = argvs[5].decode('cp932').split(',')

    db.create_tables([Twitte], True)

    api = twitter.Api(base_url="https://api.twitter.com/1.1",
                      consumer_key=consumer_key,
                      consumer_secret=consumer_secret,
                      access_token_key=access_token_key,
                      access_token_secret=access_token_secret)
    for item in GetStreamFilter(api, track=track):
        print '---------------------'
        if 'text' in item:
            print (item['id_str'])
            print (dateutil.parser.parse(item['created_at']))
            print (item['text'])
            print (item['place'])
            row = Twitte(createAt=dateutil.parser.parse(item['created_at']),
                         idStr=item['id_str'],
                         contents=item['text'])
            row.save()
            row = None

if __name__ == '__main__':
    sys.stdout = codecs.getwriter(sys.stdout.encoding)(sys.stdout, errors='backslashreplace')
    argvs = sys.argv
    argc = len(argvs)
    sys.exit(main(argvs, argc))

使い方

python twitter_stream.py consumer_key consumer_secret access_token_key access_token_secret  #総選挙,#衆院選,選挙

アクセストークンの情報とキーワードを指定して一考すると、カレントディレクトリにtwitter_stream.sqliteというSQLITEのデータベースを作成します。

説明

・最新のコードではGetStreamFilterメソッドが提供されていますが、Python2.7のeasy_installで取得できるバージョンでは存在しません。ここでは同じコードを実装しています。

・ここではWindowsで動作させているので、cp932でtrack変数を変換していますが、これはターミナルの文字コードに合わせてください。

・大量のデータが流れているときは気になりませんが、最後にツイートされたフィルター対象のデータの取得には数分の遅延があります。

・基本的にデータベースを登録しておいて、時間のかかる処理は別プロセスでやったほうがいいでしょう。

・過去のデータは取れないので、常駐プロセスにする必要があります。

・created_atはUTC時刻なので日本の時間より9時間おそいです。ここで登録された時間に9時間加算した時刻が日本の時刻になります。

WindowsユーザがUnix系のOSに触れてみる方法

2014年の8月に発表されたOSのシェアは圧倒的にWindowsです。
http://news.mynavi.jp/news/2014/08/05/247/

そのため、多くの人はWindowsしか触ったことがないかもしれません。
しかし、職業プログラマとなると、Windowsだけ触ってればいいというものではありません。

ここでは、Windowsユーザだった人間が、Unix系のOSに触ってみる方法を考えてみます。

Unix系のOSに触ってみる方法

MinGWやCygwinをWindowsにインストールする。

特定のコードをgccでコンパイルしたり、UNIXライクなCUIを使ってみるだけならMinGWやCygwinをインストールするといいでしょう。

MinGWの場合は、POSIX APIを提供していないので、Cygwinでコンパイルができるが、MinGWではコンパイルできない場合があります。

Cygwin
https://www.cygwin.com/

MinGW
http://www.mingw.org/

msysGit
http://msysgit.github.io/
以下のページにインストールをした例を記述している。
http://qiita.com/mima_ita/items/1483f2e589605abe2ea4#2-1

あくまでエミュレーションしているだけなので、完全に動作を再現できると期待するべきではないでしょう。

レンタルサーバーを借りる

実際のUNIX系のOSを安価に操作する場合、レンタルサーバーを利用する手もあります。

http://www.sakura.ne.jp/
サクラインターネットで共有サーバーをスタンダードプラン以上で借りることで、SSHを利用してログインできます。
これにより、コマンドの実験や、gccを利用した任意のアプリケーションのコンパイルや実行を行うことができます。

共有サーバーであれば、最低限の環境がそろった状態で使用することが可能です。
ただし、いくつかの制約があります。
・ApachやMySQL等の共有で使っているアプリケーションの再起動や設定は変更はできません。
・負荷のかかるプロセスを動作させると、怒られて途中で落とされます。
・メンテナンスで環境が変わります。Pythonのバージョンは勝手にあがり、OSはいつのまにか32ビットから64ビットに変更されます。
 そのたびに、自分のアプリケーションの動作チェックが必要になります。

仮想環境をインストールする

VMware PlayerにDebianなどのOSをインストールします。
以下のページはVMware PlayerにDebianをインストールする例を記述しています。
http://www.infraeye.com/study/linux5.html

必要なアプリケーションは、すべて自分でインストールする必要があり、自由にそれらの設定を変更できます。

仮想環境の強みとして、ファイルとして保存されているので、それを複製することで特定の環境への復旧が容易になることです。
このことは、慣れていない人間が壊す覚悟で色々実験できることを意味しており、学習する上では非常に大きなアドバンテージだと思います。

弱点として、VMWare Playerを動作させるマシンはそれなりの性能をもっていないと厳しいです。メモリはWindows7で動かしている場合、メモリが8GB程度あった方が安定して動作しました。(4GBだと体感でちと厳しい印象)

パソコンごと用意する

自作PCにインストールするのもいいですし、MacOSならBSD UNIXベースで作ってあるので、それで代替するのもいいかもしれません。

まとめ

いくつかの方法を紹介しました。
他にも、ここで紹介していない方法もあるかと思います。(専用サーバー借りるとか)

ここで紹介した方法で、コストパフォーマンスに優れる方法としては仮想環境だと思います。

VMWare Playerは個人使用なら無償、会社で使う場合でも、10,800円で調達できます。
好きなOSをインストールでき、環境を自由に入れ替えられるのは大きな魅力でしょう。

環境を自由に入れ替えなくていいので、触ってみるという意味ではレンタルサーバーも選択肢としてあります。
2週間は無料期間があり、その後、月500円程度で、Webサービスの作成の実験をかねて使う分には十分だと思います。

いずれにせよ、一昔前と違い、より楽に、安価にUNIX系のOSを試してみるということができるかと思います。

東京メトロAPIを使ったアプリケーションを作ってみよう

東京メトロAPIを使ったアプリケーションを作ってみよう

ここでは東京メトロAPIを使用してアプリケーションを開発する、方法について説明します。
ここで記述しているものは、独自の調査により記述してあるので、公式での内容と異なることもありますので注意してください。

ここでは実験コードをPHPで記述してますが、URL叩けばJSONで結果が帰ってくるので、任意の言語で開発できると思います。

概要

以下にその概要があります。
https://developer.tokyometroapp.jp/info

東京メトロにおける交通機関の情報をオープンデータとして公開するので、当該データを テストする 活用したアプリケーションの募集を行っています。

ユーザー登録を行い、アクセストークンを入手することで以下のようなことがAPIを使用できます。

・東京メトロの駅の情報を取得できます。駅の位置や、どのような施設が存在するか、どの路線に接続しているか調べることができます。
・位置情報を検索キーにして出口や駅を検索したり、路線をキーに駅を検索したりできます。
・各路線の線路の形を位置情報で取得したり、駅と駅の所用時間が取得できます。
・遅延や運休などの運行状況が取得できます。
・東京メトロ沿線での運賃がいくらかかるか取得できます。

など

はじめよう

1.以下の「開発サイト」からユーザー登録をします。登録まで数営業日かかるので、お茶でも飲んでゆっくり待ちましょう。

https://developer.tokyometroapp.jp/info

2.ユーザ登録が行われたら開発者サイトにログインをして、アクセストークンを追加します。

スクリーンショット 2014-10-10 21.02.19.png

注意:ここで発行したアクセストークンは他人に漏れないようにしましょう。

3.APIの仕様にあるように特定のURLにアクセストークンを付与することで様々な情報がJSON形式で取得できます。

発行するURL

https://api.tokyometroapp.jp/api/v2/datapoints?rdf:type=odpt:TrainInformation&acl:consumerKey=アクセストークン

取得結果

[{
  "@context":"http://vocab.tokyometroapp.jp/context_odpt_TrainInformation.json",
  "@id":"urn:ucode:_00001C000000000000010000030C3BE9",
  "dc:date":"2014-10-10T21:25:02+09:00","dct:valid":"2014-10-10T21:30:02+09:00",
  "odpt:operator":"odpt.Operator:TokyoMetro",
  "odpt:railway":"odpt.Railway:TokyoMetro.Tozai",
  "odpt:timeOfOrigin":"2014-10-08T15:35:00+09:00",
  "odpt:trainInformationText":"現在、平常どおり運転しています。",
  "@type":"odpt:TrainInformation"
},{
  "@context":"http://vocab.tokyometroapp.jp/context_odpt_TrainInformation.json",
  "@id":"urn:ucode:_00001C000000000000010000030C3BE7",
  "dc:date":"2014-10-10T21:25:02+09:00",
  "dct:valid":"2014-10-10T21:30:02+09:00",
  "odpt:operator":"odpt.Operator:TokyoMetro",
  "odpt:railway":"odpt.Railway:TokyoMetro.Marunouchi",
  "odpt:timeOfOrigin":"2014-09-29T11:00:00+09:00",
  "odpt:trainInformationText":"現在、平常どおり運転しています。",
  "@type":"odpt:TrainInformation"
},{
  //省略
}]

https://api.tokyometroapp.jp/api/v2/datapoints」に、以下のパラメータを指定することで、列車の運行状況を取得できます

名前 説明
rdf:type 取得したいデータ種別。
今回はodpt:TrainInformationを指定して列車運行情報を取得している
acl:consumerKey 作成したアクセストークン

あとは、仕様書を見ながら格闘すればいいでしょう。

APIの基本的な話

開発者サイトに行くと、API仕様書があります。
以下の項目に目を通しておけば、大体のことはできると思います。

API仕様書
https://developer.tokyometroapp.jp/documents
鉄道情報
https://developer.tokyometroapp.jp/documents/railway
地物情報
https://developer.tokyometroapp.jp/documents/facility

二種類の検索方法

検索には二種類存在しています。
路線名や駅名といった特定の条件を元に検索する「データ検索API」と経度・緯度から検索を行う「地物情報取得API 」です。

データ検索API

以下のURLにパラメータを指定してデータを検索します。
https://api.tokyometroapp.jp/api/v2/datapoints

駅名や、路線名、IDなどを指定して検索が可能です。
検索例:

https://api.tokyometroapp.jp/api/v2/datapoints?rdf:type=odpt:Station&odpt:railway=odpt.Railway:TokyoMetro.Marunouchi&acl:consumerKey=APIキー

この例では「路線が丸の内線であること」を検索条件にして駅情報を取得しています。

データポイントではIDを取得してデータを特定して取得することも可能であす。
このIDにはodptで始まるものを指定できます。

https://api.tokyometroapp.jp/api/v2/datapoints/odpt.Railway:TokyoMetro.Hanzomon?acl:consumerKey=APIキー

この例では半蔵門線の情報を取得しています。

地物情報取得API

以下のURLにパラメータを指定してデータを検索します。
https://api.tokyometroapp.jp/api/v2/places

経度と緯度を指定してデータを取得できます。

検索例:

https://api.tokyometroapp.jp/api/v2/places?rdf:type=odpt:Station&lat=35.6729562407498&lon=139.724074594678&radius=100&acl:consumerKey=APIキー

この例では経度、緯度と範囲を指定して、その範囲の駅を取得しています。

検索可能なrdf:type

現在は以下のようなrdf:typeを取得できます。

rdf:type datapoints places 説明
odpt:StationTimetable × 駅時刻情報を取得できます。各駅の平日、土曜、休日の列車着時刻、発車時刻と急行や各駅などの種別を表します
odpt:TrainInformation × 運行状況を取得できます。15分以上の遅延のさい更新されます。
レスポンスのdct:validをみてデータを更新すると良いでしょう。また、情報の変化の有無はodpt:timeOfOriginが前回値と変わったかどうかでチェックするといいようです。
ぶっちゃけ次のページと同様のデータです
http://www.tokyometro.jp/unkou/index.html
遅延情報の方とは一致しません
odpt:Train × 列車の在線位置を表します。どの駅にいるか、どの駅と駅の間にいるか、と5分以上の遅延を取得できます。列車のGPS情報はとれません。
レスポンスのdct:validをみてデータを更新すると良いでしょう。
また、非常時にこそ必要なデータですが、台風などの非常時には反映されないという事象も報告されています。
https://developer.tokyometroapp.jp/forum/forums/1/topics/--57
odpt:Station 駅の情報です。この情報はキャッシュしておき、ここに格納されているIDを使用して施設情報や乗降人数情報を取得するといいでしょう
odpt:StationFacility × バリアフリー対応の駅のトイレやエレベータ、乗り換え可能路線、改札外の最寄り施設が取得できます。営業時間とかはのっていることはありますが、施設の位置情報は取得できません
odpt:PassengerSurvey × 駅毎の乗降人員数を1年単位で取得できます
odpt:Railway 路線情報を取得します。運行系統名、駅間所要時間、駅間順序、路線の形状が取得できます。
odpt:RailwayFare × 2つ駅を指定して運賃を計算します。運賃にはICカードか切符、大人か子供かで変わります。
fromStation,toStationを指定しての検索は部分一致です。しかし、料金を指定しての検索は範囲ではなく完全一致です。
ug:Poi 地物情報。駅出入口情報を提供しています。
odpt:TrainTimetable × 特定の列車が其々の駅からいつ出発するかを格納している。
startStationは他社始発の時のみしか格納されない。

PHPでの実装例

以下でPHPでの実装例を示します。
slimを利用したシステムの一部なので、そのままコピーしても動きませんが、それなりに参考になると思います。

<?php
namespace MyLib;

/**
 * 東京メトロAPI実行用のクラス<br>
 * 各APIのレスポンスは以下の仕様に準拠している。<br>
 * @link https://developer.tokyometroapp.jp/documents/railway 駅情報ボキャブラリ仕様書
 * @link https://developer.tokyometroapp.jp/documents/facility 地物情報ボキャブラリ仕様書
 */
class TokyoMetroApi
{
    private $client;
    private $consumerKey;
    private $dataFolder;

    const END_POINT = 'https://api.tokyometroapp.jp/api/v2/';

    /** 結果コード:正常終了 */
    const RESULT_CODE_OK = 0;

    /** 結果コード:東京メトロAPIの異常 */
    const RESULT_CODE_ERR_API = 1;

    /** 東京メトロAPIの最大リトライ数 */
    const MAX_TRY_COUNT = 3;

    /** 東京メトロAPIのリトライ感覚 */
    const WAIT_COUNT = 100000; // 1ms

    /**
     * コンストラクタ
     * @param string $consumerKey 東京メトロAPIで登録したComsumerKey
     * @param string $dataFolder JSON情報の格納してあるフォルダ
     */
    public function __construct($consumerKey, $dataFolder)
    {
        $this->consumerKey = $consumerKey;
        $this->client = new \HTTP_Client();
        $this->dataFolder = $dataFolder;
    }

    public function getRailDirectionType() {
        return $this->readTypeJson($this->dataFolder . '/RailDirectionType.json');
    }
    public function getRailWayType() {
        return $this->readTypeJson($this->dataFolder . '/RailWayType.json');
    }
    public function getTrainOwnerType() {
        return $this->readTypeJson($this->dataFolder . '/TrainOwnerType.json');
    }
    public function getTrainType() {
        return $this->readTypeJson($this->dataFolder . '/TrainType.json');
    }

    private function readTypeJson($path) {
        $handle = fopen($path, 'r');
        $ret = fread($handle, filesize($path));
        fclose($handle);
        return (array)json_decode($ret);
    }

    /**
     * APIを実行してレスポンスの取得
     * もし、503エラーの場合は、WAIT_COUNTマイクロ秒後に
     * MAX_TRY_COUNT回までリトライする。
     * @param string $url 対象のURL
     * @param array $param パラメータの連想配列
     * @param int $trycount 現在の試行回数
     * @return array レスポンスの結果
     */
    private function getResponse($url, $param, $trycount)
    {
        $code = $this->client->get($url, $param);
        $res = $this->client->currentResponse();
        $body = null;
        if ($res) {
            if (isset($res['body'])) {
                $body = $res['body'];
            }
        }
        $ret = null;
        if ($code != 200) {
            // リトライ処理.
            // 100ms程度でブロックが掛かっているので,スリープして再実行.
            // https://developer.tokyometroapp.jp/forum/forums/1/topics/http-request-failed-http-1-1-503-service-unavailable
            if ($code == 503 and $trycount < TokyoMetroApi::MAX_TRY_COUNT) {
                usleep(($trycount + 1) * TokyoMetroApi::WAIT_COUNT);

                return $this->getResponse($url, $param, $trycount + 1);
            }

            $msg = sprintf('ResponceCode: %d Message:%s', $code, $body);
            $ret = array('resultCode' => TokyoMetroApi::RESULT_CODE_ERR_API,
                                     'errorMsg' => $msg,
                                     'contents' => null);
        } else {
            $ret = array('resultCode' => TokyoMetroApi::RESULT_CODE_OK,
                                     'errorMsg' => null,
                                     'contents' => json_decode($body));
        }

        return $ret;
    }

    /**
     * JSONを取得する。
     * @param string $url 対象のURL
     * @return array レスポンスの結果
     */
    public function getJson($url)
    {
        $param = array('acl:consumerKey' => $this->consumerKey);

        return $this->getResponse($url, $param, 0);
    }

    /**
     * データポイント用のAPIを実行する
     * @param array $param パラメータの連想配列
     * @return array レスポンスの結果
     */
    private function getDataPoints($param)
    {
        $url = TokyoMetroApi::END_POINT . 'datapoints';

        return $this->getResponse($url, $param, 0);
    }

    private function getPlaces($param)
    {
        $url = TokyoMetroApi::END_POINT . 'places';

        return $this->getResponse($url, $param, 0);
    }

    /**
     * 東京メトロ駅情報をすべて取得する
     * @return array レスポンスの結果
     */
    public function getStations()
    {
        $param = array('rdf:type' => 'odpt:Station',
                       'acl:consumerKey' => $this->consumerKey);

        return $this->getDataPoints($param);
    }

    /**
     * 東京メトロ駅情報を指定の条件で検索する.
     * <code>
     * $ctrl = new TokyoMetroApi(CONSUMER_KEY);
     * $param=array('odpt:railway' => 'odpt.Railway:TokyoMetro.Marunouchi');
     * $ret = $ctrl->findStation($param);
     * </code>
     * @param array $conditions 条件を指定した連想配列
     * @return array レスポンスの結果
     */
    public function findStation($conditions)
    {
        $param = array('rdf:type' => 'odpt:Station',
                       'acl:consumerKey' => $this->consumerKey);
        $param += $conditions;

        return $this->getDataPoints($param);
    }

    /**
     * 東京メトロ駅情報を位置情報の条件で検索する.
     * <code>
     * $ctrl = new TokyoMetroApi(CONSUMER_KEY);
     * $param=array('lat' => 35.6729562407498,
     *              'lon' =>139.724074594678,
     *              'radius' => 100);
     * $ret = $ctrl->findStation($param);
     * </code>
     * @param array $conditions 条件を指定した連想配列
     * @return array レスポンスの結果
     */
    public function findStationByPlace($conditions)
    {
        $param = array('rdf:type' => 'odpt:Station',
                       'acl:consumerKey' => $this->consumerKey);
        $param += $conditions;

        return $this->getPlaces($param);
    }
    /**
     * 東京メトロ路線情報を全て取得する.
     * @return array レスポンスの結果
     */
    public function getRailways()
    {
        $param = array('rdf:type' => 'odpt:Railway',
                       'acl:consumerKey' => $this->consumerKey);

        return $this->getDataPoints($param);
    }
    /**
     * 東京メトロ路線情報を指定の条件で検索する.
     * <code>
     * $ctrl = new TokyoMetroApi(CONSUMER_KEY);
     * $param=array('owl:sameAs' => 'odpt.Railway:TokyoMetro.Marunouchi');
     * $ret = $ctrl->findRailway($param);
     * </code>
     * @param array $conditions 条件を指定した連想配列
     * @return array レスポンスの結果
     */
    public function findRailway($conditions)
    {
        $param = array('rdf:type' => 'odpt:Railway',
                       'acl:consumerKey' => $this->consumerKey);
        $param += $conditions;

        return $this->getDataPoints($param);
    }
    public function findRailwayByPlace($conditions)
    {
        $param = array('rdf:type' => 'odpt:Railway',
                       'acl:consumerKey' => $this->consumerKey);
        $param += $conditions;

        return $this->getPlaces($param);
    }
    /**
     * 地物情報を使用して駅出入り口情報を取得する。
     * @return array レスポンスの結果
     */
    public function getPois()
    {
        $param = array('rdf:type' => 'ug:Poi',
                       'acl:consumerKey' => $this->consumerKey);

        return $this->getDataPoints($param);
    }

    /**
     * 地物情報を使用して駅出入り口情報を取得する。
     * <code>
     * $ctrl = new TokyoMetroApi(CONSUMER_KEY);
     * $ret = $ctrl->findPoi(array('@id'=>'urn:ucode:_00001C000000000000010000030C3EC1'));
     * if ($ret['resultCode'] != TokyoMetroApi::RESULT_CODE_OK) {
     *   sendJsonData($ret['resultCode'],  $ret['errorMsg'], null);
     *   exit();
     * }
     * $exit_info = $ret['contents'][0];
     * </code>
     * @param array $conditions 条件を指定した連想配列
     * @return array レスポンスの結果
     */
    public function findPoi($conditions)
    {
        $param = array('rdf:type' => 'ug:Poi',
                       'acl:consumerKey' => $this->consumerKey);
        $param += $conditions;

        return $this->getDataPoints($param);
    }
    public function findPoiByPlace($conditions)
    {
        $param = array('rdf:type' => 'ug:Poi',
                       'acl:consumerKey' => $this->consumerKey);
        $param += $conditions;

        return $this->getPlaces($param);
    }

    /**
     * 運賃を取得する
     * @return array レスポンスの結果
     */
    public function findFare($from, $to)
    {
        $param = array('rdf:type' => 'odpt:RailwayFare',
                                     'odpt:fromStation'=>$from,
                                     'odpt:toStation'=>$to,
                                     'acl:consumerKey' => $this->consumerKey);

        return $this->getDataPoints($param);
    }

    /**
     * 駅の施設に関する情報を取得する。
     * @return array レスポンスの結果
     */
    public function getStationFacilities() {
        $param = array('rdf:type' => 'odpt:StationFacility',
                       'acl:consumerKey' => $this->consumerKey);

        return $this->getDataPoints($param);
    }
    public function findStationFacility($conditions) {
        $param = array('rdf:type' => 'odpt:StationFacility',
                       'acl:consumerKey' => $this->consumerKey);
        $param += $conditions;
        return $this->getDataPoints($param);
    }

    /**
     *  列車運行情報 の取得
     * @return array レスポンスの結果
     */
    public function findTrainInformation($conditions) {
        $param = array('rdf:type' => 'odpt:TrainInformation',
                       'acl:consumerKey' => $this->consumerKey);
        $param += $conditions;
        return $this->getDataPoints($param);
    }
    /**
     *  列車ロケーション情報 の取得
     * @return array レスポンスの結果
     */
    public function findTrain($conditions) {
        $param = array('rdf:type' => 'odpt:Train',
                       'acl:consumerKey' => $this->consumerKey);
        $param += $conditions;
        return $this->getDataPoints($param);
    }

    /**
     * 駅乗降人員数を取得する。
     * @return array レスポンスの結果
     */
    public function getPassengerSurvey() {
        $param = array('rdf:type' => 'odpt:PassengerSurvey',
                       'acl:consumerKey' => $this->consumerKey);

        return $this->getDataPoints($param);
    }
}
{
  "odpt.RailDirection:TokyoMetro.Asakusa":"浅草 方面",
  "odpt.RailDirection:TokyoMetro.Ogikubo":"荻窪 方面",
  "odpt.RailDirection:TokyoMetro.Ikebukuro":"池袋 方面",
  "odpt.RailDirection:TokyoMetro.Honancho":"方南町 方面",
  "odpt.RailDirection:TokyoMetro.NakanoSakaue":"中野坂上 方面",
  "odpt.RailDirection:TokyoMetro.NakaMeguro":"中目黒 方面",
  "odpt.RailDirection:TokyoMetro.KitaSenju":"北千住 方面",
  "odpt.RailDirection:TokyoMetro.NishiFunabashi":"西船橋 方面",
  "odpt.RailDirection:TokyoMetro.Nakano":"中野 方面",
  "odpt.RailDirection:TokyoMetro.YoyogiUehara":"代々木上原 方面",
  "odpt.RailDirection:TokyoMetro.Ayase":"綾瀬 方面",
  "odpt.RailDirection:TokyoMetro.KitaAyase":"北綾瀬 方面",
  "odpt.RailDirection:TokyoMetro.ShinKiba":"新木場 方面",
  "odpt.RailDirection:TokyoMetro.Ikebukuro":"池袋 方面",
  "odpt.RailDirection:TokyoMetro.Oshiage":"押上 方面",
  "odpt.RailDirection:TokyoMetro.Shibuya":"渋谷 方面",
  "odpt.RailDirection:TokyoMetro.AkabaneIwabuchi":"赤羽岩淵 方面",
  "odpt.RailDirection:TokyoMetro.Meguro":"目黒 方面",
  "odpt.RailDirection:TokyoMetro.ShirokaneTakanawa":"白金高輪 方面",
  "odpt.RailDirection:TokyoMetro.Wakoshi":"和光市 方面",
  "odpt.RailDirection:TokyoMetro.KotakeMukaihara":"小竹向原 方面"
}
{
  "odpt.Railway:TokyoMetro.Ginza":"東京メトロ銀座線",
  "odpt.Railway:TokyoMetro.Marunouchi":"東京メトロ丸ノ内線",
  "odpt.Railway:TokyoMetro.Hibiya":"東京メトロ日比谷線",
  "odpt.Railway:TokyoMetro.Tozai":"東京メトロ東西線",
  "odpt.Railway:TokyoMetro.Chiyoda":"東京メトロ千代田線",
  "odpt.Railway:TokyoMetro.Yurakucho":"東京メトロ有楽町線",
  "odpt.Railway:TokyoMetro.Hanzomon":"東京メトロ半蔵門線",
  "odpt.Railway:TokyoMetro.Namboku":"東京メトロ南北線",
  "odpt.Railway:TokyoMetro.Fukutoshin":"東京メトロ副都心線"
}
{
  "odpt.TrainOwner:TokyoMetro":"東京メトロ",
  "odpt.TrainOwner:Seibu":"西武鉄道",
  "odpt.TrainOwner:SaitamaRailway":"埼玉高速鉄道",
  "odpt.TrainOwner:Tobu":"東武鉄道",
  "odpt.TrainOwner:ToyoRapidRailway":"東葉高速鉄道",
  "odpt.TrainOwner:Toei":"都営地下鉄",
  "odpt.TrainOwner:Tokyu":"東急電鉄",
  "odpt.TrainOwner:JR-East":"JR東日本",
  "odpt.TrainOwner:Odakyu":"小田急電鉄"
}
{
  "odpt.TrainType:TokyoMetro.Unknown":"不明",
  "odpt.TrainType:TokyoMetro.Local":"各停",
  "odpt.TrainType:TokyoMetro.Express":"急行",
  "odpt.TrainType:TokyoMetro.Rapid":"快速",
  "odpt.TrainType:TokyoMetro.SemiExpress":"準急",
  "odpt.TrainType:TokyoMetro.TamaExpress":"多摩急行",
  "odpt.TrainType:TokyoMetro.HolidayExpress":"土休急行",
  "odpt.TrainType:TokyoMetro.CommuterSemiExpress":"通勤準急",
  "odpt.TrainType:TokyoMetro.Extra":"臨時",
  "odpt.TrainType:TokyoMetro.RomanceCar":"特急ロマンスカー",
  "odpt.TrainType:TokyoMetro.RapidExpress":"快速急行",
  "odpt.TrainType:TokyoMetro.CommuterExpress":"通勤急行",
  "odpt.TrainType:TokyoMetro.LimitedExpress":"特急",
  "odpt.TrainType:TokyoMetro.CommuterLimitedExpress":"通勤特急",
  "odpt.TrainType:TokyoMetro.CommuterRapid":"通勤快速",
  "odpt.TrainType:TokyoMetro.ToyoRapid":"東葉快速"
}

単純な使用例

<?php
require dirname(__FILE__) . '/../vendor/autoload.php';
const TOKYO_METRO_CONSUMER_KEY = 'APIキー';
const TOKYO_METRO_DATA_DIR = '../tokyometro_data';

$tmCtrl = new \MyLib\TokyoMetroApi(TOKYO_METRO_CONSUMER_KEY, TOKYO_METRO_DATA_DIR);

var_dump($tmCtrl->getPassengerSurvey());

var_dump($tmCtrl->getRailWayType());

// JSONファイルの内容をゲット
var_dump($tmCtrl->getRailDirectionType());
var_dump($tmCtrl->getTrainOwnerType());
var_dump($tmCtrl->getTrainType());

// 駅を調べる
var_dump($tmCtrl->getStations());
var_dump($tmCtrl->findStation(array('owl:sameAs' => 'odpt.Station:TokyoMetro.Tozai.Nakano')));

// 路線を調べる
var_dump($tmCtrl->getRailways());
var_dump($tmCtrl->findRailway(array('owl:sameAs' => 'odpt.Railway:TokyoMetro.Marunouchi')));

// 運賃を調べる
var_dump($tmCtrl->findFare('odpt.Station:TokyoMetro.Ginza.AoyamaItchome','odpt.Station:TokyoMetro.Chiyoda.Akasaka'));

// 出口情報
var_dump($tmCtrl->getPois());
var_dump($tmCtrl->findPoi(array('@id'=>'urn:ucode:_00001C000000000000010000030C3EC1')));

// 施設情報
var_dump($tmCtrl->getStationFacilities());
var_dump($tmCtrl->findStationFacility(array('@id'=>'urn:ucode:_00001C000000000000010000030C47F5')));

//列車のロケーション情報
var_dump($tmCtrl->findTrain(array()));

//駅を場所情報探す
var_dump($tmCtrl->findStationByPlace(array('lat' => 35.6729562407498,
                                           'lon' =>139.724074594678,
                                           'radius' => 100)));
//路線を場所情報で探す
var_dump($tmCtrl->findRailwayByPlace(array('lat' => 35.6729562407498,
                                           'lon' =>139.724074594678,
                                           'radius' => 100)));
//出入り口を場所情報で探す
var_dump($tmCtrl->findPoiByPlace(array('lat' => 35.6729562407498,
                                       'lon' =>139.724074594678,
                                       'radius' => 100)));

// 運行情報取得
var_dump($tmCtrl->findTrainInformation(array()));
$trans->updateCacheDb();

実際のAPIの利用例

以上のコードを利用すると次のようなアプリケーションが作成可能だと思います。
実際の作成においては、実行結果をDBに格納したりしています。

駅出入口の確認

ここでは東京メトロが提供する駅の出入り口の付近をストリートマップで確認できます。
http://needtec.sakura.ne.jp/mtm/page/station_info?station=odpt.Station:TokyoMetro.Marunouchi.NakanoSakaue

Googleと東京メトロAPIで位置情報が異なるので正確な情報が欲しい時は使うのが厳しいでしょう

列車運行状況の確認

ここでは列車運行状況を確認できます。
現在は、15分以上の遅延が発生していなのでログが1つしかありませんが、列車が遅れれば、履歴として確認できるはずです。
基本的に東京メトロにある運行状況と同じなります。

http://needtec.sakura.ne.jp/mtm/page/train_info
http://needtec.sakura.ne.jp/mtm/page/train_info?lang=en

ロケーション情報を路線に表示する

リアルタイムなロケーション情報を路線に描画します。

http://needtec.sakura.ne.jp/mtm/page/railway_info

応用として、過去のロケーション情報のプロットも行えます。

http://needtec.sakura.ne.jp/mtm/page/subway_map

※過去情報のプロットはメモリを食うので、現在はスマホだと落ちます。

運賃計算

このアプリケーションでは2つの駅を指定して運賃を取得します。
東京メトロAPIでなく駅すぱーとのAPI使ったほうが他の路線も計算できていいです。

http://needtec.sakura.ne.jp/mtm/page/calculate_fare
http://needtec.sakura.ne.jp/mtm/page/calculate_fare?lang=en

アプリケーション開発時の注意

ここではアプリケーションを開発する際の注意について記述します。

サンプルが難しそうだが、実際はチョロイ

公式の実装サンプルがrubyで、やってない人は、二の足を踏む方もいるかもしれませんが、実際はAPIキー付けてURL叩いてJSON解析するだけなので簡単です。
とりあえず、APIキーをつけてURLをたたけばなんとかなります。

サンプルコードは飾りです。偉い人にはわからんのです。

アクセストークンの管理

アクセストークンは外部にもれないようにする必要があります。
規約がかわってサーバーサイドを作成しないで、クライアントだけでの応募も可能になったようですが、そのアクセストークンの取り扱いには注意しなければなりません。
たとえば逆コンパイルが容易な、.NETのクライアントアプリを作った場合、そのアクセストークンをどう隠蔽するか、良いアイディアは思いつきませんでした。

開発者フォーラムの監視が必須

怪しげな動作をしたり、API仕様書に一切書いてない仕様をサラっと述べたりするので開発者フォーラムの利用が必須になります。
https://developer.tokyometroapp.jp/forum/forums/1

この開発者フォーラムはMarkup言語が利用できるらしいので、Qiita程度の修飾はできるようです。
しかしながら、検索ができないという致命的弱点があります。

もちろん別途、バグ管理システムを立ち上げているわけではないので、トラブルが発生したら開発者は目視で開発者フォーラムを目を通し、関係のあるトピックが更新されたかどうかは手動でチェックするという苦行が必須になります。

なお、自分に返信があったときはメールで通知がくるようです。

いいぜ、てめえが検索させないっていうのなら、まずはそのふざけた幻想 をぶち殺す。

Webスクレイピングによって検索ができるようにしました。
ないよりマシです。

東京メトロオープンデータサイト開発者サイト 操作ツール
https://github.com/mima3/tokyometro_dev

怒られたら削除します。

レスポンスの文字コードはutf8(BOMなし)

Content-Typeに文字コードの情報が入っていないので文字化けする環境があります。
utf8(bomなし)が帰ってくることを前提に処理しましょう。

「503:Your request rate is too high」が応答される

短い期間でAPIを叩くと、この応答になります。
同一IPアドレス+APIキーという条件にて100msのガードタイムがかかっています。間をあけてリトライしましょう。
https://developer.tokyometroapp.jp/forum/forums/1/topics/http-request-failed-http-1-1-503-service-unavailable

なお、この記事で示したPHPのコードは一定の感覚をおいて、数回リトライする作りになっています。
この実装で、運賃情報のAPIを3万回ほど実行してエラーにはならなかったので、上記コードを参考にするといいかもしれません。

追記

https://developer.tokyometroapp.jp/forum/forums/1/topics/v2a-v2

・v2とv2aでは独立してガードタイムを判定していない。v2,v2aでも同じタイミングでアクセスして条件を満たすとガードタイムが発動する。

・ガードタイムは以下のすべてが一致した時に発動する。
(1)IPが同じ
(2)APIキーが同じ
(3)rdf:typeが同じ

どれか一つでも異なれば、ガードタイムは発動しない。つまり、複数端末から同じAPIキーでアクセスすることで処理の高速化がry

データの更新タイミング

リアルタイムで更新される運行情報、列車ロケーションのレスポンスにはdct:validが付与されており、これをもとにデータの再取得をするといいでしょう。
その他の情報については更新する可能性はありますが、機械的にそれを検知する方法はありません。開発者がニュース等を目視で監視して更新しろと公式では述べています。

https://developer.tokyometroapp.jp/forum/forums/1/topics/--43

駅情報とかをキャッシュしている場合、任意のタイミングで、それを更新するしくみを考えたほうがいいでしょう。

遅延の概念は2つある。

運行情報で取得できる情報は、東京メトロホームページの遅延情報と同じ物で、15分以上の遅延が出た場合にご案内が提示されます。
http://www.tokyometro.jp/unkou/index.html

一方、列車ロケーション情報は15分に達していない遅延に関しても取得できます。
公式ページにある遅延証明書はおそらく、この情報を使用しています。

https://developer.tokyometroapp.jp/forum/forums/1/topics/delay

位置情報はGoogleMapとずれている。

以下の検証用アプリはug:Poiで取得した情報を利用して出入り口をマークしていますが、GoogleMapと結構ずれています。
http://needtec.sakura.ne.jp/mtm/station_map

おそらく、シビアな位置情報の一致が必要なアプリケーションの作成にはリスクがあるでしょう。

国土地理院の地図で確認すると、それっぽいところに表示されているので、なんらかの差異があるようです。
http://vldb.gsi.go.jp/sokuchi/surveycalc/tky2jgd/main.html

ちなみに世界測地系と日本測地系の違いかとも思いましたが、変換すると余計ずれるので、そうではなさげです。

検索の方法が部分一致のものがある。

以下の例はowl:sameAsを検索条件に路線情報を取得するものです。

https://api.tokyometroapp.jp/api/v2/datapoints?rdf:type=odpt:Railway&owl:sameAs=odpt.Railway:TokyoMetro.Marunouchi&acl:consumerKey=XXXX

普通、この例では「odpt.Railway:TokyoMetro.Marunouchi」の結果のみが取得されると思いますが、実際は「odpt.Railway:TokyoMetro.Marunouchi」と「odpt.Railway:TokyoMetro.MarunouchiBranch」が取得されます。

これは、検索を部分一致でしている場合があるからです。

運賃を取得するときの、odpt:fromStation、odpt:toStationは部分一致なので、レスポンスデータの取り扱いを間違うと予期せぬ料金になります

列車ロケーション情報で特定タイミングの特定路線のデータが取得できない疑義がある。

https://developer.tokyometroapp.jp/forum/forums/1/topics/startingstation-terminalstation-fromstation-tostation

検証した結果、5:00-6:00 , 7:00-10:00の間で方南町を含むstartingStation、terminalStation、fromStation、toStationのデータがAPI側から配信されていない。すくなくとも、startingStation、terminalStationについては配信していない疑義は濃厚です。

当然、リアルタイムのデータ扱うのはバグがでやすいので、他にもある可能性はあります。

追記 11/09:

この事象自体はなおったようですが、列車ロケーション情報の挙動自体は、くっそ怪しいです。

一応,11/2時点で関連バグをまとめましたが、運営様におきましては、イシュー管理には、一切の興味がないようですので、まぁ、しかたないね。

https://developer.tokyometroapp.jp/forum/forums/1/topics/11-2

つまり、列車ロケーション情報を使用した場合、元データを検証用にしばらく保存しておく仕組みにしたほうがいいです。

おまえはいつから、全てのトイレの情報を教えてもらえるとおもっていた?

施設情報で取得できるトイレの情報は、レスポンスデータにバリアフリーとあるように車椅子のトイレだけです。

https://developer.tokyometroapp.jp/forum/forums/1/topics/--80

車椅子乗っていない人は漏らすか、自分で頑張って探してください。

odpt:TrainTimetableの時刻には出発時刻と到達時刻がある。

odpt:TrainTimetableの時刻には出発時刻と到達時刻がある

終着駅は到着時刻を表し、arrivalTime,arivalStationとなっており、それ以外は出発時刻を表しdepartureTime、departureStationとなる。

2014/10/27現在、以下のようなデータが取得できる。

[{
  "@context":"https://vocab.tokyometroapp.jp/context_odpt_TrainTimetable.jsonld",
  "@id":"urn:ucode:_00001C000000000000010000030D0AA5",
  "@type":"odpt:TrainTimetable",
  "owl:sameAs":"odpt.TrainTimetable:TokyoMetro.Marunouchi.A11321.Weekdays",
  "odpt:trainNumber":"A11321",
  "odpt:railway":"odpt.Railway:TokyoMetro.Marunouchi",
  "odpt:operator":"odpt.Operator:TokyoMetro",
  "dc:date":"2014-10-24T21:56:21+09:00",
  "odpt:trainType":"odpt.TrainType:TokyoMetro.Local",
  "odpt:railDirection":"odpt.RailDirection:TokyoMetro.Honancho",
  "odpt:weekdays":[
    {
      "odpt:departureTime":"11:04",
      "odpt:departureStation":"odpt.Station:TokyoMetro.MarunouchiBranch.NakanoSakaue"
    },{
      "odpt:departureTime":"11:07",
      "odpt:departureStation":"odpt.Station:TokyoMetro.MarunouchiBranch.NakanoShimbashi"
    },{
      "odpt:departureTime":"11:08",
      "odpt:departureStation":"odpt.Station:TokyoMetro.MarunouchiBranch.NakanoFujimicho"
    },{
      "odpt:arrivalTime":"11:11",
      "odpt:arrivalStation":"odpt.Station:TokyoMetro.MarunouchiBranch.Honancho"
    }
  ],
  "odpt:train":"odpt.Train:TokyoMetro.Marunouchi.A11321",
  "odpt:terminalStation":"odpt.Station:TokyoMetro.MarunouchiBranch.Honancho",
  "odpt:trainOwner":"odpt.TrainOwner:TokyoMetro"
}]

施設情報のspac:hasAssistantの説明。

トイレ内のバリアフリー施設をあらわすspac:hasAssistantには次の値が入る可能性があります。

ug:BabyChangingTable : おむつ交換台

spac:WheelchairAssesible : 車いす対応

ug:BabyChair : ベビーチェア 用を足している時に赤ん坊を座らせておく椅子

ug:ToiletForOstomate : オストメイト 人工肛門・人工膀胱を造設した人が使えるトイレ

列車運行情報のデータ更新の検知方法

列車運行情報はodpt:timeOfOriginというデータ発生時刻を持っています。
通常は、これが変化したらデータが更新されたと見なしていいかもしれませんが、特定の路線で35日以上、運行情報が更新されず履歴が亡くなった場合に、データを取得すると取得のたびodpt:timeOfOriginは更新されます。

これは下記の不具合に対応した際、過去履歴情報が空の場合情報取得時点でのタイムスタンプを挿入しているためです。

https://developer.tokyometroapp.jp/forum/forums/1/topics/api--8

変化を検知するにはodpt:trainInformationTextが前回値と変わったかどうかをチェックした方が良いでしょう。

なお、odpt:trainInformationStatusはだめです。odpt:trainInformationStatusが同じで、odpt:trainInformationTextが変わる場合があります。

多言語対応

公式にはやってません。
自分でがんばりましょう。

コストを掛けずにやるには、200万文字までは無料で使用できるMsTranslatorが良いと思います。
なお、方南町を「How to Minami-Cho」とか訳すので、金があるならgoogle翻訳、使うなりしましょう。
ちなみに駅情報、路線情報、施設情報の結果を、必要そうな箇所を翻訳しただけで、5万文字程度でしたので、英語以外も対応できるでしょう。
ただし、機械翻訳は遅いので、大量のデータを一気に翻訳しないように工夫が必要です。

不安定さ

絶賛開発中という感じがするので、現時点でこのAPIを前提にした、止まると洒落に成らないシステムを構築するのはやめておいたほうがいいでしょう。
(そもそも免責事項にはいっていた気もします)

また、デグレを起こしたり、修正により、別のAPIのデータが不正になるなど、おそらく、開発プロセスに回帰テストは含まれていません。
APIがなにか回収した場合は、基本、アプリ側で回帰テストをする必要があるでしょう。

予告なしで挙動が変わる前提でデバッグすること

わりと予告なしにリリースして、挙動を変えた数時間後にニュースに表示されることがあります。

そして、その数時間の間は、挙動の変わったアプリケーションのデバッグを実に無駄に行うはめになることがあります。

開発者は開発フォーラムやニュースに一切、書き込みがなくても、APIそのものの挙動が突然変わる可能性も考慮しつつ、デバッグする必要があります。

応募時の注意

コンテストに応募をしないアプリケーションの存在を許さないオープンデータの定義に挑戦するオープンデータなので、11/17までに嫌でも応募しましょう。

その際、5分を上限としたYoutubeの動画を作成が 必須 になります。

また、スマホの人はGoogle Play、App Store、Windowsストアの登録が必須なので、ギリギリはまずいです。

2015年4月以降もAPIの存続がきまり、4月以降に新規APIキーの発行が可能になると発表がありました。当然、アプリケーションコンテストに参加しないアプリの存在もみとめてもらえます。

odpt:Stationで取得するodpt:connectiongRailwayと公式ページの「駅の情報・路線図」に食い違いがある

公式ページの「駅の情報・路線図」とodpt:Stationで取得するodpt:connectiongRailwayにいくつか食い違いがあります。

この詳細は下記の通りです。

http://needtec.sakura.ne.jp/doc/connectingRailwayBug_20141113.xlsx

わたしはこのバグを18日まで報告する気はないので、もし致命的だと考えるひとがいたら、ご自由にお使いください。

各停運転区間の停車駅では、駅時刻表の列車タイプは必ず各駅停車になる。それが列車としては急行であっても!

列車時刻表の列車タイプ(各駅停車や急行)は直観通りの意味です。

ただし、駅時刻表の列車タイプには注意が必要です。

急行や各駅停車がまざる区間の駅時刻表の列車タイプは急行や各停が入ります

しかし、各停運転区間の駅時刻表の列車タイプは必ず各駅停車になります。

https://developer.tokyometroapp.jp/forum/forums/1/topics/odpt-traintype-tokyometro-rapid

フォーラムより抜粋した例

例)
列番:B695SR  種別:通勤快速
西船橋 定刻 6時33分発 駅時刻表:odpt.TrainType:TokyoMetro.CommuterRapid
浦安  定刻 6時40分発 駅時刻表:odpt.TrainType:TokyoMetro.Local
飯田橋 定刻 7時 7分発 駅時刻表:odpt.TrainType:TokyoMetro.Local

列番:B964TR 種別:快速
浦安  定刻 9時49分発 駅時刻表:odpt.TrainType:TokyoMetro.Rapid
飯田橋 定刻10時11分発 駅時刻表:odpt.TrainType:TokyoMetro.Local

この仕様は、公式の時刻表と合わせるための、仕様です。

http://www.tokyometro.jp/station/iidabashi/timetable/tozai/b/index.html

いいぜ、運営、お前が、締切3日前にAPIをリリースするっていうなら、まずは、そのふざけた機能をテストするアプリを応募してやる。

「締切3日前にリリースされたAPIの挙動を確認するアプリ」

http://needtec.sakura.ne.jp/mtm_alpha/?lang=ja

中野坂上駅のIDがカオス。

中野坂上は、2種類のsameAsがあります。「odpt.Station:TokyoMetro.MarunouchiBranch.NakanoSakaue」と「odpt.Station:TokyoMetro.Marunouchi.NakanoSakaue」です。
そして、これらの使い分けは統一しようと頑張ったけどダメでしたという感じです。

丸の内分岐線を走る列車時刻表ではodpt.Station:TokyoMetro.MarunouchiBranch.NakanoSakaueを使用しています。
しかしながら、列車ロケーション情報ではodpt.Station:TokyoMetro.Marunouchi.NakanoSakaueになっています。
https://developer.tokyometroapp.jp/errata

そして、駅時刻表の到着駅にかんしても、odpt.Station:TokyoMetro.Marunouchi.NakanoSakaueになっています。

ヒャッハー、中野坂上は本当に地獄だぜ!

副都心線と有楽町線の和光市へ向かう一部区間の駅時刻表の行先がごっちゃになっている。

和光市へむかう駅時刻表の行先の駅のsameAsはodpt.Station:TokyoMetro.Yurakucho.Wakoshiかodpt.Station:TokyoMetro.Fukutoshin.Wakoshiの二つあります。

んで例によって駅時刻表の終着駅と列車時刻表の終着駅で、ごちゃごちゃにつかっているので正常にリンクできません。

どうも、駅時刻表は何も考えずに、odpt.Station:TokyoMetro.Yurakucho.Wakoshiをつかっているようです。

和光市~池袋を仲良くはしる有楽町線と副都心線・・・おそろしい子・・・

列車番号とはいったいなんなのだ

列車番号は物理的な車両を表すものではないと推測できます。

以下の例を見てください。

https://api.tokyometroapp.jp/api/v2/datapoints?rdf:type=odpt:StationTimetable&odpt:station=odpt.Station:TokyoMetro.MarunouchiBranch.NakanoFujimicho&acl:consumerKey=(APIキー

中野富士見町から荻窪行の列車が出ているのが確認できます。

これは時刻表的に正しいです。
http://www.tokyometro.jp/station/nakano-fujimicho/timetable/marunouchi/zb/

列車時刻表から逆引きすると、この列車の列車番号B05371です。B05371は中野坂上で列車番号A0537となり、荻窪へ向かいます。

つまり、同じ列車ですが、列車番号は異なるのです。

列車番号とはいったいなんなのか・・・いまのぼくには理解できない。

そして駅時刻表と列車時刻表の行先が一致しているか確認したのが下記のとおり。
http://needtec.sakura.ne.jp/doc/TokyoMetroTimeTableBug.xlsx

・有楽町線、副都心線の共通線路で,Fukutoshin.Wakoshiが終着駅の列車に対して駅時刻表の行先が.Yurakucho.Wakoshiとなる
・中野坂上のIDがBranchつけるか否かが統一されていない。
・中野富士見町発、列車番号B05371は中野坂上で列車番号A0537となり、荻窪へ向かう(これは仕様だとおもわれる)

なぜか夜中にヒンディー語の入ったファイルを修正しないといけない場合の対応策

人間長い間生きていると、一度くらい夜中にヒンディー語の入ったファイルを修正する必要がでてきます。

手持ち(Windows)のサクラエディタや、vimでは残念ながらインドの偉大な英知を表示してくれはしませんでした。

ここでは、夜中にヒンディー語を修正するはめになり、かつ、グーグル先生に聞いてクールな多言語対応のエディタを探してインストールする余裕もないときに、どう対応すべきかを記述します。

ここでは、ブラウザー経由でヒンディー語の入ったファイルを修正してファイルに保存するようにします。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<script src="./js/jquery-1.11.1.min.js"  type="text/javascript"></script>

<script type="text/javascript">
$(function() {
  $(document).ready(function(){
  });
}); 
function loadFile() {
  var fs = document.getElementById('inputFile').files;
  var f = fs[0];
  var reader = new FileReader();
  reader.onload = function(event) {
    $('#contents').val(event.target.result);
  };
  reader.onerror = function (event){
    // 読み込み失敗時の処理を指定
    alert('load error.');
  }; 
  console.log(reader.readAsText(f));
}

function saveFile() {
  var txt = $('#contents').val();
  window.location.href = 'data:application/octet-stream,' +
                         encodeURIComponent(txt);
}

</script>
</head>
<body>
<textarea id="contents" rows="4" cols="40"></textarea><br>
<input type="file" id="inputFile"  onchange="loadFile()" ><br>
<input type="button" value="save" onClick="saveFile()"/><br/>
</body>
</html>

このように、FileReaderを使ってファイルをブラウザのTEXTAREAに表示します。
ブラウザは流石、たいていの文字は表示・編集できるので、そこで、任意の編集を加えます。
最後は、data:application/octet-streamを使用して、TEXTAREAの中身をダウンロードします。
残念ながら、ファイル名の変更はできませんので、ダウンロード後、自分でリネームして上書きしてください。

http://needtec.sakura.ne.jp/mtm/multiLangEditor.html

ヒンディー語.png

やったね、たえちゃん、ヒンディー語が保存できたよ!

たぶん、ロシア語だろうが、アラビア語だろうが、ブラウザが表示できるなら編集できると思います。




追記:
なお、MacOSのデフォルトのテキストエディタや、miだとヒンディー語表示できました。

Windowsの場合は、改行コードがCRLFになってしまうのと、UTF8のBOMがついてしまうのを気にしなければメモ帳でヒンディー語をひらいて編集できます。

もし、それが嫌な場合はVisualStudioでファイルを開くと、BOMなし、改行コードLFでヒンディー語を編集できます。

せっかくだから俺は衆院選のニコ生の党首討論のコメントを解析するぜ

背景

ネット選挙が解禁され、インターネットを活用した選挙情報の提供が盛んになると思いきや、ダメな方向の活用が目立つ、昨今皆様、いかがお過ごしでしょうか。

2014年11月29日、衆院選を控えてニコ生で党首討論がおこなわれました。安住先生がおしゃる「偏った動画サイト」に登場して討論を繰り広げた党首の皆様に敬意を表すると共に、実際のところ偏っているのかどうか調べてみました。

ソースコード
https://github.com/mima3/analyze_election

解析結果

【衆院選2014】ネット党首討論

http://needtec.sakura.ne.jp/analyze_election/page/nicolive/lv200730443

結論からいうと、新聞の世論調査では影の薄い「次世代」が注目されていたり、ひとりで数百のコメントをする猛者がおったり、社民党の吉田党首のほうが野党一党の海江田党首より多く抽出されたり、正直、バグでもあるような結果になりました。

党首討論会 in 日本記者クラブ(時事通信チャンネル)

http://needtec.sakura.ne.jp/analyze_election/page/nicolive/lv201303080

さすがに、コメント数は土曜の夜より少なめ。だけど、一人で200程度はコメントする方はいる。仕事or学校いry。

やはり、「安倍」とか「次世代」という単語がおおく抽出される。
ただ、ニコ生主催のときより「海江田」とか「民主」の存在感がある。

あと、なんで、党首討論に椿事件の「椿」とか「朝日」とかいう単語が大量にでてくんですかー(棒)

Pythonでニコニコ生放送のコメントを取得する方法

ニコニコ生放送のコメントを取得するには、まず、プレミアム会員でなければなりません。
通常の会員では、1000件程度が上限となります。

その上で、以下の手順で取得できます。

  1. メールアドレスとパスワードを用いて、ログインページにてログインして、user_sessionを取得する
  2. http://watch.live.nicovideo.jp/api/getplayerstatus にアクセスしてメッセージサーバのIPとポート、そしてユーザIDを取得する。
  3. http://watch.live.nicovideo.jp/api/getwaybackkey にアクセスしてwaybackkeyを取得する。このキーとユーザIDを組み合わせて過去のコメントが取得できます。
  4. メッセージサーバに接続してコメントを取得します。この際、HTTP通信かSOCKET通信のどちらかで接続できます。HTTP通信の際はポート番号から2725を引いたポートに接続する必要があります。
  5. 一回のGETで1000件しか取得できないので、whenを適切に設定しつつ全てのコメントを取得します。

具体的なコードは次のようになります。

niconico.py

# coding: utf-8
import sys
import cookielib
import cgi
import urllib
import urllib2
from lxml import etree
import socket
import datetime
import time
import json

class NicoCtrl():
    def __init__(self, nicovideo_id, nicovideo_pw):
        self.nicovideo_id = nicovideo_id
        self.nicovideo_pw = nicovideo_pw
        # ログイン
        cj = cookielib.CookieJar()
        self.opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj))
        req = urllib2.Request("https://secure.nicovideo.jp/secure/login")
        req.add_data( urllib.urlencode( {"mail": self.nicovideo_id, "password":self.nicovideo_pw} ))
        res = self.opener.open(req).read()
        if not 'user_session' in cj._cookies['.nicovideo.jp']['/']:
            raise Exception('PermissionError')

    def _getjson(self, url, errorcnt):
        # 途中でJSONが切れて帰ってくる場合があるので、リトライ処理
        try:
            res = self.opener.open(url, timeout=100).read()
            return json.loads(res)
        except ValueError:
            if errorcnt < 3:
                errorcnt = errorcnt + 1
                return self._getjson(url, errorcnt)
            else:
               raise

    def get_live_comment(self, movie_id):
        self.movie_id = movie_id

        # 動画配信場所取得(getflv)
        res = self.opener.open("http://watch.live.nicovideo.jp/api/getplayerstatus?v="+self.movie_id).read()
        root = etree.fromstring(res)
        messageServers = root.xpath('//ms')
        if len(messageServers) == 0:
            raise Exception('UnexpectedXML')

        user_ids = root.xpath('//user_id')
        if len(user_ids) == 0:
            raise Exception('NotfoundUserId')
        user_id = user_ids[0].text

        thread_id = messageServers[0].find('thread').text
        addr = messageServers[0].find('addr').text
        port = int(messageServers[0].find('port').text) - 2725

        # waybackkey の取得
        waybackkeyUrl = ('http://watch.live.nicovideo.jp/api/getwaybackkey?thread=%s' % thread_id)
        req = urllib2.Request(waybackkeyUrl)
        res = self.opener.open(waybackkeyUrl).read()
        waybackkey = cgi.parse_qs(res)['waybackkey'][0]

        msUrl = 'http://%s:%d/api.json/thread?' % (addr, port)
        chats = []
        req = urllib2.Request(msUrl)
        when = '4294967295'
        while True:
            data = {
                'thread' : thread_id, 
                'version' : "20061206",
                'res_from' : '-1000',
                'waybackkey' : waybackkey,
                'user_id' : user_id,
                'when': when,
                'scores' : '1'
            }
            list = self._getjson(msUrl+urllib.urlencode(data), 0)
            chatcnt = 0
            insertdata = []
            for l in list:
                if 'chat' in l:
                    if chatcnt == 0:
                        when = int(l['chat']['date']) - 1
                    if l['chat']['content'] != '/disconnect':
                        insertdata.append(l['chat'])
                    chatcnt += 1
            chats = insertdata + chats
            if chatcnt == 0:
                break
        return chats

nicolive.py

# coding: utf-8
import sys
from niconico_ctrl import NicoCtrl
import json

def main(argvs, argc):
    if len(argvs) != 4:
        print ('python nicolive.py email pass lv142315925')
        return 1
    nicovideo_id = argvs[1]
    nicovideo_pw = argvs[2]
    move_id = argvs[3]

    t = NicoCtrl(nicovideo_id, nicovideo_pw)
    chats = t.get_live_comment(move_id)
    f = open(move_id + '.json', 'w')
    f.write(json.dumps(chats))
    f.close()
    return 0

if __name__ == '__main__':
    argvs = sys.argv
    argc = len(argvs)
    sys.exit(main(argvs, argc))

このスクリプトは以下のように実行することで、コメント情報が格納されたJSONファイルを生成します。

 python nicolive.py   メールアドレス パスワード lv200730443

あとは作成されたJSONファイルのコメントを形態素解析して単語を抽出したり、ユーザ毎に集計したりなんやかんやを行えば、ニコ生のコメントが解析できるよ、やったね、たえちゃん。

参考

niconicoのメッセージ(コメント)サーバーのタグや送り方の説明
http://blog.goo.ne.jp/hocomodashi/e/3ef374ad09e79ed5c50f3584b3712d61

ニコニコ動画のコメントを取得する
http://d.hatena.ne.jp/MOOOVe/20120229/1330512626

なるべく楽にWebアプリケーションの多言語対応する方法

このたび、東京メトロのオープンデータと福岡市のオープンデータを無理やり多言語対応しました。

地下鉄多言語化MOD
https://developer.tokyometroapp.jp/app/15

福岡市オープンデータ多言語化MOD
http://needtec.sakura.ne.jp/fukuoka_map

この記事では、なるべく、楽にWebアプリケーションを多言語対応する方法について考察したいと思います。

機械翻訳の利用

MicrosoftTranslatorを用いることで、月あたり200万文字の機械翻訳がおこなえます。

PHPでMicrosoft Translate APIの翻訳機能を使ってみる
http://www.arubeh.com/archives/783

Translator Language Codes
対応言語のコード一覧
http://msdn.microsoft.com/en-us/library/hh456380.aspx

bing翻訳
MsTranslatorを使用している翻訳サイト
http://www.bing.com/translator/

基本、これを使用することになるのですが2つ問題があります。

1つは、速度です。APIのレスポンスは相当に遅いです。
2つ目は機械翻訳の精度です。

機械翻訳をそのままリリースして、東北観光博は新聞沙汰になったことは記憶に新しいことだと思います。

東北観光博機械翻訳騒動~翻訳者の視点
http://togetter.com/li/288496

これに対応するためにアプリケーションが直接、MicrosoftTranslatorで翻訳を行うのでなく、データベースを経由するようにします。

データベースを利用した機械翻訳の利用

翻訳の仕組み.png

翻訳結果をデータベースに保存することで外部APIへのアクセスを減らしパフォーマンスを向上させるのと同時に、人による翻訳の修正もしくは、あらかじめ翻訳結果を登録しておくことができます。

結局、人手を介すなら機械翻訳使わなくてもいいじゃないかという意見もあるでしょうが、翻訳情報が存在しない場合に機械翻訳を利用するにはいくつかの利点があります。

(1)人手による翻訳ができない場合に最低限の翻訳情報の提供。これは、資金がたりなくて翻訳できない場合もそうですが、時間的余裕がない場合にも有効です。たとえば、先の東京メトロオープンデータの場合、駅の施設情報は人による翻訳情報を提供するチャンスはありますが、運行情報などは、時間的に翻訳する余裕はありません。

(2)開発初期における最低限の翻訳の提供。開発初期において、不正確でいいから翻訳情報が欲しい場合があります。たとえばオランダ語などは、かなり長い文章になったり、アラビア語は右から読んだりしますが、そういう想定が開発中につくのは非常に大きいアドバンテージです。
よく、英語だけで多言語対応を開発していて、開発の末期で別の言語固有のバグが出てくるというのを防ぐ効果があると思います。

具体的なPHPの実装

今回作ったコードを以下に公開しています。
https://github.com/mima3/MultilanguageTokyoMetro

※ただし、東京メトロAPIのアクセストークンの配布が停止されたので動作しません。

翻訳に関係する主なコードは下記のとおりです。

MultilanguageTokyoMetro/src/Model/MsTranslatorCacheModel.php
このコードは翻訳情報をキャッシュするためのモデルです

<?php
namespace Model;

/**
 * Microsoft Translatorの結果をキャッシュするモデル<br>
 */
class MsTranslatorCacheModel extends \Model\ModelBase
{
    /**
     * データベースの設定を行う.
     */
    public function setup()
    {
        $this->db->exec(
            "CREATE TABLE IF NOT EXISTS translator_cache (
              id INTEGER PRIMARY KEY AUTOINCREMENT,
              lang TEXT,
              src TEXT,
              result TEXT,
              updated TIMESTAMP DEFAULT (DATETIME('now','localtime')),
              author TEXT,
              UNIQUE(lang, src)
            );"
        );
        $this->db->exec(
            "CREATE INDEX IF NOT EXISTS translator_cache_lang_index ON  translator_cache(lang);"
        );
        $this->db->exec(
            "CREATE INDEX IF NOT EXISTS translator_cache_src_index ON  translator_cache(lang,src);"
        );
    }

    /**
     * 特定の言語のキャッシュを取得する
     * @param  string                                                 $lang 言語
     * @return キャッシュの内容.ない場合はnullとなる.
     */
    public function getCache($lang)
    {
        $ret = \ORM::for_table('translator_cache')
            ->where_equal('lang', $lang)
            ->find_array();
        $pairs = array();
        foreach ($ret as $r) {
            $pairs += array($r['src']=>$r['result']);
        }

        return $pairs;
    }

    /**
     * キャッシュを登録する
     * @param string $lang  言語
     * @param array  $pairs srcとresultのペアの一覧
     */
    public function addCache($lang, $pairs)
    {
        $this->db->beginTransaction();
        $updated = time();

        foreach ($pairs as $src => $result) {
            $row = \ORM::for_table('translator_cache')->create();
            $row->lang = $lang;
            $row->src = $src;
            $row->result = $result;
            $row->updated = $updated;
            $row->save();
        }

        $this->db->commit();
    }
    public function deleteCache($lang)
    {
        \ORM::for_table('translator_cache')
            ->where_equal('lang', $lang)
            ->delete_many();
    }

    private function createCond($id, $lang, $src, $result, $author)
    {
        $cond = \ORM::for_table('translator_cache');
        if ($id) {
            $cond = $cond->where_equal('id', $id);
        }
        if ($lang) {
            $cond = $cond->where_equal('lang', $lang);
        }
        if ($src) {
            $cond = $cond->where_like('src', '%' . $src .'%');
        }
        if ($result) {
            $cond = $cond->where_like('result', '%' . $result . '%');
        }
        if ($author) {
            $cond = $cond->where_like('author', '%' . $author . '%');
        }

        return $cond;
    }

    /**
     * 特定の要件による検索を行う<BR>
     * 複数指定された場合はAND検索となる
     * @param  int                                                    $offset 取得開始位置
     * @param  int                                                    $limit  取得数上限
     * @param  int                                                    $id     ID
     * @param  string                                                 $lang   言語
     * @param  string                                                 $src    翻訳元
     * @param  string                                                 $result 翻訳結果
     * @param string $author 修正を行ったユーザ名
     * @return キャッシュの内容.ない場合はnullとなる.
     */
    public function search($offset, $limit, $id, $lang, $src, $result, $author)
    {
        $res->rows = $this->createCond($id, $lang, $src, $result, $author)->
                            limit($limit)->
                            offset($offset)->
                            find_array();

        $res->records = $this->createCond($id, $lang, $src, $result, $author)->count();

        return $res;
    }

    /**
     * 翻訳内容の更新を行う
     * @param int    $id     対象のID
     * @param string $result 検索結果
     * @param string $author 修正を行ったユーザ名
     * @param int $updated 更新日時
     */
    public function update($id, $result, $author, $updated)
    {
        $ret = \ORM::for_table('translator_cache')
            ->where_equal('id', $id)
            ->find_one();
        $ret->result = $result;
        $ret->updated = $updated;
        $ret->author =$author;
        $ret->save();
    }
}

MultilanguageTokyoMetro/src/MyLib/MsTranslator.php
以下のコードが、先のモデルを使用して、データベースに情報があればそれを返し、なければMicrosoftTranslatorを使用するようにしています。

<?php
namespace MyLib;

/**
 */
class MsTranslator
{
    private $apiKey;
    private $model;
    private $lang;
    private $cache;
    private $needUpdate;
    private $addedlist;
    const BASE_LANG='ja';

    /**
     * コンストラクタ
     * @param string                        $apiKey apiKey
     * @param \Model\MsTranslatorCacheModel $model  キャッシュ用のモデル
     */
    public function __construct($apiKey, $model, $lang)
    {
        $this->apiKey = $apiKey;
        $this->model = $model;
        if ($lang) {
            $this->lang = $lang;
        } else {
            $this->lang = \MyLib\MsTranslator::BASE_LANG;
        }
        $this->cache = $this->model->getCache($this->lang);
        $this->needUpdate = false;
        $this->addedlist = array();
    }

    public function translator($src)
    {
        if (!$src) {
            return $src;
        }
        if (!is_string($src)) {
            return $src;
        }
        if (\MyLib\MsTranslator::BASE_LANG===$this->lang) {
            return $src;
        }
        if (isset($this->cache[$src])) {
            return $this->cache[$src];
        }
        $ret = $this->doApi($src);
        if ($ret) {
            $this->needUpdate = true;
            $this->cache[$src] = $ret;
            array_push($this->addedlist, $src);

            return $ret;
        } else {
            return $src;
        }
    }

    public function needUpdate()
    {
        return $this->needUpdate;
    }

    public function updateCacheDb()
    {
        if (\MyLib\MsTranslator::BASE_LANG===$this->lang) {
            return;
        }
        if ($this->needUpdate()) {
            //$this->model->deleteCache($this->lang);
            $targets = array();
            foreach ($this->addedlist as $a) {
                $targets += array($a => $this->cache[$a]);
            }
            $this->model->addCache($this->lang, $targets);
            $this->needUpdate = false;
            $this->addedlist = array();
        }
    }

    private function doApi($src)
    {
        $ch = curl_init(
            'https://api.datamarket.azure.com/Bing/MicrosoftTranslator/v1/Translate?Text=%27' .
            urlencode($src).
            '%27&To=%27'.
            $this->lang.'%27'
        );
        curl_setopt($ch, CURLOPT_USERPWD, $this->apiKey.':'.$this->apiKey);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);

        $result = curl_exec($ch);

        $errno = curl_errno($ch);
        if ($errno === 0) {
            $result = explode('<d:Text m:type="Edm.String">', $result);
            $result = explode('</d:Text>', $result[1]);
            $result = $result[0];

            return $result;
        } else {
            $error = curl_error($ch);

            return null;
        }
    }
}

これらのコードの実際の使い方は以下のようになります。
以下のテストコードより抜粋
https://github.com/mima3/MultilanguageTokyoMetro/blob/master/test/MsTranslatorTest.php

$app = new \Slim\Slim();
ORM::configure('sqlite::memory:');
$db = ORM::get_db();

// モデルの初期化
$model = new \Model\MsTranslatorCacheModel($app, $db);

// データベースの構築
$model->setup();

// 翻訳情報追加
$en_inp = array();
$en_inp += array('猫'=>'cat');
$en_inp += array('犬'=>'dog');
$model->addCache('en', $en_inp);

$de_inp = array();
$de_inp += array('猫'=>'Katze');
$model->addCache('de', $de_inp);

// 翻訳コントロールの初期化
// MS_AZURE_KEYはマイクロソフトから手一驚されるAPIキー
$trans = new \MyLib\MsTranslator(MS_AZURE_KEY, $model, 'en');

// 翻訳情報が更新されたか調べる。(この場合は未更新なのでFalse)
echo $trans->needUpdate()

// 登録済みの翻訳をする ・・・ 結果はcat
echo $trans->translator('猫')

// 翻訳情報が更新されたか調べる。(この場合は未更新なのでFalse)
echo $trans->needUpdate()

// 未登録の翻訳情報を取得するとMsTranslatorAPIを実行して結果が「Humanになる」
echo $trans->translator('人間')

// 翻訳情報が更新されたか調べる。(この場合は更新されたのでTrue)
echo $trans->needUpdate()

// 新規に更新された翻訳情報をDBに書き込む
$trans->updateCacheDb();

みてわかる通りアプリケーション開発者としては、文字を使用するときに下記のような実装をするだけで多言語対応がおこなえるようになります。

$trans->translator('人間')

まとめ

MicrosoftTranslatorを用いて機械翻訳を利用して多言語対応を行うことで、低コストで多言語対応のサイトにとりくむことができるという事例を示しました。開発初期で利用することで大きな効果があげられると期待できます。

なお、注意点として以下のような問題があります。

・文字をバカ正直に翻訳しているだけなので、ローカライズ情報でよく使う以下のような書式の検証が不十分です。

「I have %d pens」 %dは可変の整数

・アラビア語などのRTL対応はアプリケーションが個別におこなわなければなりません。ウェブアプリの場合は、CSSに以下のようなスタイルを追加して適用してやるといいでしょう。

.rtl {
direction: rtl;
/*unicode-bidi:bidi-override;*/
}

・多言語対応は文字だけでは不十分です。日付の表現、数値の表現にも気をつけましょう。
Node.js + expressにおける多言語化の考察 一般的な多言語対応における注意事項
http://qiita.com/mima_ita/items/d7e48126f326a82af4c7#1-6