今日から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); }