歴代野球選手人名辞典をWebスクレイピングしてMeCabユーザ辞書に導入する

# 目的
　MeCabを用いた形態素解析について、目的が例えば「Twitterの一般的な話題の分析」ならば、優れた新語辞書である["mecab-ipadic-NEologd"](https://github.com/neologd/mecab-ipadic-neologd)を導入することで、解析が十分に可能です。
　しかし、今回私が分析しようとしていた**「野球」**のような、著名でないルーキーや外国人選手などが頻出するような分野では、適切に解析しきれないケースが見られました。

　今回、これを改善するために、MeCab用ユーザ辞書として**野球選手人名辞典を作り、導入しました**。
　Webスクレイピング方法および辞書データ制作方法について、自分の備忘録とし、また情報共有するために本記事を作成します。

　なお野球選手名のWebスクレイピングは、["日本プロ野球記録"](http://2689web.com/)様のデータを利用しました。選手データをはじめ、膨大な過去のボックススコアをまとめておられ、野球ファンとして勉強になります。
　利用希望にあたっては、快諾を頂きました。感謝いたします。

# 結論
　以下3つのケースとして、テストワードを入力した際の応答を確認します。
　
1. MeCabのみ（他辞書未使用）
2. MeCab + mecab-ipadic-NEologd（新語辞書を導入）
3. MeCab + **dicBBP_20170226.dic（「野球選手人名辞書」を導入）**

　表の通り、**ケース3のみ適切に変換を行えたことが確認できました**。

|case |筒香嘉智|後藤<ruby>G<rp>（</rp><rt>ゴメス</rt><rp>）</rp>武敏|<ruby>王溢正<rp>（</rp><rt>わんいーぜん</rt><rp>）</rp></ruby>|
|-----|-------|------|--------|
|1.|× [筒,香,嘉,智]|× [後藤,G,武敏]|× [王,溢正]|
|2.|○ [筒香嘉智]|× [後藤,G,武敏]|○ [王溢正]|
|3.|○ [筒香嘉智]|○ [後藤G武敏]|○ [王溢正]|

　尚、ケース3で「後藤G武敏」を入力した場合の応答は以下の通り。「ゴメス」の読みまで完璧に解析しています。

```
後藤G武敏       名詞,固有名詞,人名,一般,*,*,ごとうごめすたけとし,ゴトウゴメスタケトシ,ゴトウゴメスタケトシ,野球選手人名辞典
```


# 内容
　以下、4セクションに分けて説明します。

1. [Webスクレイピングするサイトの選定](#1-スクレイピングするサイトの選定)
2. [Webスクレイピング結果csv出力](#2-Webスクレイピング結果csv出力)
3. [MeCabユーザ辞書形式にコンパイル](#3-MeCabユーザ辞書形式にコンパイル)
4. [辞書導入](#4-辞書導入)

## 1. スクレイピングするサイトの選定
　目的でも述べた通り、**["日本プロ野球記録"](http://2689web.com/)様のデータをスクレイピングしました**。
　["選手全成績 あ行"](http://2689web.com/ind/11.html)のページを見ると、テーブル形式で「選手名」と「読み」が掲載されています。この2つのデータが欲しいデータです。選手名と読み方を並べて掲載しているサイトは私の探した限り少なく、こちらのサイトを選定するに至りました。

## 2. Webスクレイピング結果csv出力
　Webスクレイピングモジュールは["cheerio-httpcli"](http://qiita.com/ktty1220/items/72109a6419e23a26002c)(@ktty1220 氏)を使用しました。
　以下のコードにより、**Web上のテーブルデータを「MeCabでコンパイル可能な辞書形式(csv)」に変換出力します**。

```js
function convertArrToDic(n, y) {
  var setName = createNameObj();
  var obj;
  var result  = "";
  
  var dot = /\./;
  if (dot.test(n)) {
    var arr_fulln = n.split(dot);
    // 文字数の長い方を名前と判定
    var name_fam  = arr_fulln[0];
    arr_fulln.forEach( function(v){
      if (name_fam.length < v.length) name_fam = v;
    });
    // 名前登録
    obj = setName(name_fam, y);
    
  } else {
    var arr_fulln = n.split("　");
    var arr_yomi  = y.split("・");
    // スペースが2つある名前は例外処理
    if (arr_fulln.length > 2) {
      obj = setName(arr_fulln.join(''), arr_yomi.join(''));
      
    } else {
      // 括弧を除去し配列化する
      var name_fam = setSecondName(arr_fulln[0]);
      if (arr_fulln[1]) { var name_fir = setSecondName(arr_fulln[1])};
      // 苗字登録
      for (var i = 0; i < name_fam.length; i++){
        obj = setName(name_fam[i], arr_yomi[0]);
      }
      // 名前登録
      if (arr_fulln[1]) {
        for (var i = 0; i < name_fir.length; i++){
          obj = setName(name_fir[i], arr_yomi[1]);
        }
      }
      // フルネーム登録
      if (arr_fulln[1]) {
        for (var i = 0; i < name_fam.length; i++) {
          for (var j = 0; j < name_fir.length; j++) {
            obj = setName(name_fam[i] + name_fir[j], arr_yomi.join(''));
          }
        }
      }
    }
  }
  // 登録されたオブジェクトを辞書形式に変換する
  obj.forEach(function(v){
    //console.log(v);
    result += convertDic(v);
  });
  return result;
  
  // ---------------------------
  // 名前に括弧が含まれる（複数の登録名）の場合分割する関数
  function setSecondName(n) {
    var res = [];
    n.split('(').forEach(function(v) {res.push(v.replace(/\)/,''));});
    return res;
  }
  
  // 名前登録結果を格納する関数（クロージャ）
  function createNameObj() {
    var arr = [];
    
    return function(n, y) {
      var obj = {};
      obj.name = n;
      obj.yomi = y;
      arr.push(obj);
      return arr;
    }
  }
  
  // 辞書形式変換関数
  function convertDic(obj) {
    var cost = 8000 - obj.name.length * 500;
    var res = obj.name + ',' +              // 表層系
              ',' +                         // 左文脈ID
              ',' +                         // 右文脈ID
              cost + ',' +                  // コスト
              '名詞' + ',' +                // 品詞
              '固有名詞' + ',' +            // 品詞細分類1
              '人名' + ',' +                // 品詞細分類2
              '一般' + ',' +                // 品詞細分類3
              '*' + ',' +                   // 活用形
              '*' + ',' +                   // 活用形
              obj.yomi + ',' +              // 原型
              hiraTokana(obj.yomi) + ',' +  // 読み
              hiraTokana(obj.yomi) + ',' +  // 発音
              '野球選手人名辞典' + '\n';      // (追加エントリ)
    
    return res;
    
    function hiraTokana(str) {
      return str.replace(/[\u3041-\u3096]/g, function(match) {
        var chr = match.charCodeAt(0) + 0x60;
        return String.fromCharCode(chr);
      });
    }
  }
}
```

### 【ポイント】姓名の変換方法
　このコードでは、与えられた名前を「姓」「名」「姓＋名（フルネーム）」と別けて辞書ファイルにします。
　例えば、「筒香　嘉智」と1つの名前を与えた場合、以下の3つの結果に分かれます。これらの結果をそれぞれ辞書に登録しています。

* 筒香(読み：つつごう)
* 嘉智(読み：よしとも)
* 筒香嘉智(読み：つつごうよしとも)

　この手法をとったのは理由があります。
　Twitter実況では**「選手を下の名前で呼んで応援する」**ことが多いと感じるため、このような辞書にすることで、より適切な形態素解析を行うことが狙いです。

### 【ポイント】MeCabの「コスト」
　MeCab辞書では単語ごとに「コスト」という数値が設定されています。ユーザ辞書を作成する際、この値を検討しなければなりません。
　本コードでは以下の記事を参考にし、以下の数式としました。

```math
cost = 8000 - [文字長] * 500
```

> Noun.name.csvを見ると**大体2文字の名前で7000から9000前後**なので、まぁ少し自信無さげに10000を入れている。
> 本来、長い名前ほど低いコストにしないといけないので、**適当に10000 - length * 500みたいにしても良いかも**。
> *["MeCabに人名辞書を追加": Qiita](http://qiita.com/awakia/items/9a0dec41d91c997c74b4)より抜粋*

## 3. MeCabユーザ辞書形式にコンパイル
　2で得たcsvデータを、以下の手順に従ってコンパイルします。

> システム辞書の更新は時間がかかります. 辞書の更新が頻繁な場合や, システム辞書を変更する権限が無い場合は, ユーザ辞書を作るのがいいでしょう.
> 
> * 適当なディレクトリに移動 (例: /home/foo/bar)
> * foo.csv というファイルを作成
> * foo.csv に単語を追加
> * 辞書のコンパイル
> 
> ```vim
> % /usr/local/libexec/mecab/mecab-dict-index -d/usr/local/lib/mecab/dic/ipadic \
 -u foo.dic -f euc-jp -t euc-jp foo.csv
> ```
>     -d DIR: システム辞書があるディレクトリ
>     -u FILE: FILE というユーザファイルを作成
>     -f charset: CSVの文字コード
>     -t charset: バイナリ辞書の文字コード 
>
> * /home/foo/bar/foo.dic ができていることを確認 
> 
> *["MeCab: 単語の追加方法": 公式](https://taku910.github.io/mecab/dic.html)より抜粋*

　なお、csvデータは以下のようになっています。

```dicBBP_20170227.csv
アーノルド,,,5500,名詞,固有名詞,人名,一般,*,*,あーのるど,アーノルド,アーノルド,野球選手人名辞典
阿井,,,7000,名詞,固有名詞,人名,一般,*,*,あい,アイ,アイ,野球選手人名辞典
英二郎,,,6500,名詞,固有名詞,人名,一般,*,*,えいじろう,エイジロウ,エイジロウ,野球選手人名辞典
阿井英二郎,,,5500,名詞,固有名詞,人名,一般,*,*,あいえいじろう,アイエイジロウ,アイエイジロウ,野球選手人名辞典
(以下略)
```

## 4. 辞書導入
　作った辞書の導入は、**MeCeb起動時に"-u [dicファイル]"を引数とする**ことで可能です。

```vim
% mecab -u /home/dicBBP20170227.dic
```

# 展望
1. 現時点(2017/2/26)では、スクレイピングサイトの選手名鑑は、2016年シーズン終了時点である。よって本辞書も、**2016年ドラフトや、17年度からの新外国人をカバーできていない**。野球実況ではやはり必要であるため、**数も少ないし、手入力で補完しようか**考えている。
2. せっかく辞書を自作したので、**野球関係のニュースや、[ネット上の長文](http://wikiwiki.jp/livejupiter/?%C6%E2%C0%EE%A5%B3%A5%D4%A5%DA)を解析し結果を比較**しても面白そう。
3. 実際にはmecab-ipadic-NEologdも野球選手について相当網羅性が高い。[結論](#結論)で述べた「後藤G武敏」は2016年ベイスターズの選手だが、彼の2015年の登録名である「後藤武敏G.」はばっちり登録されていた。**私の辞書を使うメリットは、「下の名前だけでも識別できる」というくらい**で、mecab-ipadic-NEologdだけでも、充分な解析能力を持っている。

# 所見
1. 「Node.jsによるWebスクレイピングの練習」ができ、自身のスキルアップに繋がった。
2. cheerioのclient.fetch()は非同期関数であるため、スクレイピング後の結果をfs.writeFile()で出力しようとすると、**空のファイルができてしまう場合がある**。["client.fetchSync()"](http://qiita.com/ktty1220/items/72109a6419e23a26002c#fetchsync)で解決できました。

# 参考
1. ["MeCabに人名辞書を追加": Qiita](http://qiita.com/awakia/items/9a0dec41d91c997c74b4) @awakia 氏
2. ["MeCab: 単語の追加方法": 公式](https://taku910.github.io/mecab/dic.html)
3. ["日本プロ野球記録"](http://2689web.com/) ウスコイ企画 殿
4. ["JavaScriptでカタカナをひらがなに変換する(その逆も)": Qiita](http://qiita.com/mimoe/items/855c112625d39b066c9a) @mimoe 氏