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

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

いつの間にか夏至もすぎ、そろそろ今年も半分が過ぎるころになりました。毎年のことながら、この季節になると少しでも部屋を片づけて風通しをよくしたくなります。筆者の場合、まず整理しなければならないのは本棚に溢れている書籍です。

本連載でも何度か書籍の電子化の話題をとりあげたように、SANEプロジェクトが対応しているスキャナを選べば、Linux環境だけで十分自炊が可能で、手元でも蔵書の電子化を少しずつ進めています。もっとも、しっかり綴じられたハードカバー本や数百ページにおよぶ大著はスキャンするのも大変なので、いきおい雑誌や語学テキストの類に手が伸びます。

そのような作業をしている中で「おもしろい使い方ができそうだ」と思ったのが「きょうの料理」のテキストです。今回はそのアイデアを簡単なコードで実装してみた例を紹介しましょう。

「きょうの料理」のテキスト

いうまでもなく「きょうの料理」は、NHKで50年以上に渡って放送されている料理番組です。季節に応じた旬の食材を使い、著名な講師が実際に調理しながらさまざまなレシピを紹介する番組スタイルは高い人気を誇っています。

もっとも筆者自身はテレビを見なくなって久しく、⁠きょうの料理」も実際の番組は見ていません。しかしながら、紹介される旬のレシピは実家の料理屋の参考になるかと思って、番組テキストはここ10年ほど買い貯めていました。

「きょうの料理」のテキスト群
「きょうの料理」のテキスト群

「きょうの料理」のテキストは1冊ずつを見ると薄いものの、毎月発行されるので1年で12冊、数年分も貯まるとずいぶんな嵩(かさ)になります。そこで少し古くなったものから電子化してみることにしました。

1冊150ページ前後のテキストは、手元のScanSnapなら20分もあればスキャンできます。当初は、他の自炊書籍同様、読み取った画像ファイルはzip形式でアーカイブし、"knr-2009_01.zip"のような形で整理していました。しかしスキャンを進めていくうち、⁠きょうの料理」のテキストは「本」というよりは「料理のレシピ集」なので、先頭から順に読み進める必要はないし、むしろ料理名や食材から該当するページを直接引けた方が便利じゃなかろうか、と思い始めました。

もちろん「きょうの料理」には、年単位の連載記事や季節に応じた食材やメニューの紹介、人気講師の特集記事など、単純な「レシピ集」とは異なる、ページの大きなまとまりや読み物的な要素もあります。しかし、各号の末尾には紹介した料理を主要食材ごとに整理した「さくいん」が付きますし、毎年3月号には1年間の「総さくいん」が用意され、食材や講師名から料理を探すことも可能です。 この「さくいん」的な機能を電子書籍に持たせることができれば利用範囲は大いに広がりそうです。

ページをOCRで読み取ってテキストファイル化できれば全文検索も簡単なのになぁ…、と考えてみたものの、⁠きょうの料理」のテキストの場合、1ページ中に料理や手順の写真、食材を示す横書き文章、手順を示す縦書き文章が混在していて、OCRに読み取らせるのも難しそうです。

何かいい方法はないかなぁ…、としばらく考えた結果、⁠レシピ集」に必要なのは料理名食材⁠、あとはその料理の紹介者である講師名ぐらいなものだから、これくらいのデータなら手動で入力しても何とかなるんじゃなかろうか、という気になりました。各ページごとにこれらキーワードを入力したデータファイルさえ用意できれば、指定したキーワードから該当ページを表示するようなシステムを作るのは簡単でしょう。入力は人力なものの「料理名」⁠食材」⁠講師名」ぐらいならそう時間もかかるまい、こんな甘い目論みから「きょうの料理」のデータベース化作業を始めてしまいました(苦笑⁠⁠。

ページごとのキーワード付け

ページ単位でアクセスするにはzipファイルで1冊の形を保つより、各ページを直接読めた方が便利です。そこで、zipファイルにまとめていた画像ファイルを再度展開し、"2009/01/page_001.jpg"のような構成にまとめ直しました。その上で各号ごとに全ページのキーワードをまとめたデータファイルを作ることにしました。

各ページは"page_0100.jpg"のようなファイル名になっているので、まず保存されたディレクトリにある全ページをまとめた雛形ファイルを作ります。

$ cd 2009/01
$ ls page* | sed "s/jpg/jpg: /" > 2009-01.dat
$ cat 2009-01.dat
page_0001.jpg: 
page_0002.jpg: 
page_0003.jpg:
...

次に、comix等のビューワーでそれぞれのページを眺めながら、このファイルに「料理名」⁠食材」⁠講師名」を入力していきます。しばらく入力してみたところ、それぞれの項目を大きくまとめる見出しがあっても便利かな、と思い、⁠美しい味ことば」「冬のお手軽洋風おかず」といった特集名も追加することにしました。

page_0001.jpg: 放送カレンダー
page_0002.jpg: 目次1
page_0003.jpg: 目次2
page_0004.jpg: CM タカラ本みりん
page_0005.jpg: 美しい味ことば 一月 雪花和 しめさば おから
page_0006.jpg: 冬のお手軽洋風おかず
page_0007.jpg: 冬のお手軽洋風おかず
page_0008.jpg: 冬のお手軽洋風おかず 塩田ノア バスク風おかずきんぴら 豚肩ロース ピーマン ごぼう にんにく
page_0009.jpg: 冬のお手軽洋風おかず 塩田ノア バスク風おかずきんぴら 写真
...

最近の横長ディスプレイだと、Emacsと画像ビューワを横に並べて一覧しながら作業できるので便利です。

キーワードの手動入力中
キーワードの手動入力中

当初は、このデータファイルをgrepして該当するページを返すようなスクリプトを書けばいいかと思っていたものの、データファイルが増えるとそれぞれのファイルをgrepして回るのも面倒です。また、検索結果もページ名を返すだけでなく実際にそのページを見れた方が便利だろうと考えて、データベースを利用した簡単なWebアプリとして組んでみることにしました。

データベースとWebアプリの作成

システムは以前紹介した「お家通信カラオケ」同様、データベースにSQLite3を用い、Webアプリ側はPHPデータを登録する作業はPythonで組むことにしました。

SQLite3の場合、テキスト型のデータはUTF形式のコーディング(UTF-8やUTF-16)しか使えないものの、格納する文字数をあらかじめ決めておく必要がないので設計は簡単です。まずはデータファイルの構造をなぞって、1レコードにページ名とそのページに関するキーワードをテキスト形式で収める、単純な構造のデータベースを作ってみることにしました。

$ sqlite3 knrdb.sql
SQLite version 3.8.5 2014-06-04 14:06:34
Enter ".help" for usage hints.
sqlite> create table pages(page text, keyword text);
sqlite> .q
$ 

先に作ったデータファイルをこのデータベースに登録する処理は、以前に書いたコードを切り貼りしてPythonで組みました。データベースに登録する際のページ名は、複数冊の中から一意にページを区別できるように、データファイルから与えられる年、月のデータを各ページ番号に追加してlong_pageという名前にしています(27行目⁠⁠。なおデータファイルはEUC-JPな文字コードと想定しています(29行目⁠⁠。

 1 #!/usr/bin/python
 2 # -*- coding: euc-jp -*-
 3 
 4 import sys,os,sqlite3
 5 
 6 def insert_db(cursor, t):
 7     try:
 8         print("inserting {}".format(t))
 9         cursor.execute('insert into pages values(?, ?)', t)
10     except sqlite3.Error, e:
11         print("An error occurred:{}".format(e.args[0]))
12 
13 def main():
14     dbname = 'knrdb.sql'
15 
16     connection = sqlite3.connect(dbname)
17     cursor = connection.cursor()
18 
19     file = sys.argv[1]
20     (year, month) = file.replace('.dat','').split('-')
21 
22     with open(file, 'r') as f:
23         lines = f.readlines()
24 
25     for l in lines:
26         (short_page, conts) = l.strip().split(':')
27         long_page = year + '-' + month + '-' + short_page
28         
29         conts_utf = conts.decode('euc-jp')
30         insert_db(cursor, (long_page, conts_utf))
31         connection.commit()
32 
33 if __name__ == "__main__":
34     main()

このコードをinsert.pyという名前にして、先に作ったデータファイルを流しこんでみます。

$ python insert.py 2009-01.dat 
inserting ('2009-01-page_0001.jpg', u' \u76ee\u6b21 \u30ab\u30ec\u30f3\u30c0\u30fc')
inserting ('2009-01-page_0002.jpg', u' \u76ee\u6b211')
inserting ('2009-01-page_0003.jpg', u' \u76ee\u6b212')
inserting ('2009-01-page_0004.jpg', u' CM \u30bf\u30ab\u30e9\u672c\u307f\u308a\u3093')
...

データが正しく登録されたかsqlite3コマンドで確認します。この際、使用する端末の文字コードはUTF-8にしておかないとsqlite3の出力を正しく表示できません。

$ sqlite3 knrdb.sql 
SQLite version 3.8.5 2014-06-04 14:06:34
Enter ".help" for usage hints.
sqlite> select * from pages;
2009-01-page_0001.jpg| 目次 カレンダー
2009-01-page_0002.jpg| 目次1
2009-01-page_0003.jpg| 目次2
2009-01-page_0004.jpg| CM タカラ本みりん
...
sqlite> select * from pages where keyword like '%ベーコン%';
2009-01-page_0012.jpg| 冬のお手軽洋風おかず 塩田ノア タルティフレット じゃがいも たまねぎ カリフラワー ベーコン カマンベールチーズ
2009-01-page_0017.jpg| 冬のお手軽洋風おかず 城川朝 レンジミネストローネ キャベツ にんじん たまねぎ にんにく じゃがいも ベーコン 赤いんげん豆 ミックスハーブ トマトの水煮
2009-01-page_0019.jpg| 冬のお手軽洋風おかず 城川朝 たらのベーコン巻き 生だら ベーコン
  ...

データベースはきちんと作成できているようなので、このデータベースを検索するためのコードをPHPで書いてみます。11~21行がデータベースと接続、23~34行がデータベースを検索する関数で、FORM経由で送られてきたキーワードを後者の関数を使ってデータベースから引き、その結果を一覧表示するだけのコードです。このコードはsearch.phpという名前にしました。

 1 <!DOCTYPE html>
 2 <html>
 3 <head>
 4 <meta http-equiv="Content-Language" content="ja">
 5 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
 6 <title>KNR keyword search</title>
 7 </head>
 8 <body>
 9 
10 <?php
11 function DBconnect($name) {
12    $dbname = sprintf("sqlite:%s", $name);
13    try {
14        $db = new PDO($dbname);
15        echo"DB connected<br>\n";
16    }
17    catch (PDOException $e) {
18        exit("cannot connect to DB".$e->getMessage());
19    }
20    return $db;
21 }
22   
23 function query_db($db, $key) {
24     $sql = sprintf("select * from pages where keyword like '%%%s%%' order by page", $key);
25     echo "sql:$sql<br>\n";
26     foreach ($db->query($sql) as $line) {
27          $item = array(
28                  "page" => trim($line['page']),
29                  "keyword" => trim($line['keyword'])
30                  );
31          $results[] = $item;
32     }
33     return $results;
34 }
35 
36 $dbname = 'knrdb.sql';
37 
38 $key = $_GET['key'];
39 echo "key:$key <br>";
40 $db = DBconnect($dbname);
41 $res = query_db($db,  $key);
42 foreach ($res as $line) {
43     printf("<a href=\"show.php?id=%s\">%s : %s </a> <br>\n", $line['page'], $line['page'], $line['keyword']);
44 }
45 printf("<br><a href=\"index.html\">検索ページへ戻る </a><br>\n");
46 ?>
47 
48 </body>
49 </html>

search.phpを呼び出す側はindex.htmlという名前にしました。このコードはキーワード入力を受け付けて、FORM経由でsearch.phpを呼び出すだけです。

 1 <!DOCTYPE html>
 2 <html>
 3 <head>
 4 <meta http-equiv="Content-Language" content="ja">
 5 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
 6 
 7 <title>KNR search</title>
 8 </head>
 9 <body>
10 <H2>キーワード入力</H2>
11 <form action="search.php">
12 <p>
13 キーワード<input type="text" name="key">
14 </p>
15 </form>
16 
17 </body>
18 </html>

search.phpでは43行目でキーワードに該当するページの画像を表示するためのリンクを出力します。そのリンクを辿った際、実際に画像を表示するコードはshow.phpとしました。show.phpでは、search.phpから送られた"2009-01-page_0100.jpg"といったページ情報を"Pages/2009/01/page_0100.jpg"のような実際のファイル名に変換し、そのファイルを読み込んで<img>タグで表示します。

 1 <!DOCTYPE html>
 2 <html>
 3 <head>
 4 <meta http-equiv="Content-Language" content="ja">
 5 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
 6 <title>KNR page show</title>
 7 </head>
 8 <body>
 9 <?php
10    $id = $_GET['id'];
11    list($year, $month, $page) = explode('-', $id);
12    printf("<img src=\"Pages/%s/%s/%s\" width=\"800\">\n", $year, $month, $page);
13 ?>
14 </body>
15 </html>

先に作ったSQLite3のデータベースファイルとこれら3つのファイルは、httpd経由で利用できるように~/public_html/KNR_Test/に用意しました。

一方、⁠きょうの料理」をスキャンした画像ファイルはファイルサーバ上の/Document/Scans/KNR/// というディレクトリに保存しています。show.phpからこれら画像ファイルにアクセスするため~/public_html/KNR_Test/Pages/ というディレクトリを作って、そこに/Document/Scans/KNR/以下を--bindオプションを指定してマウントしておきます。--bindオプションを指定したマウントは、既存のディレクトリを別のディレクトリ以下に見せる、いわばディレクトリへのシンボリックリンクのような機能を果します。

$ mkdir ./public_html/KNR_Test/Pages
$ sudo mount --bind /Document/Scans/KNR ./public_html/KNR_Test/Pages

動作テスト

以上の環境を整えて、実際にキーワードから該当するページが表示できるかを試してみました。まず、index.htmlを開いて、⁠卵」というキーワードを指定します。

index.htmlにキーワードを入力
index.htmlにキーワードを入力

「卵」というキーワードはsearch.phpに送られ、データベースを検索して該当するページの一覧が表示されました。

データベースの実行結果
データベースの実行結果

適当なリンクを開いてやると、show.phpがスキャンしたページを表示しました。

該当するページの表示例
該当するページの表示例

今回のコードは必要最低限の機能しか実装していないので、ページ表示から戻るにはブラウザの「戻る」機能を使わなければならない等、さまざまな制約があります。しかしながら、わずか100行ほどのコードでキーワードからレシピを検索するシステムのプロトタイプを作れたのは大きな収穫です。次回は、このコードにさまざまな機能を追加して、より実用的なシステムにしてみる予定です。

おすすめ記事

記事・ニュース一覧