どうせ和布蕪るなら人名辞書くらいは

せっかくなので。

どうせ和布蕪るなら辞書の拡張も考えたいところである。基礎的なことだけなら 単語の追加方法に書いてある。すぐに始められるものもあれば、とっても大変なものもある。

思うに、こうしたことを「個人で」やりたい場合って、大抵は適用対象のドキュメントには「大幅な偏り」がある。だから多分「おれさま専用の辞書」があると良い結果になることが多いんじゃないかと思うんだよね。特に「真っ先に始められて即戦力になる」ものは、やっぱり人名辞書じゃないかと。だって例えばね、「声優」ばかりが登場するなら声優ばかりの辞書があると良い結果になるだろうけれど、そうでない場合はそれが理由で解析を誤ることもあるはずなんだよね。特に「ひらがなだけの芸名」なんかね。(実際 Perfume の「のっち」がまさにそうであろう。「あのっちがうんです!」から人名として「のっち」を切り出しやすくなっちゃうのは困る。)

てわけで、ひとまず「ユーザ辞書(人名)」を作り、それを使うことを考えてみる。


単語の追加方法には詳細な仕様が書かれてないんだけれど、人名に関しては3種類書けるみたい:

1 康弘,1291,1291,8349,名詞,固有名詞,人名,名,*,*,康弘,ヤスヒロ,ヤスヒロ
2 金潤万,1289,1289,7438,名詞,固有名詞,人名,一般,*,*,金潤万,キンジュンマン,キンジュンマン
3 三ツ藤,1290,1290,7647,名詞,固有名詞,人名,姓,*,*,三ツ藤,ミツフジ,ミツフジ

「姓」、「名」はそのまんまとして、「一般」は姓・名で分割出来ない名前ということらしい。どうなんだろうねぇ、たとえば「黒沢ともよ」は当然「黒沢ともよ」「黒沢 ともよ」両方の表記方法があるわけだから、「黒沢ともよ,一般」と「ともよ,名」の両方を登録するのがいいんだろうか? まぁ「のっち」ほどには「ともよ」が害になることはなさそうではあるけれども。とりあえず両方登録するノリにしとくか。

で。当然「こんな csv のまま書くのめんどい」わけだね。だから「メンテナンスツール」を作っちゃおうかと。「ちゃんとした」は遠いかもしれんけど、「名前専用」ならすぐであろう、と。

csv のフィールドの意味は

1 表層形, 左文脈ID, 右文脈ID, コスト, 品詞, 品詞細分類1, 品詞細分類2, 品詞細分類3, 活用型, 活用形, 原形, 読み, 発音

だが、「人名だから」が理由で保守を考えたくないフィールドは

1 左文脈ID, 右文脈ID, 品詞, 品詞細分類1, 品詞細分類2, 活用型, 活用形, 原形

ただし:

  1. 「コスト」に関しては保守するしかないけれど、一応トレーニングは可能、ゆえ、いずれは半自動目指せるだろう。
  2. 原形は人名の場合は表層形と同じでいいはずだけど、あるいはないこともないかも。

てわけで、「おれさま専用メンテナンスツール」の人名用入力としては

1 表層形, コスト, 品詞細分類3, 読み, 発音

だけを取ればいいだろう。csv? うーん…以後「活用するやつ」まで考え出すと json みたいなのがかえって問題起こす可能性あるんだよなぁ…、csv のままにしとく? こんだけフィールドを絞り込めてれば、そんなに保守が大変ではないよな。


というかあれだね。csv にするなら、ヘッダにフィールド名を書くフレキシブルなやつにすればいいね。Python の csv.reader はそれが一撃で出来るわけだし。こんなね:

 1 # -*- coding: utf-8 -*-
 2 from __future__ import unicode_literals
 3 
 4 import io
 5 import csv
 6 import re
 7 
 8 
 9 if __name__ == '__main__':
10     csvtext = """\
11 表層形, 左文脈ID, 右文脈ID, コスト, 品詞, 品詞細分類1, 品詞細分類2, 品詞細分類3, 活用型, 活用形, 原形, 読み, 発音
12 康弘,1291,1291,8349,名詞,固有名詞,人名,名,*,*,康弘,ヤスヒロ,ヤスヒロ
13 金潤万,1289,1289,7438,名詞,固有名詞,人名,一般,*,*,金潤万,キンジュンマン,キンジュンマン
14 三ツ藤,1290,1290,7647,名詞,固有名詞,人名,姓,*,*,三ツ藤,ミツフジ,ミツフジ
15 """
16     header, _, body = csvtext.partition("\n")
17     reader = csv.DictReader(
18         io.StringIO(body),
19         fieldnames=re.split(r"\s*,\s*", header))
20     print(list(reader))

てわけで、まずは「おれ流 csv を読み込んで正式な MeCab 辞書 csv に変換(人名用)」ね:

 1 # -*- coding: utf-8 -*-
 2 from __future__ import unicode_literals
 3 
 4 import io
 5 import csv
 6 import re
 7 
 8 
 9 def _mecab_mydic_reader(csvinput):
10     header, _, body = csvinput.partition("\n")
11     return csv.DictReader(
12         io.StringIO(body),
13         fieldnames=re.split(r"\s*,\s*", header))
14 
15 
16 class _BaseMeCabDictInputBuilder(object):
17     fieldnames_all = [
18         "表層形",
19         "左文脈ID", "右文脈ID",
20         "コスト",
21         "品詞", "品詞細分類1", "品詞細分類2", "品詞細分類3",
22         "活用型1", "活用形2",
23         "原形", "読み", "発音"
24     ]
25     _defaults_tobe_blank = ("コスト",)
26 
27     def __init__(self, reader):
28         self._reader = reader
29 
30     def _default(self, k, line):
31         return "*"
32 
33     def default(self, k, line):
34         if k in line:
35             return line[k]
36         if k in self._defaults_tobe_blank:
37             return ""
38         return self._default(k, line)
39 
40     def lines(self):
41         for line in self._reader:
42             yield [
43                 line.get(k, self.default(k, line))
44                 for k in self.fieldnames_all]
45 
46 
47 class NounName_MeCabDictInputBuilder(_BaseMeCabDictInputBuilder):
48     _defaults_fixed = {
49         "品詞": "名詞",
50         "品詞細分類1": "固有名詞",
51         "品詞細分類2": "人名",
52         }
53     def __init__(self, reader):
54         _BaseMeCabDictInputBuilder.__init__(self, reader)
55 
56     def _default(self, k, line):
57         if k in self._defaults_fixed:
58             return self._defaults_fixed[k]
59         if k in ("左文脈ID", "右文脈ID"):
60             return {
61                 "一般": "1289",
62                 "姓": "1290",
63                 "名": "1291",
64                 }.get(line.get("品詞細分類3", ""))
65         if k == "原形":
66             return line.get("表層形", "")
67         if k == "読み" and "発音" in line:
68             return line["発音"]
69         elif k == "発音" and "読み" in line:
70             return line["読み"]
71         #raise ValueError("...")
72         return "*"
73 
74 
75 #
76 if __name__ == '__main__':
77     csvtext = """\
78 表層形, コスト, 品詞細分類3, 原形, 読み
79 康弘,8349,名,康弘,ヤスヒロ
80 金潤万,7438,一般,金潤万,キンジュンマン
81 三ツ藤,7647,姓,三ツ藤,ミツフジ
82 """
83     builder = NounName_MeCabDictInputBuilder(_mecab_mydic_reader(csvtext))
84     #import json
85     #print(json.dumps(list(builder.lines()), ensure_ascii=False, indent=4))
86     print("\n".join(list(builder.lines())))  # 最終的には .encode("utf-8")など

今この段階でまだ実際に辞書コンパイルしてない状態なので、まだ上のコードが正しいとは言わない。あとでコンパイルするとこまでやるけど、その時点までは上はまだ「たぶんこう」てことは踏まえといてね。(あと reader を入力にしてるのがダサいがこれはあとで直す。)


さて。実際に「コンパイルして配備する」のをスクリプトでこなしたいところなんだけれども。

問題は定義ファイル(mecabrc または .mecabrc)なのよね。mecabrc の保守もスクリプトでまかなえるほど単純ではあるけれども、そうしてしまうのがいいかどうかはまた別問題。上の方で書いた通り、「適用対象が偏るのでの俺様辞書」なわけだから、(俺様だけにとってであっても)いつでも有効なものとして考えるよりは、「使いたいときだけ使う」ノリの方がいいことも多いだろう。例えば「AV 女優人名辞書」を、健全なドキュメント相手に有効にする必要はあるまい。

なので…、ひとまずは「コンパイルするだけ」があればいいかね。

ひとまず「オレんちでしか多分動かない」バカスクリプトから:

  1 # -*- coding: utf-8 -*-
  2 from __future__ import unicode_literals
  3 
  4 import io
  5 import os
  6 import csv
  7 import re
  8 import subprocess
  9 import tempfile
 10 
 11 
 12 class _BaseMeCabDictInputBuilder(object):
 13     fieldnames_all = [
 14         "表層形",
 15         "左文脈ID", "右文脈ID",
 16         "コスト",
 17         "品詞", "品詞細分類1", "品詞細分類2", "品詞細分類3",
 18         "活用型1", "活用形2",
 19         "原形", "読み", "発音"
 20     ]
 21     _defaults_tobe_blank = ("コスト",)
 22 
 23     def __init__(self, csvinputfile, encoding="utf-8"):
 24         self._mecab_official_formatted = os.path.join(
 25             tempfile.gettempdir(), "_my_mecab_dict_.csv")
 26         self._reader = self._mecab_mydic_reader(
 27             io.open(csvinputfile, encoding=encoding).read())
 28 
 29     def _default(self, k, line):
 30         # should be overidden
 31         return "*"
 32 
 33     def _fallback(self, k, line):
 34         if k in line:
 35             return line[k]
 36         if k in self._defaults_tobe_blank:
 37             return ""
 38         return self._default(k, line)
 39 
 40     def _lines(self):
 41         for line in self._reader:
 42             yield [
 43                 line.get(k, self._fallback(k, line))
 44                 for k in self.fieldnames_all]
 45 
 46     def _mecab_mydic_reader(self, csvinput):
 47         header, _, body = csvinput.partition("\n")
 48         return csv.DictReader(
 49             io.StringIO(body),
 50             fieldnames=re.split(r"\s*,\s*", header))
 51 
 52     def _write_inputcsv(self, outname):
 53         # TODO: エスケープ必要だったりする?
 54         content = ("\n".join(
 55                 [",".join(line) for line in self._lines()
 56                  ]) + "\n").encode("utf-8")
 57         with io.open(outname, "wb") as fo:
 58             fo.write(content)
 59 
 60     def __enter__(self):
 61         return self
 62 
 63     def __exit__(self, exc, value, tb):
 64         if os.path.exists(self._mecab_official_formatted):
 65             try:
 66                 os.remove(self._mecab_official_formatted)
 67             except Exception:
 68                 pass  # no problem.
 69 
 70     def compile(self, args):
 71         # TODO: MECAB_HOME なんて標準があるのか未確認。
 72         _mecab_home = os.environ.get(
 73             "MECAB_HOME", "c:/Program Files (x86)/MeCab")
 74         _dict_index_bin = os.path.join(
 75             _mecab_home, "bin", "mecab-dict-index")
 76         # TODO: 「ipadic」以外の置き場もあるんではないかと。
 77         _sysdict_dir = os.path.join(_mecab_home, "dic", "ipadic")
 78 
 79         self._write_inputcsv(self._mecab_official_formatted)
 80         #
 81         dictname = args.output_dictionary_name
 82         if not dictname:
 83             dictname, _ = os.path.splitext(
 84                 os.path.basename(args.input_csv))
 85             dictname += ".dic"
 86         subprocess.check_call([
 87                 _dict_index_bin,
 88                 "-d", _sysdict_dir,
 89                 "-u", dictname,
 90                 "-f", "utf-8",
 91                 "-t", args.dict_encoding,
 92                 self._mecab_official_formatted,
 93                 ])
 94 
 95 
 96 class _NounName(_BaseMeCabDictInputBuilder):
 97     _defaults_fixed = {
 98         "品詞": "名詞",
 99         "品詞細分類1": "固有名詞",
100         "品詞細分類2": "人名",
101         }
102     def __init__(self, csvinputfile, encoding="utf-8"):
103         _BaseMeCabDictInputBuilder.__init__(
104             self, csvinputfile, encoding)
105 
106     def _default(self, k, line):
107         if k in self._defaults_fixed:
108             return self._defaults_fixed[k]
109         if k in ("左文脈ID", "右文脈ID"):
110             return {
111                 "一般": "1289",
112                 "姓": "1290",
113                 "名": "1291",
114                 }.get(line.get("品詞細分類3", ""))
115         if k == "原形":
116             return line.get("表層形", "")
117         if k == "読み" and "発音" in line:
118             return line["発音"]
119         elif k == "発音" and "読み" in line:
120             return line["読み"]
121         return "*"
122 
123 
124 #
125 if __name__ == '__main__':
126     import argparse
127 
128     parser = argparse.ArgumentParser()
129     parser.add_argument("input_csv")
130     parser.add_argument("--output-dictionary-name", default="")
131     parser.add_argument("--input-encoding", default="utf-8")
132     parser.add_argument("--dict-encoding", default="utf-8")
133     args = parser.parse_args()
134 
135     #
136     with _NounName(
137         args.input_csv, encoding=args.input_encoding) as b:
138         b.compile(args)

まぁ「おれんちでしか動かない」つーても、ほとんど mecab のパスの問題だけなので、概ね動くような気はするけどね。


あとは…「人名以外」のためのものがあって、してそれのファクトリがいて、それをコマンドラインオプションで指定出来て…で、まぁまぁ完全なものにはなるんじゃないかな。「コンパイルまでなら」。

今回は「人名以外」についてはやらない。

それよりも、だ。ここまでやっといて、いまだ「作った辞書を使ってみてない」ので、それを。

例えば上で作ったスクリプト名を「my-mecab-dict-index」なんて名前にして、パスの通った場所に置いたとして。(ファイル頭のマジックナンバーも書いたとして、ね。)

 1 [me@host: ~]$ cat Noun.name.anime.csv
 2 表層形, コスト, 品詞細分類3, 原形, 読み
 3 黒沢ともよ,7438,一般,黒沢ともよ,クロサワトモヨ
 4 犬吠埼樹,7438,一般,犬吠埼樹,イヌボエザキイツキ
 5 [me@host: ~]$ my-mecab-dict-index Noun.name.anime.csv
 6 reading C:\Users\HHSPRI~1\AppData\Local\Temp\_my_mecab_dict_.csv ... 2
 7 emitting double-array: 100% |###########################################| 
 8 
 9 done!
10 [me@host: ~]$ ls -l Noun.name.anime.dic
11 -rw-r--r-- 1 hhsprings Administrators 4461 Oct  6 03:52 Noun.name.anime.dic

とりあえず辞書は作れる。まずはこの辞書なしで:

 1 [me@host: ~]$ cat some.txt  # ほんとは utf-8 で Windows でなんてのは苦労のもと。脳内補完しちくり。
 2 犬吠埼樹はアニメのキャラ。
 3 [me@host: ~]$ mecab < some.txt  # mecab のパスが通ってると仮定して
 4 犬吠埼	名詞,固有名詞,地域,一般,*,*,犬吠埼,イヌボウザキ,イヌボーザキ
 5 樹	名詞,接尾,一般,*,*,*,樹,ジュ,ジュ
 6 は	助詞,係助詞,*,*,*,*,は,ハ,ワ
 7 アニメ	名詞,一般,*,*,*,*,アニメ,アニメ,アニメ
 8 の	助詞,連体化,*,*,*,*,の,ノ,ノ
 9 キャラ	名詞,一般,*,*,*,*,キャラ,キャラ,キャラ
10 。	記号,句点,*,*,*,*,。,。,。
11 EOS
12 [me@host: ~]$ 

良いね、「期待通りでない」。で、作った辞書を「一時的に使う(rcファイルに追加せずに)」:

1 [me@host: ~]$ mecab -u Noun.name.anime.dic < some.txt
2 犬吠埼樹	名詞,固有名詞,人名,一般,*,*,犬吠埼樹,イヌボエザキイツキ,イヌボエザキイツキ
3 は	助詞,係助詞,*,*,*,*,は,ハ,ワ
4 アニメ	名詞,一般,*,*,*,*,アニメ,アニメ,アニメ
5 の	助詞,連体化,*,*,*,*,の,ノ,ノ
6 キャラ	名詞,一般,*,*,*,*,キャラ,キャラ,キャラ
7 。	記号,句点,*,*,*,*,。,。,。
8 EOS

おけ。(ちなみに私は「犬吠埼樹」が出てくるアニメを実際みてないので、「イヌボエザキイツキ」で合ってるか知らない。)

rc ファイルに登録しておく方法については単語の追加方法の「ユーザ辞書への追加」に書いてある。ひとまずは手で保守する前提。

ただ最初に書いた通り、ここまで面倒を見るものを書くのも大変なことではないので、いずれ自分向けにはそうするかも。(実際作った辞書ファイルの実体をどこに配置するかの問題があって、簡単ではあるが悩むべきことがないわけではない。)

それと「コスト」についてだけれど、原則として「似たものと近い値を探して埋め、トライアンドエラーで頑張って調整する」か、mecab-cost-train でトレーニングするかなんだけれど、後者を採るのであれば、ある程度自動化も出来るのかもしれなくて、これについては、さすがに「おれのためだけ」にすると勿体無いので、やる気になったらここに追記の形で書く可能性はある。⇒ 18:20追記: 続きに書いた。


ひとまずこのネタ本体としてはここまで、なんだけれど、「個人用途としてのこの先」としてはまだあって。実際、自分の趣味のある作業の中で「声優一覧」みたいなのをあるサイトから列挙出来てて、それを全部「えっちな声優辞書」として作ってしまえるわけね。今のこの作業だけでなく、前にネタとしてやってた「声優関連図作り子ちゃん」からも大量の「声優・キャラクター」を取れちゃうんで、そこからも一気に作れるし。

まぁそうして作った辞書を公開する、みたいなことは出来ないけれど、仮にそうした「声優抽出」みたいな部分がネタとして面白いものであれば、そのうち何か紹介するかも。