=head1 NAME 英辞郎 - コマンドラインから和英/英和単語翻訳 =head1 DESCRIPTION 英辞郎というサイトがあります。ALCという会社のサイト上で、フォームから和英/英和の単語翻訳ができる優れものです。技術用語なども豊富なのでなかなか使い勝手があります。 これを、シェル上から使えるようにしたい、というのが今回の企画です。職場 で英語の技術書を読んでいるとき、またドキュメントを英語でかく必要がでて きたとき、いちいち重いブラウザを使うことなく (w3m や lynx を常用してい るひとならそれでも構いませんが)、コマンドラインからちょっとした単語の 訳が出せると、非常に便利だと思うのです。 インターフェースとしては、nslookup コマンドのように、 $ eijiro.pl foo $ eijiro.pl ほげほげ $ eijiro.pl eijjiro> foo 引数で渡すものと、インタラクティブなシェルのものの双方をサポートしよう と思います。 =head1 libwww-perl ブラウザのフォームからCGIをたたく部分を実装するには、Perl の libwww-perl パッケージを使うと簡単です。LWP は、Gisle Aas さんによって メンテナンスされており、HTTP 通信まわりを、すっきりとしたクラス階層で 実装しています。 簡単な使い方として、LWP::Simple というクラスが提供されています。単純に あるURLからコンテンツやヘッダ情報を取得したり、ローカルディスクに保存 したりするのであれば、この LWP::Simple モジュールによって提供される関 数 get(), head(), mirror() などを使って簡単に実装することができます。 たとえば、ある URL に対してGETリクエストを投げ、HTMLを取得して表示する スクリプトは以下のようになります。 1 #!/usr/local/bin/perl 2 3 use strict; 4 use LWP::Simple; 5 6 print get('http://www.asahi.com/'); 簡単ですね。 単純に GET|HEAD を投げるだけでなく、パラメータも投げる、Cookie も受け 渡す、プロキシを通す、Basic 認証を行う、となると LWP::Simple では厳し くなってきます。この場合、LWP::UserAgent クラスを使って実装することが できます。 あるサイトに対して Basic 認証を行い、正しく反応がかえってきた場合に、 その内容を表示するスクリプトは以下のようになります。 1 #!/usr/local/bin/perl 2 3 use strict; 4 use LWP::UserAgent; 5 6 my $url = 'http://www.some.domain/auth_required/'; 7 my $username = 'user'; 8 my $password = 'XXXX'; 9 10 my $ua = LWP::UserAgent->new; 11 my $request = HTTP::Request->new(GET => $url); 12 $request->authorization_basic($username, $password); 13 my HTTP::Response $response = $ua->request($request); 14 15 if ($response->is_success) { 16 print $response->as_string; 17 } このように、LWP::UserAgent の request() メソッドに HTTP::Request クラ スを渡すことにより、HTTP::Response クラスが返ってくるという仕様になっ ています。HTTP::Request, HTTP::Response は双方とも HTTP::Message のサ ブクラスとして実装されています。詳細は perldoc lwpcook をみてください。 =head1 URI::Escape 和英翻訳の場合、日本語をフォームに入力することになりますが、HTTPリクエ ストにそのまま日本語を渡すのではなく、ブラウザが非ASCII文字を URI-escape という方式でエンコードして渡しています。 URI エスケープの仕様は非常に簡単です。ある文字列 $str をエンコードする には、以下のようにします。 $str =~ s/(\W)/'%' . unpack('H2', $1)/eg; sprintf() と ord() を使うなどいろいろな方法がありますが、モジュールを 使うのであれば URI::Escape が用意されています。使い方は以下の通りです。 1 #!/usr/local/bin/perl 2 3 use strict; 4 use URI::Escape; 5 6 my $query = 'エスケープ=escape'; 7 print uri_escape($query, '\W'); ここで uri_escape() の第2引数に C<\W> を渡しているところがポイントです。 uri_escape 関数は、デフォルトでは「URIに使ってはいけない文字」を escape します。今回、CGIに入力した値のエスケープを考えていますから、こ れではまずいのです。C<=>, C<&> といった文字は、URI としては使用可能で すが、CGI のパラメータとして渡す場合、C<=> や C<&> は特殊な意味を持ち ます。ですから、C<[a-zA-Z0-9_]> でない文字はすべて escape しなければなりま せん。 =head1 Term::ReadLine 以上の2つで、引数から翻訳結果を表示するものはできそうです。インタラク ティブにそれ自身、シェルとして実行するものはどうすればよいでしょうか。 CPAN モジュールをインストールするときに、 perl -MCPAN -e shell で立ち上がる CPAN シェル、これのシェル部分を実装している Term::ReadLine が使えます。実際には Term::ReadLine::Perl, Term::ReadLine::Gnu のどちらかを利用するのですが、これによって Ctrl-P でヒストリをさかのぼったり、といったことが簡単にできるようになります。 =head1 IMPLEMENTATION 準備は整いました。あとはユーザの入力を受け取って、HTTPリクエストを生成 し eijiro に問い合わせ、結果を表示すればよいことになります。 細かい実装として以下のところを工夫しています。 まず和英/英和モードの切り替えは、正規表現によって自動で行います。また、 返ってくる HTML を、シェルで見やすくするためにはレンダリングエンジンが 必要になってきますが、ここでは HTML::TreeBuilder, HTML::FormatText を 使用しています。 インタラクティブシェルの場合には、終了して次に立ち上げた際にヒストリ補 完できるよう、ヒストリファイルに記録する、といったこともしています。イ ンタラクティブモードは Ctrl-D もしくは !exit という入力で終了するよう になっています。 1 #!/usr/local/bin/perl 2 # eijiro.pl - http://www.alc.co.jp/eijiro/ 3 4 use strict; 5 use Jcode; 6 use LWP::Simple; 7 use FileHandle; 8 use URI::Escape; 9 use HTML::FormatText; 10 use HTML::TreeBuilder; 11 use Term::ReadLine; 12 13 my $historyfile = $ENV{HOME} . '/.eijirohistory'; 14 my $pager = $ENV{PAGER} || 'less'; 15 16 my $action = 'http://www.alc.co.jp/eijiro351.php3'; 17 18 # Terminal mode / Argv mode 19 if (@ARGV) { 20 my $line = join ' ', @ARGV; 21 translate($line); 22 } else { 23 my $term = Term::ReadLine->new('Eijiro'); 24 # read history 25 if (my $fh = FileHandle->new($historyfile)) { 26 my @h = $fh->getlines; 27 chomp @h; 28 $fh->close; 29 my %seen; 30 $term->addhistory($_) foreach (grep { /\S/ && !$seen{$_}++ } @h); 31 } 32 # readline & translate 33 while ( defined ($_ = $term->readline('eijiro> ')) ) { 34 exit if /^!exit/; 35 translate($_); 36 # Add history 37 { 38 my $fh = FileHandle->new(">>$historyfile") or die $!; 39 $fh->print("$_\n"); 40 $fh->close; 41 } 42 $term->addhistory($_) if /\S/; 43 } 44 } 45 46 sub translate { 47 my $line = shift or return; 48 49 # ej or je 50 my $type_in = $line =~ /^[\x00-\x7f]*$/ ? 'ej' : 'je'; 51 52 # URI-Escape 53 my $word_in = Jcode->new($line)->sjis; 54 $word_in = uri_escape($word_in, '\W'); 55 56 # get Simply 57 my $url = sprintf '%s?word_in=%s&type_in=%s', $action, $word_in, $type_in; 58 my $content = LWP::Simple::get($url) or die $!; 59 60 my $parser = new HTML::TreeBuilder; 61 my $html = $parser->parse(Jcode->new($content)->euc); 62 my $format = new HTML::FormatText(leftmargin=>0); 63 64 my $p = new FileHandle "| $pager"; 65 $p->print($format->format($html)); 66 $p->close; 67 } =head1 SEE ALSO =over 4 =item libwww-perl http://search.cpan.org/search?dist=libwww-perl =item Term::ReadLine::Perl http://search.cpan.org/search?dist=Term-ReadLine-Perl =item 英辞郎 http://www.alc.co.jp/ http://member.nifty.ne.jp/eijiro/index.html =back =head1 AUTHOR Tatsuhiko Miyagawa =cut