Last active
February 8, 2020 13:37
-
-
Save hhsprings/d35d956a2297f5e4360aac3d5d21898b to your computer and use it in GitHub Desktop.
MeCab のユーザ辞書作成支援的ななにか
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- coding: utf-8 -*- | |
from __future__ import unicode_literals | |
import io | |
import os | |
import sys | |
import csv | |
if hasattr("", "decode"): # python 2 | |
class _reader(object): | |
def __init__(self, f, dialect=None, *args, **kwds): | |
self.line_num = 0 | |
self._f = f | |
def __iter__(self): | |
return self | |
def next(self): | |
line = None | |
if hasattr(self._f, "readline"): | |
line = self._f.readline().strip() | |
else: | |
if self.line_num < len(self._f): | |
line = self._f[self.line_num] | |
if not line: | |
raise StopIteration() | |
self.line_num += 1 | |
return re.split(r"(?<!\\),", line.rstrip()) | |
csv.reader = _reader | |
import re | |
import subprocess | |
import tempfile | |
import atexit | |
import logging | |
# TODO: ちゃんとドキュメント書こう。 | |
# ひとまず、「おれ流 csv 独自仕様」について4つ。 | |
# 1. 行頭を「列指定ヘッダ」として使える。モジュール変数の | |
# fieldnames_all 参照。 | |
# 2. 列は適宜省略できる。何を省略できるかはシステム辞書 | |
# の場所にある left-id.def を参照。このマップ | |
# は「品詞~活用形2」のみを管理していることに注意。) | |
# 3. 標準以外の列を書いた場合、mecab-dict-index に対する | |
# 「ユーザ設定フィールド」として追加される。 | |
# ただし→4. | |
# 4. どうしてもコメントを使いたかったので、「特殊列名」を導入 | |
# している。「:」で始まり「:」で終わる列名の列は単純に無視 | |
# するようにしている。ご自由にお使いなされ。(私はコメント | |
# 目的で使うが、ほかの用途もあるだろう。たとえば「:情報源:」 | |
# とか。) | |
try: | |
# ソースから mecab-config を持ってきて書き換えた上で | |
# 見えるところにおけばプアな MSYS ですら動かせる | |
# (めちゃくちゃシンプルなシェルスクリプトなので、自分で | |
# 直せる)ように、まわりくどい呼び方をしている。bash が必要、 | |
# なので、頼まないと bash が入らない FreeBSD とかなんか | |
# では弱るが…、まぁそれくらいは許して。 | |
__mecab_config_exec = 'bash -c "`type -p mecab-config` {}"' | |
_mecab_config = {} | |
for q in ("--exec-prefix", "--dicdir",): | |
_mecab_config[q] = subprocess.check_output( | |
__mecab_config_exec.format(q), | |
shell=True).strip().decode() | |
except subprocess.CalledProcessError as e: | |
# たぶん Windows。公式の標準でインストールしたとして。 | |
# inosetup なのでレジストリに書かれてるんじゃないかとは | |
# 思うんだけどとりあえず面倒なので決め打ちで。 | |
_mecab_config = { | |
"--exec-prefix": "c:/Program Files (x86)/MeCab", | |
"--dicdir": "c:/Program Files (x86)/MeCab/dic" | |
} | |
_dict_index_bin = os.path.join( | |
_mecab_config["--exec-prefix"], "bin", "mecab-dict-index") | |
# TODO: 「ipadic」以外の置き場もあるんではないかと。 | |
_sysdict_dir = os.path.join(_mecab_config["--dicdir"], "ipadic") | |
# システム辞書の「ソースの」エンコーディング。バイナリになった | |
# 辞書のエンコーディングではない。MeCab はユーザがシステム辞書 | |
# をメンテナンス出来るように標準インストールで辞書のソースが | |
# 配備される。(Windows でも euc-jp なり utf-8 のままにしとけば | |
# いいのに、なぜかインストーラのポストプロセスでせっせと SJIS | |
# に変換する。バイナリ辞書を utf-8 と選択してもなお、である。) | |
if ":" in _mecab_config["--exec-prefix"]: # Windows | |
_mecab_config["--sysdic-src-encoding"] = "cp932" | |
else: | |
# Unix「系」でここに落ちるはずだが、Windows にインストール | |
# した「もどき」では mecab-config の記述次第。 | |
# つまり「/c/msys/1.0/bin」形式で記述してあると混同しうる。 | |
# (さすがに素の msys (と MinGW) で MeCab ビルドを頑張る | |
# 人は極少だと思うが、そうして野良ビルドしたものを手で | |
# c:/Program Files なんぞに置いて /c/ 形式で書き、なおかつ | |
# 辞書ソースを cp932 エンコードで置いたりなんかすればアウト。) | |
_mecab_config["--sysdic-src-encoding"] = "utf-8" | |
# shift-jis (cp932) と utf-8 のマッピングにまつわる問題がある。 | |
# それが \u2212 と \uff0d で、これは見た目でも、エディタなど | |
# の検索等々でも違いを区別出来ない(ともに同一フォント「−」)。 | |
# これが活用形1で使われている。(サ変・−ズル、サ変・−スル。) | |
# たとえばエディタで ipadic システム辞書からコピーアンド | |
# ペーストでエディタで持ってきたとする。それでも \uff0d だった | |
# ものは \u2212 となる。Python の utf-8 エンコーディング固有 | |
# の問題なのか、utf-8 (と shift-jis) そのものが持つ問題なのか | |
# 私にはわからないが、とにかく Python で Windows 版のシステム | |
# 辞書を取り込んだ時点で爆弾を抱えてしまう。すなわち処理上は | |
# この二つを同一扱いしなければならない。そうしないと文脈IDを | |
# 引けない。 | |
def _norm_hyphen(s): | |
# TODO: unicodedata.normalize を使えるかあとで確認しよう。 | |
return s.replace("\u2212", "\uff0d") | |
def _remove_silent(fn): | |
try: | |
os.remove(fn) | |
except Exception: | |
pass # no problem. | |
def _replace_file_with_backup(fromfile, tofile): | |
def _get_origname(f): | |
from glob import glob | |
origs = list(sorted(glob(f + ".orig*"))) | |
if not origs: | |
return f + ".orig" | |
sufs = [s for s in [ | |
re.sub(r".*.orig\.?", r"", of) | |
for of in origs] | |
if re.match(r"\d+", s)] | |
if sufs: | |
suf = max(map(int, [s for s in sufs])) | |
return "{}.orig.{}".format(f, suf + 1) | |
else: | |
return "{}.orig.{}".format(f, 1) | |
if os.path.exists(tofile): | |
os.rename(tofile, _get_origname(tofile)) | |
os.rename(fromfile, tofile) | |
# もともとはこの位置にあった map で「全」候補(のつもり)を記述 | |
# していたが、rewrite.def を処理する必要性が出た副作用で、候補 | |
# の全羅列は不要になった。そのかわり「left-id.def を参照」という | |
# ハメに。 | |
# その「もともとあった map」は、既存の辞書エントリから列挙したもの | |
# だったのだが、これだと全 ID の可能性を列挙出来ない。具体的には | |
# 「動詞,接尾,*,*,五段・サ行,仮定縮約1,*」。これは ID は定義されて | |
# いるが辞書エントリで実際に使われていない。 | |
# 当初の目論見からはだいぶ異なる姿にはなったが、今でも近い目的で | |
# 使っている。 | |
_fields_cand_map = { | |
"Adj": [ | |
[ | |
"形容詞", "自立", "*" | |
], | |
[ | |
"形容詞", "接尾", "*" | |
], | |
[ | |
"形容詞", "非自立", "*" | |
] | |
], | |
"Adnominal": [ | |
[ | |
"連体詞", "*", "*" | |
] | |
], | |
"Adverb": [ | |
[ | |
"副詞", "一般", "*" | |
], | |
[ | |
"副詞", "助詞類接続", "*" | |
] | |
], | |
"Auxil": [ | |
[ | |
"助動詞", "*", "*" | |
] | |
], | |
"Conjunction": [ | |
[ | |
"接続詞", "*", "*" | |
] | |
], | |
"Filler": [ | |
[ | |
"フィラー", "*", "*" | |
] | |
], | |
"Interjection": [ | |
[ | |
"感動詞", "*", "*" | |
] | |
], | |
"Noun": [ | |
[ | |
"名詞", "一般", "*" | |
] | |
], | |
"Noun.adjv": [ | |
[ | |
"名詞", "形容動詞語幹", "*" | |
] | |
], | |
"Noun.adverbal": [ | |
[ | |
"名詞", "副詞可能", "*" | |
] | |
], | |
"Noun.demonst": [ | |
[ | |
"名詞", "代名詞", "一般" | |
], | |
[ | |
"名詞", "代名詞", "縮約" | |
] | |
], | |
"Noun.nai": [ | |
[ | |
"名詞", "ナイ形容詞語幹", "*" | |
] | |
], | |
"Noun.name": [ | |
[ | |
"名詞", "固有名詞", "人名" | |
] | |
], | |
"Noun.number": [ | |
[ | |
"名詞", "数", "*" | |
] | |
], | |
"Noun.org": [ | |
[ | |
"名詞", "固有名詞", "組織" | |
] | |
], | |
"Noun.others": [ | |
[ | |
"名詞", "引用文字列", "*" | |
], | |
[ | |
"名詞", "接続詞的", "*" | |
], | |
[ | |
"名詞", "動詞非自立的", "*" | |
], | |
[ | |
"名詞", "特殊", "助動詞語幹" | |
], | |
[ | |
"名詞", "非自立", "一般" | |
], | |
[ | |
"名詞", "非自立", "形容動詞語幹" | |
], | |
[ | |
"名詞", "非自立", "助動詞語幹" | |
], | |
[ | |
"名詞", "非自立", "副詞可能" | |
] | |
], | |
"Noun.place": [ | |
[ | |
"名詞", "固有名詞", "地域" | |
] | |
], | |
"Noun.proper": [ | |
[ | |
"名詞", "固有名詞", "一般" | |
] | |
], | |
"Noun.verbal": [ | |
[ | |
"名詞", "サ変接続", "*" | |
] | |
], | |
"Others": [ | |
[ | |
"その他", "間投", "*" | |
] | |
], | |
"Postp": [ | |
[ | |
"助詞", "格助詞", "一般" | |
], | |
[ | |
"助詞", "格助詞", "引用" | |
], | |
[ | |
"助詞", "係助詞", "*" | |
], | |
[ | |
"助詞", "終助詞", "*" | |
], | |
[ | |
"助詞", "接続助詞", "*" | |
], | |
[ | |
"助詞", "特殊", "*" | |
], | |
[ | |
"助詞", "副詞化", "*" | |
], | |
[ | |
"助詞", "副助詞", "*" | |
], | |
[ | |
"助詞", "副助詞/並立助詞/終助詞", "*" | |
], | |
[ | |
"助詞", "並立助詞", "*" | |
], | |
[ | |
"助詞", "連体化", "*" | |
] | |
], | |
"Postp-col": [ | |
[ | |
"助詞", "格助詞", "連語" | |
] | |
], | |
"Prefix": [ | |
[ | |
"接頭詞", "形容詞接続", "*" | |
], | |
[ | |
"接頭詞", "数接続", "*" | |
], | |
[ | |
"接頭詞", "動詞接続", "*" | |
], | |
[ | |
"接頭詞", "名詞接続", "*" | |
] | |
], | |
"Suffix": [ | |
[ | |
"名詞", "接尾", "サ変接続" | |
], | |
[ | |
"名詞", "接尾", "一般" | |
], | |
[ | |
"名詞", "接尾", "形容動詞語幹" | |
], | |
[ | |
"名詞", "接尾", "助数詞" | |
], | |
[ | |
"名詞", "接尾", "助動詞語幹" | |
], | |
[ | |
"名詞", "接尾", "人名" | |
], | |
[ | |
"名詞", "接尾", "地域" | |
], | |
[ | |
"名詞", "接尾", "特殊" | |
], | |
[ | |
"名詞", "接尾", "副詞可能" | |
] | |
], | |
"Symbol": [ | |
[ | |
"記号", "アルファベット", "*" | |
], | |
[ | |
"記号", "一般", "*" | |
], | |
[ | |
"記号", "括弧開", "*" | |
], | |
[ | |
"記号", "括弧閉", "*" | |
], | |
[ | |
"記号", "句点", "*" | |
], | |
[ | |
"記号", "空白", "*" | |
], | |
[ | |
"記号", "読点", "*" | |
] | |
], | |
"Verb": [ | |
[ | |
"動詞", "自立", "*" | |
], | |
[ | |
"動詞", "接尾", "*" | |
], | |
[ | |
"動詞", "非自立", "*" | |
] | |
] | |
} | |
fieldnames_all = [ | |
"表層形", | |
"左文脈ID", "右文脈ID", | |
"コスト", | |
"品詞", "品詞細分類1", "品詞細分類2", "品詞細分類3", | |
"活用形1", "活用形2", | |
"原形", "読み", "発音" | |
] | |
class _MeCabRewriteDef(object): | |
# ipadic では left-id.def と right-id.def は完全に同一内容。 | |
# かつ、 rewrite.def 内での [left rewrite] セクションと | |
# [right rewrite] セクションも 1bitとて違いはない。 | |
# 私の目的の範囲内では既存の ipadic 以外はターゲットに | |
# しないので、煩雑になるのを避けるために [left rewrite] | |
# のみ処理する。[unigram rewrite] は私のスクリプトでは | |
# ほかの処理が担ってるのでいらない。 | |
# | |
def __init__(self): | |
rwdef_cont = io.open( | |
os.path.join(_sysdict_dir, "rewrite.def"), | |
encoding=_mecab_config["--sysdic-src-encoding"]).read().strip() | |
self._rwentries = [] | |
st = False | |
for line in [ | |
line.strip() for line in re.split(r"\r?\n", rwdef_cont) | |
if line.strip() and line[0] != "#"]: | |
if line == "[right rewrite]": | |
break | |
if line == "[left rewrite]": | |
st = True | |
elif st: | |
dfrom_orig, dto_orig = re.split(r"\s+", line) | |
class _fieldconv(object): | |
def __init__(self, fd, rx): | |
self._rx = rx | |
self._repl = None | |
if fd != "*": | |
m = re.match(r"\$(\d)", fd) | |
if m: | |
self._repl = int(m.group(1)) - 1 | |
else: | |
self._repl = fd | |
def match(self, s): | |
return self._rx.search(s) | |
def sub(self, dictent, s): | |
if self._repl is None: | |
repl = s | |
elif isinstance(self._repl, (str,)): | |
repl = self._repl | |
else: | |
repl = dictent[self._repl] | |
if repl == s: | |
return s | |
return self._rx.sub(repl, s) | |
fromto_orig = zip( | |
re.split(r"\s*,\s*", dfrom_orig), | |
re.split(r"\s*,\s*", dto_orig)) | |
entry = [] | |
for i, (fr, to) in enumerate(fromto_orig): | |
entry.append( | |
_fieldconv( | |
to, | |
re.compile(fr if fr != "*" else ".*"))) | |
self._rwentries.append(entry) | |
def rewrite(self, dictent): # 品詞から先のみ対応 | |
for rwent in self._rwentries: | |
if all(([sf.match(dictent[i]) for i, sf in enumerate(rwent)])): | |
dictent = [sf.sub(dictent, dictent[i]) for i, sf in enumerate(rwent)] | |
return dictent | |
class _MeCabIDDef(object): | |
# ipadic では left-id.def と right-id.def は完全に同一内容。 | |
# | |
def __init__(self, rwdef): | |
self._rwdef = rwdef | |
# | |
iddef_cont = io.open( | |
os.path.join(_sysdict_dir, "left-id.def"), | |
encoding=_mecab_config["--sysdic-src-encoding"]).read().strip() | |
# ID, [品詞,品詞細分類1~3,活用形1~2,制約条件] | |
self._ctxids_all = list(reversed([ | |
tuple((list(map(_norm_hyphen, re.split(r"\s*,\s*", lr))) if i == 1 else int(lr)) | |
for i, lr in enumerate(re.split(r"\s", ent))) | |
for ent in re.split(r"\r?\n", iddef_cont)])) | |
# _fields_cand_map が管理するのは[品詞,品詞細分類1] | |
self._ctxids = [] | |
def select_class(self, cands): | |
self._ctxids = [] | |
for ctxid, ctxdef in self._ctxids_all: | |
if any((c == ctxdef[:3] for c in cands)): | |
self._ctxids.append((ctxid, ctxdef)) | |
uniqs = [set(), set(), set(), set(), set(), set()] | |
for _, ctxdef in self._ctxids: | |
for i, f in enumerate(ctxdef[:-1]): | |
uniqs[i].add(f) | |
uniqs = [list(f) for f in uniqs] | |
self._uniqs = [f[0] if len(f) == 1 else "*" for f in uniqs] | |
def search(self, actual_entry): | |
_ckeys = [ | |
k_ for k_ in fieldnames_all | |
if k_ not in ("左文脈ID", "右文脈ID", "コスト")] | |
actua = [_norm_hyphen(actual_entry.get(k_, "*")) for k_ in _ckeys] | |
if actua[7] == "*": | |
actua[7] = actua[0] | |
for i in range(1, 1 + len(self._uniqs)): | |
if actua[i] == "*": | |
actua[i] = self._uniqs[i - 1] | |
matched = None | |
cand = "" | |
for ctxid, ctxdef in self._ctxids: | |
is_matched = False | |
ctxid = "{}".format(ctxid) | |
if all([(rh == "*" or lh == rh) | |
for lh, rh in zip(actua[1:7], ctxdef[:-1])]): | |
cand = [ | |
a if a != "*" else d | |
for a, d in zip(actua[1:-2], ctxdef[:-1] + ["*"])] | |
cand = self._rwdef.rewrite(cand) | |
if ctxdef[-1] == "*": | |
is_matched = True | |
elif ctxdef[-1] == "*" or cand[6].endswith(ctxdef[-1]): | |
# 調べるのは原形で良いのか? また、末尾の合致で良いのか? | |
# 105 形容詞,非自立,*,*,形容詞・アウオ段,ガル接続,難い | |
# 106 形容詞,非自立,*,*,形容詞・アウオ段,ガル接続,良い | |
# 原形がちゃんとしてること前提なので、はっきりどちらかに | |
# 特定しなければならない場合は、自分で文脈IDを埋めて欲しい。 | |
# (曖昧な部分以外は「*」にしておけば良いので、一気にどん底 | |
# に落ちるわけではない。) | |
is_matched = True | |
if is_matched: | |
return [actua[0], ctxid, ctxid, "*"] + ctxdef[:-1] | |
raise ValueError("Invalid entry: {}".format(actual_entry)) | |
class _MeCabUserDictCsvBuilder(object): | |
def __init__(self, args, iddef): | |
self._args = args | |
self._iddef = iddef | |
# | |
_kind = args["kind"] | |
if not _kind: | |
tmp, _ = os.path.splitext(os.path.basename(args["input_csv"])) | |
for k in sorted(_fields_cand_map.keys(), key=lambda x: (-len(x), x)): | |
if k in tmp: | |
_kind = k | |
break | |
else: | |
_kind = "Noun" | |
self._iddef.select_class(_fields_cand_map[_kind]) | |
# | |
csvinput = io.open( | |
args["input_csv"], encoding=args["dictionary_charset"]).read() | |
header, _, body = csvinput.partition("\n") | |
_fieldnames = [ | |
# 「活用形」と「活用型」のどっちが正しいのかわからんが、公式説明で | |
# "活用型1", "活用形2" となってて騙された…。 | |
# 「品詞分類」については私のどこか処理の間違いで「品詞分類」と | |
# しているようなのだがなぜか場所が特定出来ない。どのみち間違いやすい | |
# とは思うので、問答無用で読み替えておく。 | |
s.replace("活用型", "活用形").replace("品詞分類", "品詞細分類") | |
for s in re.split(r"\s*,\s*", header)] | |
self._fieldnames = _fieldnames | |
if not any([(f in fieldnames_all) for f in self._fieldnames]): | |
self._fieldnames = fieldnames_all | |
if len(self._fieldnames) != len(_fieldnames): | |
raise ValueError("{}: invalid format".format(args["input_csv"])) | |
self._extra_fieldnames = [ | |
fn for fn in self._fieldnames | |
if fn not in fieldnames_all and not re.match(r":.*:", fn)] # | |
self._reader = csv.DictReader( | |
io.StringIO(body), | |
fieldnames=self._fieldnames) | |
self._defaults_cache = None | |
def _fallback(self, k, line): | |
if line.get(k) is not None: | |
return line[k] | |
if k in self._extra_fieldnames: | |
return line.get(k) | |
if k == "コスト": | |
return "*" | |
elif k == "原形": | |
return line.get("表層形", "") | |
if k == "読み" and "発音" in line: | |
return line["発音"] | |
elif k == "発音" and "読み" in line: | |
return line["読み"] | |
if not self._defaults_cache: | |
self._defaults_cache = self._iddef.search(line) | |
return self._defaults_cache[fieldnames_all.index(k)] | |
def _lines(self): | |
def _get(k, line): | |
# 「発音」に関してだけちょっと別扱い。「列」として | |
# 存在していて中身が空、というケースでも _read2pron | |
# したいので。 | |
if k == "発音" and "読み" in line: | |
pron = line.get(k) | |
if pron and pron != "*": | |
return pron | |
return self._read2pron(line["読み"]) | |
else: | |
return line.get(k, self._fallback(k, line)) or "*" | |
for line in self._reader: | |
self._defaults_cache = None | |
yield [ | |
_get(k, line) | |
for k in (fieldnames_all + self._extra_fieldnames)] | |
def _read2pron(self, r): | |
# 「発音」未入力時に、「読み」から生成する。 | |
# | |
# IPA辞書での「読み」と「発音」の対応関係に出来るだけ似せた | |
# かったが、無理があった。結構違う生成になるが、実用的には | |
# あまり問題ないのではないかと思う。たとえば「東奔西走」に対 | |
# する発音はこの処理では「トーホンセーソー」とするが、IPA | |
# 辞書ではこれに対して「トーホンセイソー」を与えている。 | |
# | |
# 「オオヤスウリ」を「オーヤスーリ」に、「ヒガシウスヅカ」 | |
# を「ヒガシュースズカ」してしまうなどについてはかわいいやつ | |
# だとでも思ってくれ。耳で聞くぶんにはそうおかしくもない。 | |
# 読み上げ用途ならそれほど問題にはならないと思うし。 | |
# | |
# あと基本的に固有名詞以外にはよろしくない。つまり「ハ」 | |
# を「ワ」に変換することをしていないので、「コンニチハ」 | |
# のままになってしまう。 | |
# | |
# つまりはこの処理がターゲットにするのは「面倒だから自分で | |
# 入力したくない」のための所詮はデフォルトと考えて欲しい。 | |
# 重要なものについてはやはり自分でちゃんと記述したほうがいい。 | |
# | |
# | |
# 古語に色目を使うと現代語でうまくいかないので、現代語のみを | |
# ターゲットにする。 | |
for fr, to in ( | |
# 以下、順番にも意味があるので、わかりやすさのために | |
# 並べ替えたりしないでね。 | |
("ヴァ", "バ",), | |
# ヴィをブイとするのビとするのも一つの流儀なので、どちらかを選ぶしかない。 | |
("ヴィ", "ビ",), | |
("ヴェ", "ベ",), | |
("ヴォ", "ボ",), | |
("ヴ", "ブ",), | |
("ヰ", "イ",), | |
("ヱ", "エ",), | |
("ヲ", "オ",), | |
("ヅ", "ズ",), | |
("ヂ", "ジ",), | |
(r"イ[ウフ]$", "ユウ"), | |
(r"^イウ", "ユウ"), | |
# 「○ア」⇒「○ー」で、○は基本的にア段。 | |
(r"([アァカガサザタダナハバパヤャラワ])[アァ]", r"\1ー",), | |
# 「○イ」⇒「○ー」で、○は基本的にイ段かエ段。 | |
(r"([イィキギセゼシジチヂニヒビピミメリレ])[イィ]", r"\1ー",), | |
# 「○ウ」⇒「○ュー」。「ニウ」⇒「ニュー」など。イ段。 | |
(r"([キシジチニヒビピミリ])[ウゥ]", r"\1ュー",), | |
# 「○オ」⇒「○ー」で、○は基本的にオ段。 | |
(r"([オコゴソゾトドノホボポモヨョロ])[オォ]", r"\1ー",), | |
# 「○エ」⇒「○ー」で、○は基本的にエ段。 | |
(r"([エェケセゼテデネヘベペメレ])[エェ]", r"\1ー",), | |
# 「○ウ」⇒「○ー」で、○は基本的にウ・オ段。 | |
(r"([オクグコゴスズソゾツヅトドヌノフブプホボポムモユュヨョルロ])[ウゥ]", r"\1ー",), | |
): | |
r = re.sub(fr, to, r) | |
return r | |
def content(self): | |
# フィールドにカンマそのものを含めたい場合はバックス | |
# ラッシュでエスケープ出来る、が、それをするのは csv の | |
# 記述者であって、スクリプトの責務ではない。 | |
return ("\n".join( | |
[",".join(line) for line in self._lines() | |
if any([(f and f.strip()) for f in line])]) + "\n") | |
class _MeCabUserDictCompiler(object): | |
def __init__(self, args): | |
self._args = args | |
self._contents = {} | |
def append_content(self, original_csvname, its_content): | |
if its_content.strip(): | |
self._contents[original_csvname] = its_content | |
logging.debug( | |
"translated: %s (%s chars)", original_csvname, len(its_content)) | |
def compile(self): | |
if not self._args["merge_all"]: | |
for n, c in self._contents.items(): | |
self._compile_one(n, c) | |
else: | |
self._compile_one( | |
self._args["merge_all"] + ".csv", | |
"".join(self._contents.values())) | |
def _merge_cost(self, user, assigned): | |
# --assign-user-dictionary-costs の振る舞いが、「空なら埋める」 | |
# ではなく「問答無用で上書く」であるため、ワタシのスクリプト | |
# と相性が悪い。「指定したいときだけ指定したい」ので。仕方が | |
# ないので、--assign-user-dictionary-costs で空を埋めてもらった | |
# あとに、「もともと指定してたやつ」を書き戻すことにする。 | |
ud = [line for line in csv.reader(filter(None, user.split("\n")))] | |
ad = [line for line in csv.reader(io.open(assigned, encoding="utf-8"))] | |
for i in range(len(ud)): | |
idx = fieldnames_all.index("コスト") | |
if ud[i][idx] and re.match(r"-?\d+", ud[i][idx]): | |
ad[i][idx] = ud[i][idx] | |
with io.open(assigned, "wb") as fo: | |
fo.write( | |
"\n".join([ | |
",".join(line) | |
for line in ad]).encode("utf-8")) | |
def _compile_one(self, original_csvname, its_content): | |
basename, _ = os.path.splitext( | |
os.path.basename(original_csvname)) | |
_mecab_official_formatted0 = os.path.join( | |
tempfile.gettempdir(), basename + "_0.csv") | |
_mecab_official_formatted1 = os.path.join( | |
tempfile.gettempdir(), basename + "_1.csv") | |
atexit.register(_remove_silent, _mecab_official_formatted0) | |
atexit.register(_remove_silent, _mecab_official_formatted1) | |
with io.open(_mecab_official_formatted0, "wb") as fo: | |
fo.write(its_content.encode("utf-8")) | |
# | |
dictname = os.path.splitext(original_csvname)[0] + ".dic" | |
# | |
cmdl = [ | |
_dict_index_bin, | |
"-d", _sysdict_dir, | |
"-f", "utf-8", | |
"-t", self._args["charset"], | |
] | |
in4mdi = _mecab_official_formatted0 | |
if self._args["model"]: | |
_remove_silent(_mecab_official_formatted1) | |
subprocess.check_call(cmdl + [ | |
"-m", self._args["model"], | |
"-a", | |
"-u", _mecab_official_formatted1, | |
_mecab_official_formatted0]) | |
in4mdi = _mecab_official_formatted1 | |
self._merge_cost(its_content, in4mdi) | |
# 予め消しとかないと mecab-dict-index が不平を言う。ので消す。 | |
# TODO: バックアップとった方がいい? | |
_remove_silent(dictname) | |
cmdl.extend([ | |
"-u", dictname, | |
in4mdi]) | |
# | |
subprocess.check_call(cmdl) | |
logging.info("dictionary '%s' was created.", dictname) | |
# | |
if self._args["replace_csv"]: | |
fn = _mecab_official_formatted1 | |
if not os.path.exists(fn): | |
fn = _mecab_official_formatted0 | |
_replace_file_with_backup(fn, original_csvname) | |
logging.info("'%s' was converted.", original_csvname) | |
def _translate_modelfile(model, charset): | |
import hashlib | |
# ワタシのスクリプトでは「コストを自動推測せよ」と等価。 | |
# この場合、mecab-dict-index を -a 付きで呼び出して | |
# 穴埋めしてもらった変換済みを先に作る。 | |
# なお、 | |
# 1. モデルファイルと辞書ファイルのエンコーディングが違うと | |
# 拒絶される | |
# 2. モデルファイルは「charset: euc-jp」のように自己主張する | |
# ので、実のエンコーディングを変えるだけではダメ。この | |
# 主張もセットで変えなければならない。 | |
# TODO: model はテキストのタイプとバイナリタイプがあるらしい。 | |
fnk = "{},{},".format(model, os.stat(model), charset) | |
cachebase = hashlib.md5(fnk.encode("utf-8")).hexdigest() | |
_mecab_model_reenc = os.path.join( | |
tempfile.gettempdir(), | |
"_my_mecab_model_{}.model".format(cachebase)) | |
if os.path.exists(_mecab_model_reenc): | |
model_path = _mecab_model_reenc | |
logging.info( | |
"using converted model '%s' (%s)", | |
model_path, charset) | |
return _mecab_model_reenc | |
model_path = model | |
model_enc = re.search( | |
b'charset: (.*)', | |
io.open(model, "rb").read(90)).group(1).decode() | |
if charset != model_enc: | |
logging.info( | |
"converting model '%s' because those charsets are different (%s -> %s)", | |
model_path, model_enc, charset) | |
model = io.open(model, encoding=model_enc).read() | |
with io.open(_mecab_model_reenc, "wb") as fo: | |
fo.write( | |
re.sub(re.escape(r"charset: {}".format(model_enc)), | |
"charset: {}".format(charset), | |
model).encode(charset)) | |
model_path = _mecab_model_reenc | |
return model_path | |
# | |
if __name__ == '__main__': | |
import argparse | |
logging.basicConfig(stream=sys.stderr, level=logging.INFO) | |
parser = argparse.ArgumentParser() | |
parser.add_argument("input_csv", nargs="+") | |
# my-mecab-dict-index specific | |
# kind 指定と複数入力は相性よくないが諦めてくれ。 | |
parser.add_argument( | |
"--kind", | |
choices=[""] + list(sorted(_fields_cand_map.keys())), | |
default="") | |
parser.add_argument("--replace-csv", action="store_true") | |
parser.add_argument( | |
"--merge-all", help="specify basename of output to build single dict") | |
parser.add_argument( | |
"-f", "--dictionary-charset", | |
help="assume charset of input CSVs as ENC", | |
default="utf-8") | |
# mecab-dict-index | |
parser.add_argument( | |
"-c", "--charset", | |
help="make charset of binary dictionary ENC", default="utf-8") | |
# 「モデルファイルを指定すること」と「コストの推測」は本来の | |
# mecab-dict-index では各々独立の指定だが、「ユーザ辞書作成」 | |
# という目的に絞った場合は「コストの推測をさせたいのでモデル | |
# ファイルを指定する」と結びつく。ゆえ、ワタシのスクリプト | |
# では「-m、-a」を一体で考えている。 | |
parser.add_argument( | |
"-m", "--model", | |
help="use FILE as model file.") | |
# | |
args = parser.parse_args() | |
args = { | |
k: getattr(args, k) | |
for k in dir(args) if k[0] != "_" } | |
files = args.pop("input_csv") | |
[os.stat(icsv) for icsv in files] | |
iddef = _MeCabIDDef(_MeCabRewriteDef()) | |
c = _MeCabUserDictCompiler(args) | |
if args["model"]: | |
args["model"] = _translate_modelfile( | |
args["model"], args["charset"]) | |
for icsv in files: | |
args["input_csv"] = icsv | |
logging.info("processing input '%s'", icsv) | |
b = _MeCabUserDictCsvBuilder(args, iddef) | |
c.append_content(icsv, b.content()) | |
c.compile() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
これをモジュールとして使えるとは思えないけれど、ハイフンの入った名前はトラブルの元なので、名前変えた。(元は my-… だったのを、my_…に変えたてこと。)