- 任务化清洗流程:Task/TaskManager + BatchProcessor 三方法
- 数据目录规范化:data/{db,sources,backup}
- CLI 入口移进包,注册 jclean 命令
- 工作流测试驱动(tests/test_cleaner_workflow.py)
- 40 测试通过
195 lines
6.8 KiB
Python
195 lines
6.8 KiB
Python
"""
|
||
日语汉字发音规律分析脚本
|
||
|
||
从 tests/data/*.txt 词表中提取「汉字 -> 中文拼音 -> 日语假名」对应样本,
|
||
统计中文读音与日语音读之间的系统性规律。
|
||
|
||
用法:
|
||
python analyze_phonetics.py [词表文件...]
|
||
不传参数时默认分析 tests/data/xinbiaori.txt
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import sys
|
||
import re
|
||
from collections import defaultdict
|
||
from pathlib import Path
|
||
|
||
# 中文声母表(按长度降序,保证 zh/ch/sh 优先于 z/c/s)
|
||
INITIALS = [
|
||
"zh", "ch", "sh",
|
||
"b", "p", "m", "f", "d", "t", "n", "l",
|
||
"g", "k", "h", "j", "q", "x", "r", "z", "c", "s", "y", "w",
|
||
]
|
||
|
||
# 罗马音 -> 判断日语音读是否含长音/拨音等特征
|
||
LONG_VOWEL_TAILS = ("う", "い", "ー")
|
||
NASAL_TAIL = "ん"
|
||
|
||
|
||
def split_pinyin(py: str) -> tuple[str, str]:
|
||
"""把拼音拆成 (声母, 韵母)。无声母时声母为空串。"""
|
||
py = re.sub(r"[0-9]", "", py.strip().lower()) # 去掉可能的声调数字
|
||
for ini in INITIALS:
|
||
if py.startswith(ini):
|
||
return ini, py[len(ini):]
|
||
return "", py
|
||
|
||
|
||
def load_samples(paths: list[Path]) -> list[tuple[str, str, str]]:
|
||
"""返回 [(汉字, 拼音, 假名), ...],只保留三段等长且拼音非空的项。"""
|
||
samples: list[tuple[str, str, str]] = []
|
||
for path in paths:
|
||
if not path.is_file():
|
||
print(f"[跳过] 文件不存在: {path}")
|
||
continue
|
||
for line in path.read_text(encoding="utf-8").splitlines():
|
||
line = line.strip()
|
||
if not line or line.startswith("#"):
|
||
continue
|
||
parts = line.split(":")
|
||
if len(parts) != 3:
|
||
continue
|
||
kanji = parts[0].split("|")
|
||
kana = parts[1].split("|")
|
||
pinyin = parts[2].split("|")
|
||
if len({len(kanji), len(kana), len(pinyin)}) != 1:
|
||
continue
|
||
for k, kn, p in zip(kanji, kana, pinyin):
|
||
if p and re.search(r"[\u4e00-\u9fff]", k):
|
||
samples.append((k, p, kn))
|
||
return samples
|
||
|
||
|
||
def analyze(samples: list[tuple[str, str, str]]) -> str:
|
||
out: list[str] = []
|
||
w = out.append
|
||
|
||
w("=" * 70)
|
||
w("日语汉字发音规律分析报告")
|
||
w("=" * 70)
|
||
w(f"总样本数(汉字-拼音-假名对应): {len(samples)}")
|
||
|
||
# ---- 1. 韵母 -> 音读韵尾特征 ----
|
||
# 统计中文韵母是否以 -ng / -n 结尾,与日语假名是否长音/拨音结尾的关系
|
||
final_tail = defaultdict(lambda: defaultdict(int)) # 中文韵尾类型 -> 日语尾音类型 -> 计数
|
||
for _, py, kana in samples:
|
||
_, final = split_pinyin(py)
|
||
if final.endswith("ng"):
|
||
cn = "-ng (后鼻音)"
|
||
elif final.endswith("n"):
|
||
cn = "-n (前鼻音)"
|
||
else:
|
||
cn = "其他(元音结尾)"
|
||
if kana.endswith(NASAL_TAIL):
|
||
jp = "假名以 ん 结尾"
|
||
elif kana.endswith(LONG_VOWEL_TAILS):
|
||
jp = "假名以 う/い 结尾(长音)"
|
||
else:
|
||
jp = "假名其他结尾"
|
||
final_tail[cn][jp] += 1
|
||
|
||
w("")
|
||
w("-" * 70)
|
||
w("【规律一】中文韵尾 → 日语音读韵尾")
|
||
w("-" * 70)
|
||
w("核心规律:中文后鼻音 -ng 多对应日语长音(う/い);前鼻音 -n 多对应 ん。")
|
||
w("")
|
||
for cn in ["-ng (后鼻音)", "-n (前鼻音)", "其他(元音结尾)"]:
|
||
row = final_tail.get(cn, {})
|
||
total = sum(row.values())
|
||
if not total:
|
||
continue
|
||
w(f"中文 {cn} (共 {total} 例):")
|
||
for jp, cnt in sorted(row.items(), key=lambda x: -x[1]):
|
||
w(f" {jp:<24} {cnt:>4} 例 ({cnt*100//total}%)")
|
||
|
||
# ---- 2. 声母 -> 日语首假名(行) ----
|
||
initial_head = defaultdict(lambda: defaultdict(int)) # 声母 -> 日语首假名 -> 计数
|
||
for _, py, kana in samples:
|
||
ini, _ = split_pinyin(py)
|
||
if not kana:
|
||
continue
|
||
head = kana[0]
|
||
initial_head[ini][head] += 1
|
||
|
||
w("")
|
||
w("-" * 70)
|
||
w("【规律二】中文声母 → 日语音读首假名")
|
||
w("-" * 70)
|
||
w("列出每个中文声母最常对应的日语首假名(取前 3)。")
|
||
w("")
|
||
for ini in sorted(initial_head, key=lambda x: -sum(initial_head[x].values())):
|
||
row = initial_head[ini]
|
||
total = sum(row.values())
|
||
if total < 3: # 样本太少略过
|
||
continue
|
||
top = sorted(row.items(), key=lambda x: -x[1])[:3]
|
||
top_str = " ".join(f"{h}({c})" for h, c in top)
|
||
label = ini if ini else "(零声母)"
|
||
w(f"声母 {label:<4} 共{total:>4}例 → {top_str}")
|
||
|
||
# ---- 3. 同一汉字的多音读(音读/训读现象) ----
|
||
kanji_readings: dict[str, set[str]] = defaultdict(set)
|
||
for k, _, kana in samples:
|
||
kanji_readings[k].add(kana)
|
||
multi = {k: v for k, v in kanji_readings.items() if len(v) > 1}
|
||
|
||
w("")
|
||
w("-" * 70)
|
||
w("【规律三】多音读汉字(同一汉字出现多种假名读法)")
|
||
w("-" * 70)
|
||
w(f"共 {len(kanji_readings)} 个不同汉字,其中 {len(multi)} 个有 2 种以上读法。")
|
||
w("这些多为「音读 vs 训读」或不同音读,是记忆难点,建议重点关注:")
|
||
w("")
|
||
for k, readings in sorted(multi.items(), key=lambda x: -len(x[1]))[:30]:
|
||
w(f" {k} : {' / '.join(sorted(readings))}")
|
||
|
||
# ---- 4. 拼音相同 -> 音读是否相同(同音字规律) ----
|
||
pinyin_kana: dict[str, set[str]] = defaultdict(set)
|
||
pinyin_kanji: dict[str, set[str]] = defaultdict(set)
|
||
for k, py, kana in samples:
|
||
base = re.sub(r"[0-9]", "", py.lower())
|
||
pinyin_kana[base].add(kana)
|
||
pinyin_kanji[base].add(k)
|
||
|
||
w("")
|
||
w("-" * 70)
|
||
w("【规律四】中文同音字 → 日语音读高度一致的例子")
|
||
w("-" * 70)
|
||
w("中文读音相同的汉字,日语音读也常相同。以下是同一拼音、")
|
||
w("多个汉字却共享同一日语读音的典型组(最能体现规律):")
|
||
w("")
|
||
shown = 0
|
||
for py in sorted(pinyin_kanji, key=lambda x: -len(pinyin_kanji[x])):
|
||
kanjis = pinyin_kanji[py]
|
||
kanas = pinyin_kana[py]
|
||
if len(kanjis) >= 3 and len(kanas) <= 2:
|
||
w(f" 拼音 {py:<6} → 汉字 {' '.join(sorted(kanjis))}")
|
||
w(f" {'':>11} 日语读音 {' '.join(sorted(kanas))}")
|
||
w("")
|
||
shown += 1
|
||
if shown >= 15:
|
||
break
|
||
|
||
return "\n".join(out)
|
||
|
||
|
||
def main() -> None:
|
||
root = Path(__file__).resolve().parent
|
||
if len(sys.argv) > 1:
|
||
paths = [Path(a) for a in sys.argv[1:]]
|
||
else:
|
||
paths = [root / "tests" / "data" / "xinbiaori.txt"]
|
||
|
||
samples = load_samples(paths)
|
||
report = analyze(samples)
|
||
|
||
out_file = root / "phonetics_report.txt"
|
||
out_file.write_text(report, encoding="utf-8")
|
||
print(f"分析完成,样本 {len(samples)} 条,报告已写入: {out_file}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|