jsoupとMicrosoft Translator APIを使ってJSRの日本語リストを作る

岡崎さんにつづき、Java Advent Calendar 2011の12月21日分を書きます。明日は@kakkun61さんです。

その昔、マイナビニュース(旧マイコミジャーナル)でJSRを中心に気になるJava APIをピックアップして紹介していく「Java API、使ってますか?」っていう連載をやらせてもらっていました。そのネタ探しのためにJSRのdescriptionとproposalの一覧表を作るプログラムを書いたんですが、その頃からJSRもだいぶ増えたし、jcp.orgもリニューアルされたので、最近それをイチから作り直して、ついでなので翻訳まで自動でやっちゃおうとしたわけです。

HTMLのパースにはjsoupを使いました。HTTPでリクエストを投げるだけでDOMオブジェクトとしてアクセスできる形まで仕上げてくれる素敵なライブラリです。
翻訳は、Google Translate APIが有料化してしまったので、代わりにMicrosoft Translator APIを使いました。Javaから簡単に利用できるmicrosoft-translator-java-apiというラッパーが公開されているので、これを利用しています。
できたリストはこんな感じ(xlsファイル)。実用性はほとんど無いです。要するにこのエントリの結論は、Web上の情報を翻訳して利用するのに前述の2つのライブラリを使ったら便利かもよ、という紹介で、以下は蛇足です。

jsoupの使い方

jsoupでは、Jsop.connect()メソッドにURLを渡すことで対象のページに接続してorg.jsoup.Connectionオブジェクトを取得できます。これに対してget()やpost()を呼び出すと、そのページの内容をパースしたorg.jsoup.nodes.Dcumentが返ってきます。次のような感じです。

Document doc = Jsoup.connect("http://www.example.com").get();

jsoupが便利なのは、対象のElementをselect()を使ってパターンマッチで探すことができる点です。マッチングのためのシンタックスはここにまとめられています。例えば<a href="*.example.com">の要素を探したい場合には次のような感じになります。

Elements links = doc.select("a[href*=example.com]")

select()は要素を表すorg.jsoup.nodes.Elementのメソッドですが、DocumentもElementの一種なので、Documentに対しても使えます。org.jsoup.select.ElementsはElementのリストです。

microsoft-translator-java-apiの使い方

microsoft-translator-java-api(というかMicrosoft Translator API)を利用するには、Bing Developerに登録してBing Developer API Keyを取得する必要があります。そしてプログラム内com.memetix.mst.translate.TranslateクラスのsetKey()メソッドを使ってAPI Keyをセットします。

Translate.setKey("ここにAPIキーを記入");

翻訳はTranslateのexecute()メソッドで実行します。第1引数に翻訳対象のテキストを、どの言語からどの言語に翻訳したいのかを第2引数と第3引数に指定します。英語から日本語に訳す場合は次のような感じです。

String translatedText = Translate.execute("Hello, I am Nancy.", Language.ENGLISH, Language.JAPANESE);

JSRの各種情報の取得

今回はタイトルとDescriptionとProposalをまとめます。ちなみにタイトルとDescriptionだけならこのページをまるごと翻訳した方が早いと思います。

jsoupでDOMアクセスできるから簡単、と思っていたら大間違い。適当なJSRのページを開いてソースを見てみるとわかるのですが、HTMLが全く構造化されてなくてメチャクチャ。完全に90年代の産物。確かにJCPの設立は98年だけど、リニューアル後もいまだにこれってのは終わってるような気がする。まあリニューアルって言ってもサイトが変わっただけでコンテンツの中身を直したわけじゃないから仕方ないのかもしれませんが。せめて新しい部分くらい真っ当なHTMLで出力しようよ…。
しょうがないので、ソースを見ながら力技で引っ張り出すことにしました。

タイトルは<div class="header1">のとこに書かれていて、このクラスはタイトル以外では使われていないっぽいので、普通にselect()で取り出せました。ただしsupタグがついた上付き文字などは削除してます。

private String getTitle(Document doc) {
  Element e = doc.select("div.header1").first();
  String title = e.html();
    
  title = title.replace("\n", "").replace("\r", "");  // 改行除去
  title = title.replaceAll("<(\\w+)(?:\\s+(?:[^'\">]|\"[^\"]*\"|'[^']*')*)*>.*?</\\1>", "");  // タグ除去
  title = title.replaceFirst("JSR \\d+:\\W", "");  // JSRナンバー除去
    
  return title;
}

Descriptionは、太字(bタグ)の「Description」の文字を手掛かりに探します。まずこの文字が含まれる要素を探して、その中から「<b>Description</b>:<br >」と「<br ><br >」の間に入る文字列を取り出すようにしています。Descriptionは全てこの形式で問題なく取得できました。

private String getDescription(Document doc) {
  String description = null;
    
  Element e = doc.select("b:contains(Description)").first();
  e = e.nextElementSibling();

  String regex = "<b>Description</b>:<br />(.*)<br /><br />";
  Pattern p = Pattern.compile(regex);
  Matcher m = p.matcher(e.parent().html());
  if (m.find()) {
    description = m.group(1);
    description = description.replaceAll("<(\\w+)(?:\\s+(?:[^'\">]|\"[^\"]*\"|'[^']*')*)*>.*?</\\1>", "");  // タグ除去    
  }

  return description;
}

正直、この辺りで「jsoup使わずに自分で解析した方が早かったじゃね?」と思いました。Proposalは、セクション2.1の内容だけを取り出しています。「Section 2: Request」の直前に<a name="2">タグがあるので、まずはこの場所を探して、そこから順番にSection 2まで進みます。その次は、サブセクション2.1がある場合と、直接内容が書かれている場合があります。前者の場合はサブセクションのタイトルにh4タグが使われているので、それを頼りに2.1の中身だけ取り出しました。後者の場合は「Section 3:」までの内容を取り出すようにしています。

この部分、JSRによって記述がまちまちで、余分なpタグやbrタグが入っていたりしてうまくパースできていないことがあります。そうするとnullが返ってきちゃいます。面倒なのでその場合は全部読み飛ばすようにしました。この辺で飽きてきたのでコードもだいぶ適当です。

private List<String> getProposalTexts(Document doc) {
  List<String> proposalTexts = new ArrayList<>();
      
  Elements section2name = doc.select("a[name=2]"); // <a name="2">を探す
  Element e2 = section2name.first().nextElementSibling();  // Section 2の最初
  if (e2 == null) {
  proposalTexts.add("Could not get proposal.");
    return proposalTexts;
  }
      
  e2 = e2.nextElementSibling();  // 余分な<p>の読み飛ばし
  if (e2 == null) {
    proposalTexts.add("Could not get proposal.");
    return proposalTexts;
  }
      
  if ("h4".equals(e2.nextElementSibling().tagName())) {  // サブセクションがある場合
    e2 = e2.nextElementSibling();
        
     if (e2.nextElementSibling() == null) {
      proposalTexts.add("Could not get proposal.");
      return proposalTexts;
    }
    while (!"h4".equals(e2.nextElementSibling().tagName())) {
      e2 = e2.nextElementSibling();
      if("pre".equals(e2.tagName())) { 
        proposalTexts.add(" [omitted] ");
      } else {
        proposalTexts.add(e2.text());
      }
          
      if (e2.nextElementSibling() == null) {
        break;
      }
    }
  } 
  else {
    if (e2.nextElementSibling() == null || e2.nextElementSibling().text() == null) {
      proposalTexts.add("Could not get proposal.");
      return proposalTexts;
    }
    while (!(e2.nextElementSibling().text().contains("Section 3:"))) {  // サブセクションが無い場合
      e2 = e2.nextElementSibling();
      if("pre".equals(e2.tagName())) { 
        proposalTexts.add(" [omitted] ");
      } else {
        proposalTexts.add(e2.text());
      }
          
      if (e2.nextElementSibling() == null) {
        break;
      }
    }
  }

  return proposalTexts;
}

取得した内容はこんな感じのクラスに一旦格納しておきます。

public class JsrContents {
  private int id;
  private String title = null;
  private String description = null;
  private List<String> proposal = null;

  public String toCsvForm() {
    StringBuilder csvForm = new StringBuilder();
    csvForm.append("\"");
    csvForm.append(this.id);
    csvForm.append("\",\"");
    csvForm.append(this.title);
    csvForm.append("\",\"");
    csvForm.append(this.description);
    csvForm.append("\",\"");
    for(String proposal: this.proposal) {
      csvForm.append(proposal);
    }
    csvForm.append("\"");
    
    return csvForm.toString();
  }

翻訳する

翻訳部分はこんな感じ。英語のJsrContentsを受け取って、内容をMicrosoft Translator APIで翻訳して、日本語版のJsrContentsをつくて返します。一文ずつ翻訳リクエストするので時間かかります。

public JsrContents transrate(JsrContents contents) {
  JsrContents jpContents = new JsrContents(contents.getId());

  Translate.setKey(APIキー);
  try {
    jpContents.setTitle(contents.getTitle());
      
    String jpDescription = Translate.execute(contents.getDescription(), Language.ENGLISH, Language.JAPANESE);
    jpContents.setDescription(jpDescription);
      
    List<String> jpProposals = new ArrayList<>();
    for(String proposal: contents.getProposal()) {
      String jpProposal = Translate.execute(proposal, Language.ENGLISH, Language.JAPANESE);
      jpProposals.add(jpProposal);
    }
    jpContents.setProposal(jpProposals);
  } catch (Exception ex) {
    Logger.getLogger(JsrContentsCreator.class.getName()).log(Level.SEVERE, null, ex);
  }

  return jpContents;
}

あとはcsv形式で出力するだけです。ちなみに353個のJSRを全部まとめて翻訳しようとするとAPIの制限にひっかかるので、何回かに分けて実行してから手動で結合してxlsファイルにまとめました。