ピンインの変換 その2

前回書いたのは、内部的に保存されている(と仮定した)声調番号を後ろに付けたフォーマット(以下、保存用フォーマット)のピンインから、いわゆる表示用フォーマットに変換するロジックの話でした。一応、それとは逆のパターンも考えてみたいと思います。

どんなときに使うか

少し前に、Google DocumentやEvernoteのOCR機能を用いた自動読み取りの可能性について調べました。結果的には、少なくとも声調符号付きのピンインを読み込むには実用的ではないという結論に至った(高解像度で再トライというのはまだ残っていますが)というのは、以前の記事に書いたとおりです。

ただ、その部分がクリアされた場合、もしくはネットから拾ってきたデータで表示用フォーマットとなっているものを保存するときに、表示用から内部保存用に変換してやる必要があります。もちろん、変換せずにそのまま保存するというのも1つの考え方かも知れませんが、いろいろ解析したいなと思ったときに、すべてのデータが同じフォーマットに保存されている方が有用なのは議論を待たないと思います。

こっちの変換の方が簡単だった?

ということで、ソースを書いてみました。いわゆるコンストラクタ(__init__)の部分は、前回の記事と同じなので、変換ロジックの部分だけです。

    def pinyin_converter_s(self, pinyind):
        #Convert Pinyin from display format (yāng) tostored format (yang1)
        non_alpha = re.findall(r'[^a-z]+', pinyind)
        if len(non_alpha) > 0:
            for i  in range(len(self.tones)):
                if non_alpha[0] in self.tones[i]:
                    pinyind = re.sub(non_alpha[0], self.tones[i][0], pinyind) + \
                                      str(self.tones[i].index(non_alpha[0]))
                    if 'ü' in pinyind:
                        pinyind = re.sub('ü', 'v', pinyind)
        return pinyind

やる前は、こっちの方が難しいと思ったのですが、実際にコードにしてみるとこっちの方が簡単でした。前提として、コンストラクタの中に母音に使うアルファベットと声調記号を乗せた場合の一覧をself.tonesというリスト(正確にはタプル)で持たせてあります。

まずは正規表現を使って、声調記号の乗っていない純粋なアルファベット以外の文字列を検索しています。re.findall()なので戻り値はリストになりますが、変なデータが入り込まない限りは1つだけ、つまり声調記号の載った文字だけが選ばれるはずです。なお、r'[^a-z]+と最後にプラス(+)が付いていますが、これも実験の結果です。声調記号付きのアルファベットは、\xで始まるコード2つの組み合わせ、つまり\xyy\xzzというフォーマットになっているのですが、正規表現の中で+をつけないで実行するとこれらが分割されてしまいました。

実験した結果も出しておきましょう。Functionの名前は同じですが、正規表現のところはr'[^a-z]となっていて、プラス記号は外した状態で実行しています。

>>> ppp.pinyin_converter_s('xiù')
'xi\xc3\xb9'

最後のuの上に第4声の声調記号が載っていて、戻り値は変換されて見づらいですが、何も変換されませんでした。理由を探るためにデバッグしてみました。

-> if len(non_alpha) > 0:
(Pdb) p non_alpha
['\xc3', '\xb9']

ここだけ抜き出されてもという感じですが(笑)、re.findall()した戻り値が入る変数non_alphaの中身を見ると、本来1つの文字(ù)が \xc3 と \xb9 の2つに分解されてしまっているのが分かると思います。

あとは声調番号を付加する部分ですが、self.tonesの要素番号は、声調番号と一致するようになっているので、index()を使ってその文字が何番目の要素に入っているかを取得してやればOKです。

最後に、ü を v に変換するロジックを追加してやれば、「ほぼ」完成です。

いくつかサンプルを使って試してみましょう。

>>> reload(pinyin)
<module 'pinyin' from '/Users/ken/Documents/workspace/NLTK Learning/scripts/pinyin.py'>
>>> from pinyin import *
>>> ppp = Pinyin()
>>> ppp.pinyin_converter_s('lǚ')
'lv3'
>>> ppp.pinyin_converter_s('xiě')
'xie3'

大丈夫そうですね。

でもer化には未対応なのです

ここまで作ってer化したピンインに未対応である事に気がつきました。er化というのは、普通話というか北京でよく現れる現象で、最後が巻き舌になるというやつです。例えば、「一点」(ちょっと)という単語が「一点儿」になるという具合です。これをピンイン化した場合に、ちょっとした問題があります。漢字の上では儿(er)という字が加わっただけなので「yidiǎn + er」としたいところですが、実際には「 yidiǎnr」という具合になり、eが省略されてしまうのです。

もともと、今回作ったロジックを呼ぶ前にピンインを漢字ごとに分割するという作業が必要なのですが、このer化について対応しないといけないですね。

(たぶんつづく)

ピンインの変換

日本人(だけじゃないかな?)が中国語学習をする上で一番大変なのは声調を覚えることというのは前にも書いたとおりです。そこで一生懸命覚えることになるのですが、例えば自分で電子的に単語帳を作る、それが本格的なDBだろうがExcelだろうが、ピンインの処理というのは意外に面倒です。

ピンインというのは母音の上に声調記号を乗せて表記します。こんな感じですよね。

yāng, píng, lǚ, lè

ただ、わざわざ声調記号付きのアルファベットを入力するために文字パレットを呼び出したり、ショートカットキーを覚えるのは面倒だと思います。なので、実際問題としてこんな感じで、ピンインの後ろに声調番号をつけてやるという対応をしている人が多いのかなと思っています。

yang1, ping2, lv3, le4

この番号を振る方式、いろいろ分析にかけたいと思うときは便利で、例えばPythonのスライスを使って最後の一文字を拾うだけで声調が分かります。

>>> 'yang1'[-1]
>>> 1

以前書いた声調の頻度を分析する話も、(もうちょっと手の込んだことをしていますが)基本的には、ピンインの最後の1文字で頻度分析をしてあげているだけです。ただ、内部的に保存したり解析したりする時にはそれで良いにしても、実際にピンインを表示させたいときには、元通りの正しい書式に戻したいという思うのではないでしょうか?

というか、そう思ったので、ちょっくらPythonで書いてみました。

基本ルールを押さえれば結構簡単だった

まずは、コンストラクタの方にself.tonesという属性を定義してあげます。これの中に、声調記号が載った場合の文字を入れておきます。

class Pinyin:
    def __init__(self):
        self.consonants = ('b', 'c', 'ch', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'm', 'n', 'p', 'q', 'r', 's', 'sh', 't',
                                          'w', 'x', 'y', 'z', 'ng')
        self.vowels = ('a', 'ai', 'ang', 'ao', 'e', 'ei', 'eng', 'er', 'i', 'ia', 'ian', 'iang', 'iao', 'ie', 'in', 'ing', 'iong',
                             'iu', 'o', 'ong', 'ou', 'u', 'ua', 'uai', 'uan', 'uang', 'ue', 'ui', 'un', 'uo', 'v', 've')
        self.tones = ( ( 'a', 'ā', 'á', 'ǎ', 'à'), ('o', 'ō', 'ó', 'ǒ', 'ò'), ('e', 'ē', 'é', 'ě', 'è'),
                               ('i', 'ī', 'í', 'ǐ', 'ì'), ('u', 'ū', 'ú', 'ǔ', 'ù'), ('ü', 'ǖ', 'ǘ', 'ǚ', 'ǜ'))

こちらが実際に変換を行うファンクションです。

    def pinyin_converter_d(self, pinyins):
        #Convert Pinyin from stored format (yang1) to display format (yāng)
        splitpin = self.pinyin_splitter(pinyins)
        if splitpin[0] == True:
            if 'a' in pinyins:
                convertkey = [0, int(splitpin[2][2])]
            elif 'o' in pinyins:
                convertkey = [1, int(splitpin[2][2])]
            elif 'e' in pinyins:
                convertkey = [2, int(splitpin[2][2])]
            elif 'ui' in pinyins:
                convertkey = [3, int(splitpin[2][2])]
            elif 'iu' in pinyins:
                convertkey = [4, int(splitpin[2][2])]
            elif 'i' in pinyins:
                convertkey = [3, int(splitpin[2][2])]
            elif 'u' in pinyins:
                convertkey = [4, int(splitpin[2][2])]
            elif 'v' in pinyins:
                convertkey = [5, int(splitpin[2][2])]

            #convert from v to ü
            if 'v' in pinyins:
                pinyins = re.sub('v', self.tones[5][0], pinyins)

            #remove tone number at the end
            if pinyins[-1].isdigit():
                pinyins = pinyins[:-1]

            return re.sub(self.tones[convertkey[0]][0], self.tones[convertkey[0]][convertkey[1]], pinyins)
        else:
            return ''

声調記号が載る母音にはルールがあって、"a -> o -> e -> i, u"という優先度になっています。つまり、1つのピンインの中に複数の母音がある場合にどの文字の上に記号を載せるかが決まっているわけです。多少面倒くさいのが、i と u で、この2つが並んだ場合は、後ろに来たモノに声調記号を載せることになっています。例えば、xiu4 というピンインなら xiù というように u の上に来て、gui4なら、今度は i の上になるので guì という感じですね。

もう1つ例外処理を考えないと行けないのが、v の扱いです。この文字は正式には ü と表記するルールになっていますが、通例 v で代用することが多いです。この文字の上に声調が来る場合は良いのですが、lve4 という表記をするピンインがあって、この場合は上のルールに従うと e の上に声調記号が来ます。ただ、声調記号の有無に関係なく、v は ü に変換しないと行けないのであらかじめロジックを仕込ませておいて、最終的には lüè となるのが正解です。

とりあえずソースは書けたので、いくつかのピンインで試してみましょう。

>>> ppp = Pinyin()
>>> print ppp.pinyin_converter_d('xiu4')
xiù
>>> print ppp.pinyin_converter_d('gui4')
guì
>>> print ppp.pinyin_converter_d('lve4')
lüè
>>> print ppp.pinyin_converter_d('lv3')
lǚ

大丈夫そうですね。これでデータベースを直接見ることがないユーザでも混乱させることなく表示することができます。
 

Filemaker to Python (5) - Updateを試す

Select, InsertときたらUpdateも試しておこうということで、コードを書いてみましたというお話です。

上書きのルール

既存のレコードをUpdateするにあたって、何かしらのルールを決めておく事は重要だと思いました。例えば単語のDBを例にすると、単語そのものというのは基本的に上書きしてはダメなモノの考える事が出来るし、単語に新たな意味を追加したいのであれば、内容を比較してこれまでにない内容であれば既存の内容に追記、または問答無用に上書きした方がよい項目もあります。なので、ちょっと面倒なのですが、classのコンストラクタ(__init__)にちょいと手を加えてみました。

class AccessCWordFM:
    def __init__(self):
        self.DSN = 'DSN=CWord;UID=xxxxxx;PWD=xxxxxxxxxxx'
        self.TabName = 'CWordsDB'
        self.FieldList = ['WordID', 'Word', 'Pinyin', 'POS', 'Meaning', 'Source', 'Status', 'Batch']
        self.FieldStatus = ['NC', 'NC', 'CP', 'CP', 'CP' , 'AO', 'AO', 'AO']
        self.FieldLen = len(self.FieldList)
        
        self.cxcn = pyodbc.connect(self.DSN)
        self.cursor = self.cxcn.cursor()

self.FieldListという中に、Filemaker側のテーブルの項目名が入っていて、さらにself.FieldStatusというリストを追加して、各項目に対して上書き禁止(NC)、内容比較の上で判断(CP)、問答無用に上書き(AO)を定めてみました。

新規追加か既存レコード更新か

あるデータが受け渡されて、既存のレコードがあればそれを更新、なければ新規追加というのは、通常のアプリケーションでもよくある考え方だと思います。ということで、条件分岐のためのコードをこんな感じで書いてみました。

    def insert_cword(self, cwdata, test=False):
        if cwdata[0] == '':
            print 'Error: Either Word is missing'
        elif len(cwdata) != self.FieldLen - 1:
            print 'Error: Number of elements are not match with the database'
        else:
            # Check if the word is already registered
            self.get_cwords(cwdata[0], cwdata[1])
            if len(self.rows) == 0:
                #Add new entry to CWordDB
                self.add_new_cword(cwdata, test)
            elif len(self.rows) >= 1:
                #Modify existing record
                wid = int(self.rows[0][0])
                self.modify_record(wid, cwdata, test)

とりあえず、最低限の項目だけ検索して既存のレコードありなしを判定させ、レコードがなければレコード追加のためのFunctionを呼んで、レコードがある場合は更新のためのFunctionを呼びます。

Updateをかける

少し長いのですが、更新のためのロジックはこんな感じ。いくつかに分けて見ていきましょう。

    def modify_record(self, wid, cwdata, test=False):
        self.get_single_fullrecord(wid)
        NewRecord = self.rows[0]        #NewRecord includes WordID
        for i in range(self.FieldLen):
            if self.FieldStatus[i] == 'AO':
                NewRecord[i] = cwdata[i - 1]
            if self.FieldStatus[i] == 'CP':
                if NewRecord[i] == '':
                    NewRecord[i] = cwdata[i -1]
                elif NewRecord[i].find(cwdata[i -1]) == -1:
                    NewRecord[i] = NewRecord[i] + ', ' + cwdata[i -1]
                else:
                    pass

まずは既存レコードの全項目内容を取得(別function)して、NewRecordというリストに放り込んでいます。そこから、さっきコンストラクタの中で問答無用で上書き(AO)になっているものは、そのまま項目内容を上書きしています。さらに、比較(CP)となったものについては、元の値がブランクだったら上書き、何か入っていれば新しい値と比較して、重複がなければ元の内容に追記するという処理を各項目に対して繰り返しています。

        sqlString = []
        sqlString.append('update')
        sqlString.append(self.TabName)
        sqlString.append('set')
        for i in range(self.FieldLen):
            if self.FieldStatus[i] == 'AO' or self.FieldStatus[i] =='CP':
                if sqlString[-1] != 'set':
                    sqlString.append(',')
                sqlString.append(self.FieldList[i])
                sqlString.append('=')
                sqlString.append("'" + NewRecord[i] + "'")
        sqlString.append('where')
        sqlString.append(self.FieldList[0])
        sqlString.append('=')
        sqlString.append(str(wid))

        #commit
        self.call_commit(sqlString, test)

残りの部分については、これまでのInsertなどと同じでsqlStringというリストにSQL文の元となる要素をAppendで追加しています。1つだけ注意点は、FilemakerのテーブルにWordIDという数字が入る項目があるのですが、あとで" ".join()する関係で文字列として受け渡す必要があるので、str(wid)という表記になっています。しかし、実際のところは数字が入る項目なので(')で囲ってやる必要はありません。最後のCommit部分は外出しにしました。

最後にCommitをお忘れなく

これがCommit用のfunctionです。

    def call_commit(self, sqlString, test=False):
        sqlCommand = " ".join(sqlString)
        if test == True:
            print sqlCommand
        self.cursor.execute(sqlCommand)
        if test == False:
            self.cursor.commit()
            print 'Record(s) updated with commit work'

sqlStringというリストの要素を" ".join()で文字列として生成してやって、あとはSQLのコマンドをODBC側に投げています。一応テストモードもつけてあるのは、生成されたSQL文がきちんと確認するオプションがあった方が便利だと思ったからです。

いちおうまとめ

ということで、誰の役に立つのか分かりませんが、PythonからODBC経由でFilemakerのデータベースにアクセスしてみるという一応の目的は達成しました。自分の持っている6000件弱の全件検索に2-3秒というところからして、たかだか数千件から1万件前後のデータ量であれば十分使えるのではないかなと感じがします。

また興味のありそうなネタがあったら試してみようと思います。

Filemaker to Python (4) - Insertを試す

中国語学習のDBのプラットフォームとして、Filemakerを使ったのはいくつか理由があるのですが、大きかったのは当時WinとMac両方の環境を使っていた事と、途中からFilemaker Goが出てきたので、そのままiPadとかでも勉強できるかも(実際にはやらなかったので、今でもこのレベルですがw)という考えたのが大きかったと思います。

その後、日本への帰国だったりで熱意が薄れ、結局3年ほど放置してしまいましたが、その後PythonやNLTKと出会って、何かゴニョゴニョしたいなという段になって、こうして資産として再利用できています。

さて、本日のお題はInsertです。

あれ?更新されない

実際にPython側からDBにエントリを追加する頻度は高くないと思いますが、間口を空けておけば、今後いろいろな形で使えるだろうということで実験してみました。

>>> cxcn = pyodbc.connect('DSN=CWord;UID=xxxxxx;PWD=xxxxxxxxxx')
>>> cursor = cxcn.cursor
>>> cursor = cxcn.cursor()
>>> cursor.execute("insert into CWordsDB (Word, Pinyin) values ('爱国', 'ai4 guo2')")
<pyodbc.Cursor object at 0x10277ff90>
>>>

何でこの単語?というツッコミはさておき、さくっと戻って来ました。これだとエントリが実際に追加されたかどうかが分かりませんね。ということで、Filemaker側で確認してみたのですが、

・・・エントリが追加されていない

ダメだったらダメとなにかエラーを返しておくれよと考えること一晩(寝ただけ)。Filemaker側にWordIDという入力必須のシリアル番号を入れる項目があるのですが、ひょっとしてこいつも指定してやらないとダメなんじゃないかなどと考えてしました。

f:id:deutschina:20130726071651p:plain
f:id:deutschina:20130726071648p:plain

だとしても、Insertに失敗したならそれなりの情報がどこかに書き込まれるはずなのになぁ。

Commit をお忘れなく

で、一晩寝て、Google先生に尋ねてみました。もうタイトルでばれてますが、Commitしないと保存されないよという話でした。昨日の晩からSleep状態にしておいたMacから渾身の一撃を与えてみました。

>>> cxcn.commit()

はい、あっさりとエントリが追加されていました。

f:id:deutschina:20130726072127p:plain

Filemaker側で自動入力させるような項目については、入力必須であってもPython側から値を渡してやらなくても大丈夫なようです。

ちなみに本当に入力必須な項目を忘れるとどうなるのか?Filemakerって、色んな意味で緩い(柔軟という事だよ)ので中途半端な形で登録されてしまったらどうしようと心配になったので試してみました。

>>> cursor.execute("insert into CWordsDB (Word, Pinyin) values ('', 'ying2 guo2')")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
pyodbc.Error: ('HY000', '[HY000] [FileMaker][FileMaker]  (509): Field failed required value validation test (509) (SQLExecDirectW)')

なるほど、ちゃんとエラーを返してくれるようです。必須項目の妥当性テスト(validation test)というのをやってるんですね。

ついでにコードも書いてみた

再利用も考えて、一応コードも書いてみました。

# -*- coding: utf-8 -*-
import pyodbc

class AccessCWordFM:
    def __init__(self):
        self.TabName = 'CWordsDB'
        self.DSN = 'DSN=CWord;UID=xxxxxx;PWD=xxxxxxxxxx'
        self.FieldList = ['Word', 'Pinyin', 'POS', 'Meaning', 'Source', 'Status', 'Batch']
        
        self.cxcn = pyodbc.connect(self.DSN)
        self.cursor = self.cxcn.cursor()

   def add_new_cword(self, cwdata):
        sqlString = []
        sqlString.append('insert into')
        sqlString.append(self.TabName)
        sqlString.append('(')
        for i in range (7):
            sqlString.append(self.FieldList[i])
            if i < 6:
                sqlString.append(',')
        sqlString.append(')')
        sqlString.append('values')
        sqlString.append('(')
        for i in range(7):
            sqlString.append("'" + cwdata[i] +"'")
            if i < 6:
                sqlString.append(',')
        sqlString.append(')')

        sqlCommand = " ".join(sqlString)
#        print sqlCommand
        self.cursor.execute(sqlCommand)
        self.cxcn.commit()

もちろんInsertだけではなくてSelect用のモノなども作ってみましたが一例と言うことで。
これを使うときは、こんな感じにすれば良いと。

>>> reload(accesscwordfm)
<module 'accesscwordfm' from '/Users/ken/Documents/workspace/NLTK Learning/scripts/accesscwordfm.py'>
>>> from accesscwordfm import *
>>> cword = AccessCWordFM()                                                     
>>> cword.add_new_cword(['搬运工', 'ban1 yun4 gong1', '名詞', '運ぶ人', '本', 'New', '1'])
insert into CWordsDB ( Word , Pinyin , POS , Meaning , Source , Status , Batch ) values ( '搬运工' , 'ban1 yun4 gong1' , '名詞' , '運ぶ人' , '本' , 'New' , '1' )

最初のReloadは、ソース直した後に読み込んだから。通常はimportでOKですね。あとは、生成されたSQL文が見たかったのでPrint文が追加してあります。個人的には、" ".join(x)にこんな使い方が出来たとはというのがツボだったりします(笑)。

イメージとしては、外部ファイルを読み込んでそれをリストに形成してあげる。そのリストをパラメータにして、このファンクションを呼んでDBに登録するという流れになるかと思います。もちろん、それの前に妥当性&重複チェックを組み込む必要はあるだろうし、複数レコード処理を想定した場合は、Commit()はこのファンクションから外出しして、処理が終わったところ(or 任意のタイミング)でCommit出来た方が良いかも知れませんね。

あと、それとなくスルーしていた文字コードの問題とかも。。。

・・・・先は長いっすな。

Filemaker to Python (4) - パターン検索

とりあえず文字化けというか文字が途切れる問題は回避でき、Filemakerのデータベースにアクセスする手順は整ったと言えそうです。Filemaker ODBCドライバ経由で使用できるSQLについては、この資料の中に書いてあります。ただ、今回は中国語の単語のデータベースにアクセスするのが目的で、Python側に持ってきたデータをNLTKの枠組みでいじるので、ODBC側にそこまで複雑なSQLを発行する必要はありません。ただ、パターン検索ぐらいは使うと思うので動作検証してみましょう。

パターン検索を試す

まずはお約束の部分。

>>> import pyodbc
>>> cxcn = pyodbc.connect('DSN=CWord;UID=xxxxxx;PWD=xxxxxxxxxxx')
>>> cursor = cxcn.cursor()

中国語にありがちな、"子"で終わる単語を探してみましょう。

>>> cursor.execute("select Word, Pinyin from CWordsDB where CWordsDB.Word like '%子'")
<pyodbc.Cursor object at 0x102716b10>
>>> rows = cursor.fetchall()
>>> for row in rows:
...     print row.Word, row.Pinyin
... 
小圏子 xiao3 quan1 zi
寨子 zhai4 zi
汉子 han4 zi
盘子 pan2 zi
积极分子 ji1 ji2 fen1 zi3
钉子 ding1 zi
钻空子 zuan1 kong4 zi
不入虎穴,焉得虎子 bu2 ru4 hu3 xue2 yan1 de2 hu3 zi3
包子 bao1 zi?
杯子 bei1 zi
被子 bei4 zi?
....
桔子 ju2 zi
日子 ri4 zi

ピンインの後ろに'?'が付いているのはソースデータ側(Filemaker)の問題です。後述しますが、これはファイルメーカー側で直接データを修正することにします。修正後にもう1回SQLを発行すれば、変更後のデータを拾ってくれるようです。

もう1パターン、(あまり数は多くないと思いますが)比較の意味で"子"で始まる単語を探してみましょう。

>>> cursor.execute("select Word, Pinyin from CWordsDB where CWordsDB.Word like '子%'")
<pyodbc.Cursor object at 0x102716b10>
>>> rows = cursor.fetchall()
>>> for row in rows:
...     print row.Word, row.Pinyin
... 
子公司 zi3 gong1 si1
子弹 zi3 dan4
子孙 zi3 sun1

where句のlikeのところを弄っただけです。予想通りあまり数は多くありませんが、3つほどありましたね。ちなみに「子弹」は弾丸のことです。

もう少しだけ複雑な検索

今度は、likeのところを'%子%'で検索してみましょう。

>>> cursor.execute("select Word, Pinyin from CWordsDB where CWordsDB.Word like '% 子%'")
<pyodbc.Cursor object at 0x102716b10>
>>> rows = cursor.fetchall()
>>> for row in rows:
...     print row.Word, row.Pinyin                                              
... 
小圏子 xiao3 quan1 zi
寨子 zhai4 zi
汉子 han4 zi
盘子 pan2 zi
望子成龙 wang4 zi3 cheng2 long2
积极分子 ji1 ji2 fen1 zi3
钉子 ding1 zi
钻空子 zuan1 kong4 zi
电子邮件 dian4 zi3 you2 jian4
子公司 zi3 gong1 si1
不入虎穴,焉得虎子 bu2 ru4 hu3 xue2 yan1 de2 hu3 zi3
....
日子 ri4 zi
子孙 zi3 sun1

真ん中に'子'がある単語だけが選ばれるのかなと思いましたが、それら(望子成龙, 电子邮件)に加えて、これまで見てきた'子'ではじまるもの、'子'で終わるものも選択されています。

ここは、とりあえず「ふーん」でスルーしておきます。

今度はピンインを使っての実験です。bei ziという発音の単語を探してみます。今回はbeiの声調は問わずに選ぶのが目的です。

>>> cursor.execute("select Word, Pinyin from CWordsDB where CWordsDB.Pinyin like 'bei%zi'")
<pyodbc.Cursor object at 0x102716b10>
>>> rows = cursor.fetchall()
>>> for row in rows:
...     print row.Word, row.Pinyin                                              
... 
杯子 bei1 zi

ん?「被子」も出てくることを期待していたのですが。これは、先ほど述べたように、Filemaker側の元データにおいて、'bei4 zi?'というように不要なクエスチョンマークが付いていたからと思われます。なので、Filemaker側で元データを修正して、もう1回SQLを発行します。

>>> cursor.execute("select Word, Pinyin from CWordsDB where CWordsDB.Pinyin like 'bei%zi'")
<pyodbc.Cursor object at 0x102716b10>
>>> rows = cursor.fetchall()
>>> for row in rows:
...     print row.Word, row.Pinyin                                              
... 
杯子 bei1 zi
被子 bei4 zi

出てきましたね。

なぜこの実験が必要か

今回は語学学習の単語DBを材料にしているので、実はODBC側でそこまで込み入った条件のSQLを発行してデータを絞り込む必要はありません。というのも、単語DBは自分で学習した単語が入っているだけなので、この先どんなに単語数が増えてもせいぜい1万語前後にしかならないでしょう。なので、DB全体をPython側で読み込んで、処理してもそれほどマシンに負荷が掛かるとは考えていません。

とは言え、Python自体も処理速度は決して速くないので、大容量のデータ処理には向かないと言われています。今後、大きな別の大きなデータを扱うような場面に遭遇したときには、必要な処理に対してどこまでデータを読み込む(絞り込む)のかというのは、意外に大事になるかも知れません。

その場合は、DBもFilemakerを使うという選択にはならないと思いますが(笑)

おまけのおまけ

ここまで書いてきて、SQLが空振りに終わったときにはどうなるかという挙動を確認するのを忘れていたので、最後に付け加えておきます。

>>> cursor.execute("select Word, Pinyin from CWordsDB where CWordsDB.Pinyin like 'xxxxxxx'")
<pyodbc.Cursor object at 0x102716b10>
>>> rows = cursor.fetchall()
>>> for row in rows:
...     print row.Word, row.Pinyin                                              
... 
>>> rows
[]

エラーが吐かれるのではなく、空っぽのリストが返るようです。この方がコーディングとしては扱いやすいですよね。