NAME

英辞郎 - コマンドラインから和英/英和単語翻訳


DESCRIPTION

英辞郎というサイトがあります。ALCという会社のサイト上で、フォームから和英/英和の単語翻訳ができる優れものです。技術用語なども豊富なのでなかなか使い勝手があります。

これを、シェル上から使えるようにしたい、というのが今回の企画です。職場 で英語の技術書を読んでいるとき、またドキュメントを英語でかく必要がでて きたとき、いちいち重いブラウザを使うことなく (w3m や lynx を常用してい るひとならそれでも構いませんが)、コマンドラインからちょっとした単語の 訳が出せると、非常に便利だと思うのです。

インターフェースとしては、nslookup コマンドのように、

  $ eijiro.pl foo
  $ eijiro.pl ほげほげ
  $ eijiro.pl
  eijjiro> foo
  
引数で渡すものと、インタラクティブなシェルのものの双方をサポートしよう
と思います。


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 をみてください。


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引数に \W を渡しているところがポイントです。 uri_escape 関数は、デフォルトでは「URIに使ってはいけない文字」を escape します。今回、CGIに入力した値のエスケープを考えていますから、こ れではまずいのです。=, & といった文字は、URI としては使用可能で すが、CGI のパラメータとして渡す場合、=& は特殊な意味を持ち ます。ですから、[a-zA-Z0-9_] でない文字はすべて escape しなければなりま せん。


Term::ReadLine

以上の2つで、引数から翻訳結果を表示するものはできそうです。インタラク ティブにそれ自身、シェルとして実行するものはどうすればよいでしょうか。

CPAN モジュールをインストールするときに、

  perl -MCPAN -e shell

で立ち上がる CPAN シェル、これのシェル部分を実装している Term::ReadLine が使えます。実際には Term::ReadLine::Perl, Term::ReadLine::Gnu のどちらかを利用するのですが、これによって Ctrl-P でヒストリをさかのぼったり、といったことが簡単にできるようになります。


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  }


SEE ALSO

libwww-perl

http://search.cpan.org/search?dist=libwww-perl

Term::ReadLine::Perl

http://search.cpan.org/search?dist=Term-ReadLine-Perl

英辞郎

http://www.alc.co.jp/ http://member.nifty.ne.jp/eijiro/index.html


AUTHOR

Tatsuhiko Miyagawa <miyagawa@bulknews.net>