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

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

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

紹介した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冊分が登録できているようです。

Web画面の改良

検索画面の変更

前述のように、データベースの側でキーワードを4つのカラムに分けたので、各カラムを検索できるように検索画面でも各項目を独立させました。

図1 改良したキーワード入力画面
図1 改良したキーワード入力画面

もっともSQL的には検索対象とするカラムを替えるだけなので、inputタグのhidden要素で対象とするカラムを区別しているだけです。

<H2>食材検索</H2>

使いたい食材名を入れてください

<form action="search.php">
<p>
キーワード
<input type="text" name="key">
<input type="hidden" name="col" value="ingrs">
</p>
</form>

<H2>タイトル検索</H2>
特集記事名からの検索

<form action="search.php">
<p>
キーワード
<input type="text" name="key">
<input type="hidden" name="col" value="title">
</p>
</form>
...

このフォームを受けとるsearch.phpでは、$keyや$colの値を用いてSQL文を作成し、データベースを検索します。

合わせて「年/月検索」として、データベースに新しく追加したvolsテーブルを引いて登録済みの号を調べ、一冊全体の情報を返すような機能も追加してみました。

画面表示の改善

前回紹介した画面表示用のコード(show.php)では、search.phpから送られたページ情報を実際のファイル名に変換し、その画像ファイルを表示する機能しかありません。しかし、⁠きょうの料理」の場合、ひとつのレシピが複数ページにまたがっていることも多く、表示しているページの前後のページへ直接移動できないと不便です。そこで画面表示用のコードにページを移動する機能を付けてみることにしました。

前回のshow.phpでは"2009-01-page_0100.jpg"というページ情報を、"Pages/2009/01/page_0100.jpg"という画像ファイルへのパス名に変換しました。一方、前後のページを示すためには、page_0100.jpg を元に、page_0099.jpgとpage_0101.jpgを作ることになります。

ページ番号を4ケタに合わせるのが厄介そうですが、幸いPHPのformat文にはパディング指定子(')が用意されているので、前後のページはこんな感じで生成できそうです。

$id = $_GET['id'];             // 2009-01-page_0100.jpg
$dt = explode('-', $id);       // $dt[0]=2009 $dt[1]=01 $dt[2]=page_0100.jpg
$tmp = explode('_', $dt[2]);   // $tmp[0]=page $tmp[1]=0100.jpg 
$tmp2 = explode('.', $tmp[1]); // $tmp2[0]=0100、$tmp2[1]=jpg

$np = sprintf("%'.04d", $tmp2[0] + 1);
$next_page = sprintf("%s-%s-page_%s.jpg", $dt[0], $dt[1], $np); // 2009-01-page_0101.jpg
$pp = sprintf("%'.04d", $tmp2[0] - 1);
$prev_page = sprintf("%s-%s-page_%s.jpg", $dt[0], $dt[1], $pp); // 2009-01-page_0099.jpg

$next_page, $prev_pageとして生成した文字列はshow.phpが受けとる形式に合わせたので、これらを引数にshow.phpを呼びだせば指定したページへ移動することができます。そのためのリンクを画像ファイルの左右に配置しましょう。

画像ファイルの上下は空いているので、検索結果のページに戻ったり、キーワード入力ページへ戻るようなリンクがあっても便利でしょう。キーワード入力ページへ戻るのはindex.phpへのリンクだけでいいものの、検索結果のページに戻るには再度search.phpを呼び出す必要があるので、あらかじめ検索キーワード($key)と対象項目($col)をsearch.phpから引きついでおくことにします。これらをtableタグを使って表示するページの回りに配置します。

<table border="1">
  <tr> <td colspan="3" align="center">
           <?php printf("<a href=\"search.php?key=%s&col=%s\"> 検索結果に戻る </a>", $key, $col); ?>
       </td>
  </tr>
  <tr> <td>
           <?php printf("<a href=\"show.php?id=%s&key=%s&col=%s\"> 前ページ ", $prev_page, $key, $col); ?>
       </td>
       <td>
            <?php  printf("<img src=\"Pages/%s/%s/%s\" width=800> </a> \n",  $dt[0], $dt[1], $dt[2]);?>
       </td>
       <td> 
            <?php printf("<a href=\"show.php?id=%s&key=%s&col=%s\"> 次ページ ", $next_page, $key, $col); ?> 
      </td>
  </tr>
  <tr> <td colspan="3" align="center">
           <a href="index.php"> トップに戻る </a>
       </td>
  </tr>
</table>

実際のページ表示画面は次のようになります。

図2 改良したページ表示画面
図2 改良したページ表示画面

前後のページへのリンクを付けることで、検索結果のページだけでなく、その前後のページも自由に読み進めることができるようになり、⁠レシピ集」「電子書籍」を融合したような使い方が可能になりました。


実のところ、⁠きょうの料理」で紹介されたレシピの多くは、⁠株)NHKエデュケーショナルが運営している「みんなのきょうの料理」サイトで公開されているので、わざわざテキストからレシピデータベースを作る必要は無いのかも知れません。

しかしながら「きょうの料理」のテキストには、レシピ以外の読み物的な記事も多く、それらを1年分まとめて眺めてみると、1冊だけ見ている時は気づけなかった連載記事の意図や著者のこだわりが見えてきて結構面白かったりします。

何よりも「ああでもない」⁠こうでもない」と苦労しながらシステムを作り、それで日々の生活が便利になるのはうれしい体験です。与えられたアプリやサイトを受動的に使うだけでなく、手を動かして工夫しながら自分で作る、というのは、⁠きょうの料理」の勧める自炊の精神と同じだろう、と、個人的には納得しています(苦笑⁠⁠。

おすすめ記事

記事・ニュース一覧