Last active
November 5, 2019 16:09
-
-
Save hhsprings/3633db7864c8e2496fd7bc5f81418348 to your computer and use it in GitHub Desktop.
MeCab のユーザ辞書作成支援的ななにか (4)
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 -*- | |
# | |
# Copyright (c) 2019, Hiroaki Itoh <https://bitbucket.org/hhsprings/> | |
# All rights reserved. | |
# | |
# Redistribution and use in source and binary forms, with or without | |
# modification, are permitted provided that the following conditions | |
# are met: | |
# | |
# - Redistributions of source code must retain the above copyright | |
# notice, this list of conditions and the following disclaimer. | |
# | |
# - Redistributions in binary form must reproduce the above copyright | |
# notice, this list of conditions and the following disclaimer in | |
# the documentation and/or other materials provided with the | |
# distribution. | |
# | |
# - Neither the name of the xwhhsprings nor the names of its contributors | |
# may be used to endorse or promote products derived from this software | |
# without specific prior written permission. | |
# | |
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED | |
# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR | |
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR | |
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, | |
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, | |
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR | |
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF | |
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING | |
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | |
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
# | |
# ------------------------------------------------------------------ | |
# | |
# http://myoujijiten.web.fc2.com/ から MeCab 人名辞書(の入力 csv) | |
# を作る。本題は MeCab 辞書だがオマケとして MS IME 向け(など)にも | |
# 対応。 | |
# | |
# ○出力の csv は my_mecab_userdict_build.py 前提。 | |
# ○要: Python 3.3+ (かな?) と Mako、そして当然 MeCab 環境。 | |
# ○利用条件について: | |
# 利用条件が一切書かれていないので、利用そのものは自由なのだと | |
# 判断。 | |
# ただし、私(bitbucket.org/hhsprings, github.com/hhsprings)のこの | |
# スクリプトで生成したものについては十分に扱いに注意してください。 | |
# まずはそれは私の成果ではなく http://myoujijiten.web.fc2.com/ に | |
# よるものです。何かインターネット上で公開する情報の元として私の | |
# 成果を利用したい場合は、まずは http://myoujijiten.web.fc2.com/ | |
# に基いたことは必ず記載すべきと思います。私のスクリプトに感謝する | |
# 必要はないですが、情報源として利用したことはうたったほうがよろ | |
# しいです。というのも、私のスクリプトが完全である保証はどこにも | |
# なく、変換時のミスはあくまでも私に責任があるからです。(だからと | |
# いって何か保障の類を出来るわけではないです。ライセンス文に記載 | |
# した通りです。) | |
# ○画像になっている文字を持つ姓の扱いについて: | |
# ・「一点の辻など苗字で一般的な字体がPCで出せなくなっています」 | |
# と説明されているものは JIS90 と JIS2004 の関係であり、これに | |
# ついては苗字一覧では画像にはなっていない。ゆえにこのスクリプト | |
# でも素直に使っている。だから JIS90 vs JIS2004 問題はこのスクリ | |
# プトで作る MeCab 辞書にもストレートに引き継がれる。 | |
# (データとしては utf-8 にマッピングされている文字であれば、 | |
# 処理が何か問題を起こすわけではないので。) | |
# ・ひとつだけ「スマホ・携帯では表示できない字」として、文字データ | |
# そのものと画像の併記がある。これについても「一点の辻など苗字 | |
# で~」と扱いはまったく同じ。 | |
# ・その一つ以外で画像になっているのは JIS90、JIS2004 には関係ない、 | |
# らしい。myoujijiten による説明がこれに関してちゃんとしてないので、 | |
# それを知るには自力で調べるしかないが、調べてないのでわからない。 | |
# もともと紙の電話帳スキャンがベースらしいので、画像はそれに基く | |
# ものなのだろうが、Unicode コードに該当する文字があるのかないのか | |
# が一切説明がない。少なくとも表に文字として埋まっていないのだから、 | |
# マッピングされていないということなのだと思う。 | |
# これに該当する姓は、myoujijiten では 51 登録されているが、 | |
# myoujijiten による実在数推計は多くて 100 人程度の模様。 | |
# 少ないから無視してよいということには(場合によっては)ならないが、 | |
# そもそもがこのスクリプトの目的は「MeCab の辞書を作る」ことで | |
# あるから、文字コードにマッピングされていない文字は何をしたって | |
# 扱えないし、それをしようとすることそのものが MeCab 用途には | |
# 無意味。すなわち「無視するしかない」となる。 | |
# そうした珍名の当人は「毎日困っている」はずだけれど、ほとんどが | |
# (電子的な扱いの場合は)代用漢字を使って日常を過ごすはず。結局 | |
# MeCab がターゲットとする電子的な文書には現れない。 | |
# ○「ケ」「ツ」の扱いについて: | |
# ケ、ツの扱いに関し、http://myoujijiten.web.fc2.com では原則 | |
# 「伊ケ崎」「三ツ井」で単一表記している。「実在性」に基づく収集 | |
# ポリシーなので「小文字しか確認出来なかった場合には小文字」とは | |
# しているが、MeCab で利用するにあたっての実用性の意味では、 | |
# 「常に併記」でないと困る。MeCab が扱うのは「実在人名なのかどうか」 | |
# ではなくて、「人名を記述したものであるかどうか」なので。電子的に | |
# 文書を記述するにあたり、特に「ケ」に関し一般的な IME が「ヶ」の | |
# 方を優先してしまう関係で、どんなに「私の名前は「伊ケ崎」です | |
# 「伊ヶ崎」じゃありません」としてみたところで多くの他人が「伊ヶ崎 | |
# さん」と間違える。MeCab で扱いたい対象は必ずしも「実在として正 | |
# しい人名しか書かれていない」わけではない。 | |
# なお、私が myoujijiten を使いたいのは「実在苗字と芸名などにしか | |
# 現れない苗字」を区別できるようにしたいから。けれど「ヶ」「ッ」は | |
# その区別とは関係ない。 | |
# | |
import io | |
import sys | |
import os | |
import re | |
import urllib.request | |
from collections import defaultdict | |
from glob import glob | |
import itertools | |
import csv | |
import subprocess | |
import logging | |
import mako.template # require mako | |
# ======================================================== | |
# | |
# 補助的なもろもろ | |
# | |
_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]) | |
_hkmap = { | |
chr(i + ord("ァ")): chr(i + ord("ぁ")) | |
for i in range((ord("ン") - ord("ァ")) + 1)} | |
def _k2h(s): | |
return "".join( | |
[_hkmap.get(c, c) for c in s]) | |
# ======================================================== | |
# | |
# http://myoujijiten.web.fc2.com/ の html ページを、まずは | |
# シンプルに csv にするだけのパート。 | |
# | |
# このスクリプトの本題にとっては「プリプロセス」に過ぎない | |
# が、このステップで生成される csv 単独でも価値があるかも | |
# しれない(MeCab 辞書に登録できないエントリもある関係で特に)。 | |
# | |
_cachedir = { | |
"": ".", | |
"img": ".", | |
"JIStaishou": "./JIStaishou", | |
"JIStaishou_img": "./JIStaishou", | |
} | |
baseurl = "http://myoujijiten.web.fc2.com/" | |
def _retrieve(href): | |
logging.info("retrieving %r...\r", href) | |
req = urllib.request.urlopen(baseurl + href) | |
logging.info("done.") | |
return req.read() | |
def _retrieve_img(imgsrc, imgofn): | |
if not os.path.exists(imgofn): | |
img = _retrieve(imgsrc) | |
with io.open(imgofn, "wb") as fo: | |
fo.write(img) | |
def _get_allpage(): | |
_rgx_pa = re.compile( | |
'<A href="([^"]+)"[^<>]*>([^<>]*)</A>', | |
flags=re.I) | |
req = urllib.request.urlopen(baseurl) | |
cont = req.read().decode("utf-8") | |
cont = re.sub(r"</?(font|b)\b[^<>]*>", r"", cont, flags=re.I) | |
exclude = lambda url: any(( | |
re.search(p, url) | |
for p in ( | |
"kousin", | |
"kanji", | |
"sirabekata", | |
"todouhuken", | |
"contents" | |
))) | |
return [ | |
(url, title, "JIS" not in url) | |
for url, title in _rgx_pa.findall(cont) | |
if not exclude(url)] | |
def _all_to_csv(cachedir_base): | |
def _to_text(s): | |
s = re.sub(r"</?[fs][^<>]+>", r"", s, flags=re.I) | |
s = re.sub(r"<[^<>]+></[^<>]+>", r"", s, flags=re.I) | |
s = re.sub(r"</?t[dr][^<>]*>", r"", s, flags=re.I) | |
s = s.replace(" ", " ") | |
return s.strip() | |
def _process_each_entry(found_td): | |
converted = [_to_text(td) for td in found_td[1:]] | |
# | |
yomi = converted[2] | |
# 「機種依存文字でうんぬん」のパターンで読みがうまって | |
# いないものがいる。この場合は読み見出しを使うしかない。 | |
if not yomi: | |
yomi = _k2h(converted[0]) | |
# | |
yomi, extra = (re.sub(r"[\s ]+", " ", yomi), "") | |
# | |
# 説明では「>」(全角の大なり)だが、なぜか「>」がいる。 | |
yomi = yomi.replace(">", ">") | |
# 「せんちょうorせんてい」という説明されていない記述がある。 | |
# うそつき…。これは「・」に読み替えるしかあるまい。 | |
yomi = re.sub( | |
r"([ぁ-ん])\s*or\s*([ぁ-ん])", r"\1・\2", | |
yomi) | |
# なぜかピリオドを含むのがひとつだけ。 | |
yomi = re.sub(r"\.", "", yomi) | |
# | |
# 「備考」欄を作ってそこに書けばいいのに、というような記述 | |
# が読みカラムにガシガシ含まれている。こういうものを「あた | |
# かも備考欄」列に移動する。これに相当するものは二種類と | |
# みなす。ひとつは「よく聞くニノマエは実在の根拠なし」みた | |
# いなかなりフリーダムなやつ。解析で一番邪魔になるやつなの | |
# で、処理的には先に行うが、「備考2」扱い。もう一つは「いず | |
# も (いづも・しゅっと★(島根の一部))」のような記述で、「★」 | |
# は単に難読であることを示すだけで、要するに「えらいえらーい」 | |
# て意味でしかない。そして地域による偏りを示す「(島根の一部)」 | |
# などで、これは読みひとつに対しひとつ…なのかな、たぶん。 | |
# これを備考1として。 | |
# | |
# まずは備考2。 | |
extra2 = "" | |
spl = re.split(r"(?<![>・])\s(?![>()()・])", yomi) | |
if len(spl) == 2: | |
yomi, extra2 = spl | |
logging.debug("%s | %s", yomi, extra2) | |
if "内に画像を添えた" in extra2: | |
# このパターンの表層形のみ「○(<img ...>)和」。ほかの | |
# img は漢字が埋まってない。 | |
sur = converted[1] | |
irx = re.compile(r"(.)\((<img[^<>]+>)\)", flags=re.I) | |
m = irx.search(sur) | |
ch, img = m.group(1, 2) | |
extra2 = re.sub( | |
r"(.*できない字).*", r"{}は\1({})".format(ch, img), | |
extra2) | |
converted[1] = irx.sub(r"\1", sur) | |
else: | |
# 「(~ではない)」がなぜか一つだけ。 | |
rx = re.compile(r"\(.*?ではない\)") | |
m = rx.search(yomi) | |
if m: | |
yomi, extra2 = rx.sub("", yomi), m.group(0) | |
logging.debug("%s | %s", yomi, extra2) | |
# そして備考1 | |
rx = re.compile( | |
r"(([ぁ-んー]+)★?\s*(\([^ぁ-んー]+(?:の一部)?\))?★?)") | |
extra1 = [re.sub(r"\s*★\s*", "★", f[0]) | |
for f in rx.findall(yomi) | |
if re.search(r"[★()]", f[0])] | |
yomi = rx.sub(r"\2", yomi) | |
if extra1: | |
logging.debug("%r, %r", yomi, extra1) | |
# | |
converted[2] = yomi | |
# | |
converted.append("|".join(extra1)) | |
converted.append(extra2) | |
# | |
return converted | |
_rgx_td = re.compile(r'<td[^<>]*[><]', flags=re.I) | |
for d in _cachedir.values(): | |
d = os.path.join(cachedir_base, d) | |
if not os.path.exists(d): | |
os.makedirs(d) | |
for page, _, is_ent in _get_allpage(): | |
bn, _ = os.path.splitext(page) | |
if is_ent: | |
ofn = os.path.join( | |
cachedir_base, _cachedir[""], bn + ".csv") | |
else: | |
ofn = os.path.join( | |
cachedir_base, _cachedir["JIStaishou"], bn + ".csv") | |
# | |
if os.path.exists(ofn): | |
continue | |
cont = _retrieve(page).decode("utf-8") | |
cont = re.sub(r"<!doctype.*</head>", r"", cont, flags=re.I | re.S) | |
cont = re.sub(r"<script.*</script>", r"", cont, flags=re.I | re.S) | |
cont = re.sub( | |
r'(<[^<>]+)(?:align|width|border)="[^"]+"([^<>]*>)', | |
r"\1\2", cont, flags=re.I) | |
endtbl = list(re.finditer(r"</TABLE>", cont, re.I)) | |
cont = cont[endtbl[0].span()[1]:endtbl[1].span()[0]] | |
cont = re.sub(r" *\r?\n *", "", cont) | |
cont = re.sub(r"(</?TBODY>)", r"\1\n", cont, flags=re.I) | |
cont = re.sub(r"(</?TR>)", r"\1\n", cont, flags=re.I) | |
cont = re.sub(r"</?[fs][op][^<>]+>", r"", cont, flags=re.I) | |
cont = re.sub(r"<p [^<>]+>", r"", cont, flags=re.I) | |
cont = re.sub(r"</p>", r"", cont, flags=re.I) | |
cont = re.sub(r"<sp[^<>]+></sp[^<>]+>", r"", cont, flags=re.I) | |
cont = re.sub( | |
r'<img[^<>]*(src="[^"]+")[^<>]*>', r"<img \1>", cont, flags=re.I) | |
rescsv = [] | |
for line in re.split(r"\r?\n", cont): | |
if not _rgx_td.search(line) or (not is_ent and "img" not in line): | |
continue | |
found = [ | |
re.sub(r"</?t[dr]\s*>", r"", td, flags=re.I) | |
for td in _rgx_td.split(line)] | |
rximg = re.compile(r'<img src="(.*)">(?: )*') | |
if is_ent: | |
if len(found[1:]) == 5: | |
converted = _process_each_entry(found) | |
rescsv.append(converted) | |
m = rximg.search(converted[1]) | |
if m: | |
imgsrc = m.group(1) | |
imgofn = os.path.join( | |
cachedir_base, _cachedir["img"], imgsrc) | |
_retrieve_img(imgsrc, imgofn) | |
else: | |
found = [ | |
rximg.sub(r"\1", td) | |
for td in found if td not in (" ", " ")] | |
for imgsrc in found[2::2]: | |
_retrieve_img( | |
imgsrc, | |
os.path.join( | |
cachedir_base, | |
_cachedir["JIStaishou_img"], imgsrc)) | |
rescsv.extend(list(zip(found[2::2], found[1::2]))) | |
with io.open(ofn, "wb") as fo: | |
for line in sorted(rescsv): | |
fo.write((",".join(line) + "\n").encode("utf-8")) | |
# ======================================================== | |
# | |
# MeCab 辞書のためのパート。 | |
# このスクリプトにとっての本題。 | |
# | |
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" | |
fieldnames_all = [ | |
"表層形", | |
"左文脈ID", "右文脈ID", | |
"コスト", | |
"品詞", "品詞細分類1", "品詞細分類2", "品詞細分類3", | |
"活用形1", "活用形2", | |
"原形", "読み", "発音" | |
] | |
def _assign_cost(key1, cont, fromsysdic): | |
# コストについて、 mecab-dict-index によるアサインを期待 | |
# しようかとも考えていたのだが、付与基準がいまひとつよく | |
# わからないし、何より煩雑なので、この時点で mecab-dict-index | |
# に期待するのはやめておく。実際やってみると、「何もなければ | |
# 6836」が割り当てられ、「何かがある」と微調整が入っている | |
# のだけれど、 | |
# 嘉戸,1290,1290,6542,名詞,固有名詞,人名,姓,*,*,嘉戸,カト,カト | |
# 嘉戸,1290,1290,7361,名詞,固有名詞,人名,姓,*,*,嘉戸,カド,カド | |
# だの、 | |
# 𥱋瀬,1290,1290,-2716,名詞,固有名詞,人名,姓,*,*,𥱋瀬,ヤナセ,ヤナセ | |
# だの良くわからない。少なくとも読みにも影響されてるのは確か。 | |
# | |
# 私がやりたいのは「問題を起こしやすい姓についてはコストを | |
# とてつもなく大きくする」「推定人数が少ないものはコストを少し | |
# 大きくする」ということだけ。コストを小さくしたいということはない。 | |
# この二つに該当しない場合は後続(つまり、my_mecab_userdict_build.py) | |
# 任せにするためにブランクにすればいい。 | |
# | |
# cont: [surface, reading, weight, conflicts] | |
# weight は「推定人数っぽいもの」で、これが極端に小さいものの | |
# コストは大きくしたいということ。 | |
# | |
_defcost_value = 6836 | |
# どの品詞と衝突しているかによって傷の深さが違う。 | |
_camap = { | |
# 以下は傷は深いと思うが調整値はかなり適当。 | |
'副詞-一般': 1200, | |
'副詞-助詞類接続': 1200, | |
'助詞-副助詞': 1200, | |
'名詞-サ変接続': 1200, | |
'名詞-一般': 1200, # 「宇宙」さんなんてのがいる。 | |
'名詞-代名詞-一般': 1200, | |
'名詞-副詞可能': 800, | |
'名詞-形容動詞語幹': 400, | |
'名詞-接尾-一般': 400, | |
'名詞-接尾-人名': 1200, | |
'名詞-接尾-副詞可能': 800, | |
'名詞-接尾-助数詞': 1200, | |
'名詞-接尾-地域': 400, | |
'名詞-接尾-特殊': 800, | |
'名詞-接尾-形容動詞語幹': 800, | |
'名詞-接続詞的': 800, | |
'名詞-接尾-サ変接続': 1200, | |
'名詞-数': 1200, | |
'名詞-非自立-一般': 800, | |
'名詞-非自立-副詞可能': 800, | |
'形容詞-接尾': 800, | |
'形容詞-自立': 400, | |
'形容詞-非自立': 800, | |
'感動詞': 400, | |
'接続詞': 1200, | |
'接頭詞-動詞接続': 1200, | |
'接頭詞-名詞接続': 1200, | |
'接頭詞-形容詞接続': 1200, | |
'接頭詞-数接続': 1200, | |
'動詞-自立': 800, | |
'動詞-非自立': 800, | |
# 固有名詞との合致はさほど困らない。というか困り方の | |
# 性質が他と異なる。(固有名詞であると識別出来ている | |
# 時点でそういうこと。) ただ、「人名-名」だけは別で、 | |
# これと被っていると判定を間違いやすい。 | |
#'名詞-固有名詞-一般': 10, ##### | |
#'名詞-固有名詞-人名-一般': 10, ##### | |
'名詞-固有名詞-人名-名': 500, | |
#'名詞-固有名詞-地域-一般': 10, ##### | |
#'名詞-固有名詞-地域-国': 10, ##### | |
#'名詞-固有名詞-組織': 10, ##### | |
# | |
} | |
for sur, reading, (weight, ratio, ranking), conflicts in cont: | |
# オリジナルの IPA 辞書にいるものといないもので扱いは | |
# 変えない。IPA辞書に含まれるものの場合は、もともとト | |
# レーニングによって多少調整済みな値なのだが、別扱い | |
# にすると、本来最多読みであるはずのもののコストの方が | |
# 高くなってしまうため。 | |
fs = [ent["コスト"] | |
for ent in fromsysdic["姓"].get(sur, []) | |
if ent["読み"] == reading] | |
cost = _defcost_value | |
# 推定人数に応じて調整。 | |
# ただこれ、「テキストへの出現数」と実在人数はあまり強い | |
# 相関ではないので、やり過ぎないように注意。つまり「珍名 | |
# 著名人」がいればテキストへの出現数は爆発的に多くなる。 | |
# たとえば「貫地谷」は myoujijiten での推計で 100人未満。 | |
cost += int(10 * (1.0 - ratio)) + ranking | |
# 少数派は大きく、多数派は小さく。一文字姓は大きく。 | |
v = 50000 if weight < 1000 else 15000 | |
cost += (v // (weight - 3 * (1 - len(sur)))) | |
# 衝突品詞に応じて。 | |
# 「一文字姓」はとりわけ傷が深いので、何重にも加算。 | |
for c in conflicts: | |
cost += _camap.get(c, 5) + 1000 * (len(sur) == 1) | |
comment = "" | |
if fs: | |
comment = "(:{}:)".format(fs[0]) | |
yield sur, reading, conflicts, "{}".format(cost), comment | |
def _to_mecab_input(distdir, fn, reader, fromsysdic, tmpl, outenc="utf-8", outsuf=".csv"): | |
def _split_surf(s): | |
_origs = filter( | |
lambda s: s.strip() and not re.search(r".は", s), | |
re.split(r"[()()]", s)) | |
# ケ、ツの扱いには冒頭のコメント参照。 | |
_ke = re.compile(r"(.)ケ(.)") | |
_tu = re.compile(r"(.)ツ(.)") | |
for orig in _origs: | |
yield orig | |
if _ke.search(orig): | |
repl = _ke.sub(r"\1ヶ\2", orig) | |
logging.debug("%r -> %r", orig, repl) | |
yield repl | |
if _tu.search(orig): | |
repl = _tu.sub(r"\1ッ\2", orig) | |
logging.debug("%r -> %r", orig, repl) | |
yield repl | |
def _split_reading(r, count): | |
# 説明では: | |
# 「>」より左: | |
# 最多の読みを含む | |
# 「・」での併記は「最多の半分以上」 | |
# 「>」より右: | |
# 最多の読みの半分未満 | |
# さらに括弧内は一割未満 | |
# が、「括弧内は一割未満」は「>」を省略してでも登場する。 | |
# ゆえに、さきに「括弧内は一割未満」から解読する必要がある。 | |
# また、「>」が複数登場するのがある。説明がないから解釈 | |
# には難儀…。半分のさらに半分、なのか? | |
# | |
outer_gr = [ | |
p.strip() for p in re.split(r"\s*[()()]", r) | |
if p.strip()] | |
left = [ | |
p.strip() for p in re.split(r"\s*>", outer_gr[0]) | |
if p.strip()] | |
# 返却の count * うんぬんは単に「こんくらいの重み」にラフに | |
# 使いたいだけなので、値の選択にはくいつかないでちょうだい。 | |
# グループ1: 多数派 (50%以上) | |
ranking = 1 | |
for rcand in map(_h2k, re.split(r"\s*・\s*", left[0])): | |
yield (int(count * 10 * 0.6), 0.6, ranking), rcand | |
ranking += 1 | |
# グループ2: 少数派のなかでも 10%以上 | |
if len(left) >= 2: | |
for rcand in map(_h2k, re.split(r"\s*・\s*", left[1])): | |
yield (int(count * 10 * 0.4), 0.4, ranking), rcand | |
ranking += 1 | |
if len(left) == 3: | |
for rcand in map(_h2k, re.split(r"\s*・\s*", left[2])): | |
yield (int(count * 10 * 0.2), 0.2, ranking), rcand | |
ranking += 1 | |
# グループ3: 少数派、10%未満 | |
if len(outer_gr) == 2: | |
for rcand in map(_h2k, re.split(r"\s*・\s*", outer_gr[1])): | |
yield (int(count * 10 * 0.1), 0.4, ranking), rcand | |
ranking += 1 | |
def _classify(rtitle, sur, reading, count): | |
# 二つの観点から分類する。ひとつは myoujijiten が主張 | |
# する「推定人数」。これは「テキストでの出現頻度」とは | |
# 無関係なので、例の珍名女優コンビで困る。つまり実用性 | |
# はそれほど高くない。(例えば競輪は、テーマに沿ったレー | |
# スがよくあり、「珍名選手大集合」てなのがあった。こう | |
# いうので全然役に立たない。)もうひとつが実用的な理由 | |
# によるもの。珍しいかどうかとは無関係に、「人名以外で | |
# の合致をしてしまうかどうか」。これはテキスト解析に | |
# とってはとても重要で、たとえば「仮」さんという苗字が | |
# 実在しているが、こんなのがいちいち「電話レンジ(仮)」 | |
# で人名として認識されてはたまらない。ゆえ、目的別に | |
# 取捨選択出来るように、「conflict」に分類する。ただし、 | |
# 地名との合致、「人名,名」「人名,一般」との合致、その | |
# 他固有名詞との合致は比較的傷は小さいので別扱い。複数 | |
# の品詞と合致してしまう場合は傷が大きいとみなす。 | |
# 「サ変接続」に合致するの、結構多いよ。「寄場さん」とか。 | |
key2 = "major" | |
for ck in (3, 10, 100): | |
# 1世帯平均4~5人と仮定して考えるといい。 | |
# 「貫地谷」は推定30、なので5~6世帯。 | |
if count < ck: | |
key2 = "lt{}0".format(ck) | |
break | |
for s in list(sur): | |
if "img" in s: # 冒頭のコメント参照。 | |
logging.warn("ignored: %r (%r)", s, rtitle) | |
continue | |
# myoujijiten は「実在する苗字すべて」を網羅しよう | |
# とするプロジェクトであるから、当然 IPA 辞書に元から | |
# 含まれるものも入っている。これについては区別せずに | |
# 全てを扱うものとする。 | |
ccands = fromsysdic[""] | |
conflicts = [] | |
for s4sd in ( | |
s, | |
s.replace("ヶ", "ケ"), | |
s.replace("ケ", "ヶ"), | |
s.replace("ッ", "ツ"), | |
s.replace("ツ", "ッ")): | |
conflicts = fromsysdic[""].get(s4sd, []) | |
if conflicts: | |
break | |
key1 = "clean_all" | |
if conflicts: | |
logging.debug(conflicts) | |
for data in conflicts: | |
if data["品詞細分類1"] == "固有名詞": | |
# 「人名,名」「人名,一般」「地域」、 | |
# その他固有名詞。 | |
if key1 == "clean_all": | |
key1 = "clean" | |
else: | |
# 以外。その人には罪はないけれど、副詞だの形容詞 | |
# だの色んなものと衝突する困った名前は結構多い。 | |
key1 = "conflict" | |
# | |
targs = [(w, r) for w, r in reading if r == rtitle] | |
if targs: | |
w, r = targs[0] | |
_cks = ("品詞", "品詞細分類1", "品詞細分類2", "品詞細分類3") | |
yield (key1, key2), ( | |
s, r, w, | |
tuple( | |
sorted( | |
list( | |
set( | |
("-".join([c[k] for k in _cks if c[k] != "*"]) | |
for c in conflicts)))))) | |
ofnbase, _ = os.path.splitext(os.path.basename(fn)) | |
ofnbase = "Noun.name.first.myoujijiten_{}".format(ofnbase) | |
results = defaultdict(set) | |
for line in reader: | |
rtitle, sur, reading, count = line[:4] | |
sur = list(set(_split_surf(sur))) | |
try: | |
# 四捨五入なので1の位は無意味。 | |
count = int(count) // 10 | |
except ValueError: | |
count = 1 # 「補」10人未満とのこと | |
reading = list(_split_reading(reading, count)) | |
for (key1, key2), (s, r, w, c) in _classify(rtitle, sur, reading, count): | |
results[(key1, key2)].add((s, r, w, c)) | |
for (key1, key2), cont in results.items(): | |
dd = os.path.join(distdir, key1, key2) | |
if not os.path.exists(dd): | |
os.makedirs(dd) | |
ofn = os.path.join( | |
dd, "{}{}".format(ofnbase, outsuf)) | |
cont = sorted(_assign_cost(key1, cont, fromsysdic)) | |
result = [] | |
for s, r, c, cost, comment in cont: | |
# 発音に関して、 my_mecab_userdict_build.py 前提 | |
# なので埋めなくてもいいといえばいいのだが、 | |
# my_mecab_userdict_build.py での「読み to 発音」 | |
# 生成と IPA 辞書でのルールが少し違ってこの差異が | |
# なかなか鬱陶しいので、表層形と読みが一致してい | |
# るものについては、IPA 辞書から「発音」を複製。 | |
p = "*" | |
fs = [ent["発音"] | |
for ent in fromsysdic["姓"].get(s, []) | |
if ent["読み"] == r] | |
if fs: | |
p = fs[0] | |
# 表層形, 読み, 発音, コスト, :衝突品詞:, :IPA辞書でのコスト: | |
try: | |
s.encode(outenc) | |
except UnicodeEncodeError as e: | |
logging.warn("ignored: %r", e) | |
continue | |
result.append((s, r, p, cost, | |
"(:{}:)".format("|".join(c)), comment)) | |
with io.open(ofn, "wb") as fo: | |
fo.write( | |
tmpl.render( | |
result=result, | |
outfilename=os.path.abspath(ofn), | |
h2k=_h2k, k2h=_k2h).encode(outenc)) | |
# | |
_predefined_templates = { | |
"MeCab": """\ | |
表層形, 品詞細分類3, 読み, 発音, コスト, :衝突品詞:, :IPA辞書でのコスト: | |
% for (sur, reading, pronoun, cost, conflict, ipacost) in result: | |
${sur},姓,${reading},${pronoun},${cost},${conflict},${ipacost} | |
% endfor | |
""", | |
"MSIMEJP10": r"""\ | |
!Microsoft IME Dictionary Tool | |
!Version: | |
!Format:WORDLIST | |
% for (sur, reading, pronoun, cost, conflict, ipacost) in result: | |
${k2h(reading)} ${sur} 姓 | |
% endfor | |
""", | |
} | |
for k, v in list(_predefined_templates.items()): | |
_predefined_templates[k] = mako.template.Template(v) | |
# | |
def _main(args): | |
fromsysdic = { | |
"姓": defaultdict(list), | |
"": defaultdict(list), | |
} | |
_sys_fields = [ | |
"コスト", | |
"品詞", | |
"品詞細分類1", "品詞細分類2", "品詞細分類3", | |
"原形", | |
"読み", | |
"発音", | |
] | |
for fn in glob(os.path.join(_sysdict_dir, "[A-CI-SV]*.csv")): | |
bn, _ = os.path.splitext(os.path.basename(fn)) | |
if bn in ("Symbol",): | |
continue | |
reader = csv.DictReader( | |
io.open(fn, encoding=args.sysdic_src_encoding), | |
fieldnames=fieldnames_all) | |
for line in reader: | |
sur = line["表層形"] | |
data = {k: line[k] for k in _sys_fields} | |
if line["品詞細分類3"] == "姓": | |
fromsysdic["姓"][sur].append(data) | |
elif re.match(r"([^ぁ-んa-zA-Z0-9_]|[ケヶツッ々])+", sur): | |
# 漢字、[ケヶツッ々]のみで構成される | |
fromsysdic[""][sur].append(data) | |
# | |
_all_to_csv(args.converted_csv_distdir) | |
# myoujijiten の構造は「ブラウザで閲覧しやすい」ことを目的として | |
# いる。つまり「あまりに多いと人間が大変」というページングなので、 | |
# 機械処理で困るほどの量ではない。所詮は全部合わせても1万数千 | |
# レコード。処理後の csv は元のものより遥かに少ないレコード数に | |
# なるので、人が読むのにも「ファイル数が多くて閲覧しにくい」ハメに | |
# なる。ゆえ、ある程度まとめてしまう。 | |
def _grp(it): | |
bn = os.path.splitext(os.path.basename(it))[0] | |
if re.match(r"^[aiueok]", bn): | |
return "ak" | |
if re.match(r"^[stn]", bn): | |
return "stn" | |
return "hmyr" | |
origs = sorted( | |
glob( | |
os.path.join( | |
args.converted_csv_distdir, | |
_cachedir[""], | |
"*.csv"), | |
), key=_grp) | |
for fg in itertools.groupby(origs, _grp): | |
if "J" not in fg[0]: | |
_reader = itertools.chain.from_iterable( | |
(csv.reader(io.open(fn, encoding="utf-8")) for fn in fg[1])) | |
_to_mecab_input( | |
args.mecabdic_src_distdir, | |
fg[0], _reader, fromsysdic, | |
tmpl=args.template, | |
outenc=args.out_encoding, | |
outsuf=args.out_suffix) | |
# | |
if __name__ == '__main__': | |
logging.basicConfig(stream=sys.stderr, level=logging.INFO) | |
import argparse | |
parser = argparse.ArgumentParser() | |
parser.add_argument("--mecabdic-src-distdir", default="_dist") | |
parser.add_argument( | |
"--converted-csv-distdir", default="_myoujijiten") | |
parser.add_argument( | |
"--sysdic-src-encoding", | |
default=_mecab_config["--sysdic-src-encoding"]) | |
group = parser.add_argument_group() | |
group.add_argument( | |
"--template", | |
choices=_predefined_templates.keys(), default="MeCab") | |
group.add_argument("--templatefile") | |
parser.add_argument("--out-encoding", default="utf-8") | |
parser.add_argument("--out-suffix", default=".csv") | |
args = parser.parse_args() | |
if args.templatefile: | |
# テンプレートは utf-8 で書いてね、くらいの制約は | |
# 許してちょ。インターフェイスが煩雑になるのが | |
# やなの。 | |
args.template = mako.template.Template(io.open( | |
args.templatefile, encoding="utf-8").read()) | |
elif args.template == "MSIMEJP10": | |
# 本来はどうやら UCS2 みたいだが、UCS で書き出す方法が | |
# 私にはわからないので。これによりかなりエントリが削られて | |
# しまうが許してちょうだい。あるいはこの読み替えが大きな | |
# お世話かもしらんなぁ。なんなら生成後にメモ帳で「変換」 | |
# も出来るわけだし…。 | |
enc4msime, suf4msime = "cp932", ".txt" | |
logging.info( | |
"""changed: encoding %r => %r, suffix %r => %r because of %r""", | |
args.out_encoding, enc4msime, | |
args.out_suffix, suf4msime, args.template) | |
args.out_encoding = enc4msime | |
args.out_suffix = suf4msime | |
args.template = _predefined_templates[args.template] | |
else: | |
args.template = _predefined_templates[args.template] | |
_main(args) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
これをモジュールとして使えるとは思えないけれど、ハイフンの入った名前はトラブルの元なので、名前変えた。(元は my-… だったのを、my_…に変えたてこと。)