今日からPerlプログラマー 《最終回》スキャン資料検索システム スキャナとOCRソフトでgoogle風検索。 紙資料スキャン/OCR 「♪いつしか年も杉(過ぎ)の戸を……」。三月でもないのに、いきなり最終回だ。 長い間、ありがとうございました。では、来月から、『新・今日からPerlプログラ マー』を……バキ。そんなことはないのである。 スキャナとOCRソフトはもっているだろうか。紙資料はバンバンスキャンしてデー タに焼いて紙は棄てる。省スペースに加え、データは、検索できるんだから、くー。 見果てぬ夢とはこのことだ。現実にはスキャンがあまりに遅く、OCRソフトがナメ たように頭悪すぎで、1ページをデータ化するのに、10分もかかることが分かり、挫 折したことだろう。 バイトか?バイトを雇ってデータを取り込んでいくしかないのか?そんなことができ るのは、立花隆くらいのものである。俺がバイトしたいよ!あー、もう、このスキャナ 要らない(泣)。悲観に暮れずとも、安心してよい。だって、この件に関する限り、た いていそういうことになっているのだ。 そこで、視点を変えてみよう。このOCRソフト、全自動モードで使っても、キーワ ード用のテキスト・ファイルくらいは吐き出せる。つまり、前後の内容は訳が判らなく ても、たとえば、「学会資料」なんて言葉が拾えればよいのだ。 最近のスキャナは、速くなった。しかも、オート・シート・フィーダ(っていうの? )付き。で、OCRソフトが1ファイル・複数ページのTIFにしてくれる。BMPや JPGに較べ、TIFが強力なのは、1資料・1ファイルにできること。さらに、TI FからPDFへは一発変換。 ■スキャン資料保存の流れ 紙資料 ↓ スキャン画像(TIF) │ ├→PDF(表示・印刷) └→txt(キーワード検索用) おお。美しい流れではないか。txtとPDFを同じ名前にしておけば、「花とみつ ばち」なんて検索したら、「昭和歌謡リスト.txt」にマッチして、「昭和歌謡リス ト.pdf」を開けば、郷ひろみの名曲の情報にたどり着くという寸法。 そんなわけで、今回は、「d:\scan資料」以下に、好きにディレクトリを掘っ て、日本語ファイル名をつけて整理されたテキスト・データをgoogle風に前後の 文章付きで検索、クリック一発でPDFを開くスクリプトである。 名付けて、「スキャン資料検索システム」(カッコイイ。母さん、ついに俺もここま できたよ)。 AN HTTP Server(Webサーバ) 確かにApache好きと言われもした。安定しているとも。しかし、AN HTT P ServerがApacheに勝利することが、わずかに一点ある。それは、sj isファイル名完全対応。 「sjisファイル名を扱いたいのなら、cgiで、ファイル名をURLエンコード ・デコードすれば、たった一行の手間でしょう?Apacheは、ここでもメゲずに、 立派にやってのけるだろうよ。オイ?」。 それはそーなのだが、Webサーバには、「インデックス・リスト」という機能があ る。エクスプローラのように、ディレクトリをたどり、目的のファイルを手にできる仕 組み。検索システムの場合、検索にひっかからなかったときなど、ディレクトリからフ ァイルをたどる場合も多いだろう。 こんなとき、スクリプト側で手抜きして「一覧」というリンクをクリックすれば、w ebサーバの「インデックス・リスト」に渡すように設計したい。そうすれば、余計な ルーチンに頭を痛めることなく、サーバの機能を活かした運用ができるというもの。 この「インデックス・リスト」で、sjis完全対応をうたっているのがAN HT TP Serverなのだ。「http://www.st.rim.or.jp/~ nakata/」 からgetしよう。重いcgi、重いpdfファイルを使ったとき 、どうも、Apacheより不安定な気がするが、導入が簡単なのは、魅力的である。 何かかわいいし。 準備 「d:\scan資料」以下に、好きにディレクトリを作り、テキスト・ファイルと 、同じ名称のpdfを置いていこう。単にテキスト・ファイル検索として使っても便利 なので、pdfは必須ではない。 また、「d:\scan資料\img」に、「top.gif」という名前で、幅4 53×高さ88ピクセルのタイトルを作って入れておいてほしい。 AN HTTP Serverで、「d:\scan資料」をドキュメント・ルート とする。「オプション 一般」→「一般」タブ→「ドキュメント・ルート」に、「d: \scan資料」と記述。 ここで、「拡張子」の列から「.pl,.cgi」を選択後、「編集」をクリックし 、「#!の行を調べる」を必ずチェック。これにより、AN HTTP Server 上でperlとjperlが使い分けられる。今回は、sjisしか扱わないという前 提で、jperlで製作したので、これを忘れると動作しない。<「#!の行を調べる」をチェック> さらに、「エイリアス」タブから、「仮想パス」を「c\home\httpd\c gi-bin」などとして、ここにスクリプトを置くようにする。 最も重要なのは、「表示/インデックス」タブの「インデックス」項目の、「インデ ックス・リスト(ディレクトリ・リスト)を表示」に、必ずチェックを入れること。 スクリプト リスト1を見てほしい。追加機能は、いろいろ考えられるが、シンプルな部分だけに 機能を絞ったからこそ、このように短いスクリプトで実現できた。
<検索した様子> 最終回に相応しく、これまでの連載の総集編ともなっている。 たとえば、「sub saiki_filelist」は、再帰的にディレクトリを下り、ファイル名とデ ィレクトリ付きファイル名を「@flist」「@dflist」に入れるルーチンで 、以前使ったものをそのまま借用した。 デザインがgoogle風である。上の「一覧」をクリックすると、「インデックス ・リスト」が開く。これは大変便利だが、手法としては、単に「/」(ルート)に対し てリンクを張っているだけなのだ。
<インデックス・リスト> 「$marg = 60」の数字は、検索にマッチしたときに表示される前後の文字 数。好きに変更できる。 検索には、超簡単な手法を使った。面倒なのは、検索文字が複数行にまたがってマッ チする場合だろう。そこで、対象テキスト・ファイルの全行を、改行などをカットして 、いったん「$alltext」に格納している。ここで、 「split(/検索文 字列/, "$alltext")」により、検索文字列のところで、一発でテキスト を分割している。 あとは、前後の必要な文章を表示させればよいだけだ。なるほど、splitは、cs vを項目ごとに分割するだけが能ではなかったのだ。 リンクから、テキストとpdfへジャンプし、表示できる。ここはWebサーバの機 能であり、cgi側で表示を操作しているのではない。
<pdfファイルを開いた様子> なかなか良いスクリプトが出来た。これを完成させただけでも、この連載を1年間続 けてきた甲斐があった。読者に深く感謝したい。(しんみり)。 では、『今日からPerlプログラマー』あらため、『今日からCGIプログラマー (仮題)』でお会いしましょう(笑)。いや、これはマヂです。乞ご期待! HP「今日からPerlプログラマー(http://www.geocitiesc o.jp/SiliconValley-Cupertino/6103/)」 (辻豊史) ■リスト1 google風検索スクリプト #!C:/Perl/bin/MSWin32-x86-object/jperl $dir = "D:/scan資料";#←最後は/で終わらないこと! $action = "/cgi-bin/search-pdf.pl";$txt ="txt";$pdf ="pdf"; $marg = 60;$bakmarg = 0 - $marg;$spmar = ' ' x $marg; $sp = " ";$aki = "<img width=1 height=1>";$fn1 = "<font size=-1>"; $fn2 = "<font size=-1 color=#ffffff>";$ten = " <b>...</b> "; $qs = "$ENV{'QUERY_STRING'}";$text = &post ("q");#入力POST $td = "td width=1 bgcolor";$text = &urldecode ($text);#textの記号を削除 $text =~ s/[\\\"\/\(\)\.]+//g;$text =~ s/[ ]+//g;&saiki_filelist($dir); print "Content-type: text/html\n\n<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\"><HTML><HEAD><META http-equiv=Content-Type content=\"text/html; charset=Shift_JIS\"><TITLE>スキャン資料サーチ </TITLE><BODY><img src=/img/top.gif width=453 height=88><table border=0 cellpadding=0 cellspacing=0 width=1%><tr><td bgcolor=#3366cc width=25% nowrap><center>$fn2$sp<b>検索</b>$sp</font></center></td><$td=#808080> $aki</td><$td=#ffffff>$aki</td><td bgcolor=#efefef width=25% nowrap> <center>$fn1<a href=\"/\">$sp一覧$sp</a></font></center></td></table> <table width=100% border=0 cellpadding=2 cellspacing=0><tr><td bgcolor= #3366cc>$fn2 検索語を入力し、検索して下さい(※)。一覧から資料を探す場 合は、<b>一覧</b>をクリックして下さい。$sp </font></td></tr></table> <form name=s method=GET action=$action><input type=text name=q size=61 maxlength=2048 value=\"$text\">$fn1 <input type=submit name=bttn value=\"Scan資料 検索\"><br>※いくつかの記号は、無視されます。<BR><br>"; $disno = 0;$fno = 0;($text eq "") && (exit);#検索文字がない場合終わり print "「$text」で検索 (pdfとテキストにリンクを貼っています)<HR><BR>"; foreach $i (@dflist) { if ($i =~ /\.txt$/){ $alltext = $spmar;open (IN , "< $i"); while (<IN>) { chomp;s/[ ]+//g;$alltext = "$alltext" . "$_";} close (IN);$alltext = "$alltext" ."$spmar"; if ($alltext =~ /$text/) {$tit = $flist[$fno];$tit =~ s/(.*)\.$txt/$1/; $pdflink = $dflist[$fno];$pdflink =~ s/^$dir(.*)\.$txt/$1\.$pdf/; $txtlink = $dflist[$fno];$txtlink =~ s/^$dir(.*)/$1/; print "<p><a href=\"$pdflink\">$tit</a><BR>\n$fn1"; @bun = split(/$text/, "$alltext");$disno++; for ($pt = 0; $pt <= $#bun; $pt++) { $atotxt = substr ($bun[$pt + 1],0, $marg); if (length($bun[$pt]) <= $marg) {$maetxt = "";} else {$maetxt = "$ten" . substr ($bun[$pt], $bakmarg);} if ($pt < $#bun ){print "$maetxt<b>$text</b>$atotxt\n"; } else { ($mtime) = (stat($i))[9]; ($sec,$min,$hour,$mday,$mon,$year) = localtime($mtime);$mon++; $year += 1900;print "$ten\n<BR><font color=\"#008000\">$i<BR>\n"; print (int((-s $i) / 1000 ) + 1);print "k - $year年 $mon/$mday"; print " →<a href=\"$txtlink\">テキストへ</a></font></font>"; } } } } $fno++; } if ($disno) {print "\n<BR><HR>$disno ケのファイルが見つかりました。";} else {print "\n<BR><HR>該当ファイルはありません。";} sub saiki_filelist {local($dir) = @_; local *DIR; opendir(DIR,$dir) or die $!;local ($file,$dirfile) ; while($_ = readdir(DIR)){next if $_ =~ m/^\.\.$/;next if $_ =~ m/^\.$/; $file = $_;$dirfile = "$dir/$file";$file =~ y/A-Z/a-z/; if( -d $dirfile){ &saiki_filelist($dirfile); } else{ push (@dflist,$dirfile);push (@flist,$file); } } closedir(DIR);} sub urldecode {local ($url) = @_; $url =~tr/+/ /; $url =~s/%(..)/pack("C", hex($1))/ge;return ($url);} sub post { local ($hensu) = @_;local ($postline) = $qs; $postline =~ s/^.*$hensu=([^&]*)\&?.*$/$1/;return ($postline); }