Skip to content

Instantly share code, notes, and snippets.

@hhsprings
Last active February 8, 2020 13:37
Show Gist options
  • Save hhsprings/d35d956a2297f5e4360aac3d5d21898b to your computer and use it in GitHub Desktop.
Save hhsprings/d35d956a2297f5e4360aac3d5d21898b to your computer and use it in GitHub Desktop.
MeCab のユーザ辞書作成支援的ななにか
# -*- 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()
@hhsprings
Copy link
Author

これをモジュールとして使えるとは思えないけれど、ハイフンの入った名前はトラブルの元なので、名前変えた。(元は my-… だったのを、my_…に変えたてこと。)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment