玩式草子─ソフトウェアとたわむれる日々

第72回 レシピデータベースを自炊する[その2]

この記事を読むのに必要な時間:およそ 5.5 分

前回⁠きょうの料理」のテキストをスキャンした画像ファイルを使って,指定したキーワードから該当するページを表示する機能だけを持ったレシピデータベースを試作してみました。

紹介した100行ほどのコードには最低限の機能を実装しただけなので,しばらく使っているとあちこちに必要な機能や改良すべき箇所が見えてきました。今回はそれらを紹介しつつ,システムをどのように修正していったかを紹介します。

データベースの改良

よみがなの追加

スキャンした画像データを見ながらキーワード・ファイルを作っているうち,⁠きょうの料理」の食材名や料理名の表記が,普段使いなれている表記と少し違っていることが気になり始めました。

例えば料理名を「卵焼き」とするか「玉子焼き」とするか(⁠きょうの料理」では「卵焼き」⁠魚の名前を「鯛」とするか「たい」とするか(同じく「たい」⁠野菜の名前を「玉ねぎ」とするか「たまねぎ」とするか(⁠たまねぎ」⁠調味料を「醤油」とするか「しょうゆ」とするか(⁠しょうゆ」⁠といった表記です。

これらの表記は「きょうの料理」内では統一されているものの,普段自分が使いなれている表記と異なっていると,検索キーワードを指定する際に「これはどう表記するんだっけ?」と考えることになって面倒そうです。そこで,VHDカラオケの曲名や歌手名をデータベース化した時と同様,キーワードにひらがなの読みを振ってみることにしました。

カラオケの楽曲名とは異なり,料理や食材の名前にそう凝った読み方はないだろう,と,たかを括って,以前書いたkakasiの共有ライブラリ(libkakasi.so)をPythonから呼び出すコードを流用してみたものの,⁠パン粉」「ぱんこな」になったり,⁠一味とうがらし」「ひとあじとうがらし」になったり,⁠紅しょうが」「くれないしょうが」になったりと,結果は芳しくありません。

この問題はkakasiが使っている辞書(kanwadict)内で優先される読みがこちらの想定と異なっているためで,読みの順番を修正すれば対応できるものの,バイナリ形式のkanwadictを修正するには,元となるkakasidictを編集してからmkkanwaコマンドで再生成する必要があり,気になる読みを修正するたびにこの作業を行うのも面倒です。

そこでPythonスクリプトの中にあらかじめキーワードと読みを対応させた辞書形式のデータを用意しておき,該当するキーワードはそこから読みを引くようにしてみました。講師名等の固有名詞もここに登録しておくのが便利そうです。

def set_namedict():
    name_dic = {
        '小林カツ代':'こばやしかつよ',
        '平野レミ':'ひらのれみ',
        ...
        '鶏手羽先':'とりてばさき',
        '高野豆腐':'こうやどうふ',
        'パン粉':'ぱんこ',
        '粗塩':'あらじお',
        '一味とうがらし':'いちみとうがらし',
        ....
    }
    return name_dict

def to_hiragana(str):
    dt = str.split(' ')
    kakasi = CDLL("libkakasi.so")
    argArray = c_char_p * 4
    args =  argArray( c_char_p("kakasi"), c_char_p("-ieuc"), c_char_p("-JH"), c_char_p("-KH"))
    kakasi.kakasi_getopt_argv(4, args)

    kakasi_do = kakasi.kakasi_do
    kakasi_do.restype = c_char_p

    dt_yomi = []
    for i in dt:
        if i in name_dict:
            dt_yomi.append(name_dict[i])
        else:
            cstr = c_char_p(i)
            dt_yomi.append(kakasi_do(i))

    yomi = " ".join(dt_yomi)
    return yomi

こうしておけば,気になる読みはスクリプトレベルで調整できるので,いちいちkanwadictに手を入れる必要が無くなりました。

データベースの拡張

前回紹介したように,このシステムで利用しているデータベースは1レコードにページ番号とキーワードをそれぞれテキスト型のカラムで並べただけのシンプルな構成です。しかしながら,前節で紹介したようにキーワードに読みを振ろうとすると,キーワードのカラムとは別に読みがなのカラムが欲しくなります。

また,データが増えてくるにつれキーワードのミスマッチも目立つようになりました。例えば魚の「たい」を使うレシピを調べたつもりなのに,⁠伝えたい味」「もっと知りたいレシピ」といったタイトルのページも引っかかります。このような状況を改善するために,データベースの構造を見直すことにしました。

今回のデータベースでは「きょうの料理」の各ページからキーワードとして「特集名」⁠料理名」⁠講師名」⁠食材リスト」を拾っています。そこで,これら4つのキーワードをそれぞれ独立のカラムにし,合わせてそれぞれの読みも対応するカラムに登録するように変更しました。また,データベースを作成する度にsqlite3コマンドを使うのも面倒なので,データ登録用スクリプトからデータベースを作成するような機能を組み込みました。

def init_db(dbname):
    conn = sqlite3.connect(dbname)
    conn.isolation_level = None
    cursor = conn.cursor()
    cursor.execute('''create table pages (page text, title text, title_yomi text, author text, \ 
         author_yomi text, name text, name_yomi text, ingrs text, ingrs_yomi text)''')
    return conn

def insert_pages(cursor, t):
    try:
        print("inserting {}".format(t))
        cursor.execute('insert into pages values(?, ?, ?, ?, ?, ?, ?, ?, ?)', t)
    except sqlite3.Error, e:
        print("An error occurred at titles:{}".format(e.args[0]))

一方,このスクリプトの入力になるキーワード・ファイル側も,どのキーワードがどのカラムに対応するのかを明示する必要があります。awk/gawkに慣れた世代としては,この手の作業には何か適当な文字を区切り記号にした固定順レコードを使いたいところですが,すでに各項目の並び順がまちまちなキーワード・ファイルを複数作成してしまっていたこともあり,最低限の修正で済むように異なる種類の括弧を使ってカラムと対応付けすることにしました。具体的には「特集名」を[],⁠料理名」を<>,⁠講師名」を(),⁠食材リスト」を{}で括って,それぞれの項目を示すことにしました。この仕様を使うと前回紹介したキーワード・ファイルは以下のようになります。

page_0005.jpg: [美しい味ことば 一月] <雪花和> {しめさば おから}
page_0006.jpg: [冬のお手軽洋風おかず]
page_0007.jpg: [冬のお手軽洋風おかず]
page_0008.jpg: [冬のお手軽洋風おかず] (塩田ノア) <バスク風おかずきんぴら> {豚肩ロース ピーマン ごぼう にんにく}
page_0009.jpg: [冬のお手軽洋風おかず] (塩田ノア) <バスク風おかずきんぴら> 
...

作成済みのキーワード・ファイルは手作業で修正しなければならないものの,このスタイルならばエディタの文字列置換機能を使って一括変換すれば多少は楽できるでしょう。

キーワード・ファイルの仕様変更に合わせてデータベース登録用スクリプトも修正が必要となり,Pythonのre(正規表現)モジュールを使って括弧の部分を抽出することにしました。PythonのreモジュールはsedやPerlとは異なり,正規表現のパターンを記述したオブジェクトを作って,そのメソッドで文字列を処理する形になります。

import re
...
get_title  = re.compile(r'\[.+?\]')
get_author = re.compile(r'\(.+?\)')
get_name   = re.compile(r'\<.+?\>')
get_ingr   = re.compile(r'\{.+?\}')

for l in lines:
    try:
        (page, txt) = l.strip().split(': ')
        long_page = year + '-' + month + '-' + page

        title = "".join(get_title.findall(txt)).replace(']','').replace('[','')
        title_yomi = to_hiragana(title)

        author = "".join(get_author.findall(txt)).replace(')','').replace('(','')
        author_yomi = to_hiragana(author)
        ....

取り出したtitleやauthorは,to_hiragana()でひらがなに変換してtitle_yomiやauthor_yomiに収めます。

必要な項目が揃えば,それらをタプルにまとめてデータベースに登録します。前回に比べると,ずいぶん登録すべきデータが増えてしまいました。

dt = (long_page, title.decode('euc-jp'), title_yomi.decode('euc-jp'), \
      author.decode('euc-jp'), author_yomi.decode('euc-jp'),  \
      name.decode('euc-jp'), name_yomi.decode('euc-jp'),  \
      ingrs.decode('euc-jp'), ingrs_yomi.decode('euc-jp'))
insert_pages(cursor, dt)
connection.commit()

さて,以上はページ番号とキーワード情報を関連づけたpagesテーブルに関する修正でした。一方,このようにデータベースや登録用スクリプト,キーワード・ファイルの修正を繰り返していると,どの号のデータが登録済みでどの号が未登録かが分かりづらくなってきました。そのため,登録の有無を記録するvolsというテーブルを新しく用意することにしました。このテーブルには登録済みの巻数(何年何月号という情報)を記録しておき,データの二重登録を予防します。煩雑になるのでコードの紹介は省き,sqlite3コマンドでデータベースの概略を紹介しておきます。

$ sqlite3 knrdb.sql
SQLite version 3.8.10.1 2015-05-09 12:14:55
Enter ".help" for usage hints.
sqlite> .schema
CREATE TABLE vols (vol text);
CREATE TABLE pages (page text, title text, title_yomi text, author text, author_yomi text, name\
 text, name_yomi text, ingrs text, ingrs_yomi text);
sqlite> select * from vols order by vol;
2005-02
2005-07
2005-08
...
2011-11
2012-07
sqlite> select count(*) from vols order by vol;
25

「きょうの料理」のテキストはあちこちに散らばっていて,目についたものから適当にスキャン,登録しているので,必ずしも年月の順には並んでないものの,およそ25冊分が登録できているようです。

著者プロフィール

こじまみつひろ

Plamo Linuxとりまとめ役。もともとは人類学的にハッカー文化を研究しようとしていたものの,いつの間にかミイラ取りがミイラになってOSSの世界にどっぷりと漬かってしまいました。最近は田舎に隠棲して半農半自営な生活をしながらソフトウェアと戯れています。

URLhttp://www.linet.gr.jp/~kojima/Plamo/index.html

コメント

コメントの記入