Perlとバーコードリーダーで本棚整理をするよ! #vgadvent2012


こんにちはこんにちは、VOYAGE GROUPエンジニアblog Advent Calendar 17日めを担当するmonmonといいます!
どんなネタにしようか土曜日まで迷ったあげく、せっかくの会社のAdvent Calendarなので会社に関係する話にしました!

どんな話をするの?


会社の1FにOASISという図書室がありまして、その本の整理をしたいなぁと思いました(あくまで今回のネタ用なので実際はやらないです)。
なので、今回はその話をしようと思います。
ちなみに上の展開写真がOASISね。

何でこの話にしたの?


理由は3つありまして、

という感じです。


仕事でもそうだけど、何かを作り出すときにこういう部品をつなぎ合わせることで形になっていくってのは楽しいものですね。
あぁ!そういえばジョブズも「役に立つ可能性があるとは思わなかった点と点が、10年後に繋がって素晴らしいものができた」みたいな良い話をしてましたね!それだ!

処理の流れ


さぁ!ということで処理の流れだ!最終的に以下のような感じになったよ!

  1. バーコードリーダーでバーコードを読み取り、ISBN13をqueueに入れる
  2. queueからISBN13を取り出しAmazon Product Advertising APIで検索
  3. あればさらにY!の日本語形態素解析API形態素解析
  4. それらをSQLiteに突っ込む
  5. あとは全文検索でうまー

これをひとつずつ見ていこう!

バーコードリーダーでバーコードを読み取る


バーコードリーダーはこれです。
USBで繋ぐと「ぴっ!」って鳴って、バーコードを読み込むと上の部分が緑に光り、ISBNを入力してくれます。便利!

ジョブキューを使う


ということで次。
ISBNを読み込んだ後、その検索結果を待たずに次のISBNを読み込みたいためジョブキューを使います。
で、今年のYAPC::AsiaでもQudoの話があったし、なのでQudoを使ってみようかなと調べたんですが、
調べているうちにこの辺の記事で今回の用途ならJonkでいいかもと思い変更しました。


Jonkの使い方は
https://metacpan.org/module/Jonk

https://metacpan.org/module/Jonk::Cookbook::Basic
に書いてあり、これをまるっとコピーしてきたらサクッと動くくらい楽で、ほぼそのままコピペです。ありがたいありがたい。
2年前のPerl Advent Calendarでも取り上げられているのでもっと難しいことやるときはこっちも参考にしようかなと思ってました(が、時間なくてここまでいかず)。


具体的なコードはこんな感じ。while 1で無限ループ作ってひたすらISBNをキューに突っ込むだけです。

#!perl
# script/jonk-client.pl
use strict;
use warnings;
use FindBin;
use DBI;
use Jonk::Client;

use lib "$FindBin::Bin/../lib";
use MyApp;

my $setting = MyApp->new(appdir => "$FindBin::Bin/../")->setting;

my $dbh = DBI->connect("dbi:SQLite:$setting->{jonk}->{db}",'','');
my $jonk = Jonk::Client->new($dbh);

while (1) {
    print 'input ISBN: ';
    next if <STDIN> !~ m/(\d+)/xms;

    my $isbn = $1;
    my $job_id = $jonk->enqueue('Book', $isbn);
    print "enqueued job_id: $job_id\n";
}


で、取り出して仕事する方はこんな感じ。実際のロジックはMyApp::Worker::Bookにあるので、そっちを呼び出しているだけですね。

#!perl
# script/jonk-worker.pl
use strict;
use warnings;
use FindBin;
use DBI;
use Jonk::Worker;

use lib "$FindBin::Bin/../lib";
use MyApp;
use MyApp::Worker::Book;

my $setting = MyApp->new(appdir => "$FindBin::Bin/../")->setting;

my $dbh = DBI->connect("dbi:SQLite:$setting->{jonk}->{db}",'','');
my $jonk = Jonk::Worker->new($dbh => {functions => [qw/Book/]});
my $worker = MyApp::Worker::Book->new(
    appdir => "$FindBin::Bin/../",
    setting => $setting,
);

while (1) {
    if (my $job = $jonk->dequeue) {
        warn "work job: $job->{arg}\n";
        $worker->work($job);
    } else {
        sleep(3); # wait for 3 sec.
    }
}


ここで呼び出されたWorkerが

の3つの処理を行います

Amazon Product Advertising APIを使う


これは超簡単。Amazonのサイトで自分のkeyとsecret、それにassociate_tagの3つを取得した後、
CPANにあるURI::Amazon::APAを使えば良いです。
弾さんのブログに使い方の記事もあるのでハマる所はないですね。
唯一のハマりどころは日本のURLである「http://ecs.amazonaws.jp/onca/xml」の方を使うということかしら。


コピペするだけで本の検索ができちゃうなんて素敵ですね!

形態素解析APIを使う


さて、つづいて形態素解析です。
これは何で必要になったかと言うと、FTSを使うものの、タイトルなどを一綴りのまま入れてしまうとヒットしないパターンがあるのでそれをどうにかしたかったから。
以下のブログも参考にしました。
http://blog.mwsoft.jp/article/34911345.html


なので、「MeCabを入れて…」と考えたんですが、「今回そんなにガッツリやる必要ないのでもっと楽なのがいいなぁ」と方向修正。
結果、Y!の日本語形態素解析APIです。
で、「これもCPANにあるんじゃないかなぁ」と思ったらやっぱりあったよ!
https://metacpan.org/module/WebService::YahooJapan::WebMA


ただしURLが古いので以下のように変更して使います。

$WebService::YahooJapan::WebMA::APIBase = 'http://jlp.yahooapis.jp/MAService/V1/parse';


具体的なコードはこんな感じ。ほぼSYNOPSISのままですが、word_listの要素が1つの場合に配列になっていなかったのでそこだけ対応しました。

    my $api = WebService::YahooJapan::WebMA->new(
        appid => $self->appid,
    );
     
    my $res = $api->parse(
        sentence => $sentence,
        ress => 'ma',
        response => 'surface,reading,base form'
    ) or croak $api->error;
    my @word_list = (ref $res->{ma_result}->{word_list} eq 'ARRAY')
        ? @{$res->{ma_result}->{word_list}}
        : ($res->{ma_result}->{word_list});
     
    $self->_to_string($self->_flatten(@word_list));

コレを使って、「タイトル」「作者」「出版社」の3つを形態素解析し、それらをspase区切りの文字にしてDBに突っ込むようにしてます。


具体例をあげるとPerl CPANモジュールガイドの場合には、
Perl CPAN もじゅーる モジュール ガイド がいど つみた 冨田 尚樹 なおき わーくす こーぽれーしょん ワークス コーポレーション」のような文字列がDBに突っ込まれるわけです。


ふむ!すごいですね!これで「perl」でも「cpan」でも「モジュール」でも「冨田」でも検索にかかるようになりました!それだけじゃなく「もじゅーる」みたいな読みでもかかるようになりました!そして、なんと「つみた」でもかかります!すごい!

SQLite FTSを使う


さてさて、最後にDBについて。
今回の本データをためるDBですが、今まで話に出て来た通りSQLiteのFTS4を使いました
(ファイルだから開発も楽だしね!)


FTSの使い方は簡単で、いつものcreate tableの文を以下のようにするだけです。

CREATE VIRTUAL TABLE book USING fts4(
    isbn,
    asin,
    title,
    author,
    manufacturer,
    words
);

「CREATE VIRTUAL TABLE」になっているところと、「USING fts4」になっているところが違う所で、他は特に変わりません。
ただ、UNIQUE INDEX利かななくなっちゃうため、アプリ側でユニーク確認をするようにしました。
(もしかしたら何かやり方あるのかもだけど調べきれず。Stack Overflowにはtriggerとかでどうにかする例があったけどそこまでの話ではなかったので深く見てないです)


SQLの発行方法は「match」というキーワードを使うだけ。
また、SQLite FTS3 and FTS4 Extensionsにあるように「where table名 match 'hogehoge'」というようにテーブル名を指定すればカラムをまたいで探してくれます。


具体的なqueryは以下のような感じ。

SELECT * FROM book WHERE book MATCH 'perl cpan';

これで全てのカラムでperlcpanに引っかかるデータを取り出せます。

DEMO的なやつ


以上でできあがった仕組みがこちらです。
https://github.com/monmon/vgadvent2012-app


それでは、写真で使っている様子を見て行きます。


1. まずjonk-client.plを起動するとISBNの入力待ちになります。

2. あとはひたすらバーコードリーダーでISBNを読みまくります。入力するとすぐに「早く次のISBNくれよぉ!」状態になるのでひたすら入れまくります。

3. で、一方違うターミナルでjson-worker.plを起動させておけば、以下のようにキューに入ったISBNを取り出し、一連の処理を始めます。

4. 結果、SQLiteに本データがたまるので、あとは全文検索で検索してうまーです。

5. 「タイトル」と「作者」の両方にあるような「単語」も検索できて便利ですね!!!!

まとめ


初めは「iPhoneでガンガンscanしてブクログに突っ込んで終わり」みたいなネタを考えたんですが、いざバーコードリーダーでやってみるとISBN読み込んでqueueに突っ込むだけだからよりガンガンscanできて楽しかったです。
(ちょうど大そうじの季節だったし、このネタにして良かった!)
まぁエラー処理とか全然考えてないし、既に「原作」と「漫画」のように2人の作者がいる場合にはauthorカラムに「ARRAY(0x7f8f6ac2fe30)」のように突っ込まれちゃうbugが判明したけど気にしません。
作ってみないとわからないことあるしね!リーン何ちゃらだ!


ということで、「コピペばっかりでも簡単にここまで作れるよ、でもコピペばっかりだと全然考慮が足りないから仕事ではちゃんと理解して物を作っていこう」という話でした!
明日は誕生日が終わったばかりの@tadasyさんです!今からでも間に合うのでみんなmention飛ばして祝ってみてください!