Skip to content

Instantly share code, notes, and snippets.

@hhsprings
Last active November 5, 2019 15:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hhsprings/1124d2c7ab811434c0ddc54120d0df5a to your computer and use it in GitHub Desktop.
Save hhsprings/1124d2c7ab811434c0ddc54120d0df5a to your computer and use it in GitHub Desktop.
MeCab のユーザ辞書作成支援的ななにか (3)
# -*- coding: utf-8 -*-
# 入力の Adj.csv (形容詞) とか Noun.adjv.csv (名詞,形容動詞語幹) に
# 長促音を追加するやーつ。「あったかい」→「あったかーい」とか。
# Python 2.7 では動かない。対応するつもりもない。
#
# TODO: 取捨選択の規則が埋め込みだけれど、コントロール可能にすべきね。
# TODO: そもそもターゲットの絞込みフィルタも同じくね。(狙ったものだけ
# 欲しい、とかだろ、普通。)
import io
import os
import csv
import re
import sys
import logging
def _nop(s):
return s
_khmap = {
chr(i + ord("ぁ")): chr(i + ord("ァ"))
for i in range((ord("ん") - ord("ぁ")) + 1)}
def _h2k(s):
return "".join(
[_khmap.get(c, c) for c in s])
fieldnames_all = [
"表層形",
"左文脈ID", "右文脈ID",
"コスト",
"品詞", "品詞細分類1", "品詞細分類2", "品詞細分類3",
"活用形1", "活用形2",
"原形", "読み", "発音"
]
class _DictType(object):
# 種別によって少しずつやることが違う。
# 単一ファイルにいろいろ混ぜてることは想定しない。
_cands = sorted(
(
"Adj",
"Adnominal",
"Adverb",
#"Auxil", # こいつは微妙。
"Conjunction",
"Filler",
"Interjection",
"Noun.adjv",
"Noun.adverbal",
# やってもいいんだけどこれは愉快な一致が多いので注意。
# 「そち」という呼称を「そっち」にしてしまうとかね。
"Noun.demonst",
"Noun.nai",
#"Noun.others", # こいつは微妙。
# これ対象にする場合は、絞り込んだほうがいいだろうな。
# 多いから、ではなくて、無意味な結果になるものが
# 多いと思う。
"Noun.verbal",
# これについてはほんとに量が膨大過ぎるので、必ず
# 対象にしたいものを絞り込むべし。Adj も多いが
# Adj のさらに3倍ある。
"Verb",
),
key=lambda s: (-len(s), s))
def __init__(self, input_csv):
bn = os.path.basename(input_csv)
self.ifnbase, _ = os.path.splitext(bn)
for cand in self._cands:
if self.ifnbase in cand:
self.detected = cand
logging.info("'%s': type of dict is '%s'", bn, self.detected)
break
else:
raise ValueError("unsupported dictionary type: " + self.ifnbase)
class _NewEntriesBuilder(object):
# 原形はそのままキープし…た方がいいのか変えたほうがいいのか
# 判別つかん。とりあえず変えない方で。
_rep_targs = ("表層形", "読み", "発音")
def __init__(self, input_csv):
self.dtype = _DictType(input_csv)
#
cont = io.open(input_csv, encoding=encoding).read()
lines = re.split(r"\r?\n", cont)
if not lines:
raise ValueError("{} is empty.".format(self.dtype))
self._has_header = None
if not any(((fn in lines[0]) for fn in fieldnames_all)):
self._fieldnames = fieldnames_all
reader = csv.DictReader(lines, fieldnames=self._fieldnames)
else:
self._fieldnames = re.split(r"(?<!\\),\s*", lines[0])
reader = csv.DictReader(lines[1:], fieldnames=self._fieldnames)
self._has_header = self._fieldnames
#
_rgx_targ = re.compile(r"[あ-ん]{2,}")
#
self._all_orig_ents = []
last = ""
for line in reader:
m = _rgx_targ.match(line["原形"])
if line["原形"] != last:
if m:
self._all_orig_ents.append([])
if m:
self._all_orig_ents[-1].append(line)
last = line["原形"]
def _patterns(self, bases):
def _gen(base, s):
for pos in range(1, len(base) + 1):
repl = base[:pos] + s + base[pos:]
kana_no_tsy = "あいうえおか-ぢつ-もやゆよら-んアイウエオカ-ヂツ-モヤユヨラ-ン"
kana_no_ts = "あいうえおか-ぢつ-んアイウエオカ-ヂツ-ン"
kana_no_ty = "あいうえおか-もやゆよら-んアイウエオカ-モヤユヨラ-ン"
kana_no_t = "ぁ-んァ-ン"
kana = "ぁ-んァ-ンー"
if all((
not re.search(r"っ[ぁぃぅぇぉっゃゅょー]", repl),
not re.search(r"[^{kana_no_ts}]っ[^{kana_no_ts}]".format(**locals()), repl),
not re.search(r"[^{kana_no_ty}]ー[^{kana_no_t}]".format(**locals()), repl),
not re.search(r"ー[ぁぃぅぇぉゃゅょ]", repl),
not re.search(r"(っっ|ーー)", repl),
# 「だだっ広ーい」はもちろんいいんだけれど、長促音を書く人は
# 大抵は「耳での聴こえ方」重視でひらがなで書くことが多いと
# 思う。ので除外。というのも、送り仮名に混ざってしまって読みが
# 曖昧になるものまで生成してしまうから。たとえば「凄ーまじ」は
# 「すごーまじ」ではない?
not re.search(r"[^{kana_no_t}]ー".format(**locals()), repl),
not re.search(r"[^{kana}][っッ]".format(**locals()), repl),
)):
yield repl
# 「のける」と「のっける」の関係のように、意味が違ってきて
# しまうものを生成してしまうのをなるたけ避けるために、
# 促音追加と長音だけ追加の2バージョンを区別して返す。
pats_s, pats_t = set(), set()
for b in bases:
_full_s = list(_gen(b, "っ"))
pats_s |= set(_full_s)
pats_t |= set(_gen(b, "ー"))
for p in (_gen(repl, "ー") for repl in _full_s):
pats_s |= set(p)
return pats_s, pats_t
def newentries(self):
# よく末尾の「い」以外をカタカナにするやつ。「ダサい」「ウザい」etc.
kanaver = (
"うざ", "ひど", "きも", "ださ",
"くさ", "ちょろ", "やば", "むず",
)
def _split_into_base_and_suf(base):
# 活用語(形容詞など)の場合は「やぼった」+「い」などに分解。
# これは「やぼったきゃ」との共通部分が「やぼった」だから。
# 正直言って「やぼったいー」「やぼったいっ」もないとは思わない
# んだけど、とりあえずやると結構大変なので。
if self.dtype.detected in ("Adj", "Verb"):
return base[:-1], base[-1]
return base, ""
for i, ent in enumerate(self._all_orig_ents):
base, suf = _split_into_base_and_suf(ent[0]["原形"])
rgx = re.compile("^({}|{})".format(base, _h2k(base)))
bases = [base]
for kb in kanaver:
if base.startswith(kb) and suf == "い":
# このパターンでも「原形」はそのまま…がいいと
# 思うんだけどね、たぶん。
bases.append(
re.sub(r"^{}".format(kb), _h2k(kb), base))
break
use_pat = set()
for pats in self._patterns(bases):
# 「とくに」と「とっくに」のように、促音の追加で別物に
# なってしまうものがある。ゆえに、促音の追加でほかの
# 原形に一致するならば促音の追加はしない。このような
# 愉快な一致のないものの場合は、むしろ促音の位置が
# だいたい決まっているということ。「あったかい」が
# 「あたっかい」となっては、なんだかよくわからない、
# さすがに。(実際「あったかい」のケースは「たた」
# の音便形なので位置は動かないわけだが、そうでない
# ものも、促音の有無の両方が入っているものはその位置
# 以外には普通は入らない。「ぴたり」と「ぴったり」など。
# ただし「あったりまえ」と「あたっりまえ」など、全部が
# 全部というわけではないのだけれど。)
for repl in pats:
if any([(repl + suf == e[0]["原形"])
for j, e in enumerate(self._all_orig_ents) if j != i]):
logging.info("'%s' is already in original.", repl + suf)
break
else:
use_pat |= pats
for repl in sorted(list(use_pat)):
if any([(repl + suf == e[0]["原形"])
for j, e in enumerate(self._all_orig_ents) if j != i]):
logging.info("'%s' is already in original.", repl + suf)
continue
for line in ent:
newent = {
k: v for k, v in line.items()
if k not in self._rep_targs}
for k in self._rep_targs:
fun = _nop if k == "表層形" else _h2k
newent[k] = rgx.sub(fun(repl), line[k])
newent[k] = re.sub(r"([ア-ンー])っ([ア-ン])", r"\1ッ\2", newent[k])
yield ",".join([newent[k] for k in self._fieldnames])
def dump(self, outname, encoding):
result = list(self.newentries())
with io.open(outname, "wb") as fo:
if self._has_header:
fo.write((", ".join(self._has_header) + "\n").encode(encoding))
fo.write("\n".join(result).encode(encoding))
logging.info("'%s.csv' -> '%s' done.", self.dtype.ifnbase, outname)
if __name__ == '__main__':
logging.basicConfig(stream=sys.stderr, level=logging.INFO)
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("adj_csv")
parser.add_argument("--in-encoding", default="")
parser.add_argument("--out-encoding", default="")
parser.add_argument("--out-filename-format", default="{}.tyousoku.csv")
args = parser.parse_args()
#
encoding = None
if args.in_encoding:
encoding = args.in_encoding
else:
# 自動推測をがんばってみたが、あんましよろしくない。
# 明示的に指定したほうがいいと思うよ。
enc_cands = ["cp932", "utf-8", "euc-jp"]
for i, enc in enumerate(enc_cands):
try:
io.open(args.adj_csv, encoding=encoding)
encoding = enc
break
except UnicodeError:
if i == len(enc_cands) - 1:
raise
#
builder = _NewEntriesBuilder(args.adj_csv)
builder.dump(
args.out_filename_format.format(builder.dtype.ifnbase),
args.out_encoding or encoding)
@hhsprings
Copy link
Author

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

というかこやつに関してはそもそもまったく実用にはならんお遊びなので、モジュールとして使う云々以前の問題だけれども。

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