Perl から Google Translate API を使う


Google Labs の閉鎖により Google Translate API が 2011年12月から使えなくなってしまうという事なので、その前にとりあえず機械翻訳しておきたいものをがつがつ取るためにモジュール作成。

しかし課金を受けずに使うと、割とすぐに利用上限に達してしまう。おまけに1日の区切りがどこで付くのかが分からず翌日の昼を過ぎても上限がクリアされない…。ぽちぽちやろう。

参考リソース

Google Translate API Developer’s Guide

モジュール(パッケージ)

ポイントは、そこそこの文章を翻訳する時はGETだと文字数オーバーしてしまうのでPOSTで送信することと、その際、GETとして扱ってくれという指示をする http ヘッダ「X-HTTP-Method-Override」を送信する事。

package Google::Translate::Translator;

use strict;
use warnings;

use utf8;
use Encode qw(encode_utf8);

use URI::Escape;
use HTTP::Request;
use LWP::UserAgent;
use JSON;

#許可されたオプション
our @Options = qw(
target
key
source
translator_url
format
);

sub new {
    my $class = shift;
    if(@_ % 2) {
        die "Default options must be name=>value pairs (odd number supplied)";
    }
    my %raw_opt = @_;
    my %known_opt = map {$_ => 1} @Options;
    my %defined_opt;
    while(my($key, $val) = each %raw_opt) {
        my $lkey = lc($key);
        exists($known_opt{$lkey}) or die "Unknown option: $key";
        #TODO:本当は形式チェックも必要
        $defined_opt{$lkey} = $val;
    }
    return bless \%defined_opt, $class; #return self
}

#アクセサ
for my $optf (@Options) {
    no strict 'refs';
    *$optf = sub {
        my $self = shift;
        exists $self->{retrieved_json} and delete $self->{retrieved_json}; #リセット
        my $ret = $self->{$optf};
        if (@_){
            $self->{$optf} = shift @_;
        }
        return $ret;
    }
}

#原稿指定
#引数: textarrayref
sub set_sourcetext{
    my $self = shift;
    exists $self->{retrieved_json} and delete $self->{retrieved_json}; #リセット
    if (@_){
        $self->{sourcetext} = shift @_;
    }
    ref $self->{sourcetext} or $self->{sourcetext} = [$self->{sourcetext}];

    #文字数チェック: 1回送信5K上限-パラメータ200
    #しかしAPIの利用制限の方に引っ掛かる場合の方が多いのでは
    my $charlen;
    $charlen += length $_ for (@{$self->{sourcetext}}); #utf-8での文字数カウント
    $charlen > 4800 and die "source text too long: $charlen chars.";
}

#JSON形式のテキスト取得
sub get_json{
    my $self = shift;
    exists $self->{sourcetext}
        and ref $self->{sourcetext} eq 'ARRAY'
        and scalar(grep {length $_} @{$self->{sourcetext}})
        or die 'source text not set.';
        #TODO:dieしないエラーメッセージ
    exists $self->{retrieved_json} and return $self->{retrieved_json};
    my $querystr;
    foreach (qw(
target
source
key
format
    )){
        $self->{$_} or next;
        $querystr .= sprintf('&%s=%s',
            $_,
            uri_escape($self->{$_}),
        );
    }
    #順番の保持が必要なので手動で値構成
    foreach (@{$self->{sourcetext}}){
        $querystr .= sprintf('&q=%s',
            uri_escape(utf8::is_utf8($_)?encode_utf8($_):$_),
        );
    }

    #最初の'&'を削除
    $querystr and $querystr =~ s/^\&//;
    my $clength = length($querystr); #全てuri_encodeされているのでuse utf8環境のままでよい

    my $req = new HTTP::Request(
        'post',
        $self->{translator_url},
    );
    $req->header(Content_Type => 'application/x-www-form-urlencoded');
    $req->header(X_HTTP_Method_Override => 'GET');
    $req->header(Content_Length => $clength);
    $req->content($querystr);

    my $ua = new LWP::UserAgent;
    $ua->timeout(10);
    my $res = $ua->request($req);
    if ($res->is_success){
        $self->{retrieved_json} = $res->content;
    } else {
        $self->{retrieved_json} = undef; #エラーの時はハッシュキーがexists
        #TODO:dieしないエラーメッセージ。もっとエレガントに。
        die $res->error_as_HTML;
    }
    return $self->{retrieved_json};
}

#翻訳済テキストを配列の参照で戻す
sub get_translated{
    my $self = shift;
    my $parsed = decode_json $self->get_json;
    my @resultblocks = @{$parsed->{data}->{translations}};
    my @results;
    for (@resultblocks){
        push(@results, $_->{translatedText});
    }
    return \@results;
}

1;

使い方

Google の APIキーは APIs Console から取得できる(Google アカウントが必要)。

#!/usr/local/bin/perl

use strict;
use warnings;
use Data::Dumper;
use utf8;

#use Google::Translate::Translator;
require '/path/to/package/google_translate.pl';

my $trtor = new Google::Translate::Translator(
    translator_url => 'https://www.googleapis.com/language/translate/v2',
    key => 'YOUR-API-KEY',
);

$trtor->source('ja');
$trtor->set_sourcetext(['これは翻訳のテストです。', 'こんにちは、世界。']);

$trtor->target('en');
warn Dumper($trtor->get_translated);

$trtor->target('es');
warn Dumper($trtor->get_translated);

$trtor->target('zh-CN');
warn Dumper($trtor->get_translated);

__END__
出力結果(特殊文字エスケープは Data::Dumperの方の作用)

$VAR1 = [
          'This is a test translation.',
          'Hello world.'
        ];
$VAR1 = [
          "Esto es una prueba de traducci\x{f3}n.",
          "\x{a1}Hola, mundo."
        ];
$VAR1 = [
          "\x{8fd9}\x{662f}\x{4e00}\x{4e2a}\x{6d4b}\x{8bd5}\x{7ffb}\x{8bd1}\x{3002}",
          "\x{4f60}\x{597d}\x{4e16}\x{754c}\x{3002}"
        ];