そこそこめんどいのかと思ってたら全然そうでもなかったので。
ひとつ前のでちょっと誤解してて。
mecab-cost-train
でトレーニングするかなんだけれど、後者を採るのであれば、ある程度自動化も出来るのかもしれなくて、これについては、さすがに「おれのためだけ」にすると勿体無いので、やる気になったらここに追記の形で書く可能性はある。
と書いたけれど、mecab-cost-train
するのは別に不可欠というわけではなくて、誰かが作ったモデルファイルさえ渡せば自動推測自体はやってくれるのだね。(同じく単語の追加方法に書いてある。mecab-ipadicのモデルファイルが手に入る。)
ついでにいえば「文脈ID」も埋めてくれる機能があった。だから私のスクリプトが頑張る理由が少し薄れてる。まぁこれはいいか。
見出しにした本題の前に。これも誤解起因なんだけれど、私のスクリプトは「ユーザ辞書作成専用」にすべきだった。mecab-dict-index が役割過多というか。「辞書作成以外のこと」がいくつか出来てしまうので、ただのラッパーにしてしまうと非常にわかりにくいものになってしまう。ので、スクリプトの名前も「my-mecab-userdict-build」にしようと思う。
そんなわけで、ひとまずこうなった:
1 # -*- coding: utf-8 -*-
2 from __future__ import unicode_literals
3 # TODO: すまん、python 2.7 対応が出来てない。
4
5 import io
6 import os
7 import csv
8 import re
9 import subprocess
10 import tempfile
11
12
13 class _BaseMeCabDictInputBuilder(object):
14 fieldnames_all = [
15 "表層形",
16 "左文脈ID", "右文脈ID",
17 "コスト",
18 "品詞", "品詞細分類1", "品詞細分類2", "品詞細分類3",
19 "活用型1", "活用形2",
20 "原形", "読み", "発音"
21 ]
22 _defaults_tobe_blank = ("コスト",)
23
24 def __init__(self, args):
25 self._args = args
26 self._mecab_official_formatted0 = os.path.join(
27 tempfile.gettempdir(), "_my_mecab_dict0_.csv")
28 self._mecab_official_formatted1 = os.path.join(
29 tempfile.gettempdir(), "_my_mecab_dict1_.csv")
30 self._mecab_model_reenc = os.path.join(
31 tempfile.gettempdir(), "_my_mecab_model_.model")
32 #
33 csvinput = io.open(
34 args.input_csv, encoding=args.dictionary_charset).read()
35 header, _, body = csvinput.partition("\n")
36 self._fieldnames = re.split(r"\s*,\s*", header)
37 self._extra_fieldnames = [
38 fn for fn in self._fieldnames
39 if fn not in self.fieldnames_all]
40 self._reader = csv.DictReader(
41 io.StringIO(body),
42 fieldnames=self._fieldnames)
43
44 def _default(self, k, line):
45 # should be overidden
46 return "*"
47
48 def _fallback(self, k, line):
49 if k in line:
50 return line[k]
51 if k in self._defaults_tobe_blank:
52 return ""
53 if k == "読み" and "発音" in line:
54 return line["発音"]
55 elif k == "発音" and "読み" in line:
56 return line["読み"]
57 return self._default(k, line)
58
59 def _lines(self):
60 for line in self._reader:
61 yield [
62 line.get(k, self._fallback(k, line))
63 for k in (self.fieldnames_all + self._extra_fieldnames)]
64
65 def _write_inputcsv(self, outname):
66 # TODO: エスケープ必要だったりする?
67 content = ("\n".join(
68 [",".join(line) for line in self._lines()
69 ]) + "\n").encode("utf-8")
70 with io.open(outname, "wb") as fo:
71 fo.write(content)
72
73 def __enter__(self):
74 return self
75
76 def __exit__(self, exc, value, tb):
77 for fn in (
78 self._mecab_official_formatted0,
79 self._mecab_official_formatted1,
80 self._mecab_model_reenc,):
81 if os.path.exists(fn):
82 try:
83 os.remove(fn)
84 except Exception:
85 pass # no problem.
86
87 def compile(self):
88 # TODO: MECAB_HOME なんて標準があるのか未確認。
89 _mecab_home = os.environ.get(
90 "MECAB_HOME", "c:/Program Files (x86)/MeCab")
91 _dict_index_bin = os.path.join(
92 _mecab_home, "bin", "mecab-dict-index")
93 # TODO: 「ipadic」以外の置き場もあるんではないかと。
94 _sysdict_dir = os.path.join(_mecab_home, "dic", "ipadic")
95
96 self._write_inputcsv(self._mecab_official_formatted0)
97 #
98 dictname = self._args.output_dictionary_name
99 if not dictname:
100 dictname, _ = os.path.splitext(
101 os.path.basename(self._args.input_csv))
102 dictname += ".dic"
103 #
104 cmdl = [
105 _dict_index_bin,
106 "-d", _sysdict_dir,
107 "-f", "utf-8",
108 "-t", self._args.charset,
109 ]
110 in4mdi = self._mecab_official_formatted0
111 if self._args.model:
112 # ワタシのスクリプトでは「コストを自動推測せよ」と等価。
113 # この場合、mecab-dict-index を -a 付きで呼び出して
114 # 穴埋めしてもらった変換済みを先に作る。
115 # なお、
116 # 1. モデルファイルと辞書ファイルのエンコーディングが違うと
117 # 拒絶される
118 # 2. モデルファイルは「charset: euc-jp」のように自己主張する
119 # ので、実のエンコーディングを変えるだけではダメ。この
120 # 主張もセットで変えなければならない。
121 # TODO: model はテキストのタイプとバイナリタイプがあるらしい。
122 model_path = self._args.model
123 model_enc = re.search(
124 b'charset: (.*)',
125 io.open(self._args.model, "rb").read(90)).group(1).decode()
126 if self._args.charset != model_enc:
127 model = io.open(self._args.model, encoding=model_enc).read()
128 with io.open(self._mecab_model_reenc, "wb") as fo:
129 fo.write(
130 re.sub(re.escape(r"charset: {}".format(model_enc)),
131 "charset: {}".format(self._args.charset),
132 model).encode(self._args.charset))
133 model_path = self._mecab_model_reenc
134 subprocess.check_call(cmdl + [
135 "-m", model_path,
136 "-a",
137 "-u", self._mecab_official_formatted1,
138 self._mecab_official_formatted0])
139 in4mdi = self._mecab_official_formatted1
140 cmdl.extend([
141 "-u", dictname,
142 in4mdi])
143 #
144 subprocess.check_call(cmdl)
145
146
147 class _NounName(_BaseMeCabDictInputBuilder):
148 _defaults_fixed = {
149 "品詞": "名詞",
150 "品詞細分類1": "固有名詞",
151 "品詞細分類2": "人名",
152 }
153 def __init__(self, args):
154 _BaseMeCabDictInputBuilder.__init__(self, args)
155
156 def _default(self, k, line):
157 if k in self._defaults_fixed:
158 return self._defaults_fixed[k]
159 if k in ("左文脈ID", "右文脈ID"):
160 return {
161 "一般": "1289",
162 "姓": "1290",
163 "名": "1291",
164 }.get(line.get("品詞細分類3", ""))
165 if k == "原形":
166 return line.get("表層形", "")
167 return "*"
168
169
170 #
171 if __name__ == '__main__':
172 import argparse
173
174 parser = argparse.ArgumentParser()
175 parser.add_argument("input_csv")
176
177 # my-mecab-dict-index specific
178 parser.add_argument("--output-dictionary-name", default="")
179 parser.add_argument(
180 "-f", "--dictionary-charset",
181 help="assume charset of input CSVs as ENC",
182 default="utf-8")
183 # mecab-dict-index
184 parser.add_argument(
185 "-c", "--charset",
186 help="make charset of binary dictionary ENC", default="utf-8")
187 # 「モデルファイルを指定すること」と「コストの推測」は本来の
188 # mecab-dict-index では各々独立の指定だが、「ユーザ辞書作成」
189 # という目的に絞った場合は「コストの推測をさせたいのでモデル
190 # ファイルを指定する」と結びつく。ゆえ、ワタシのスクリプト
191 # では「-m、-a」を一体で考えている。
192 parser.add_argument(
193 "-m", "--model",
194 help="use FILE as model file.")
195 #
196 args = parser.parse_args()
197 #
198 #
199 with _NounName(args) as b:
200 b.compile()
冒頭コメントの通り、前回のも今回のも Python 2.7 で動かない、すまん。
その「コストの自動推測」以外については:
- 「ユーザ設定フィールド」も使えるようにしといた
- インターフェイスを
mecab-dict-index
に少し似せた
ことだけ、かな、確か。コードは少し整理はしてあるけど本質的にはそれだけ。
1 [me@host: ~]$ cat Noun.name.anime.csv
2 表層形, 品詞細分類3, 原形, 読み, ジャンル
3 黒沢ともよ,一般,黒沢ともよ,クロサワトモヨ,声優
4 犬吠埼樹,一般,犬吠埼樹,イヌボエザキイツキ,アニメキャラ
5 [me@host: ~]$ #
6 [me@host: ~]$ # my-mecab-userdict-build.py にマジックナンバー付いてて
7 [me@host: ~]$ # としてなおかつパスが通ってるとして…
8 [me@host: ~]$ my-mecab-userdict-build.py Noun.name.anime.csv -m=mecab-ipadic-2.7.0-20070801.model
9 C:\Users\HHSPRI~1\AppData\Local\Temp\_my_mecab_model_.model is not a binary model. reopen it as text mode...
10 reading C:\Users\HHSPRI~1\AppData\Local\Temp\_my_mecab_dict0_.csv ...
11 done!
12 reading C:\Users\HHSPRI~1\AppData\Local\Temp\_my_mecab_dict1_.csv ... 2
13 emitting double-array: 100% |###########################################|
14
15 done!
16 [me@host: ~]$ # mecab にパスが通ってるとして…
17 [me@host: ~]$ cat some.txt
18 犬吠埼樹はアニメのキャラ。
19 [me@host: ~]$ mecab < some.txt
20 犬吠埼樹 名詞,固有名詞,人名,一般,*,*,犬吠埼樹,イヌボエザキイツキ,イヌボエザキイツキ,アニメキャラ
21 は 助詞,係助詞,*,*,*,*,は,ハ,ワ
22 アニメ 名詞,一般,*,*,*,*,アニメ,アニメ,アニメ
23 の 助詞,連体化,*,*,*,*,の,ノ,ノ
24 キャラ 名詞,一般,*,*,*,*,キャラ,キャラ,キャラ
25 。 記号,句点,*,*,*,*,。,。,。
26 EOS
ユーザ設定フィールドは使うならちゃんと「設計」して使ったほうがいいかな。入力によってフィールドの意味を変えちゃうんでは管理できなくなるだろう。
前回の『あとは…「人名以外」のためのものがあって、してそれのファクトリがいて、それをコマンドラインオプションで指定出来て…で、まぁまぁ完全なものにはなるんじゃないかな。「コンパイルまでなら」。』はそのままで、新たに『穴埋めした csv も吐き出せた方がいいかしら?』が増えた。特にコスト推測の結果を知りたいというのはありそう。(今のスクリプトは作ったら消してる。)
トレーニングしてモデルを作成するネタも一緒にやろうかと思ったんだけど、コードが既に長くなってることもあって、一緒に書くと煩わしい(読みにくい)ので、これについてはここではやめとく。
追記: あかん。「トレーニング」は簡単に手を出せるシロモノではなかった。
see オリジナル辞書/コーパスからのパラメータ推定. いずれは遊んでみようかとも思うが、すぐではない。