Sponsored Link


傀儡師のプログラミング日記

CaboCha を ctypes で使ってみようとして玉砕 (2003/11/29)

日本語係り受け解析器 CaboCha/南瓜: Yet Another Japanese Dependency Structure Analyzerを Python から使うのに,ctype から使えるようにしてみようかと思いました。以下は,その玉砕過程です。

Yasushi Masudaさんが 日本語訳されているのをまず参考に大体の使い方を見まして,MeCabLib というのも公開されているので,これを参考にやってみることにしました。MeCab C ライブラリ仕様と,CaboCha C ライブラリ仕様と,を見比べて,mecab_new → cabocha_new, mecab_new2 → cabocha_new2 と,このあたりは,単純に置換していくだけで同じようなものだからいけるそう。cabocha_sparse_tostr, cabocha_sparse_tostr2, cabocha_sparse_tostr3, strerror,destroy も MeCabLib からぱくってみます。MeCab の方は n-best やlock, unlockといった関数がないので,これらは削除してしまいます。さくさくと編集して試してみたのですが,エラーでダメでした。

ということで Python のコマンドプロンプト上で手打ちでひとつひとつ試してみることにしました。まずは,MeCab でやってみます。

from ctypes import *
lib=cdll.LoadLibrary("c:/Program Files/mecab/bin/libmecab.dll")
getattr(lib, "mecab_new2")
lib.mecab_new2.restype = c_void_p
lib.mecab_new2.argtypes = [c_char_p, c_char_p]
getattr(lib, "mecab_sparse_tostr")
lib.mecab_sparse_tostr.restype = c_char_p
m = lib.mecab_new2('-a', 'c:/mecab/bin/libmecab.dll')
print lib.mecab_sparse_tostr(m, "今日は元気だ。")

>>>from ctypes import *
>>>lib=cdll.LoadLibrary("c:/Program Files/mecab/bin/libmecab.dll")
>>>getattr(lib, "mecab_new2")
<ctypes._CdeclFuncPtr object at 0x008D6CE8>
>>>lib.mecab_new2.restype = c_void_p
>>>lib.mecab_new2.argtypes = [c_char_p, c_char_p]
>>>getattr(lib, "mecab_sparse_tostr")
<ctypes._CdeclFuncPtr object at 0x008D6D40>
>>>lib.mecab_sparse_tostr.restype = c_char_p
>>>m = lib.mecab_new2('-a', 'c:/mecab/bin/libmecab.dll')
>>>print lib.mecab_sparse_tostr(m, "今日は元気だ。")
今日    名詞,副詞可能,*,*,*,*,今日,キョウ,キョー
は      助詞,係助詞,*,*,*,*,は,ハ,ワ
元気    名詞,形容動詞語幹,*,*,*,*,元気,ゲンキ,ゲンキ
だ      助動詞,*,*,*,特殊・ダ,基本形,だ,ダ,ダ
。      記号,句点,*,*,*,*,。,。,。
EOS
ちゃんと動くので,基本的な ctypes の使い方は,一応理解できた?ようです。ここで同じようにして,CaboCha を使ってみます。
from ctypes import *
lib=cdll.LoadLibrary("c:/Program Files/cabocha/bin/libcabocha.dll")
getattr(lib, "cabocha_new2")
lib.cabocha_new2.restype = c_void_p
lib.cabocha_new2.argtypes = [c_char_p, c_char_p]
getattr(lib, "cabocha_sparse_tostr")
lib.cabocha_sparse_tostr.restype = c_char_p
c = lib.cabocha_new2('-f1', 'c:/Program Files/cabocha/bin/libcabocha.dll')
print lib.cabocha_sparse_tostr(c, "今日は元気だ。")
そうすると
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
WindowsError: exception: access violation
と落ちてしまいます...。同じようなことをやっているので,基本的には間違っていないと思うのですが,エラーになる理由が良く分かりません。ctypes なら簡単に使えると思ったので,試してみたのですが,なぜエラーになるか分からないので,あっけなく玉砕です。素直に Visual Studio .NET C++ インストールして,CaboCha に付属している標準の Python ライブラリ使うことにしようかと思います(^^;;

  [this]

Windows 版の time.time() は精度が悪い (2003/11/24)

次の短いプログラムを Windows と Linux で実行してみると面白いことが分かります。

Windows 版の場合、出てくる値と値の間がそれぞれ0.0010秒程度(100,000ループさせているのに32回しかそもそも値が表示されません)。これに対して、Linux で試すと 0.000017秒~0.000020 秒程度ごとに表示されるのです(100,000行表示される)。つまり、Linux で試すと、そもそも time.time() が 0.0000020秒(より短い)ぐらいで実行できるので、ループが回ってくるたびにちゃんと異なる時間を返してくれています。そうしたわけで、実行環境によってもやや状況は変わるかもしれませんが、Windows 版の Python だと time.time() を使うと、あまり時間を正確に測ることができないということになるようです。

#!/usr/bin/env python
# -*- encoding: utf-8 -*-

import time

i = 100000  # 100000 回試す

# 時刻 t1 を設定
t1 = time.time() 

while i:
    # 時刻 t2 を設定
    t2 = time.time() 

    # t1 と t2 で差があれば、表示する。
    # 差がなければ0なので表示されない。
    if t2 - t1:  
        print t2, t2 - t1
        # t1 に新しい時刻を設定しなおす。
        t1 = t2
    # ループカウンタを減らす
    i -= 1

ちなみに、次のように、単純に time.time() を 100000 回繰り返すだけのプログラムを実行してみると、Windows 0.231000065804秒、Linux 0.182734966278秒と、やや Linux の方が速いです。といっても、実は Windows の方が Linux のマシンより CPU 自体が遅いので正確ではありません。それでも、1.3 倍にも満たないわけで、オーダーがぜんぜん違うことの説明になりません。Windows 版の Python の time.time() はダメということになります。速度が遅いというわけではなく、精度が粗いということになります。

#!/usr/bin/env python
# -*- encoding: utf-8 -*-

import time

i = 100000
t1 = time.time()

while i:
    i -= 1
    time.time()

t2 = time.time()
print t2 - t1

なぜこういうことになってしまうのでしょう。Windows が OSレベルでダメなのでしょうか。こういうレベルのことは、今まで関心がなかったので気にしていませんでした。そこで、少し調べてみることにしました。運よく、RunTime: LinuxおよびWindows 2000におけるハイパフォーマンス・プログラミング手法にちょうどよい記事が見つかりました。

Windows 2000には、時間を測定する2つのAPIがあります。1つはGetTickCount()です。この関数は、システムがブートされてから経過した時間をミリ秒単位で報告します。GetTickCount()には、「クロック・ティック」精度があります。つまり、システム・クロックが刻まれたときだけ更新されるということです。Windowsの場合は、10ミリ秒ごとに更新されるだけです。そのためクロック・ティック精度は、最高でも10ミリ秒すなわち 10,000マイクロ秒程度です。

という理由によって、GetTickCount() を使っているようであれば、まず、その点で問題がありだということになります。GetTickCount を使っている限り、10ミリ秒単位でしか計測できないということになるからです。もう少し見てみると、

Windows 2000にはまた、64ビット高精度パフォーマンス・カウンターの現行値を取り出すQueryPerformanceCounter() APIがあります。QueryPerformanceCounter()に対するコールの結果に含まれるそれぞれの「ティック」が表す内容は、 QueryPerformanceFrequency()が返す値で決まります。頻度数は、1秒当たりのカウンター増加数であり、したがって秒は次のように表されます。

QueryPerformanceCounter を使って「カウンタの値/1秒あたりのカウンタ増加数」のような感じになっていれば、この記事と同様の精度になってくれるはずです。

同じハードウェア上でWindows 2000のQueryPerformanceCounter()システム・コールは、Linuxのgettimeofday() APIよりも速度がはるかに遅いということです。今回の目的には、タイミング・ルーチンの2マイクロ秒という精度で十分です。

Windows でも、一応、2マイクロ秒の精度で、タイミング・ルーチンが書けるということになります。ということは、Python のソースで、GetTickCount を使っているか、QueryPerformanceCounter を使っているかをまずチェックしてみればよいと思い、Python のソースを grep してみると、GetTickCount は使っていないようです。QueryPerformanceCounter は _hotshot.c と、timemodule.c で使われています。Linux の場合は gettimeofday が使われています。そうであれば、理屈上 Linux の倍程度ではあっても、もう少し Windows 版の時間の粒度が細かくなってもよさそうなものなのに、なぜなのでしょう。_hotshot.c を見ると、MS_WIN32 の場合、次のようになっているので、上記の記事と同じような手法を使っているように見えます。

#ifdef MS_WIN32
#include 
#include 
#include     /* for getcwd() */
typedef __int64 hs_time;
#define GETTIMEOFDAY(P_HS_TIME) \
        { LARGE_INTEGER _temp; \
          QueryPerformanceCounter(&_temp); \
          *(P_HS_TIME) = _temp.QuadPart; }


HAVE_GETTIMEOFDAY

static char resolution__doc__[] =
#ifdef MS_WIN32
"resolution() -> (performance-counter-ticks, update-frequency)\n"
"Return the resolution of the timer provided by the QueryPerformanceCounter()\n"
"function.  The first value is the smallest observed change, and the second\n"
"is the result of QueryPerformanceFrequency().";
#else
"resolution() -> (gettimeofday-usecs, getrusage-usecs)\n"
"Return the resolution of the timers provided by the gettimeofday() and\n"
"getrusage() system calls, or -1 if the call is not supported.";
#endif

time_clock(PyObject *self, PyObject *args)など見ながら、ふと考えてみると時間の間隔を測りたいわけではないのだから、time.clock() を使えばいいと気がつきました。これで試してみると、0.0000282秒程度で、100,000行ちゃんと表示されるようになりました。ということで、これから、Windows で時間測定をするときは、time.clock() でプロセス開始からの時間をチェックするようなやり方をしようと思う。しかし、なぜ time.time() だと精度がそんなに違うのでしょう。time_time(PyObject *self, PyObject *args)を見てみると、secs = floattime(); に問題がありそうな気がしてきます。

static double
floattime(void)
{
        /* There are three ways to get the time:
          (1) gettimeofday() -- resolution in microseconds
          (2) ftime() -- resolution in milliseconds
          (3) time() -- resolution in seconds

(2) に流れてしまっているようです。しかし、今頃、バグということはないでしょう。ということで、また検索をしてみると Python ライブラリリファレンス 10.10 timeitが見つかりました。

Windows の場合、 time.clock() はマイクロ秒の精度がありますが、 time.time() は 1/60 秒の精度しかありません。一方 Unixの場合、time.clock() でも 1/100 秒の精度があり、 time.time() はもっと正確です。いずれのプラットフォームにおいても、デフォルトのタイマ関数は CPU 時間ではなく通常の時間を返します。つまり、同じコンピュータ上で別のプロセスが動いている場合、タイミングの衝突する可能性があるということです。
ここに最初から答えがありました。考えてみると、QueryPerformanceCounter はあくまでカウンタだから時間をとることができません。ですから、時刻をとる場合は、ftime() を使わざるを得ないということなのでしょう。しかし、起動時の時刻を保存しておいて、カウンタから求めた時間を足してやったらどうなのだろうと思って試してみることにしました。
import time

#基本の時刻を取得しておいて
#クロックを開始する
BASETIME= time.time()
time.clock()

def _mytime():
    global BASETIME
    return time.clock() + BASETIME

time.time = _mytime

このように time.time を上書きして置き換えてしまえば、呼び出しごとに確実に異なる時間が返ることが保証できそうです。ただし、100000回の呼び出しテストをしてみる time.time() < time.clock() < _mytime() の順にと、0.241000056267 , 0.340696932152, 0.514129638672 となって、ほんものの time.time() にくらべて偽ものは倍の実行時間がかかってしまいます。それでも、時刻としてできるだけきめ細かくとりたいときは、そうするしかないのではないかと思います。1回呼び出しするときのコストは、0.00002秒程度と換算できます。ただ、パフォーマンステストで時間を細かくとるなら、時刻ではなく時間の差分だけ分かればよいのですから time.clock でいいわけでしょう。
細かく時間をとる必要がない場合は、本物の time.time() を使って、時刻を使って値を作り出したいときなど、time.time() より細かく時刻をとりたい場合は、ニセモノでやることにしました。Zope などは、time.time() を内部でたくさん使っているようですが、Windows 版だと問題になることはないのでしょうか、ちょっと気になるところです。誰も問題にしないということは、それでも大丈夫なようにちゃんとできているということなのかもしれません。すくなくとも、Zope のプロダクトにある CallProfiler みたいなものを使った場合、正確な値はとれないとは思いますが...。なにはともあれ原因が分かってすっきりしました。

それ以前に、パフォーマンスの測定したいなら不精をして time.time() で時刻の差分をとるなどせずに、プロファイラをちゃんと使えば、よいという話もあります。Python 2.3 でプロファイラも改善されているようです。しかし、なんなのでしょうね、このオチは。Python のマニュアルを最初から全部読んでいれば、問題はなかったのに。


  [this]

IndexedCatalog について (2003/09/18)

IndexedCatalog 0.6 RC1 がリリースされたので、ちょっと IndexedCatalog に ついてまとめてみることにしました。詳しくは、IndexedCatalog のオリジナルページを参照してください このページは、IndexedCatalog のページから主として情報を取り出したものです。

IndexCatalog とは何か

IndexCatalog は、Zope のオブジェクトデータベース(Zope Object Database (ZODB)) を使用することを前提としたもので、ZODB に格納されたオブジェクトの一覧を作成し (カタログ化)し、このカタログに格納されたオブジェクトを、属性値などを検索キー として高速に検索できるように索引(インデックス)をつける仕組みです。
つまり、ZODB を拡張し、オブジェクトの属性値などのすべてのフィールドの値を string/integer/float のいずれかの型の値をキーとした取り出しやすい形式で ZODB 内に格納し(つまり索引を作成し)、これに対して検索うインターフェイスを 備えたものということになるでしょう。まだ、こなれた説明になっていませんが、 気にしないことにして、先に進みます。
ZODB のカタログとはいっても、Zope ではこのパッケージは使用できません。 IndexCatalog は、ZODB を使ったスタンドアロンのその他のパッケージ用に 現状で開発されているものです。ライセンスは Lesser GNU Public Licence (LGPL)です。

IndexCatalog を機能から見ると次のようになります
  • オブジェクトを格納するクラスとしてCatalog クラスがあり、この Catalog クラスは クエリインターフェイスを備えています。
  • インデックス化できるのは、文字列、浮動小数点数、整数、日付、およびインスタンスです。
  • オブジェクトをカタログ化しインデックスを付与するためには、格納するオブジェクトを若干 変更する必要があります。基本的には、from IndexedCatalog.IndexedObject を継承したクラス を作成し、そのクラスにいくつか特別の属性を追加するだけです。そして、オブジェクトの インスタンスをカタログに追加するには catalog.insert() を使います。
  • 複合オブジェクト(composite object) にも対応しており、サブオブジェクトの初期化と インデックス生成は、自動的に行われます。
  • 型による検索(クエリ)に対応し、サブオブジェクトや参照されている オブジェクト(referenced object)の属性値に対してクエリを発行することも可能です。
  • 検索結果の並び順は、フィールドの値によって昇順にも降順にも整列することが可能です。
  • 100% pure python で作成されており、インストールには Distutils を使用しています。 このため非常に簡単にインストールできます。
以上、IndexedCatalog のアバウトに訳したもの

例として、まず IndexedCatalog の例を参考にし、これをちょっと変形して 単語を登録できるようなものを作成してみました。

#!/usr/bin/env python

from IndexedCatalog import Catalog, IndexedObject

# 最初にオブジェクトを定義します。
# このオブジェクトは (IndexedCatalog.)IndexedObject を
# 継承したものにしなければなりません。
# また、
# 整数型(IntType) の属性(フィールド)と
# 文字列型(StringType) の属性(フィールド)を追加しておきます。
class WordObject(IndexedObject):
    length = int
    word = str

    def __init__(self, word):
        IndexedObject.__init__(self)
        self.word = word
        self.length = len(word)


wordlist = ['zero','one','two','three','four','five','six','seven','eight','nine','ten']

# カタログを作成しますが、このときコンストラクタに
# 引数として、上記のクラスを渡します。
catalog = Catalog(WordObject)

# サンプルとして、11個のオブジェクトを作成してみます。
for s in wordlist:
    # カタログオブジェクトを作成し
    obj = catalog.new(s)
    # ここは入れるとエラーになってしまう...
    # catalog.insert(obj)


# すべてのオブジェクトに対して、
# length が 4 以上のもの(4文字以上の単語) を検索してみます
print "length が 4 以上のもの"
results = catalog.query('length >= 4')
for result in results.sort('length'):
    print result, result.word, result.length

# すべてのオブジェクトに対して
# word のうち three を含むものを検索してみます。
print "word が three のもの"
results = catalog.query('word == "three"')
for result in results.sort('length'):
    print result, result.word, result.length

print "word が eight のもの"
results = catalog.query('word == "eight"')
for result in results.sort('length'):
    print result, result.word, result.length

# 実行してみると

$ python a.py
length が 4 以上のもの
<__main__.WordObject ( 145760928) at 0x137197648> nine 4
<__main__.WordObject ( 643557441) at 0x137063272> zero 4
<__main__.WordObject ( 897793739) at 0x137194448> five 4
<__main__.WordObject (1027051739) at 0x137193648> four 4
<__main__.WordObject (-873929443) at 0x137196048> seven 5
<__main__.WordObject (-183619027) at 0x137196848> eight 5
<__main__.WordObject ( 697292769) at 0x137192712> three 5
word が three のもの
<__main__.WordObject ( 697292769) at 0x137192712> three 5
word が eight のもの
<__main__.WordObject (-183619027) at 0x137196848> eight 5


とりあえずは、なんとなく、動いているような感じなので、 次は、もう少し実用的なものにチャレンジしてみることにしよう。

  [this]

Pyblosxom を調べてみる (2) (2003/08/02)

とりあえず、pyblosxom をインストールしてみました。実は、まだぜんぜん分かっていません。ということで、ちょっとずつ中身を確認しながら、いじっていこうかと思います。ちなみに下記の内容は、pyblosxom 0.7beta1 を見たときにすでに書いてしまったので、今動かしているのは、pyblosxom 0.8rc1 なので若干違うかもしれません。
CGI の本体の pyblosxom.cgi を見ると、sys.path の追加などを行ったあとに、libs.pyblosxom の PyBlosxom クラスをインポートしています。そして、libs.Request クラスをインポートしていますが、これはブラウザからのリクエストを格納するためのものでしょう。そして、os と cgi に関しては、通常の Python で CGI を書く場合のお決まりのパターンでしょう。
from libs.pyblosxom import PyBlosxom
from libs.Request import Request
import os, cgi
そして、ここからが本筋にあたるところです。Request() でリクエストクラスをインスタンス化して、reg.addConfiguration(config.py) で設定ファイルを読み込んで、それをリクエストクラスのオブジェクトに反映させているようです。
req = Request()
req.addConfiguration(config.py)
そして、次に空のディクショナリを作って、変数 d に代入します。
d = {}
次に、["HTTP_HOST" ....] のリストの各要素をキーとして、os.environ.get(mem, "") で環境変数から、 それぞれの値を拾ってディクショナリに入れています。
for mem in ["HTTP_HOST", "HTTP_USER_AGENT", "HTTP_REFERER", "PATH_INFO",
    "QUERY_STRING", "REMOTE_ADDR", "REQUEST_METHOD", "REQUEST_URI", 
    "SCRIPT_NAME", "HTTP_IF_NONE_MATCH", "HTTP_IF_MODIFIED_SINCE"]:

d[mem] = os.environ.get(mem, "")
このディクショナリを req オブジェクトに addHttp メソッドを使って追加し、さらにこれに、Python の CGI では定番の cgi.FieldStorage() を、"form" というキーでディクショナリに追加しています。フォーム変数にアクセスするときには、これを使います。
req.addHttp(d)
req.addHttp({"form": cgi.FieldStorage()})
そうしたら、この req オブジェクトを引数として、PyBlosxom クラスをインスタンス化してやります。PyBlosxom がメインのオブジェクトなわけです。そして、p.startup() でたぶん、内部的に起動した状態を作り出して(初期化して)、p.run() で実際に何らかの動作を行うといったことなのでしょう。このように、pyblosxom.cgi 自体は、普通に Python で CGI を書くときのように単純なものです。
p = PyBlosxom(req)
p.startup()
p.run()
さらに Request.py を見てみると、Request クラスは、リクエストや Pyblosxom 自体の設定を格納しておくためのクラスであることが改めて確認できます。このリクエストオブジェクトを参照しながら Pyblosxom クラスが表示するための HTML を作り出すようです。def __init__ (初期化)のところを見ると、self._configuration = {}、self._http = {}、self._data = {} と単に3つの空のディクショナリを作ってやっているだけで初期化はやはり単純です。設定、HTTPリクエスト、データと3つに分けて格納する場所をつくってやります。その後ろも基本的に、このオブジェクトのプロパティに値を設定したり、取り出したりだけの単純なものです。 肝心の pyblosxom.py を見てみると、これは多少大きいのですが、それでもたったの338行。コアの部分は非常にシンプルなプログラムのようです。

  [this]

Pyblosxom を調べてみる (1) (2003/07/26)

ここに書き込むのは、ずいぶん久しぶりになってしまいました。というのも、編集用の パスワードを忘れてしまって、それっきりになっていたからです。 でも、こちらに移行しようと最近また思い始め、Pyblosxom を使ってみようと思っているところです。 それにしても、TinyBlog を捨てようと思ったその矢先にパスワードを見つけてしまうとは、 なんとも皮肉なものです。これは簡単に捨てないでくれという啓示かもしれないので、 とりあえず Pyblosxom に移行するまでのメモを TinyBlog を使って残しておくことにしようと思います。
新バージョンの pyblosxom を使うと、こんな感じになるようです。移行するまでには時間がかかると思うので(私の時間的な問題で)、最新バージョンの pyblosxom ですこしずつ試しながら、その情報をここに載せておこうかと思うしだいです。

今日の作業


まずは、SourceForge Project の pyblosxom のページから、pyblosxom-0.7beta1.tar.gz をダウンロードしました。そして、圧縮されたファイルを解凍すると、ChangeLog, INSTALL, README.contrib, config.py, README.plugins, xmlrpc.cgi, pyblosxom.cgi, ReadMeForPlugins.py の各ファイルと、contrib, libs, flavour_examples のディレクトリがあったので、とりあえず、INSTALL を開いてみます。INSTALL ファイルには、具体的に分かりやすくインストールが書かれているので、これを読み進めながら、インストール方法を検討してみることにします。「1.1 Simple Installation」 では、pyblosxom.cgi, config.py と libs/ ディレクトリを cgi-bin/ に置けばいいと書かれています。libs/ を cgi-bin に置くのに抵抗がある場合は、

1 import sys
2 sys.path.append('/path/to/where/lib/directory/is')

を pyblosxom.cgi の4行目に入れればいいということなので、この方法で試すことにします。Blogger API に対応させたい場合は、xmlrpc.cgi も cgi-bin/ に置かなければならないようです。 「1.2 Using .htaccess」には、もうひとつのインストール方法として、.htaccess を使ったものがあり、http://www.example.com/cgi-bin/pyblosxom.cgi のような URL より、http://www.example.com/blog がいい場合は、.htaccess を使ったインストール方法もある書かれています。ということで、やはり、この方法にしようかと方向転換しました。この場合、まず pyblosxom.cgi の動作を確認した上で blog というファイル名に変更し、.htaccess に次の行を入れればよいようです。
Options ExecCGI        

<Files blog>
ForceType application/cgi-script
SetHandler cgi-script
</Files>
http://www.example.com だけでアクセスさせたいなら、blog ではなく index にすればよいようなので、最終的には、この方式にしようかと思います。 「1.3 Denying access to config.py or the INI file」には、アクセス制限の方法が書かれています。libs/ や config.py をウェブからアクセスできるようなディレクトリに置きたくない場合、/home/joe/www に html ファイルが置いてあるとすれば、/home/joe/pyblosxom に config.py と libs を置くことができるので、安全のためにこの方法をとることにします。ということで、XREA では、ホームディレクトリ上に pyblosxom というディレクトリを作ってそこに config.py と libs を置き、www に pyblosxom.cgi をとりあえず、そのままの名前で置くことにします。そして、次の行を pyblosxom.cgi の先頭の方に追加します。
1 import sys
2 sys.path.append('/home/joe/pyblosxom')
最後に、ひとつにディレクトリにすべて放り込んでおきたい場合は、.access に次の行を入れて、config.py にアクセスされないようにするという手が紹介されていました。
<Files config.py>
order deny, allow
deny from all
</Files>
基本的なインストールとしては、これだけで済むようなので、いたって簡単であることが分かりました。今日のところは、ここまでにしておきます。

  [this]


Blog

TinyBlog について

傀儡師の館(楽天)

pyblosxom テスト中

自然言語処理について

人工無能について

Python について

Copyright (c) 2003, Kugutsushi