Node.jsとYahooテキスト解析APIで振り仮名ジェネレーターを作ってみた

まとまった量のWebコンテンツに振り仮名をつける必要があるので、良い機会だと思ってrubyタグを自動生成できるアプリケーションを作ってみた。Herokuにアップしているけど、業務での使用が終わったら非公開にするかも。コードはGitHubに公開しています。

[振り仮名ジェネレーター](https://furigana-generator.herokuapp.com/)
[GitHub > furigana-generator] (https://github.com/takuya0206/furigana-generator)

開発環境はこんな感じ。

- ubuntu v16.04.3
- node v6.11.1
- express ~4.15.2 
- [Yahoo > テキスト解析WebAPI > ルビ振り](https://developer.yahoo.co.jp/webapi/jlp/furigana/v1/furigana.html)

以下、ハマった & 工夫したところをメモ。

## node-yjがインストールできない
YahooデベロッパーネットワークにWebAPIライブラリ集というページがあって、そこで [node-yj](https://www.npmjs.com/package/node-yj) が紹介されていた。「これを使えば簡単に実装できそう」なんて思っていたのだけど、そもそもインストールができなくてハマった。どうやら求められているpythonのバージョン差異が原因だったようだけど（ubuntu16.04.3はpython3系がデフォルト）、ubuntuにpython2系を入れてもインストールできなかった。node-yjは2013年が最後のcommitになっていたから、他の部分でもバージョンの差異があったのかもしれない。
結局、直接YahooAPIを叩くことにした。nodeのrequestをインストールして、超シンプルに実装した。

``` index.js
router.post('/generate', (req, res, next) => {
  // 送信するデータを生成
  let options = {
    url: config.API_URL,
    headers: {
      'User-Agent': 'Yahoo AppID: ' + config.appid,
    },
    form: {
      sentence : req.body.sentence
    }
  };
  // apiにリクエストを送り、callbackを処理
  request.post(options, (error, res, body) => {
```

## カタカナや送り仮名にはルビをつけない
YahooAPIから返ってくるデータはこんな感じ。

```
<?xml version="1.0" encoding="UTF-8"?>
<ResultSet xmlns="urn:yahoo:jp:jlp:FuriganaService" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:yahoo:jp:jlp:FuriganaService https://jlp.yahooapis.jp/FuriganaService/V1/furigana.xsd">
  <Result>
    <WordList>
      <Word>
        <Surface>漢字</Surface>
        <Furigana>かんじ</Furigana>
        <Roman>kanzi</Roman>
      </Word>
      <Word>
        <Surface>かな交じり</Surface>
        <Furigana>かなまじり</Furigana>
        <Roman>kanamaziri</Roman>
        <SubWordList>
          <SubWord>
            <Surface>かな</Surface>
            <Furigana>かな</Furigana>
            <Roman>kana</Roman>
          </SubWord>
          <SubWord>
            <Surface>交</Surface>
            <Furigana>ま</Furigana>
            <Roman>ma</Roman>
          </SubWord>
          <SubWord>
            <Surface>じり</Surface>
            <Furigana>じり</Furigana>
            <Roman>ziri</Roman>
          </SubWord>
        </SubWordList>
      </Word>
    </WordList>
  </Result>
</ResultSet>
```
カタカナや送り仮名にまでルビがつくし、xml形式なので直接触れない。そこで、まずnodeのxml2jsというライブラリを使ってJavaScriptのオブジェクトに変換し、漢字のみ・送り仮名あり・カタカナなどを判定しながらHTML用にマークアップする実装にした。

``` index.js
const ruby = ['<ruby>','<rt>','</rt></ruby>'];
const data = {rubySentence : ''};

  // apiにリクエストを送り、callbackを処理
  request.post(options, (error, res, body) => {
    let word, subword, katakana = '';
    xml2js.parseString(body, (err, callback) => {
      //エラー（入力が空白）
      if (callback.hasOwnProperty('Error')) {
        data.rubySentence += "入力が適切ではありません";
      } else {
      for (let i = 0, len = callback.ResultSet.Result[0].WordList[0].Word.length; i < len; i++) {
       word = callback.ResultSet.Result[0].WordList[0].Word[i];
       katakana = word.Surface.toString();
       // 数字やローマ字の場合
       if (!word.hasOwnProperty('Furigana')) {
        data.rubySentence += word.Surface;
      }
       // ひらがなをスキップ
       else if (word.hasOwnProperty('Furigana') && word.Furigana[0] == word.Surface[0]) {
        data.rubySentence += word.Surface;
      }
       // SubWord(=送り仮名)がある場合
       else if (word.hasOwnProperty('SubWordList')) {
        for (let j = 0, len2 = word.SubWordList[0].SubWord.length; j < len2; j++) {
          subword = word.SubWordList[0].SubWord[j];
          if (subword.Furigana[0] == subword.Surface[0]) {
            data.rubySentence += subword.Surface;
          } else {
           data.rubySentence += ruby[0] + subword.Surface + ruby[1] + subword.Furigana + ruby[2];
         };
       };
     }
   // カタカナの場合
   else if (katakana.match(/^[\u30A0-\u30FF]+$/)) {
    data.rubySentence += word.Surface;
  }
  else {
   data.rubySentence += ruby[0] + word.Surface + ruby[1] + word.Furigana + ruby[2];
 };
};
};
});
```
もう少し綺麗に書けた気もするが、意図通りに動くので良しとしよう。尚、Qiitaにあげているコードは抜粋なので、詳しく見たい人はGitHubをどうぞ。

## Node.jsの非同期処理への対応
一番ハマったのはここ。POSTが完了する前に、次の処理が走ってしまいバグが発生していた。「Javascriptは上から順番にプログラムが動いていく」という安易な覚え方がよくなかった。setIntervalで動作の完了を確認するように実装。

```index.js
wait = setInterval (() => {
    if (data.rubySentence !== '') {
      clearInterval(wait);
      res.redirect('/');
    }
  }, 250);
```

## 終わり
気になる振り仮名の精度だけど、固有名詞の珍しい単語でたまに間違いがあるぐらいで、通常の文章ではほぼ問題がなかった。業務で使用する関係で、念のため小まめに目視確認をしてもらいたかったので、文字数の制限をかけているが、1リクエストの上限は100KBまでいけるらしい。もし業務でルビ振りをする必要がある人はご活用ください。

## 参照記事
- [YahooAPIを利用してルビを振る] (http://qiita.com/99nyorituryo/items/55cb36a733d4745588eb)