Initial commit: 日语词表清洗工具(任务化架构)

- 任务化清洗流程:Task/TaskManager + BatchProcessor 三方法
- 数据目录规范化:data/{db,sources,backup}
- CLI 入口移进包,注册 jclean 命令
- 工作流测试驱动(tests/test_cleaner_workflow.py)
- 40 测试通过
This commit is contained in:
panli 2026-08-19 15:04:18 +08:00
commit 1f64c87d0e
54 changed files with 25258 additions and 0 deletions

15
.gitattributes vendored Normal file
View File

@ -0,0 +1,15 @@
# 统一换行为 LF防止 Windows 下 checkout 变成 CRLF
# 本项目所有文本文件约定 UTF-8 无 BOM + LF
* text=auto eol=lf
# 明确的文本类型
*.py text eol=lf
*.txt text eol=lf
*.md text eol=lf
*.json text eol=lf
*.toml text eol=lf
*.yaml text eol=lf
*.yml text eol=lf
# 二进制文件(不做换行转换)
*.db binary

37
.gitignore vendored Normal file
View File

@ -0,0 +1,37 @@
# Python
__pycache__/
*.py[cod]
*.egg-info/
*.egg
build/
dist/
.eggs/
# 虚拟环境
.venv/
venv/
env/
# 测试与缓存
.pytest_cache/
.mypy_cache/
.ruff_cache/
htmlcov/
.coverage
.coverage.*
# IDE
.vscode/
.idea/
*.swp
# 任务批次日志(每次处理生成,非源数据)
tasks/**/batch_*.json
# 词典数据库(体积大,本地生成/下载)
*.db
src/pl_japanese/jamdict.db/
# 临时/调试产物
scripts/_*
*.tmp

1
README.md Normal file
View File

@ -0,0 +1 @@
# 日语学习

192
README_cleaner.md Normal file
View File

@ -0,0 +1,192 @@
# Japanese Cleaner - 日语词表清洗工具
工程化的日语词表清洗流水线,自动化处理汉字-假名-拼音对齐。采用**任务化架构**
每次清洗是一个跨会话、有状态、可并存的任务。
> 完整协作规范见 [`WORKFLOW.md`](WORKFLOW.md)。
## 功能特性
- **任务化**:每次清洗是独立任务(独立配置 + 独立临时文件 + 状态机),多任务可并存
- **两层分离**:单批临时工作区 / 最终只增去重的权威库
- **数据源只读**:处理时不修改原文件,支持任意切片(起始行 + 条数)
- **严格校验**:输入输出条数自动校验
- **半自动协作**:自动分流 + 人工 review + 改代码重跑
- **单元测试**:核心逻辑全覆盖
## 数据目录布局
```
data/
├── db/ # 权威成品库(只增不删 + 去重)
│ ├── vocabulary.txt # 所有批次累积的成品
│ └── skipped.txt # 所有批次累积的跳过项
├── sources/ # 原始数据源(只读)
│ └── xinbiaori_1.txt
└── backup/ # 备份
└── *.backup.txt
tasks/
└── {task_id}/ # 每个任务独立目录
├── task.json # 任务配置 + 状态
├── auto_done.txt # 单批结果
├── skip.txt
└── review_*.txt # 待确认桶
```
## 入口方式
本项目的清洗流程**需要人工介入**review 循环:改字典/规则 → 重跑),因此
**推荐用测试驱动**协作(见 [`tests/test_cleaner_workflow.py`](tests/test_cleaner_workflow.py)
命令行只是便捷入口。
三种等价的命令行入口:
```bash
jclean <cmd> ... # 安装后的 console 命令(推荐)
python -m pl_japanese.cleaner.cli <cmd> ... # 模块调用(无需 console 入口)
python scripts/clean.py <cmd> ... # 兼容薄壳(未安装包时)
```
安装(开发模式,注册 `jclean` 命令):
```bash
pip install -e .
```
## 快速开始
```bash
# 创建任务(数据源只读,可指定处理范围)
jclean create batch1 --source data/sources/xinbiaori_1.txt --start 1 --count 300
# 处理一批数据(分流到单批文件)
jclean process batch1
# 查看任务状态
jclean status batch1
# 列举所有任务
jclean list
# 重跑某个 review 桶(修正字典/规则后)
jclean reprocess batch1 --bucket pinyin
# 合并单批结果到权威库review 全清零后)
jclean merge batch1 --dry-run # 先校验
jclean merge batch1 # 正式合并
# 清空任务的单批文件
jclean clear batch1
```
## 代码调用
```python
from pl_japanese.cleaner import TaskManager, BatchProcessor
tm = TaskManager()
# 创建任务(权威库路径默认全局,可覆盖)
task = tm.create_task(
task_id='batch1',
source='data/sources/xinbiaori_1.txt',
start_line=1,
count=300,
)
# 处理一批
bp = BatchProcessor(task)
result = bp.process_batch()
print(f"处理 {result['valid_lines']} 条auto {result['buckets']['auto']} 条")
# review 全清零后合并
bp.merge_all(dry_run=True) # 校验
bp.merge_all() # 正式合并
```
## 任务状态机
```
created → processing → reviewing → ready → merged
```
- `created` — 已创建,未处理
- `processing` — 已跑分类
- `reviewing` — review 中,部分桶待确认
- `ready` — review 全清零,待合并
- `merged` — 已合并进权威库,完成
## 核心规则
| 规则 | 说明 | 例 |
|------|------|-----|
| 无汉字词跳过 | 汉字段不含汉字 → `skip` | `IT:アイティー:` |
| 混合词正常处理 | 字母作为独立段 | `JC|自|動|車:...` |
| 含~自动处理 | ~ 是任意长通配,对齐后丢弃 | `経営~:けいえいします:``経|営:けい|えい:jing|ying` |
| ます形转原型 | 五段/一段动词转辞書形(词典验证) | |
| 完整表达保留 | 寒暄句保留 ます → `review_verb` | |
| 々展开 | 同字重复符号自动展开 | `我々:われわれ:``我|我:われ|われ:wo|wo` |
完整规则见 [`tests/data/rules.md`](tests/data/rules.md)。
## 输出分类桶
**自动分类:**
- `auto_done.txt` — 高置信度,可直接使用
- `skip.txt` — 无汉字词,已跳过
**需人工确认:**
- `review_pinyin.txt` — 含多音字,需校对拼音
- `review_split.txt` — 假名分割失败,需手动处理
- `review_verb.txt` — 动词/完整表达,需确认形式
- `review_special.txt` — 含字母/片假名,需人工判断
## 关键设计原则
1. **任务化** — 每次清洗是有状态、可并存的任务,路径是任务配置而非全局配置
2. **数据源只读** — 处理时不修改原文件
3. **单批/最终两层分离** — 单批临时工作区,权威库只增去重
4. **改代码而非改文件** — 指出问题后改字典/规则重跑,不手改输出文件
5. **合并需授权** — 只有明确说"合并"才执行合并到权威库
6. **去重保证幂等** — 多次合并同一数据不会重复
## 项目结构
```
japanese/
├── src/pl_japanese/cleaner/ # 核心模块
│ ├── task.py # 任务模型(配置 + 状态机)
│ ├── task_manager.py # 任务管理器
│ ├── batch_processor.py # 批处理调度
│ ├── classifier.py # 词条分类
│ ├── aligner.py # 汉字-假名对齐
│ ├── pinyin_maker.py # 拼音生成
│ ├── pinyin_overrides.py # 多音字覆盖字典
│ ├── rules.py # 规则配置
│ ├── validator.py # 格式校验
│ ├── cli.py # 命令行外壳jclean 入口)
│ └── __main__.py # python -m 入口
├── scripts/clean.py # CLI 兼容薄壳(未安装包时用)
├── data/ # 数据db / sources / backup
├── tasks/ # 任务目录
└── tests/
├── test_cleaner_workflow.py # 工作流测试(任务驱动协作)
└── ...
```
## 运行测试
```bash
pytest tests/ -v
```
工作流本身需要人工介入,日常"处理 → review → 重跑 → 合并"协作推荐用
[`tests/test_cleaner_workflow.py`](tests/test_cleaner_workflow.py) 里的任务驱动方式,
`jclean` / `python -m pl_japanese.cleaner.cli` 命令行。
## 依赖
- Python 3.11+
- jamdict / pypinyin / pydantic / loguru
- pytest测试

115
REVIEW_ANALYSIS.md Normal file
View File

@ -0,0 +1,115 @@
# Review 桶模式分析报告
总计 443 条待确认。以下按桶分析常见模式和处理建议。
---
## 1. review_pinyin230 条)— 多音字
### 多音字频率 TOP15
| 汉字 | 次数 | 主要读音 | 拼音 |
|------|------|----------|------|
| 会 | 39 | かい/がい | hui全部正确 |
| 見 | 37 | み/けん | jian全部正确 |
| 間 | 23 | ま/かん/あいだ | jian全部正确 |
| 行 | 23 | こう/ぎょう/い | xing多数正确 |
| 長 | 22 | ちょう/なが | zhang/**chang** |
| 当 | 17 | あ/とう | dang正确 |
| 場 | 15 | じょう/ば | chang正确 |
| 重 | 14 | じゅう/おも/かさ | zhong/**chong** |
| 難 | 11 | | nan |
| 要 | 9 | | yao |
### 关键发现
**大部分拼音其实是正确的**pypinyin 取常见读音)。真正需要修正的是少数假名读音提示不同音的:
**需要修正的(约 6-9 条)**
- `長`**なが** → 应为 **chang**(不是 zhang
- 例:細長い、長い目、長芋、首を長くする
- `重`**かさ**(重ねる/重なる)→ 应为 **chong**(重叠义)
- 例:重ねる、積み重ねる、重なる
**注意(不用改)**
- `行`**ぎょう**(行政/行事)→ 中文仍是 **xing**(行政 xíngzhèng
- `会` 全部 hui ✓
- `見` 全部 jian ✓
### 处理建议
大部分可**直接合并**(拼音正确)。重点检查 `長(なが)``重(かさ)` 这两类。
---
## 2. review_split201 条)— 分割/对齐失败
### 三大类别
| 类别 | 数量 | 特征 | 处理方式 |
|------|------|------|----------|
| A. 熟字训读 | 89 | 纯汉字但假名对不齐 | 整词不分割 |
| B. 汉字+片假名 | 11 | 汉字配片假名读音 | 整词不分割 |
| C. 含~省略标记 | 101 | 带~的省略词 | 去~后处理 |
### A. 熟字训读89条— 汉字与假名无法逐字对应
按你的规则:**整词不分割,拼音连写**
- `葛飾区:かつしかく:``葛飾区:かつしかく:geshiquku`(地名)
- `吹雪:ふぶき:``吹雪:ふぶき:chuixue`
- `博士:はかせ:``博士:はかせ:boshi`
- `漬物:つけもの:``漬物:つけもの:zewu`
- `成田:なりた:``成田:なりた:chengtian`(地名)
### B. 汉字+片假名11条— 多为中文人名/菜名的日语音译
- `宮保鶏丁:ゴンバオジーディン:` → 宫保鸡丁
- `魚香肉絲:ユイシャンロウスー:` → 鱼香肉丝
- `張芸謀:チャン・イーモウ:` → 张艺谋(导演)
- `王府井:ワンフーチン:` → 王府井(地名)
这些片假名是**中文发音的日语音译**,整词处理。
### C. 含~省略标记101条— 词表用~表示省略部分
- `~賞:ノーベルしょう:` → 诺贝尔奖(~代表具体名称)
- `~茶:プーアルちゃ:` → 普洱茶
- `飲料~:いんりょうメーカー:` → 饮料厂商
这类需要决定:**保留~还是补全**。多数片假名部分是外来词。
---
## 3. review_special11 条)— 含字母/片假名混合
### 含字母(拼音段已按规则留空)
- `CS|貿|易:シーエス|ぼう|えき:|mao|yi`CS贸易
- `A|型:エー|がた:|xing`A型
- `|小|説:エスエフ|しょう|せつ:|xiao|shuo`SF小说=科幻)
- `|放|送...`NHK广播
这些**格式已正确**(字母拼音留空),可直接合并。
### 含特殊符号
- `阪|神|・|淡|路|大|震|災`(阪神·淡路大地震)含中点·
- `三|ツ|村|電|機`(三ツ村電機)含片假名ツ
---
## 4. review_verb1 条)— 完整表达
- `心を1つにする:こころをひとつにする:` — 含数字1和完整句
建议:`心|を|1つ|にする:こころ|を|ひとつ|にする:xin||...` 或整句处理
---
## 处理优先级建议
1. **先合并 special11条** — 格式基本正确,字母已留空
2. **再处理 pinyin230条** — 大部分正确,重点改 `長(なが)``重(かさ)`
3. **split 分三批**
- A类熟字训读89整词+连写拼音
- B类中文音译11整词处理
- C类~省略101统一决定~的处理方式
4. **verb1条** — 单独处理
---
## 可考虑的规则改进
1. **假名读音辅助拼音**:建立 `汉字+假名→拼音` 映射表
- `長+なが→chang``重+かさ→chong`
- 可自动修正部分 pinyin 桶条目
2. **熟字训读词典**:常见熟字训读(吹雪、博士、成田等)直接查表
3. **~处理规则**:统一决定省略标记的展开或保留策略

191
WORKFLOW.md Normal file
View File

@ -0,0 +1,191 @@
# 清洗工作流完整协作规范(任务化架构)
## 核心抽象任务Task
整个清洗流程**无法全自动完成**(中间必须人工 review 干预),所以每次处理是一个
**跨会话、有生命周期状态的任务**。所有文件路径都是任务的配置项,不是全局静态配置。
**一个任务 = 独立配置 + 独立临时文件 + 状态机**,多任务可并存。
### 任务目录结构
每个任务在 `tasks/{task_id}/` 下有独立目录:
```
tasks/
└── {task_id}/
├── task.json # 任务配置 + 状态
├── auto_done.txt # 该任务的单批结果
├── skip.txt
├── review_pinyin.txt
├── review_split.txt
├── review_verb.txt
└── review_special.txt
```
### 任务状态机
```
created → processing → reviewing → ready → merged
```
- `created` — 已创建,未处理
- `processing` — 已跑分类
- `reviewing` — review 中,部分桶待确认
- `ready` — review 全部清零,待合并
- `merged` — 已合并进最终库,完成
---
## 文件两层结构
**单批处理结果任务独立临时review 完成后归入 auto_done/skip**
- `auto_done.txt` — 本批成功处理的词条
- `skip.txt` — 本批无汉字跳过的词条
- `review_pinyin.txt` — 含多音字,待确认拼音
- `review_split.txt` — 假名无法分割,待判断能否处理
- `review_verb.txt` — 动词/完整表达,待确认形式
- `review_special.txt` — 含字母/片假名,待确认
**最终权威数据(累积,只增不删+去重)**
- `data/db/vocabulary.txt` — 所有批次累积的成品词表
- `data/db/skipped.txt` — 所有批次累积的跳过项
权威库路径也是任务配置项(默认指向全局那份,任务可覆盖)。
### 项目数据目录布局
```
data/
├── db/ # 权威成品库(只增不删+去重)
│ ├── vocabulary.txt # 成品词表
│ └── skipped.txt # 累积跳过项
├── sources/ # 原始数据源(只读)
│ └── xinbiaori_1.txt
└── backup/ # 历史备份
├── xinbiaori.backup.txt
└── xinbiaori_tobe.backup.txt
```
---
## 入口方式
清洗流程**需要人工介入**review 循环),推荐用**测试驱动**协作
`tests/test_cleaner_workflow.py`)。命令行是便捷入口,三种等价写法:
```bash
jclean <cmd> ... # 安装后 console 命令(推荐)
python -m pl_japanese.cleaner.cli <cmd> ... # 模块调用
python scripts/clean.py <cmd> ... # 兼容薄壳(未安装包时)
```
## CLI 命令
```bash
# 创建任务(数据源只读,可指定处理范围)
jclean create <task_id> --source data.txt [--start 1] [--count 300] [--name "..."]
# 列举所有任务
jclean list
# 查看任务状态
jclean status <task_id>
# 处理一批数据(分流到单批文件)
jclean process <task_id> [--start N] [--count N]
# 重跑某个 review 桶(修正字典/规则后)
jclean reprocess <task_id> --bucket <pinyin|split|verb|special>
# 合并单批结果到最终库review 全清零后)
jclean merge <task_id> [--dry-run]
# 清空任务的单批文件
jclean clear <task_id>
```
---
## 完整协作流程5步
### 第1步创建任务并处理一批数据
AI 执行:
```bash
jclean create batch1 --source data/sources/xinbiaori_1.txt --start 1 --count 300
jclean process batch1
```
输出分流到6个桶`auto_done` / `skip` / `review_*`4个
任务状态变为 `reviewing`(若有 review`ready`(若无 review
### 第2步你逐个 review 文件检查
你打开任务目录下的 `review_pinyin.txt` / `review_split.txt` 等,**逐条指出问题**。
**重要约定**
- **你只指出问题AI 只记录**(不立即修改代码)
- **单个文件你说完所有问题后**AI 才汇总处理
### 第3步AI 汇总修正并重跑单个 review 桶
**3.1 AI 汇总你指出的问题**
- 多音字错误 → 更新 `pinyin_overrides.py` 字典
- 分割规则错误 → 修改 `classifier.py` / `aligner.py` 逻辑
- 新发现的特殊情况 → 补充规则
**3.2 AI 重新处理该 review 桶**
```bash
jclean reprocess batch1 --bucket pinyin
```
执行后:`review_pinyin.txt` 清零,重新分类的条目进入 `auto_done` / `skip` / 其他 `review_*`
**3.3 重复 3.1~3.2,直到该 review 桶清零**
### 第4步所有 review 清零后,核对总数
所有 `review_*` 清零后AI 核对:
```
auto_done 条数 + skip 条数 == 本批原始输入有效行数
```
校验通过后,任务状态为 `ready`AI 告诉你"本批单批处理完成,待合并"。
### 第5步你说"合并"AI 执行合并
**你明确说"合并"后**AI 执行:
```bash
jclean merge batch1 --dry-run # 先校验
jclean merge batch1 # 正式合并
```
合并操作(两条并行去重管道):
- `auto_done.txt` → 去重合并进 `data/db/vocabulary.txt`
- `skip.txt` → 去重合并进 `data/db/skipped.txt`
合并成功后单批文件自动清零,任务标记 `merged`。**最终权威数据永远只增不删**。
---
## 关键设计原则
1. **任务化** — 每次清洗是一个有状态、可并存的任务,路径是任务配置而非全局配置
2. **数据源只读** — 处理时不修改原文件,支持任意切片(起始行+条数)
3. **单批/最终两层分离** — 单批是临时工作区,最终库是只增去重的权威数据
4. **review 是中间态** — 处理中存在,确认完必须清零(条目归入 auto_done/skip
5. **改代码而非改文件** — 你指出问题AI 改字典/规则后重跑,不是你手改输出文件
6. **合并需你授权** — 只有你明确说"合并"AI 才执行合并到最终库
7. **去重保证幂等** — 多次合并同一数据不会重复,最终库始终去重
---
## 测试
```bash
pytest tests/ -v
```
当前测试覆盖:
- 29/29 测试通过
- 清洗规则测试(含~处理、记号过滤)
- 格式校验测试

194
analyze_phonetics.py Normal file
View File

@ -0,0 +1,194 @@
"""
日语汉字发音规律分析脚本
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()

77
apply_corrections.py Normal file
View File

@ -0,0 +1,77 @@
"""
应用人工确认后的修正到 xinbiaori.txt
每条修正是整行旧文本 -> 整行新文本脚本会校验旧文本恰好出现一次
避免误改运行后打印每条修正结果
"""
from pathlib import Path
FILE = Path(r"\\192.168.3.200\work\workspace\python\japanese\tests\data\xinbiaori.txt")
# (说明, 旧行, 新行)
CORRECTIONS = [
# ---- 汉语拼音手误 ----
("太 tao->tai", "太|郎:た|ろう:tao|lang", "太|郎:た|ろう:tai|lang"),
("明日 jinri->mingri","明日:あした:jinri", "明日:あした:mingri"),
("美 nei->mei", "美|術|館:び|じゅつ|かん:nei|shu|guan", "美|術|館:び|じゅつ|かん:mei|shu|guan"),
("国 huo->guo", "韓|国:かん|こく:han|huo", "韓|国:かん|こく:han|guo"),
("海 gai->hai", "北|海|道:ほっ|かい|どう:bei|gai|dao", "北|海|道:ほっ|かい|どう:bei|hai|dao"),
("渋 she->se", "渋|谷:しぶ|や:she|gu", "渋|谷:しぶ|や:se|gu"),
("紙 空->zhi", "新|聞|紙:しん|ぶん|し:xin|wen|", "新|聞|紙:しん|ぶん|し:xin|wen|zhi"),
("曇 yun->tan", "曇|り:くも|り:yun|", "曇|り:くも|り:tan|"),
("写 xue->xie", "写|真|展:しゃ|しん|てん:xue|zhen|zhan", "写|真|展:しゃ|しん|てん:xie|zhen|zhan"),
("荘 su->zhuang", "別|荘:べっ|そう:bie|su", "別|荘:べっ|そう:bie|zhuang"),
("酎 zhuo->zhou", "焼|酎:しょう|ちゅう:shao|zhuo", "焼|酎:しょう|ちゅう:shao|zhou"),
("撮 she->cuo", "撮|影|禁|止:さつ|えい|きん|し:she|ying|jin|zhi","撮|影|禁|止:さつ|えい|きん|し:cuo|ying|jin|zhi"),
("計 jia->ji", "設|計:せっ|けい:she|jia", "設|計:せっ|けい:she|ji"),
("嬢 nang->niang", "お|嬢|さん:お|じょう|さん:|nang|", "お|嬢|さん:お|じょう|さん:|niang|"),
("濯 di->zhuo (機)", "洗|濯|機:せん|たく|き:xi|di|ji", "洗|濯|機:せん|たく|き:xi|zhuo|ji"),
("濯 di->zhuo", "洗|濯:せん|たく:xi|di", "洗|濯:せん|たく:xi|zhuo"),
("絶 hue->jue", "絶|対|に:ぜっ|たい|に:hue|dui|", "絶|対|に:ぜっ|たい|に:jue|dui|"),
("道 gong->dao", "高|速|道|路:こう|そく|どう|ろ:gao|su|gong|lu", "高|速|道|路:こう|そく|どう|ろ:gao|su|dao|lu"),
("渋 she->se (滞)", "渋|滞:じゅう|たい:she|zhi", "渋|滞:じゅう|たい:se|zhi"),
("束 su->shu", "約|束:やく|そく:yue|su", "約|束:やく|そく:yue|shu"),
("雰 wu->fen", "雰|囲|気:ふん|い|き:wu|wei|qi", "雰|囲|気:ふん|い|き:fen|wei|qi"),
("孫 suan->sun", "孫:まご:suan", "孫:まご:sun"),
("金 空->jin", "貯|金:ちょ|きん:zhu|", "貯|金:ちょ|きん:zhu|jin"),
("交 ijao->jiao", "交|通|事|故:こう|つう|じ|こ:ijao|tong|shi|gu", "交|通|事|故:こう|つう|じ|こ:jiao|tong|shi|gu"),
("則 zhe->ze", "規|則:き|そく:gui|zhe", "規|則:き|そく:gui|ze"),
("具 u->ju", "具|合:ぐ|あい:u|he", "具|合:ぐ|あい:ju|he"),
("円 en->yuan", "円|高:えん|だか:en|gao", "円|高:えん|だか:yuan|gao"),
("欠 qina->qian", "欠|席:けっ|せき:qina|xi", "欠|席:けっ|せき:qian|xi"),
("弁 bing->ban", "合|弁|会|社:ごう|べん|がい|しゃ:he|bing|hui|she","合|弁|会|社:ごう|べん|がい|しゃ:he|ban|hui|she"),
# ---- 日语读音修正 ----
("降る ふり->ふ", "降|る:ふり|る:jiang|", "降|る:ふ|る:jiang|"),
("餃 ぎょー->ぎょう", "餃|子:ぎょー|ざ:jiao|zi", "餃|子:ぎょう|ざ:jiao|zi"),
("通 つ->つう", "交|通|事|情:こう|つ|じ|じょう:jiao|tong|shi|qing","交|通|事|情:こう|つう|じ|じょう:jiao|tong|shi|qing"),
]
def main():
text = FILE.read_text(encoding="utf-8")
applied, missing, ambiguous = [], [], []
for desc, old, new in CORRECTIONS:
cnt = text.count(old)
if cnt == 0:
missing.append(desc)
elif cnt > 1:
ambiguous.append(f"{desc} (出现{cnt}次)")
else:
text = text.replace(old, new)
applied.append(desc)
FILE.write_text(text, encoding="utf-8")
print(f"已应用 {len(applied)} 条:")
for d in applied:
print(" OK ", d)
if missing:
print(f"未找到(可能已改或原文不符) {len(missing)} 条:")
for d in missing:
print(" MISS", d)
if ambiguous:
print(f"歧义(出现多次,跳过) {len(ambiguous)} 条:")
for d in ambiguous:
print(" AMB ", d)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,15 @@
L1499: CS公司:シーエス|コン|ス:|gong|si
L1565: 青|島:チンタオビール:?
L1566: 留|守|にします:るすに:?
L1567: 大|型:おおがたスーパー:?
L1568: ご|存|じです:ごぞんじ:?
L1569: 存|じています:ぞんじてい:?
L1570: 東|京|料|理:とうきょうりょうりスクール:?
L1571: いつもお|世|話|になっております:いつもおせわになっており:?
L1572: 高|層:こうそうビル:?
L1573: 感|じがします:かんじが:?
L1574: 自|動:じどうドア:?
L1575: 大|使|館:スペインたいしかん:?
L1865: 制度:せい|ど:zhi|du
L1866: [原] これからお|世|話|になります:これからお|せ|わ|になります:|shi|hua| [建议砍词尾] これからお|世|話|になります:これからおせわになり:?
L1867: [原] よろしくお|願|い|申|し|上|げます:よろしくお|ねが|い|もう|し|あ|げます:|yuan||shen||shang| [建议砍词尾] よろしくお|願|い|申|し|上|げます:よろしくおねがいもうしあげ:?

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

42
data/db/skipped.txt Normal file
View File

@ -0,0 +1,42 @@
IT:アイティー:it
WTO:ダブリューティーオー:wto
Japan Railways:ジャパンレールウェイズ:
:パーセント:
CM:シーエム:
PR:ピーアール:
ことはない:そんな:
GDP:ジーディーピー:
べん:~弁:
DVD:ディーブイディー:
:エヌジーオー:
MIT-7900:エムアイティーななせんきゅうひゃく:
CM:シーエムソング:
SUSHI:すし:
:アイシータグ:
ASIMO:アシモ:
:エル・ウー・エル:
:ビービーケーラジオ:
:イーユー:
『1Q84』:いちきゅうはちよん:
:フィファ:
:シーディーアルバム:
:リバーズ:
「YASAKA」:やさか:
:ビージーエム:
:エヌビーエー:
:ビタミンエー:
:ジェーリーガー:
:ジェーリーグ:
:エスピー:
:ジーピーエス:
:ユーエスビーメモリ:
2:シーオーツー:
:シーエスアール:
:フォーアール:
:エーティーエム:
:ピーティーエー:
:アールエヌエー:
:エルイーディー:
アクトロイド- :アクトロイドエフ:
1つ1つ:ひとつひとつ:
:ブイシーディー:

6161
data/db/vocabulary.txt Normal file

File diff suppressed because it is too large Load Diff

7088
data/sources/xinbiaori_1.txt Normal file

File diff suppressed because it is too large Load Diff

131
phonetics_report.txt Normal file
View File

@ -0,0 +1,131 @@
======================================================================
日语汉字发音规律分析报告
======================================================================
总样本数(汉字-拼音-假名对应): 2500
----------------------------------------------------------------------
【规律一】中文韵尾 → 日语音读韵尾
----------------------------------------------------------------------
核心规律:中文后鼻音 -ng 多对应日语长音(う/い);前鼻音 -n 多对应 ん。
中文 -ng (后鼻音) (共 421 例):
假名以 う/い 结尾(长音) 272 例 (64%)
假名其他结尾 138 例 (32%)
假名以 ん 结尾 11 例 (2%)
中文 -n (前鼻音) (共 536 例):
假名以 ん 结尾 373 例 (69%)
假名其他结尾 157 例 (29%)
假名以 う/い 结尾(长音) 6 例 (1%)
中文 其他(元音结尾) (共 1543 例):
假名其他结尾 1165 例 (75%)
假名以 う/い 结尾(长音) 373 例 (24%)
假名以 ん 结尾 5 例 (0%)
----------------------------------------------------------------------
【规律二】中文声母 → 日语音读首假名
----------------------------------------------------------------------
列出每个中文声母最常对应的日语首假名(取前 3)。
声母 y 共 229例 → よ(31) い(30) え(28)
声母 sh 共 212例 → し(72) じ(38) せ(19)
声母 j 共 201例 → き(58) か(24) け(21)
声母 x 共 191例 → し(34) こ(21) せ(20)
声母 d 共 158例 → た(22) て(20) ど(19)
声母 zh 共 155例 → し(54) ち(32) な(9)
声母 h 共 129例 → か(52) こ(11) あ(11)
声母 b 共 120例 → ほ(21) へ(11) か(11)
声母 ch 共 118例 → し(21) じ(18) さ(14)
声母 l 共 114例 → り(48) れ(13) ら(13)
声母 g 共 106例 → こ(34) か(33) ご(6)
声母 q 共 99例 → き(39) ぜ(8) け(8)
声母 w 共 98例 → も(17) ぶ(14) や(9)
声母 t 共 90例 → と(20) た(12) て(11)
声母 r 共 79例 → に(33) び(16) じ(10)
声母 f 共 78例 → は(15) ふ(11) か(10)
声母 m 共 69例 → ま(11) め(8) も(6)
声母 z 共 64例 → さ(16) ざ(8) こ(7)
声母 s 共 41例 → そ(10) し(9) さ(6)
声母 n 共 39例 → ね(8) じ(6) と(6)
声母 k 共 38例 → か(11) こ(8) く(6)
声母 (零声母) 共 26例 → あ(7) や(4) お(3)
声母 p 共 24例 → ひ(10) つ(2) か(2)
声母 c 共 22例 → さ(5) む(3) じ(3)
----------------------------------------------------------------------
【规律三】多音读汉字(同一汉字出现多种假名读法)
----------------------------------------------------------------------
共 929 个不同汉字,其中 237 个有 2 种以上读法。
这些多为「音读 vs 训读」或不同音读,是记忆难点,建议重点关注:
生 : い / う / き / しょう / じょう / せい / なま
日 : じつ / に / にち / にっ / ひ / び
小 : お / こ / しょう / ち / ちい
下 : お / か / くだ / さ / した
作 : さ / さく / さっ / つく / づく
立 : た / たち / だ / りっ / りつ
人 : じん / にん / ひと / びと
出 : しゅっ / しゅつ / だ / で
太 : おお / た / たい / ふと
手 : しゅ / ず / て / で
食 : く / ぐ / しょく / た
物 : ぶっ / ぶつ / もつ / もの
入 : い / いり / にゅう / はい
通 : かよ / つ / つう / とお
空 : あ / から / くう / そら
葉 : は / ば / みじ / よう
間 : あいだ / かん / げん / ま
国 : くに / こく / ごく
会 : あ / かい / がい
大 : おお / たい / だい
迎 : げい / む / むか
北 : きた / ほっ / ペ
話 : はな / はなし / わ
時 : じ / と / ど
方 : かた / がた / ほう
母 : かあ / はは / ぼ
図 : ず / と / はか
建 : けん / た / たて
所 : しょ / じょ / ところ
地 : じ / ち / ちい
----------------------------------------------------------------------
【规律四】中文同音字 → 日语音读高度一致的例子
----------------------------------------------------------------------
中文读音相同的汉字,日语音读也常相同。以下是同一拼音、
多个汉字却共享同一日语读音的典型组(最能体现规律)
拼音 guan → 汉字 官 慣 管 観 関 館
日语读音 かん な
拼音 liu → 汉字 劉 流 瑠 留
日语读音 りゅう る
拼音 huan → 汉字 患 換 歓 環
日语读音 かえ かん
拼音 dan → 汉字 丼 単 担 誕
日语读音 たん どん
拼音 gan → 汉字 乾 幹 感 甘
日语读音 あま かん
拼音 han → 汉字 寒 漢 韓
日语读音 かん さむ
拼音 san → 汉字 三 傘 散
日语读音 かさ さん
拼音 zhen → 汉字 振 真 震
日语读音 しん ふ
拼音 mao → 汉字 帽 猫 貿
日语读音 ねこ ぼう
拼音 fei → 汉字 費 非 飛
日语读音 と ひ
拼音 diao → 汉字 彫 調 釣
日语读音 ちょう つ
拼音 duan → 汉字 断 段 短
日语读音 だん みじか

443
pipeline.py Normal file
View File

@ -0,0 +1,443 @@
"""
半自动流水线处理 xinbiaori_tobe.txt 的分割 + 拼音校正
输入行格式tobe: 汉字词:假名: 汉字词:假名:第三段拼音为空
输出目标格式: 汉字|分段:假名|分段:拼音|分段
处理策略分级隔离拿不准的交人工:
1. auto_done.txt 🟢 高置信单音字 + 词典唯一分割 + 无动词/特殊
2. review_pinyin.txt 🟡 含多音字已填最常见读音
3. review_split.txt 🟡 假名分割失败或多解靠猜
4. review_verb.txt 🔴 する/します/でした 等结尾拿不准砍还是留
5. review_special.txt🔴 片假名/字母/~/符号 等特殊格式
用法: python pipeline.py <tobe文件> [起始行] [条数]
"""
import sys
import re
from pathlib import Path
from pypinyin import pinyin, Style
from jamdict import Jamdict
DB = r"C:\Users\panli\jamdict_local.db"
jam = Jamdict(db_file=DB)
HANZI = re.compile(r'[\u4e00-\u9fff]')
KATA = re.compile(r'[\u30A0-\u30FF]')
HIRA = re.compile(r'[\u3040-\u309F]')
LATIN = re.compile(r'[A-Za-z--]')
# 动词/敬语结尾标记
VERB_ENDINGS = ('します', 'しました', 'します。', 'でした', 'ください', 'です', 'ます',
'する', 'させる', 'される')
# ます形 i段→u段映射五段动词变位
I_TO_U_MAP = {'':'','':'','':'','':'','':'','':'','':'','':'','':'','':''}
def is_verb_kana(kana):
"""查词典该读音是否对应动词"""
try:
r = jam.lookup(kata2hira(kana))
for e in r.entries:
for s in e.senses:
for pos in s.pos:
if 'verb' in str(pos).lower():
return True
except:
pass
return False
def masu_to_dict_form(kana):
"""ます形转辞書形,返回 (原型假名, 类型)。类型:'ichidan', 'godan', 'ambiguous', 'failed'"""
if not kana.endswith('ます'):
return None, 'not-masu'
stem = kana[:-2]
cands = []
# 一段: stem + る
ichidan = stem + ''
if is_verb_kana(ichidan):
cands.append((ichidan, 'ichidan'))
# 五段: 末尾i音→u音
if stem and stem[-1] in I_TO_U_MAP:
godan = stem[:-1] + I_TO_U_MAP[stem[-1]]
if is_verb_kana(godan):
cands.append((godan, 'godan'))
if len(cands) == 1:
return cands[0]
elif len(cands) > 1:
return cands, 'ambiguous'
# 词典未找到,返回猜测
guess = ichidan if stem else None
return guess, 'failed'
# 高频多音字黑名单(只有这些才标记为"需确认拼音",其他直接用最常见读音)
COMMON_POLYPHONIC = {
'', '', '', '', '', '', '', '', '', '', '', '',
'', '', '', '', '', '', '', '', '', '', '', '',
'', '', '', '便', '', '', '', '', '', '', '', '',
'', '', '', '', '', '', '', '', '', '', '', '',
'', '', '', '', '', '', '', '', '', '', '', '',
'', '', '', '', '', '宿', '', '', '', '', '', '',
'', '', '', '', '', '', '', '', '', '', '', '',
'', '', '', '', '', '', '', '', '', '', '', '',
'', '', '', '', '', '', '', '', '', '', '', ''
}
def kata2hira(s):
return ''.join(chr(ord(c) - 0x60) if 0x30A1 <= ord(c) <= 0x30F6 else c for c in s)
def clean(s):
s = s.replace('-', '').replace('.', '')
return kata2hira(s).strip()
# ---- 缓存 ----
_read_cache = {}
_py_cache = {}
def readings(ch):
if ch in _read_cache:
return _read_cache[ch]
rs = set()
try:
for c in jam.lookup(ch).chars:
for g in c.rm_groups:
for x in g.on_readings:
rs.add(clean(str(x)))
for x in g.kun_readings:
rs.add(clean(str(x)))
except Exception:
pass
rs.discard('')
_read_cache[ch] = rs
return rs
def valid_pinyins(ch):
if ch in _py_cache:
return _py_cache[ch]
res = pinyin(ch, style=Style.NORMAL, heteronym=True)
s = res[0] if res else []
_py_cache[ch] = s
return s
DAK = {'':'','':'','':'','':'','':'','':'','':'','':'',
'':'','':'','':'','':'','':'','':'','':'','':'',
'':'','':'','':'','':''}
HAN = {'':'','':'','':'','':'','':''}
SMALL = 'ゃゅょぁぃぅぇぉ'
def variants(r):
base = {r}
if r:
f = r[0]
if f in DAK:
base.add(DAK[f] + r[1:])
if f in HAN:
base.add(HAN[f] + r[1:])
out = set()
for b in base:
out.add(b)
if b.endswith(('', '', '', '')):
out.add(b[:-1] + '')
if b.endswith(''):
out.add(b[:-1])
i = 1
while i < len(b):
if b[i] not in SMALL:
out.add(b[:i])
i += 1
out.discard('')
return out
def align(kanji_tokens, kana):
"""把 kana 分配到每个 token汉字或假名或字母。返回所有可能解的集合。"""
# 统一转平假名kanjidic 读音都是平假名,所以目标 kana 也要转)
kana_hira = kata2hira(kana)
n = len(kanji_tokens)
results = []
def rec(i, pos, acc):
if i == n:
if pos == len(kana):
results.append(tuple(acc))
return
tok = kanji_tokens[i]
tok_hira = kata2hira(tok) # token 也可能含片假名
if HANZI.search(tok):
cands = set()
for r in readings(tok):
cands |= variants(r)
for v in sorted(cands, key=len, reverse=True):
if kana_hira.startswith(v, pos):
acc.append(kana[pos:pos + len(v)]) # 用原始 kana保留片假名
rec(i + 1, pos + len(v), acc)
acc.pop()
elif LATIN.search(tok):
# 字母段:尝试所有可能长度
remain = len(kana) - pos
min_len = 1
max_len = max(1, remain - (n - i - 1))
for l in range(max_len, min_len - 1, -1):
seg = kana[pos:pos + l]
acc.append(seg)
rec(i + 1, pos + l, acc)
acc.pop()
else:
# 假名 token原样匹配转平假名后比较
if kana_hira.startswith(tok_hira, pos):
acc.append(kana[pos:pos + len(tok)])
rec(i + 1, pos + len(tok), acc)
rec(0, 0, [])
return set(results)
def split_kanji_tokens(kanji_part):
"""把汉字词拆成 token 序列:每个汉字单独,连续假名合并成一段,连续字母合并。"""
tokens = []
buf = ''
buf_type = None # 'kana', 'latin', None
for ch in kanji_part:
if HANZI.search(ch):
if buf:
tokens.append(buf)
buf = ''
buf_type = None
tokens.append(ch)
elif LATIN.search(ch) or ch in '':
if buf_type == 'latin':
buf += ch
else:
if buf:
tokens.append(buf)
buf = ch
buf_type = 'latin'
else:
# 假名
if buf_type == 'kana':
buf += ch
else:
if buf:
tokens.append(buf)
buf = ch
buf_type = 'kana'
if buf:
tokens.append(buf)
return tokens
def make_pinyin(tokens):
"""为 token 序列生成拼音段;返回 (拼音列表, 是否含高频多音字)。"""
pys = []
multi = False
for tok in tokens:
if len(tok) == 1 and HANZI.search(tok):
cands = valid_pinyins(tok)
if not cands:
pys.append('')
else:
pys.append(cands[0])
# 只有在高频多音字黑名单里才标记
if len(cands) > 1 and tok in COMMON_POLYPHONIC:
multi = True
elif LATIN.search(tok):
# 字母段:整段转小写作为拼音(如 CS -> cs, Q -> q
pys.append(tok.lower())
else:
pys.append('') # 假名段留空
return pys, multi
def classify_and_process(kanji_part, kana_part):
"""返回 (bucket, 处理后字符串, 备注)。"""
note = []
# ~ 标记
has_tilde = '' in kanji_part or '~' in kanji_part
kanji_clean = kanji_part.replace('', '').replace('~', '')
# 规则1纯英文/纯字母词 → 跳过不处理(汉字段无任何汉字)
if not HANZI.search(kanji_clean):
return 'skip', f"{kanji_part}:{kana_part}:", "纯英文/无汉字,跳过"
# 处理々(同字重复符号):替换成前一个字
kanji_expanded = []
for i, ch in enumerate(kanji_clean):
if ch == '' and i > 0:
kanji_expanded.append(kanji_expanded[-1]) # 重复前一个字
else:
kanji_expanded.append(ch)
kanji_clean = ''.join(kanji_expanded)
# 动词/敬语结尾
is_verbish = has_tilde or any(kana_part.endswith(e) for e in VERB_ENDINGS)
tokens = split_kanji_tokens(kanji_clean)
# 含字母:按正常流程对齐(字母作为独立 token
has_latin = LATIN.search(kanji_clean)
has_kata = KATA.search(kanji_clean)
if is_verbish:
if has_tilde:
# 规则3含~:去掉 ~ 和 します/する,保留名词词干,直接处理
stem_kana = kana_part
for e in sorted(VERB_ENDINGS, key=len, reverse=True):
if stem_kana.endswith(e):
stem_kana = stem_kana[:-len(e)]
break
sols = align(tokens, stem_kana)
if len(sols) == 1:
kana_tokens = list(next(iter(sols)))
pys, multi = make_pinyin(tokens)
out = f"{'|'.join(tokens)}:{'|'.join(kana_tokens)}:{'|'.join(pys)}"
# 分割成功,按正常流程分类
if multi:
return 'pinyin', out, "含~+多音字,请确认"
return 'auto', out, ""
else:
# 分割失败,进 split
return 'split', f"{kanji_part}:{kana_part}:", "含~但假名无法分割"
else:
# ます形动词:转原型(辞書形)
dict_result, vtype = masu_to_dict_form(kana_part)
if vtype == 'ambiguous':
# 歧义:列出所有可能
opts = ''.join([f"{k}({t})" for k, t in dict_result])
return 'verb', f"[原] {kanji_part}:{kana_part}: [ます→原型有歧义] {opts},请人工确认", "ます形歧义"
elif vtype in ('ichidan', 'godan'):
# 唯一解:汉字段的送り仮名也要同步转换
# 根据变位类型处理
if kanji_clean.endswith('ます'):
if vtype == 'ichidan':
# 一段:ます→る,直接替换
kanji_dict = kanji_clean[:-2] + ''
else:
# 五段ます前的i段音→u段音例如 かり→かる
stem = kanji_clean[:-2]
if stem and stem[-1] in I_TO_U_MAP:
kanji_dict = stem[:-1] + I_TO_U_MAP[stem[-1]]
else:
kanji_dict = kanji_clean # 异常情况,保持原样
else:
kanji_dict = kanji_clean
tokens_dict = split_kanji_tokens(kanji_dict)
sols = align(tokens_dict, dict_result)
if len(sols) == 1:
kana_tokens = list(next(iter(sols)))
pys, _ = make_pinyin(tokens_dict)
suggest = f"{'|'.join(tokens_dict)}:{'|'.join(kana_tokens)}:{'|'.join(pys)}"
return 'verb', f"[原] {kanji_part}:{kana_part}: [建议ます→{vtype}原型] {suggest}", f"ます→{vtype}"
else:
return 'verb', f"[原] {kanji_part}:{kana_part}: [建议ます→原型] {kanji_dict}:{dict_result},但分割失败", f"ます→{vtype}但分割失败"
else:
# 词典未找到或不是ます形,按原来逻辑砍词尾
stem_kana = kana_part
for e in sorted(VERB_ENDINGS, key=len, reverse=True):
if stem_kana.endswith(e):
stem_kana = stem_kana[:-len(e)]
break
sols = align(tokens, stem_kana)
if len(sols) == 1:
kana_tokens = list(next(iter(sols)))
pys, _ = make_pinyin(tokens)
suggest = f"{'|'.join(tokens)}:{'|'.join(kana_tokens)}:{'|'.join(pys)}"
else:
suggest = f"{'|'.join(tokens)}:{stem_kana}:?"
return 'verb', f"[原] {kanji_part}:{kana_part}: [建议砍词尾] {suggest}", "动词/敬语结尾"
# 普通词(含字母/片假名也走这里):对齐
sols = align(tokens, kana_part)
if len(sols) == 0:
return 'split', f"{kanji_part}:{kana_part}:", "假名无法分割(词典缺读音)"
if len(sols) > 1:
# 多解,取第一个但标记
kana_tokens = list(sorted(sols)[0])
pys, multi = make_pinyin(tokens)
out = f"{'|'.join(tokens)}:{'|'.join(kana_tokens)}:{'|'.join(pys)}"
return 'split', out, f"分割有{len(sols)}种可能,取其一,请确认"
kana_tokens = list(next(iter(sols)))
pys, multi = make_pinyin(tokens)
out = f"{'|'.join(tokens)}:{'|'.join(kana_tokens)}:{'|'.join(pys)}"
# 含字母/片假名标记到 special但已完成分割和拼音
if has_latin or has_kata:
tag = "含字母" if has_latin else "含片假名"
if multi:
return 'special', out, f"{tag}+多音字,请确认"
return 'special', out, f"{tag},已自动处理,请确认"
if multi:
return 'pinyin', out, "含多音字,拼音取最常见,请确认"
return 'auto', out, ""
def main():
if len(sys.argv) < 2:
print("usage: python pipeline.py <tobe文件> [起始行] [条数]")
return
src = Path(sys.argv[1])
start = int(sys.argv[2]) if len(sys.argv) > 2 else 1
count = int(sys.argv[3]) if len(sys.argv) > 3 else 10**9
buckets = {'auto': [], 'pinyin': [], 'split': [], 'verb': [], 'special': [], 'skip': []}
processed = 0
with open(src, 'r', encoding='utf-8') as f:
for lineno, raw in enumerate(f, 1):
if lineno < start:
continue
if processed >= count:
break
line = raw.rstrip('\n').strip()
if not line:
continue
processed += 1
norm = line.replace('', ':')
parts = norm.split(':')
if len(parts) < 2:
buckets['special'].append(f"L{lineno} {line} | 格式异常")
continue
kanji_part = parts[0].strip()
kana_part = parts[1].strip()
if not kana_part:
buckets['special'].append(f"L{lineno} {line} | 无假名")
continue
bucket, out, note = classify_and_process(kanji_part, kana_part)
tag = f"L{lineno}"
if note:
buckets[bucket].append(f"{tag} {out} # {note}")
else:
buckets[bucket].append(out)
outdir = src.parent
files = {
'auto': ('auto_done.txt', '高置信度,可直接用'),
'pinyin': ('review_pinyin.txt', '含多音字,校对拼音'),
'split': ('review_split.txt', '假名分割待确认'),
'verb': ('review_verb.txt', 'する/敬语结尾,拿不准砍还是留'),
'special': ('review_special.txt', '片假名/字母/特殊格式'),
'skip': ('skip.txt', '纯英文/无汉字,跳过不处理'),
}
summary = []
for key, (fname, desc) in files.items():
fpath = outdir / fname
# 追加模式:先读取已有内容
existing = []
if fpath.exists():
existing = [ln.rstrip('\n') for ln in fpath.read_text(encoding='utf-8').splitlines() if ln.strip()]
new_content = existing + buckets[key]
fpath.write_text('\n'.join(new_content) + ('\n' if new_content else ''), encoding='utf-8')
summary.append(f" {fname:22s} {len(buckets[key]):5d} 条新增 (共{len(new_content)}条, {desc})")
print(f"处理 {processed} 条 (从第 {start} 行起)")
print('\n'.join(summary))
# 严格条数校验(铁律)
total_output = sum(len(buckets[k]) for k in buckets)
if total_output != processed:
print(f"\n[!] 条数校验失败!输入 {processed} 条,输出 {total_output} 条,差异 {total_output - processed}")
print(" 条数不对结果一定不对,请检查流水线逻辑")
else:
print(f"\n[OK] 条数校验通过:输入 {processed} 条 = 输出 {total_output}")
if __name__ == '__main__':
main()

32
pyproject.toml Normal file
View File

@ -0,0 +1,32 @@
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
[project]
name = "pl_japanese"
version = "0.0.1"
description = "pl japanese study"
authors = [{name = "panli", email = "pl@gzjunbo.net"}]
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"pydantic>=2.0",
"loguru>=0.7",
"pypinyin>=0.50",
"jamdict>=0.1a11",
]
[project.optional-dependencies]
test = [
"pytest>=8.0",
]
[project.scripts]
jclean = "pl_japanese.cleaner.cli:main"
[tool.setuptools.packages.find]
where = ["src"]
[tool.pytest.ini_options]
pythonpath = ["src"]
testpaths = ["tests"]

0
requirements.txt Normal file
View File

16
scripts/clean.py Normal file
View File

@ -0,0 +1,16 @@
"""
清洗工具 CLI 兼容入口
实际逻辑已移入包 `pl_japanese.cleaner.cli`安装后推荐直接用 `jclean` 命令
`python -m pl_japanese.cleaner.cli ...`此脚本仅为在未安装包时的便捷入口
"""
import sys
from pathlib import Path
# 未安装包时,把 src 加进路径
sys.path.insert(0, str(Path(__file__).parent.parent / 'src'))
from pl_japanese.cleaner.cli import main
if __name__ == '__main__':
main()

View File

@ -0,0 +1,6 @@
from pathlib import Path
# 词典数据库路径,基于本包所在目录动态解析,避免硬编码绝对路径
_PACKAGE_DIR = Path(__file__).resolve().parent
JAM_DICT_DB_PATH = str(_PACKAGE_DIR / "jamdict.db" / "jamdict.db")

View File

@ -0,0 +1,33 @@
"""
Japanese Cleaner - 日语词表清洗模块
任务化架构
- Task / TaskManager任务模型与管理配置 + 状态机 + 独立目录
- BatchProcessor基于任务的批处理器处理/合并/重跑
- 底层能力Classifier / Aligner / PinyinMaker / validator
"""
from .batch_processor import BatchProcessor
from .task import Task, TaskConfig, TaskState, REVIEW_BUCKETS
from .task_manager import TaskManager
from .classifier import Classifier
from .aligner import Aligner
from .pinyin_maker import PinyinMaker
from .validator import clean_line, validate_line
from . import rules
__version__ = '0.2.0'
__all__ = [
'BatchProcessor',
'Task',
'TaskConfig',
'TaskState',
'TaskManager',
'REVIEW_BUCKETS',
'Classifier',
'Aligner',
'PinyinMaker',
'clean_line',
'validate_line',
'rules',
]

View File

@ -0,0 +1,5 @@
"""模块入口python -m pl_japanese.cleaner ..."""
from .cli import main
if __name__ == '__main__':
main()

View File

@ -0,0 +1,196 @@
"""
汉字-假名对齐算法
基于 jamdict 词典进行贪婪匹配
"""
from typing import Set, Tuple, List
from jamdict import Jamdict
import re
class Aligner:
def __init__(self, db_path: str = r"C:\Users\panli\jamdict_local.db"):
self.jam = Jamdict(db_file=db_path)
self.hira = re.compile(r'[\u3040-\u309F]')
self.kata = re.compile(r'[\u30A0-\u30FF]')
self.hanzi = re.compile(r'[\u4e00-\u9fff]')
self.latin = re.compile(r'[A-Za-z--]')
# 缓存
self._read_cache = {}
# 浊化/半浊化映射
self.DAK = {'':'','':'','':'','':'','':'','':'','':'','':'',
'':'','':'','':'','':'','':'','':'','':'','':'',
'':'','':'','':'','':''}
self.HAN = {'':'','':'','':'','':'','':''}
self.SMALL = 'ゃゅょぁぃぅぇぉ'
def _pure_kana(self, tok: str) -> bool:
"""判断 token 是否为纯假名段(平/片假名+长音ー),可原样匹配读音。
含字母/数字/~等无法预测读音的字符则返回 False"""
for ch in tok:
if self.hira.match(ch) or self.kata.match(ch) or ch == '':
continue
return False
return len(tok) > 0
# 标点/括号类符号:不消费任何假名(零宽匹配),也不出现在假名读音里
SYMBOLS = set('『』「」()()【】〔〕《》〈〉・,、。.,,./~-—  ')
def _pure_symbol(self, tok: str) -> bool:
"""判断 token 是否全为标点/括号类符号(不含 ~~ 单独处理)。
这类 token 在假名里没有对应读音,应做零宽匹配"""
for ch in tok:
if ch not in self.SYMBOLS or ch in '~':
return False
return len(tok) > 0
def kata2hira(self, text: str) -> str:
"""片假名转平假名"""
result = []
for ch in text:
code = ord(ch)
if 0x30A0 <= code <= 0x30FF:
result.append(chr(code - 0x60))
else:
result.append(ch)
return ''.join(result)
# ます形 i段→u段映射五段动词变位
I_TO_U_MAP = {'':'','':'','':'','':'','':'',
'':'','':'','':'','':'','':''}
def is_verb_kana(self, kana: str) -> bool:
"""查词典该读音是否对应动词"""
try:
r = self.jam.lookup(self.kata2hira(kana))
for e in r.entries:
for s in e.senses:
for pos in s.pos:
if 'verb' in str(pos).lower():
return True
except Exception:
pass
return False
def masu_to_dict_form(self, kana: str):
"""ます形转辞書形,返回 (原型假名, 类型)。
类型: 'ichidan' | 'godan' | 'ambiguous' | 'failed' | 'not-masu'"""
if not kana.endswith('ます'):
return None, 'not-masu'
stem = kana[:-2]
cands = []
# 一段: stem + る
ichidan = stem + ''
if self.is_verb_kana(ichidan):
cands.append((ichidan, 'ichidan'))
# 五段: 末尾i音→u音
if stem and stem[-1] in self.I_TO_U_MAP:
godan = stem[:-1] + self.I_TO_U_MAP[stem[-1]]
if self.is_verb_kana(godan):
cands.append((godan, 'godan'))
if len(cands) == 1:
return cands[0]
elif len(cands) > 1:
return cands, 'ambiguous'
guess = ichidan if stem else None
return guess, 'failed'
def _clean_reading(self, s: str) -> str:
"""清理读音字符串:去掉 . - 等符号katakana转hiragana"""
s = s.split('.')[0] # 去掉送り仮名标记后的部分
s = s.replace('-', '').replace('.', '').strip()
return self.kata2hira(s)
def readings(self, ch: str) -> Set[str]:
"""查询单个汉字的所有音读/训读(从 kanjidic"""
if ch in self._read_cache:
return self._read_cache[ch]
rs = set()
try:
for c in self.jam.lookup(ch).chars:
for g in c.rm_groups:
for x in g.on_readings:
rs.add(self._clean_reading(str(x)))
for x in g.kun_readings:
rs.add(self._clean_reading(str(x)))
except Exception:
pass
rs.discard('')
self._read_cache[ch] = rs
return rs
def variants(self, r: str) -> Set[str]:
"""生成读音变体:浊化、半浊化、促音、长音省略、读音前缀"""
base = {r}
if r:
f = r[0]
if f in self.DAK:
base.add(self.DAK[f] + r[1:])
if f in self.HAN:
base.add(self.HAN[f] + r[1:])
out = set()
for b in base:
out.add(b)
# 促音化:结尾 つく ち き → っ
if b.endswith(('', '', '', '')):
out.add(b[:-1] + '')
# 长音 う 省略
if b.endswith(''):
out.add(b[:-1])
# 读音前缀≥1 mora不在小假名前截断
i = 1
while i < len(b):
if b[i] not in self.SMALL:
out.add(b[:i])
i += 1
out.discard('')
return out
def align(self, kanji_tokens: List[str], kana: str) -> Set[Tuple[str, ...]]:
"""
对齐汉字tokens和假名返回所有可能解的集合
每个 token 是单个汉字连续假名段或连续字母段
"""
kana_hira = self.kata2hira(kana)
n = len(kanji_tokens)
results = []
def rec(i, pos, acc):
if i == n:
if pos == len(kana):
results.append(tuple(acc))
return
tok = kanji_tokens[i]
tok_hira = self.kata2hira(tok)
if self.hanzi.search(tok):
# 汉字:查读音+变体
cands = set()
for r in self.readings(tok):
cands |= self.variants(r)
for v in sorted(cands, key=len, reverse=True):
if kana_hira.startswith(v, pos):
acc.append(kana[pos:pos + len(v)])
rec(i + 1, pos + len(v), acc)
acc.pop()
elif self._pure_kana(tok):
# 纯假名段(含长音ー):原样匹配
if kana_hira.startswith(tok_hira, pos):
acc.append(kana[pos:pos + len(tok)])
rec(i + 1, pos + len(tok), acc)
elif self._pure_symbol(tok):
# 纯标点/括号:零宽匹配,不消费假名
acc.append('')
rec(i + 1, pos, acc)
acc.pop()
else:
# 字母/数字/~通配符/混合段:读音不可预测,尝试所有可能长度(≥1)
remain = len(kana) - pos
max_len = max(1, remain - (n - i - 1))
for l in range(max_len, 0, -1):
seg = kana[pos:pos + l]
acc.append(seg)
rec(i + 1, pos + l, acc)
acc.pop()
rec(0, 0, [])
return set(results)

View File

@ -0,0 +1,396 @@
"""
批处理调度器任务版
- 接收一个 Task 对象操作该任务的文件
- 处理时更新任务状态created processing reviewing ready merged
- 数据源只读不修改原文件
- 单批/最终两层分离
- 三个独立方法merge_final / reprocess_review / merge_all
BatchProcessor 不持有任何硬编码路径所有路径都来自 Task 的配置
"""
import json
from pathlib import Path
from typing import Dict, List, Optional
from datetime import datetime
from .classifier import Classifier
from .aligner import Aligner
from .pinyin_maker import PinyinMaker
from .validator import clean_line, validate_line
from .task import (
Task, REVIEW_BUCKETS,
STATUS_PROCESSING, STATUS_REVIEWING, STATUS_READY, STATUS_MERGED,
)
class BatchProcessor:
"""基于任务的批处理器:所有路径来自 task.config"""
def __init__(self, task: Task):
self.task = task
self.cfg = task.config
# 初始化组件
self.aligner = Aligner()
self.pinyin_maker = PinyinMaker()
self.classifier = Classifier(self.aligner, self.pinyin_maker)
# 单批文件路径(来自任务配置)
self.auto_done = Path(self.cfg.auto_done)
self.skip = Path(self.cfg.skip)
self.review_files = {
b: Path(self.cfg.review_path(b)) for b in REVIEW_BUCKETS
}
# 权威库(来自任务配置)
self.main = Path(self.cfg.main)
self.skipped = Path(self.cfg.skipped)
# ====================================================================
# 处理一批数据
# ====================================================================
def process_batch(
self,
start_line: Optional[int] = None,
count: Optional[int] = None,
) -> Dict:
"""
处理一批数据只读数据源分流到单批文件更新任务状态
Args:
start_line/count: 覆盖任务配置的处理范围
"""
start = start_line if start_line is not None else self.cfg.start_line
cnt = count if count is not None else self.cfg.count
source = Path(self.cfg.source)
lines = self._read_input_lines(source, start, cnt)
buckets = self._classify_lines(lines, start)
self._append_to_single_batch(buckets)
total_output = sum(len(v) for v in buckets.values())
valid_count = len([ln for ln in lines if ln.strip()])
bucket_counts = {k: len(v) for k, v in buckets.items()}
# 更新任务状态
review_total = sum(bucket_counts[b] for b in REVIEW_BUCKETS)
self.task.state.bucket_counts = self._snapshot_counts()
self.task.state.input_valid = valid_count
self.task.state.validation_ok = (valid_count == total_output)
new_status = STATUS_REVIEWING if review_total > 0 else STATUS_READY
self.task.set_status(new_status, event=f'processed {valid_count} lines from L{start}')
result = {
'task_id': self.task.task_id,
'source': str(source),
'start_line': start,
'input_lines': len(lines),
'valid_lines': valid_count,
'output_total': total_output,
'buckets': bucket_counts,
'validation': valid_count == total_output,
'status': self.task.state.status,
}
self._save_batch_log(result)
return result
def _read_input_lines(self, source: Path, start: int, count: int) -> List[str]:
"""读取输入行(只读)"""
lines = []
with open(source, 'r', encoding='utf-8-sig') as f:
for i, line in enumerate(f, 1):
if i < start:
continue
if len(lines) >= count:
break
lines.append(line.rstrip('\n'))
return lines
def _classify_lines(self, lines: List[str], start_line: int) -> Dict[str, List[str]]:
"""分类处理所有行"""
buckets = {
'auto': [], 'pinyin': [], 'split': [],
'verb': [], 'special': [], 'skip': []
}
for idx, raw in enumerate(lines):
line = raw.strip()
if not line:
continue
lineno = start_line + idx
norm = line.replace('', ':')
parts = norm.split(':')
if len(parts) < 2:
buckets['special'].append(f"L{lineno} {line} # 格式异常")
continue
kanji_part = parts[0].strip()
kana_part = parts[1].strip()
if not kana_part:
buckets['special'].append(f"L{lineno} {line} # 无假名")
continue
bucket, out, note = self.classifier.classify(kanji_part, kana_part)
if note:
buckets[bucket].append(f"L{lineno} {out} # {note}")
else:
buckets[bucket].append(out)
return buckets
def _append_to_single_batch(self, buckets: Dict[str, List[str]]):
"""追加写入单批结果文件"""
file_map = {
'auto': self.auto_done,
'skip': self.skip,
'pinyin': self.review_files['pinyin'],
'split': self.review_files['split'],
'verb': self.review_files['verb'],
'special': self.review_files['special'],
}
for bucket, fpath in file_map.items():
fpath.parent.mkdir(parents=True, exist_ok=True)
existing = []
if fpath.exists():
existing = [
ln.rstrip('\n')
for ln in fpath.read_text(encoding='utf-8-sig').splitlines()
if ln.strip()
]
new_content = existing + buckets[bucket]
fpath.write_text(
'\n'.join(new_content) + ('\n' if new_content else ''),
encoding='utf-8',
newline='\n'
)
def _save_batch_log(self, result: Dict):
"""保存批次日志到任务目录"""
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
log_file = Path(self.task.task_dir) / f'batch_{timestamp}.json'
log_file.parent.mkdir(parents=True, exist_ok=True)
log_file.write_text(
json.dumps(result, indent=2, ensure_ascii=False),
encoding='utf-8'
)
# ====================================================================
# 方法1: merge_final() — 单批结果 → 最终库(去重)
# ====================================================================
def merge_final(self, dry_run: bool = False) -> Dict:
"""
把单批结果auto_done + skip合并进最终库去重
管道:
auto_done main (去重 + 格式校验)
skip skipped (去重)
"""
main_existing = self._read_clean_lines(self.main)
main_set = set(main_existing)
merged_auto, dup_auto = [], 0
rejects = []
for s in self._read_clean_lines(self.auto_done):
ok, reason = validate_line(s)
if not ok:
rejects.append(('auto_done', reason, s))
continue
if s in main_set:
dup_auto += 1
continue
main_existing.append(s)
main_set.add(s)
merged_auto.append(s)
skip_existing = self._read_clean_lines(self.skipped)
skip_set = set(skip_existing)
merged_skip, dup_skip = [], 0
for s in self._read_clean_lines(self.skip):
if s in skip_set:
dup_skip += 1
continue
skip_existing.append(s)
skip_set.add(s)
merged_skip.append(s)
if not dry_run:
if merged_auto:
self.main.parent.mkdir(parents=True, exist_ok=True)
self.main.write_text(
'\n'.join(main_existing) + '\n',
encoding='utf-8', newline='\n'
)
if merged_skip:
self.skipped.parent.mkdir(parents=True, exist_ok=True)
self.skipped.write_text(
'\n'.join(skip_existing) + '\n',
encoding='utf-8', newline='\n'
)
return {
'merged_auto': len(merged_auto),
'merged_skip': len(merged_skip),
'duplicated_auto': dup_auto,
'duplicated_skip': dup_skip,
'rejected': len(rejects),
'main_total': len(main_existing),
'skipped_total': len(skip_existing),
'rejects': rejects,
'dry_run': dry_run,
}
# ====================================================================
# 方法2: reprocess_review() — 重新处理某个 review 桶
# ====================================================================
def reprocess_review(self, bucket: str) -> Dict:
"""
重新处理某个 review 把条目重新分类写回单批结果
使用场景: 你指出 review_pinyin 里的问题 我修正字典/规则 调用此方法重跑
重跑后清空原 review 条目重新分流到 auto_done/skip/其他 review
"""
if bucket not in self.review_files:
raise ValueError(f"未知 review 桶: {bucket},可选: {list(self.review_files.keys())}")
review_file = self.review_files[bucket]
lines = self._read_clean_lines(review_file)
new_buckets = {
'auto': [], 'pinyin': [], 'split': [],
'verb': [], 'special': [], 'skip': []
}
for ln in lines:
parts = ln.split(':')
if len(parts) < 2:
continue
kanji_part = parts[0].strip()
kana_part = parts[1].strip()
if not kanji_part or not kana_part:
continue
b, out, note = self.classifier.classify(kanji_part, kana_part)
if note:
new_buckets[b].append(f"{out} # {note}")
else:
new_buckets[b].append(out)
# 先清空原 review 桶(避免自我累加),再追加分流结果
review_file.write_text('', encoding='utf-8', newline='\n')
self._append_to_single_batch(new_buckets)
# 更新任务状态快照
self.task.state.bucket_counts = self._snapshot_counts()
review_total = self._review_total()
new_status = STATUS_REVIEWING if review_total > 0 else STATUS_READY
self.task.set_status(new_status, event=f'reprocessed review_{bucket} ({len(lines)} lines)')
return {
'original_bucket': bucket,
'reprocessed': len(lines),
'new_distribution': {k: len(v) for k, v in new_buckets.items()},
'review_total': review_total,
'status': self.task.state.status,
}
# ====================================================================
# 方法3: merge_all() — 便捷方法,组合调用
# ====================================================================
def merge_all(self, dry_run: bool = False) -> Dict:
"""
便捷方法检查 review 全清零然后合并单批结果到最终库
前提: 所有 review_* 必须已清零合并成功后清空单批文件任务标记 merged
"""
review_counts = {
b: len(self._read_clean_lines(fp)) for b, fp in self.review_files.items()
}
total_review = sum(review_counts.values())
if total_review > 0:
return {
'error': 'review 桶未全部清零,无法合并',
'review_remaining': review_counts,
'total_review': total_review,
}
auto_count = len(self._read_clean_lines(self.auto_done))
skip_count = len(self._read_clean_lines(self.skip))
if auto_count == 0 and skip_count == 0:
return {
'error': '单批结果为空(auto_done + skip = 0),无需合并',
'auto_done': 0,
'skip': 0,
}
result = self.merge_final(dry_run=dry_run)
if not dry_run and result['rejected'] == 0:
self.auto_done.write_text('', encoding='utf-8', newline='\n')
self.skip.write_text('', encoding='utf-8', newline='\n')
result['single_batch_cleared'] = True
self.task.state.bucket_counts = self._snapshot_counts()
self.task.set_status(STATUS_MERGED, event='merged to final library')
else:
result['single_batch_cleared'] = False
return result
# ====================================================================
# 辅助方法
# ====================================================================
def _read_clean_lines(self, path: Path) -> List[str]:
"""读取文件,去行号/注释,返回非空行列表"""
if not path.exists():
return []
lines = []
for raw in path.read_text(encoding='utf-8-sig').splitlines():
s = clean_line(raw)
if s:
lines.append(s)
return lines
def _snapshot_counts(self) -> Dict[str, int]:
"""当前各单批文件条数快照"""
counts = {
'auto_done': len(self._read_clean_lines(self.auto_done)),
'skip': len(self._read_clean_lines(self.skip)),
}
for b, fp in self.review_files.items():
counts[f'review_{b}'] = len(self._read_clean_lines(fp))
return counts
def _review_total(self) -> int:
return sum(
len(self._read_clean_lines(fp)) for fp in self.review_files.values()
)
def get_status(self) -> Dict:
"""获取任务当前状态与各桶条数"""
single = self._snapshot_counts()
final = {
'main': len(self._read_clean_lines(self.main)),
'skipped': len(self._read_clean_lines(self.skipped)),
}
review_total = self._review_total()
return {
'task_id': self.task.task_id,
'name': self.task.name,
'status': self.task.state.status,
'source': self.cfg.source,
'single_batch': single,
'final': final,
'review_total': review_total,
'ready_to_merge': review_total == 0 and (
single['auto_done'] > 0 or single['skip'] > 0
),
}
def clear_single_batch(self):
"""清空所有单批文件(手动重置)"""
self.auto_done.write_text('', encoding='utf-8', newline='\n')
self.skip.write_text('', encoding='utf-8', newline='\n')
for fp in self.review_files.values():
fp.write_text('', encoding='utf-8', newline='\n')

View File

@ -0,0 +1,244 @@
"""
词条分类器
根据规则将词条分类到不同桶
"""
from typing import Tuple, List
from .rules import *
from .aligner import Aligner
from .pinyin_maker import PinyinMaker
import re
class Classifier:
def __init__(self, aligner: Aligner, pinyin_maker: PinyinMaker):
self.aligner = aligner
self.pinyin_maker = pinyin_maker
def split_kanji_tokens(self, text: str) -> List[str]:
"""
将汉字段切分为 tokens
- 每个汉字单独一段
- ~通配符单独一段任意长匹配假名,不与相邻假名合并,
以免吞掉汉字的送り仮名( 干し )
- 其余连续非汉字假名/片假名/字母/记号合并成一段
"""
tokens = []
buf = ''
for ch in text:
if HANZI.match(ch):
if buf:
tokens.append(buf)
buf = ''
tokens.append(ch)
elif ch in ('', '~'):
# ~ 独立成段
if buf:
tokens.append(buf)
buf = ''
tokens.append(ch)
else:
# 非汉字(假名/片假名/字母/符号):连续合并
buf += ch
if buf:
tokens.append(buf)
return tokens
def classify(self, kanji_part: str, kana_part: str) -> Tuple[str, str, str]:
"""
分类词条
返回: (bucket, 输出字符串, 备注)
"""
# 规则1无汉字 → skip
if should_skip(kanji_part):
return 'skip', f"{kanji_part}:{kana_part}:", "无汉字,跳过"
# 清理 ~ 标记
has_tilde_mark = has_tilde(kanji_part)
kanji_clean = clean_tilde(kanji_part)
# 展开々
kanji_clean = expand_repeat_marks(kanji_clean)
# 判断是否动词相关
is_verb = is_verbish(kana_part, has_tilde_mark)
# 切分 tokens
tokens = self.split_kanji_tokens(kanji_clean)
# 处理动词
if is_verb:
return self._handle_verb(kanji_part, kana_part, tokens, has_tilde_mark)
# 普通词:对齐
return self._handle_normal(kanji_part, kana_part, tokens)
def _filter_symbols(self, tokens, kana_tokens):
"""从输出中过滤纯记号段(『』「」等),这些段应零宽,不参与拼音输出。"""
out_k, out_kn = [], []
for tk, kn in zip(tokens, kana_tokens):
if self.aligner._pure_symbol(tk):
continue
out_k.append(tk)
out_kn.append(kn)
return out_k, out_kn
def _handle_tilde(self, kanji_orig, kana_orig):
"""含~处理:~作为任意长通配符对齐,~对应的汉字/假名段不输出。
原理:
- 汉字部分保留原顺序,~处替换为占位符,与其他非汉字段一起做非汉字混合段
- 分词器把 ~ 段与相邻假名合并成非汉字段(任意长匹配)
- align 找到解后,把含 ~ 的非汉字段从输出中删除
返回 (bucket, out, note) None(无解/多解)
"""
# 用 kanji_orig(未清理~)做分词,~会被归入非汉字段
kanji_expanded = expand_repeat_marks(kanji_orig)
tokens_with_tilde = self.split_kanji_tokens(kanji_expanded)
# 对齐
sols = self.aligner.align(tokens_with_tilde, kana_orig)
if len(sols) == 0:
return None
# 多解时:选择"汉字段假名总长最大"的解(汉字取完整读音,~段取残余)
if len(sols) > 1:
def hanzi_kana_len(sol):
total = 0
for tk, kn in zip(tokens_with_tilde, sol):
if self.aligner.hanzi.search(tk):
total += len(kn)
return total
best = max(sols, key=lambda s: (hanzi_kana_len(s), tuple(-len(x) for x in s)))
kana_tokens = list(best)
else:
kana_tokens = list(next(iter(sols)))
# 过滤:含 ~ 的段和纯记号段丢弃,只保留汉字段和纯假名段
out_kanji = []
out_kana = []
for tk, kn in zip(tokens_with_tilde, kana_tokens):
if '' in tk or '~' in tk:
continue
if self.aligner._pure_symbol(tk):
continue
out_kanji.append(tk)
out_kana.append(kn)
if not out_kanji:
return None
pys, multis = self.pinyin_maker.make_pinyin(out_kanji, out_kana)
out = f"{'|'.join(out_kanji)}:{'|'.join(out_kana)}:{'|'.join(pys)}"
if multis:
return 'pinyin', out, f"含~+多音字({','.join(multis)}),请确认"
return 'auto', out, ""
def _handle_verb(self, kanji_orig, kana_orig, tokens, has_tilde_mark):
"""处理动词/含~词"""
if has_tilde_mark and RULES['auto_process_tilde']:
# 规则3含~ → ~作为任意长通配符,与汉字一起对齐,~部分不输出
result = self._handle_tilde(kanji_orig, kana_orig)
if result is not None:
return result
# 兜底:无法自动对齐
return 'split', f"{kanji_orig}:{kana_orig}:", "含~但假名无法分割"
# ます形转换规则4
if RULES['convert_masu_form'] and kana_orig.endswith('ます'):
return self._convert_masu_form(kanji_orig, kana_orig, tokens)
# 尝试自动对齐完整词(如"気にする"已正确分割为"気|にする"
sols = self.aligner.align(tokens, kana_orig)
if len(sols) == 1:
kana_tokens = list(next(iter(sols)))
pys, multis = self.pinyin_maker.make_pinyin(tokens, kana_tokens)
out = f"{'|'.join(tokens)}:{'|'.join(kana_tokens)}:{'|'.join(pys)}"
if multis:
return 'pinyin', out, f"多音字({','.join(multis)}),请确认"
return 'auto', out, ""
# 完整表达无法自动对齐规则5交人工
return 'verb', f"[原] {kanji_orig}:{kana_orig}: [需人工确认]", "完整表达/敬语"
def _convert_masu_form(self, kanji_orig, kana_orig, tokens):
"""ます形转辞書形规则4"""
dict_result, vtype = self.aligner.masu_to_dict_form(kana_orig)
# 展开々后的汉字段(用于同步转换)
kanji_clean = expand_repeat_marks(clean_tilde(kanji_orig))
if vtype == 'ambiguous':
opts = ''.join([f"{k}({t})" for k, t in dict_result])
return 'verb', f"[原] {kanji_orig}:{kana_orig}: [ます→原型有歧义] {opts}", "ます形歧义"
if vtype in ('ichidan', 'godan'):
# 汉字段的送り仮名同步转换
if kanji_clean.endswith('ます'):
if vtype == 'ichidan':
kanji_dict = kanji_clean[:-2] + ''
else:
stem = kanji_clean[:-2]
if stem and stem[-1] in I_TO_U_MAP:
kanji_dict = stem[:-1] + I_TO_U_MAP[stem[-1]]
else:
kanji_dict = kanji_clean
else:
kanji_dict = kanji_clean
tokens_dict = self.split_kanji_tokens(kanji_dict)
sols = self.aligner.align(tokens_dict, dict_result)
if len(sols) == 1:
kana_tokens = list(next(iter(sols)))
pys, multis = self.pinyin_maker.make_pinyin(tokens_dict, kana_tokens)
out = f"{'|'.join(tokens_dict)}:{'|'.join(kana_tokens)}:{'|'.join(pys)}"
if multis:
return 'pinyin', out, f"ます→{vtype}原型+多音字({','.join(multis)})"
return 'auto', out, ""
else:
return 'verb', f"[原] {kanji_orig}:{kana_orig}: [建议ms→原型] {kanji_dict}:{dict_result},分割失败", f"ます→{vtype}分割失败"
# failed词典查不到
return 'verb', f"[原] {kanji_orig}:{kana_orig}: [ます形无法验证]", "ます形未验证"
def _handle_normal(self, kanji_orig, kana_orig, tokens):
"""处理普通词"""
sols = self.aligner.align(tokens, kana_orig)
if len(sols) == 0:
return 'split', f"{kanji_orig}:{kana_orig}:", "假名无法分割"
if len(sols) > 1:
# 多解:取第一个但标记
kana_tokens = list(sorted(sols)[0])
fk, fkn = self._filter_symbols(tokens, kana_tokens)
pys, multis = self.pinyin_maker.make_pinyin(fk, fkn)
out = f"{'|'.join(fk)}:{'|'.join(fkn)}:{'|'.join(pys)}"
if multis:
return 'pinyin', out, f"多解+多音字({','.join(multis)})"
return 'pinyin', out, "多解,已取第一个"
# 唯一解
kana_tokens = list(next(iter(sols)))
fk, fkn = self._filter_symbols(tokens, kana_tokens)
pys, multis = self.pinyin_maker.make_pinyin(fk, fkn)
out = f"{'|'.join(fk)}:{'|'.join(fkn)}:{'|'.join(pys)}"
# 覆写tokens/kana为过滤后的,供后续多音字/字母判定使用
tokens = fk
kana_tokens = fkn
if multis:
return 'pinyin', out, f"多音字({','.join(multis)}),请确认"
# 含字母/片假名:标记确认
has_latin = LATIN.search(kanji_orig)
has_kata = KATA.search(kanji_orig)
if has_latin or has_kata:
note = []
if has_latin:
note.append("含字母")
if has_kata:
note.append("含片假名")
return 'special', out, ','.join(note) + ",请确认"
return 'auto', out, ""

View File

@ -0,0 +1,208 @@
"""
清洗工具 CLI 入口任务化版本
命令行外壳只做参数解析和结果打印业务逻辑全部在
TaskManager / BatchProcessor
安装后可直接用 `jclean` 命令 pyproject.toml [project.scripts]
也可 `python -m pl_japanese.cleaner.cli ...` 调用
用法
jclean create <task_id> --source <file> [--start N] [--count N] [--name ...]
jclean list
jclean status <task_id>
jclean process <task_id> [--start N] [--count N]
jclean reprocess <task_id> --bucket <pinyin|split|verb|special>
jclean merge <task_id> [--dry-run]
jclean clear <task_id>
注意本项目的清洗流程需要人工介入review 循环命令行只是便捷入口
日常的"处理→review→重跑→合并"协作推荐用 tests/test_cleaner_workflow.py 驱动
"""
import argparse
from .batch_processor import BatchProcessor
from .task_manager import TaskManager
def _print_status(st: dict):
print("=" * 50)
print(f"任务: {st['task_id']} ({st['name']})")
print(f"状态: {st['status']}")
print(f"数据源: {st['source']}")
print("-" * 50)
print("单批结果 (临时):")
for k, v in st['single_batch'].items():
flag = ""
if k.startswith('review_') and v > 0:
flag = " <- 待确认"
print(f" {k:16s}: {v:5d}{flag}")
print("-" * 50)
print("最终权威数据 (累积):")
for k, v in st['final'].items():
print(f" {k:16s}: {v:5d}")
print("-" * 50)
if st['ready_to_merge']:
print("[OK] review 已全部清零,可以合并: jclean merge <task_id>")
elif st['review_total'] > 0:
print(f"[!] 还有 {st['review_total']} 条待 review 确认")
print("=" * 50)
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog='jclean',
description='日语词表清洗工具(任务化)',
)
parser.add_argument('--tasks-root', default='tasks', help='任务根目录')
sub = parser.add_subparsers(dest='cmd', required=True)
# create
p_create = sub.add_parser('create', help='创建新任务')
p_create.add_argument('task_id')
p_create.add_argument('--source', required=True, help='数据源文件')
p_create.add_argument('--name', help='任务显示名')
p_create.add_argument('--start', type=int, default=1, help='起始行号')
p_create.add_argument('--count', type=int, default=300, help='处理条数')
p_create.add_argument('--main', help='权威库路径(默认全局)')
p_create.add_argument('--skipped', help='跳过库路径(默认全局)')
# list
sub.add_parser('list', help='列举所有任务')
# status
p_status = sub.add_parser('status', help='查看任务状态')
p_status.add_argument('task_id')
# process
p_process = sub.add_parser('process', help='处理一批数据')
p_process.add_argument('task_id')
p_process.add_argument('--start', type=int, help='起始行号(覆盖任务配置)')
p_process.add_argument('--count', type=int, help='处理条数(覆盖任务配置)')
# reprocess
p_reprocess = sub.add_parser('reprocess', help='重跑某个 review 桶')
p_reprocess.add_argument('task_id')
p_reprocess.add_argument('--bucket', required=True,
choices=['pinyin', 'split', 'verb', 'special'])
# merge
p_merge = sub.add_parser('merge', help='合并单批结果到最终库')
p_merge.add_argument('task_id')
p_merge.add_argument('--dry-run', action='store_true')
# clear
p_clear = sub.add_parser('clear', help='清空任务的单批文件')
p_clear.add_argument('task_id')
return parser
def main(argv=None):
parser = build_parser()
args = parser.parse_args(argv)
tm = TaskManager(tasks_root=args.tasks_root)
if args.cmd == 'create':
task = tm.create_task(
task_id=args.task_id,
source=args.source,
name=args.name,
start_line=args.start,
count=args.count,
main=args.main,
skipped=args.skipped,
)
print(f"[OK] 任务已创建: {task.task_id}")
print(f" 目录: {task.task_dir}")
print(f" 数据源: {task.config.source}")
print(f" 范围: L{task.config.start_line}{task.config.count}")
return
if args.cmd == 'list':
summaries = tm.list_task_summaries()
if not summaries:
print("暂无任务")
return
print(f"{'任务ID':20s} {'状态':12s} {'数据源':30s} 更新时间")
print("-" * 90)
for s in summaries:
print(f"{s['task_id']:20s} {s['status']:12s} {s['source']:30s} {s['updated_at']}")
return
if args.cmd == 'status':
task = tm.load_task(args.task_id)
bp = BatchProcessor(task)
_print_status(bp.get_status())
return
if args.cmd == 'process':
task = tm.load_task(args.task_id)
bp = BatchProcessor(task)
result = bp.process_batch(start_line=args.start, count=args.count)
print(f"[OK] 处理完成 (任务 {result['task_id']})")
print(f" 数据源: {result['source']} 起始 L{result['start_line']}")
print(f" 输入行数: {result['input_lines']} 有效: {result['valid_lines']}")
print(f" 输出总数: {result['output_total']}")
print()
print("分类统计:")
for b, c in result['buckets'].items():
print(f" {b:10s}: {c:4d}")
print()
if result['validation']:
print("[OK] 条数校验通过")
else:
print(f"[!] 条数校验失败: 有效 {result['valid_lines']} != 输出 {result['output_total']}")
print(f"任务状态: {result['status']}")
return
if args.cmd == 'reprocess':
task = tm.load_task(args.task_id)
bp = BatchProcessor(task)
result = bp.reprocess_review(args.bucket)
print(f"[OK] 重跑 review_{result['original_bucket']} 完成")
print(f" 重新处理: {result['reprocessed']}")
print(" 重新分流:")
for b, c in result['new_distribution'].items():
if c > 0:
print(f" {b:10s}: {c:4d}")
print(f" 剩余 review 总数: {result['review_total']}")
print(f" 任务状态: {result['status']}")
return
if args.cmd == 'merge':
task = tm.load_task(args.task_id)
bp = BatchProcessor(task)
result = bp.merge_all(dry_run=args.dry_run)
if 'error' in result:
print(f"[!] 无法合并: {result['error']}")
if 'review_remaining' in result:
for b, c in result['review_remaining'].items():
if c > 0:
print(f" review_{b}: {c} 条待确认")
return
tag = " [dry-run 只校验]" if args.dry_run else ""
print(f"[合并结果]{tag}")
print(f" auto_done -> 主库: 新增 {result['merged_auto']} 条, 重复 {result['duplicated_auto']}")
print(f" skip -> 跳过库: 新增 {result['merged_skip']} 条, 重复 {result['duplicated_skip']}")
print(f" 主库总数: {result['main_total']} 跳过库总数: {result['skipped_total']}")
if result['rejected']:
print(f" [!] 校验拒绝 {result['rejected']} 条:")
for bucket, reason, content in result['rejects']:
print(f" [{bucket}] {reason}: {content[:50]}")
if args.dry_run:
print("\n[dry-run] 未写入任何文件")
elif result.get('single_batch_cleared'):
print("\n[OK] 已合并到最终库,单批文件已清空,任务标记 merged")
return
if args.cmd == 'clear':
task = tm.load_task(args.task_id)
bp = BatchProcessor(task)
bp.clear_single_batch()
print(f"[OK] 任务 {args.task_id} 的单批文件已清空")
return
if __name__ == '__main__':
main()

View File

@ -0,0 +1,71 @@
"""
拼音生成模块
基于 pypinyin处理多音字标记支持特例词典覆盖
"""
from typing import List, Tuple, Optional
from pypinyin import pinyin, Style
from .rules import POLYPHONE_BLACKLIST, HANZI, LATIN, RULES
from .pinyin_overrides import apply_word_override, apply_kana_hint
class PinyinMaker:
def __init__(self):
self.polyphone_blacklist = POLYPHONE_BLACKLIST
def make_pinyin(
self,
kanji_tokens: List[str],
kana_tokens: Optional[List[str]] = None,
full_kanji: Optional[str] = None,
) -> Tuple[List[str], List[str]]:
"""
为汉字tokens生成拼音支持特例词典覆盖
Args:
kanji_tokens: 汉字分段列表 ['', '']
kana_tokens: 对应假名分段 ['かい', 'けい']用于 KANA_HINT
full_kanji: 完整汉字段 '会計'用于 WORD_OVERRIDE 整词匹配
Returns:
(拼音列表, 多音字列表)
"""
# 1. 整词覆盖:优先级最高
if full_kanji is None:
full_kanji = ''.join(kanji_tokens)
override = apply_word_override(full_kanji)
if override is not None:
# 整词命中,直接返回覆盖拼音,不标多音字
return override.split('|'), []
pinyins = []
multis = []
for idx, token in enumerate(kanji_tokens):
if not HANZI.search(token):
# 非汉字(字母/假名/片假名):拼音段留空
pinyins.append('')
continue
# 2. 假名提示覆盖(单字级)
hinted = None
if kana_tokens and idx < len(kana_tokens):
kana = kana_tokens[idx]
if len(token) == 1:
hinted = apply_kana_hint(token, kana)
if hinted is not None:
pinyins.append(hinted)
# 命中特例的字不再标记为多音字(已确定)
continue
# 3. 默认 pypinyin 生成
py_result = pinyin(token, style=Style.NORMAL, heteronym=False)
py_str = ''.join([p[0] for p in py_result])
pinyins.append(py_str)
# 检查是否含高频多音字(需人工确认)
if RULES['mark_polyphone']:
for ch in token:
if ch in self.polyphone_blacklist:
multis.append(ch)
return pinyins, multis

View File

@ -0,0 +1,119 @@
"""
多音字特例词典
==============
日语假名相同但中文拼音不同的词需要按词义指定正确拼音
原理pypinyin 对多音字默认取最常见读音但某些词的正确拼音不同
由于日语假名无法区分 会議/会計 都是 かい需在词级别指定
两种覆盖方式
1. WORD_OVERRIDE: 整词汉字 完整拼音 | 分段与汉字段对应
2. KANA_HINT: (汉字, 假名) 该字拼音利用假名读音区分同字不同音
维护建议核对时发现错误补充到对应表即可
"""
# ------------------------------------------------------------------
# 1. 整词覆盖:汉字词 → 拼音(分段用 |,与汉字逐字对应)
# 优先级最高,精确匹配整个汉字段
# ------------------------------------------------------------------
WORD_OVERRIDE = {
# 会:会计读 kuài其余读 huì
'会計': 'kuai|ji',
# 行:银行读 háng行政/行为读 xíng
'銀行': 'yin|hang',
'行員': 'hang|yuan',
'行列': 'hang|lie',
# 重:重叠/重复读 chóng重量/重要读 zhòng
'重ねる': 'chong',
'重なる': 'chong',
'積み重ねる': 'ji||chong',
'二重': 'er|chong',
'重複': 'chong|fu',
# 长:长度/长短读 cháng成长/长官读 zhǎng
'細長い': 'xi|chang',
'長い': 'chang',
'長さ': 'chang',
'長芋': 'chang|yu',
'長江': 'chang|jiang', # 长江(地名),ちょう但读chang,整词覆盖假名提示
# 以下"長"读 ちょう 但中文是 cháng(长久/长度义),需整词覆盖假名提示的 zhang
'長寿': 'chang|shou',
'長寿国': 'chang|shou|guo',
'長距離': 'chang|ju|li',
'長編': 'chang|bian',
'長期': 'chang|qi',
'特長': 'te|chang', # 特长/长处义
'延長': 'yan|chang', # 延长/长度义
'波長': 'bo|chang', # 波长/长度义
'最長': 'zui|chang', # 最长/长度义
# 重(层义):~重の塔(塔的层数)读 chóng
'五重の塔': 'wu|chong||ta',
'三重の塔': 'san|chong||ta',
# 乐:音乐读 yuè快乐读 lè
'音楽': 'yin|yue',
'楽しい': 'le',
'楽器': 'yue|qi',
# 数:数目读 shù数数读 shǔ
'数える': 'shu',
# 干:干燥读 gān干部读 gàn乾 pypinyin 默认 qián需覆盖
'乾杯': 'gan|bei',
'乾燥': 'gan|zao',
'乾燥機': 'gan|zao|ji',
'乾電池': 'gan|dian|chi',
# 得:得到读 dé得意读 dé多数 dé
# 便:方便读 biàn便宜读 pián
'便利': 'bian|li',
'郵便': 'you|bian',
}
# ------------------------------------------------------------------
# 2. 假名提示覆盖:(汉字, 假名读音前缀) → 该字拼音
# 当整词未命中 WORD_OVERRIDE 时,用假名读音辅助判断单字
# ------------------------------------------------------------------
KANA_HINT = {
# 长なが→chángちょう→zhǎng
('', 'なが'): 'chang',
('', 'ちょう'): 'zhang',
# 重かさ→chóng(重ねる)おも→zhòng(重い)じゅう→zhòng
('', 'かさ'): 'chong',
('', 'おも'): 'zhong',
('', 'じゅう'): 'zhong',
# 空:そら/から/あ→kōng
('', 'そら'): 'kong',
('', 'から'): 'kong',
# 乾:かん/かわ→gān干燥/乾かす等pypinyin 默认 qián
('', 'かん'): 'gan',
('', 'かわ'): 'gan',
# 還かん→huán還暦/還元/返還等,返回归还义pypinyin 默认 hái
('', 'かん'): 'huan',
}
def apply_word_override(kanji_segment: str) -> str | None:
"""
整词覆盖输入完整汉字段不含|返回覆盖拼音或 None
kanji_segment 是原始汉字可能含假名混合先尝试精确匹配
"""
return WORD_OVERRIDE.get(kanji_segment)
def apply_kana_hint(kanji_char: str, kana: str) -> str | None:
"""
假名提示覆盖输入单个汉字和对应假名返回覆盖拼音或 None
"""
for (ch, prefix), py in KANA_HINT.items():
if ch == kanji_char and kana.startswith(prefix):
return py
return None

View File

@ -0,0 +1,70 @@
"""
清洗规则配置
tests/data/rules.md 提炼的核心规则
"""
import re
# 正则模式
HANZI = re.compile(r'[\u4e00-\u9fff]')
KATA = re.compile(r'[\u30A0-\u30FF]')
HIRA = re.compile(r'[\u3040-\u309F]')
LATIN = re.compile(r'[A-Za-z--]')
# 动词/敬语结尾标记
VERB_ENDINGS = (
'します', 'しました', 'します。', 'でした', 'ください', 'です', 'ます',
'する', 'させる', 'される'
)
# ます形 i段→u段映射五段动词变位
I_TO_U_MAP = {
'': '', '': '', '': '', '': '', '': '',
'': '', '': '', '': '', '': '', '': ''
}
# 高频多音字黑名单(需特别标记确认)
POLYPHONE_BLACKLIST = {
'', '', '', '', '', '', '', '', '', '',
'', '', '', '便', '', '', '', '', '', '',
'', '', '', '', '', '', '', '', '', ''
}
# 规则开关
RULES = {
'skip_no_hanzi': True, # 规则1无汉字词跳过
'auto_process_tilde': True, # 规则3含~自动处理
'convert_masu_form': True, # 规则4ます形转原型
'preserve_full_phrase': True, # 规则5完整表达保留
'expand_repeat_mark': True, # 规则6々展开
'mark_polyphone': True, # 标记高频多音字
}
def should_skip(kanji_part: str) -> bool:
"""判断是否应该跳过(无汉字)"""
if not RULES['skip_no_hanzi']:
return False
return not HANZI.search(kanji_part)
def has_tilde(text: str) -> bool:
"""检查是否含 ~ 标记"""
return '' in text or '~' in text
def is_verbish(kana_part: str, has_tilde_mark: bool) -> bool:
"""判断是否动词相关"""
return has_tilde_mark or any(kana_part.endswith(e) for e in VERB_ENDINGS)
def expand_repeat_marks(text: str) -> str:
"""展开々(同字重复符号)"""
if not RULES['expand_repeat_mark']:
return text
result = []
for i, ch in enumerate(text):
if ch == '' and i > 0:
result.append(result[-1])
else:
result.append(ch)
return ''.join(result)
def clean_tilde(text: str) -> str:
"""去掉 ~ 标记"""
return text.replace('', '').replace('~', '')

View File

@ -0,0 +1,215 @@
"""
任务模型一次完整的清洗作业
任务 = 独立配置 + 独立临时文件 + 状态机
- 配置数据源处理范围单批工作文件权威库日志
- 状态created processing reviewing ready merged
- 多任务并存每任务独立目录 tasks/{task_id}/
任务的所有路径都是配置项有默认值可覆盖
权威库main/skipped默认指向全局那份任务可自定义
"""
import json
from pathlib import Path
from typing import Dict, List, Optional, Any
from dataclasses import dataclass, field, asdict
from datetime import datetime
# 任务状态机阶段
STATUS_CREATED = 'created' # 已创建,未处理
STATUS_PROCESSING = 'processing' # 已跑分类
STATUS_REVIEWING = 'reviewing' # review 中,部分桶待确认
STATUS_READY = 'ready' # review 全清零,待合并
STATUS_MERGED = 'merged' # 已合并进最终库,完成
VALID_STATUSES = [
STATUS_CREATED, STATUS_PROCESSING, STATUS_REVIEWING,
STATUS_READY, STATUS_MERGED,
]
# 单批 review 桶名
REVIEW_BUCKETS = ['pinyin', 'split', 'verb', 'special']
@dataclass
class TaskConfig:
"""任务配置:所有路径都是配置项"""
# 数据源
source: str
start_line: int = 1
count: int = 300
# 单批工作文件(任务独立,默认在任务目录下)
auto_done: str = ''
skip: str = ''
review_pinyin: str = ''
review_split: str = ''
review_verb: str = ''
review_special: str = ''
# 权威库(默认指向全局,可覆盖)
main: str = 'data/db/vocabulary.txt'
skipped: str = 'data/db/skipped.txt'
def review_path(self, bucket: str) -> str:
return getattr(self, f'review_{bucket}')
@dataclass
class TaskState:
"""任务状态与进度"""
status: str = STATUS_CREATED
# 各桶条数快照
bucket_counts: Dict[str, int] = field(default_factory=dict)
# 原始输入有效条数
input_valid: int = 0
# 条数校验结果
validation_ok: bool = False
# 时间戳
created_at: str = ''
updated_at: str = ''
# 处理历史(阶段变更记录)
history: List[Dict[str, str]] = field(default_factory=list)
@dataclass
class Task:
"""任务:配置 + 状态 + 目录"""
task_id: str
name: str
task_dir: str
config: TaskConfig
state: TaskState
# ------------------------------------------------------------------
# 工厂方法
# ------------------------------------------------------------------
@classmethod
def create(
cls,
task_id: str,
source: str,
tasks_root: str = 'tasks',
name: Optional[str] = None,
start_line: int = 1,
count: int = 300,
main: Optional[str] = None,
skipped: Optional[str] = None,
) -> "Task":
"""
创建新任务生成独立目录和默认配置
Args:
task_id: 任务唯一 id
source: 数据源文件路径
tasks_root: 任务根目录每任务一个子目录
name: 任务显示名默认同 task_id
start_line/count: 处理范围
main/skipped: 权威库路径默认全局
"""
task_dir = Path(tasks_root) / task_id
d = str(task_dir)
# 单批文件默认放任务目录下
config = TaskConfig(
source=source,
start_line=start_line,
count=count,
auto_done=str(task_dir / 'auto_done.txt'),
skip=str(task_dir / 'skip.txt'),
review_pinyin=str(task_dir / 'review_pinyin.txt'),
review_split=str(task_dir / 'review_split.txt'),
review_verb=str(task_dir / 'review_verb.txt'),
review_special=str(task_dir / 'review_special.txt'),
)
if main:
config.main = main
if skipped:
config.skipped = skipped
now = datetime.now().isoformat()
state = TaskState(
status=STATUS_CREATED,
created_at=now,
updated_at=now,
history=[{'time': now, 'event': 'created'}],
)
task = cls(
task_id=task_id,
name=name or task_id,
task_dir=d,
config=config,
state=state,
)
task.ensure_dirs()
task.save()
return task
@classmethod
def load(cls, task_id: str, tasks_root: str = 'tasks') -> "Task":
"""从 task.json 加载任务"""
task_file = Path(tasks_root) / task_id / 'task.json'
if not task_file.exists():
raise FileNotFoundError(f"任务不存在: {task_file}")
data = json.loads(task_file.read_text(encoding='utf-8-sig'))
return cls.from_dict(data)
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "Task":
return cls(
task_id=data['task_id'],
name=data['name'],
task_dir=data['task_dir'],
config=TaskConfig(**data['config']),
state=TaskState(**data['state']),
)
# ------------------------------------------------------------------
# 持久化
# ------------------------------------------------------------------
def to_dict(self) -> Dict[str, Any]:
return {
'task_id': self.task_id,
'name': self.name,
'task_dir': self.task_dir,
'config': asdict(self.config),
'state': asdict(self.state),
}
def save(self):
"""保存 task.json"""
self.ensure_dirs()
self.state.updated_at = datetime.now().isoformat()
task_file = Path(self.task_dir) / 'task.json'
task_file.write_text(
json.dumps(self.to_dict(), indent=2, ensure_ascii=False),
encoding='utf-8',
)
def ensure_dirs(self):
"""确保任务目录存在"""
Path(self.task_dir).mkdir(parents=True, exist_ok=True)
# ------------------------------------------------------------------
# 状态机
# ------------------------------------------------------------------
def set_status(self, status: str, event: Optional[str] = None):
"""更新任务状态并记录历史"""
if status not in VALID_STATUSES:
raise ValueError(f"非法状态: {status},合法: {VALID_STATUSES}")
self.state.status = status
now = datetime.now().isoformat()
self.state.history.append({
'time': now,
'event': event or f'status -> {status}',
})
self.save()
# ------------------------------------------------------------------
# 便捷访问
# ------------------------------------------------------------------
def review_files(self) -> Dict[str, str]:
"""返回 review 桶名 → 路径"""
return {b: self.config.review_path(b) for b in REVIEW_BUCKETS}

View File

@ -0,0 +1,86 @@
"""
任务管理器创建列举加载任务
多任务并存每个任务在 tasks_root 下有独立子目录
任务注册表通过扫描 tasks_root 下的 task.json 动态生成无需单独维护索引文件
"""
from pathlib import Path
from typing import Dict, List, Optional
from .task import Task
class TaskManager:
"""任务管理器:负责任务的创建、列举、加载"""
def __init__(self, tasks_root: str = 'tasks'):
self.tasks_root = Path(tasks_root)
def ensure_root(self):
self.tasks_root.mkdir(parents=True, exist_ok=True)
# ------------------------------------------------------------------
# 创建 / 加载
# ------------------------------------------------------------------
def create_task(
self,
task_id: str,
source: str,
name: Optional[str] = None,
start_line: int = 1,
count: int = 300,
main: Optional[str] = None,
skipped: Optional[str] = None,
) -> Task:
"""创建新任务(若已存在则报错)"""
self.ensure_root()
task_file = self.tasks_root / task_id / 'task.json'
if task_file.exists():
raise FileExistsError(f"任务已存在: {task_id}")
return Task.create(
task_id=task_id,
source=source,
tasks_root=str(self.tasks_root),
name=name,
start_line=start_line,
count=count,
main=main,
skipped=skipped,
)
def load_task(self, task_id: str) -> Task:
"""加载已有任务"""
return Task.load(task_id, tasks_root=str(self.tasks_root))
def task_exists(self, task_id: str) -> bool:
return (self.tasks_root / task_id / 'task.json').exists()
# ------------------------------------------------------------------
# 列举
# ------------------------------------------------------------------
def list_tasks(self) -> List[Task]:
"""扫描 tasks_root返回所有任务"""
if not self.tasks_root.exists():
return []
tasks = []
for child in sorted(self.tasks_root.iterdir()):
if child.is_dir() and (child / 'task.json').exists():
try:
tasks.append(Task.load(child.name, tasks_root=str(self.tasks_root)))
except Exception:
# 损坏的 task.json 跳过,不影响其他任务
continue
return tasks
def list_task_summaries(self) -> List[Dict[str, str]]:
"""返回任务摘要列表id / name / status / source"""
out = []
for t in self.list_tasks():
out.append({
'task_id': t.task_id,
'name': t.name,
'status': t.state.status,
'source': t.config.source,
'updated_at': t.state.updated_at,
})
return out

View File

@ -0,0 +1,59 @@
"""
词条格式校验
确保合并进主库(xinbiaori.txt)的每一行都是干净的标准格式
汉字|分段:假名|分段:拼音|分段
杜绝半成品/注释残留/长度不匹配等脏数据混入
"""
import re
from typing import Tuple, Optional
# 行号前缀 与 注释后缀
_LINE_NO = re.compile(r'^L\d+\s+')
_COMMENT = re.compile(r'\s+#.*$')
def clean_line(raw: str) -> str:
"""去掉行号前缀 'L123 ' 与注释后缀 ' # ...',并 strip。"""
s = _LINE_NO.sub('', raw)
s = _COMMENT.sub('', s)
return s.strip()
def validate_line(s: str) -> Tuple[bool, Optional[str]]:
"""
校验单行是否为合格的标准格式
返回 (是否合格, 失败原因)原因为 None 表示合格
传入前应先经过 clean_line 处理
"""
if not s:
return False, "空行"
# 残留注释/半成品标记
for marker in ('[原]', '[建议', '#'):
if marker in s:
return False, f"含注释/半成品标记 {marker}"
# 待处理标记
if ':?' in s:
return False, "含待处理标记 ':?'"
# 必须恰好 2 个主分隔符
if s.count(':') != 2:
return False, f"主分隔符数量错误(应为2个冒号,实际{s.count(':')}个)"
kanji, kana, pinyin = s.split(':')
# 汉字段与假名段不能为空
if not kanji.strip():
return False, "汉字段为空"
if not kana.strip():
return False, "假名段为空"
# 三段的 token 数必须一致
n_kanji = len(kanji.split('|'))
n_kana = len(kana.split('|'))
n_pinyin = len(pinyin.split('|'))
if len({n_kanji, n_kana, n_pinyin}) != 1:
return False, f"分段长度不匹配(汉字{n_kanji}/假名{n_kana}/拼音{n_pinyin})"
return True, None

View File

@ -0,0 +1,150 @@
from typing import Any, Dict, List, Optional, Callable, TypeVar
import csv
from io import StringIO
K = TypeVar('K')
V = TypeVar('V')
def add_to_nested_dict(
nested_dict: dict[K, Any],
keys: list[K],
value: V,
container_factory=list
) -> bool:
"""
通用嵌套字典操作函数
Args:
nested_dict: 目标字典可多层嵌套
keys: 键的层级列表 [key1, key2]
value: 要添加的值
container_factory: 未找到键时使用的容器类型默认list
Returns:
bool: 是否成功添加False表示已存在
"""
current = nested_dict
for key in keys[:-1]:
if key not in current:
current[key] = {}
current = current[key]
final_key = keys[-1]
if final_key not in current:
current[final_key] = container_factory()
if value in current[final_key]: # 去重检查
return False
current[final_key].append(value)
return True
def sort_nested_dict(d: Dict[Any, Any], reverse: bool = False) -> Dict[Any, Any]:
"""
递归排序嵌套字典的所有层按key字母顺序
Args:
d: 输入字典
reverse: 是否降序排列
Returns:
新排序后的字典原字典不变
"""
def _sort(item):
if isinstance(item, dict):
return {
k: _sort(v)
for k, v in sorted(
item.items(),
key=lambda x: str(x[0]), # 确保不同类型key可比较
reverse=reverse
)
}
elif isinstance(item, list):
return [_sort(x) for x in item]
return item
return _sort(d)
def output_nested_dict(
nested_dict: Dict[K, Any],
titles: List[str],
leaf_formatter: Optional[Callable[[K, Any], str]] = None,
output_format: str = 'csv' # 新增输出格式选项
) -> str:
"""
通用嵌套字典输出函数支持CSV/树形格式
Args:
nested_dict: 目标字典可多层嵌套
titles: 表头名称列表 ["Level1", "Level2"]
leaf_formatter: 叶子节点格式化函数签名为 (key, value) -> str
output_format: 输出格式可选 'csv' 'tree'默认csv
Returns:
格式化后的字符串
Example:
>>> data = {'A': {'a1': 1, 'a2': 2}, 'B': {'b1': 3}}
>>> print(output_nested_dict_csv(data, ["Category", "SubItem"]))
Category,SubItem,Value
A,a1,1
A,a2,2
B,b1,3
"""
assert output_format in ('csv', 'tree'), "输出格式必须是 'csv''tree'"
if output_format == 'csv':
return _generate_csv(nested_dict, titles, leaf_formatter)
else:
return _generate_tree(nested_dict, titles, leaf_formatter)
def _generate_csv(
nested_dict: Dict[K, Any],
titles: List[str],
leaf_formatter: Optional[Callable[[K, Any], str]] = None
) -> str:
"""生成CSV格式输出"""
output = StringIO()
writer = csv.writer(output)
# 写入表头追加Value列
writer.writerow(titles + ['Value'])
def recurse(d: Dict[K, Any], path: List[str]):
for key, value in d.items():
current_path = path + [str(key)]
if isinstance(value, dict):
recurse(value, current_path)
else:
formatted = leaf_formatter(key, value) if leaf_formatter else str(value)
writer.writerow(current_path + [formatted])
recurse(nested_dict, [])
return output.getvalue()
def _generate_tree(
nested_dict: Dict[K, Any],
titles: List[str],
leaf_formatter: Optional[Callable[[K, Any], str]] = None
) -> str:
"""生成树形格式输出(原逻辑)"""
lines = []
def get_key_name(depth: int) -> str:
return titles[depth] if depth < len(titles) else f"Level{depth+1}"
def recurse(d: Dict[K, Any], depth: int):
indent = ' ' * depth
current_key_name = get_key_name(depth)
for key, value in d.items():
if isinstance(value, dict):
lines.append(f"{indent}{current_key_name}: {key}")
recurse(value, depth + 1)
else:
formatted = leaf_formatter(key, value) if leaf_formatter else str(value)
lines.append(f"{indent}{current_key_name}: {key} {formatted}")
recurse(nested_dict, 0)
return '\n'.join(lines)

View File

@ -0,0 +1,18 @@
"""
tango 子包日语单词/汉字的数据模型解析与多维索引
- models: Kanji, Tango 数据模型
- parser: string_to_tango_kanjis 解析器
- index: TangoKanji 多维索引生成学习资料的核心
"""
from .models import Kanji, Tango
from .parser import string_to_tango_kanjis
from .index import TangoKanji, format_tango_list
__all__ = [
'Kanji',
'Tango',
'string_to_tango_kanjis',
'TangoKanji',
'format_tango_list',
]

View File

@ -0,0 +1,143 @@
"""
TangoKanji多维索引容器
维护 6 种组合映射用于生成给用户的学习资料多维视图
"""
from collections import defaultdict
from collections.abc import Iterator
from copy import deepcopy
from typing import Any, List
from loguru import logger
from pydantic import BaseModel, PrivateAttr
from ..dict_utils import add_to_nested_dict, output_nested_dict, sort_nested_dict
from .models import Kanji, Tango
from .parser import string_to_tango_kanjis
def format_tango_list(key, value: Any) -> str:
"""叶子节点格式化:把 Tango 列表拼成一行"""
return ' '.join([tango.format() for tango in value])
class TangoKanji(BaseModel):
"""
多维索引容器维护以下映射供生成学习资料
- kanji -> kana -> tangos
- pinyin -> kana -> tangos
- kanji -> pinyin -> kana -> tangos
- pinyin -> kanji -> kana -> tangos
- pinyin -> kana -> kanji -> tangos
"""
_tango_kanjis: dict = PrivateAttr(default_factory=dict)
_kanji_kana_tangos: dict = PrivateAttr(default_factory=dict)
_pinyin_kana_tangos: dict = PrivateAttr(default_factory=dict)
_kanji_pinyin_kana_tangos: dict = PrivateAttr(default_factory=dict)
_pinyin_kanji_kana_tangos: dict = PrivateAttr(default_factory=dict)
_pinyin_kana_kanji_tangos: dict = PrivateAttr(default_factory=dict)
def __init__(self, **data):
super().__init__(**data)
self._tango_kanjis = defaultdict(lambda: defaultdict(list))
self._kanji_kana_tangos = defaultdict(lambda: defaultdict(list))
self._pinyin_kana_tangos = defaultdict(lambda: defaultdict(list))
self._kanji_pinyin_kana_tangos = defaultdict(lambda: defaultdict(lambda: defaultdict(list)))
self._pinyin_kanji_kana_tangos = defaultdict(lambda: defaultdict(lambda: defaultdict(list)))
self._pinyin_kana_kanji_tangos = defaultdict(lambda: defaultdict(lambda: defaultdict(list)))
def add_tango_kanjis(
self,
tango_kanjis: "tuple[Tango, List[Kanji]] | str",
main_splitter: str = ':',
sub_splitter: str = '|',
strict: bool = True
) -> bool:
if isinstance(tango_kanjis, str):
tango, kanjis = string_to_tango_kanjis(
tango_kanjis,
main_splitter=main_splitter,
sub_splitter=sub_splitter,
strict=strict
)
else:
tango, kanjis = tango_kanjis
if tango is None or not kanjis:
return False
if any(existing == tango for existing in self._tango_kanjis.keys()):
logger.warning(f"单词已存在,跳过: {tango.format()}")
return False
self._tango_kanjis[tango] = kanjis
for kanji in kanjis:
add_to_nested_dict(self._kanji_kana_tangos, [kanji.kanji, kanji.kana], tango)
add_to_nested_dict(self._pinyin_kana_tangos, [kanji.pinyin, kanji.kana], tango)
add_to_nested_dict(self._kanji_pinyin_kana_tangos, [kanji.kanji, kanji.pinyin, kanji.kana], tango)
add_to_nested_dict(self._pinyin_kanji_kana_tangos, [kanji.pinyin, kanji.kanji, kanji.kana], tango)
add_to_nested_dict(self._pinyin_kana_kanji_tangos, [kanji.pinyin, kanji.kana, kanji.kanji], tango)
return True
def add_from_file_stream(
self,
file_path: str,
encoding: str = 'utf-8',
main_splitter: str = ':',
sub_splitter: str = '|',
strict: bool = True
) -> Iterator[bool]:
with open(file_path, 'r', encoding=encoding) as f:
for line in f:
line = line.strip()
if not line or line.startswith('#'):
continue
yield self.add_tango_kanjis(
line, main_splitter=main_splitter,
sub_splitter=sub_splitter, strict=strict
)
# --- 数据访问 ---
def get_tango_kanjis(self) -> dict:
return deepcopy(self._tango_kanjis)
def get_kanji_kana_tangos(self) -> dict:
return deepcopy(self._kanji_kana_tangos)
def get_pinyin_kana_tangos(self) -> dict:
return deepcopy(self._pinyin_kana_tangos)
def get_kanji_pinyin_kana_tangos(self) -> dict:
return deepcopy(self._kanji_pinyin_kana_tangos)
def get_pinyin_kanji_kana_tangos(self) -> dict:
return deepcopy(self._pinyin_kanji_kana_tangos)
# --- 学习资料输出(字符串视图)---
def get_kanji_kana_tangos_str(self) -> str:
return output_nested_dict(self._kanji_kana_tangos, ["汉字", "假名"], leaf_formatter=format_tango_list)
def get_pinyin_kana_tangos_str(self) -> str:
return output_nested_dict(self._pinyin_kana_tangos, ["拼音", "假名"], leaf_formatter=format_tango_list)
def get_kanji_pinyin_kana_tangos_str(self) -> str:
return output_nested_dict(self._kanji_pinyin_kana_tangos, ["汉字", "拼音", "假名"], leaf_formatter=format_tango_list)
def get_pinyin_kanji_kana_tangos_str(self) -> str:
return output_nested_dict(self._pinyin_kanji_kana_tangos, ["拼音", "汉字", "假名"], leaf_formatter=format_tango_list)
def get_pinyin_kana_kanji_tangos_str(self, output_format: str = 'csv') -> str:
return output_nested_dict(
self._pinyin_kana_kanji_tangos,
["拼音", "假名", "汉字"],
leaf_formatter=format_tango_list,
output_format=output_format
)
def sort_all(self, reverse: bool = False):
self._kanji_kana_tangos = sort_nested_dict(self._kanji_kana_tangos, reverse=reverse)
self._pinyin_kana_tangos = sort_nested_dict(self._pinyin_kana_tangos, reverse=reverse)
self._kanji_pinyin_kana_tangos = sort_nested_dict(self._kanji_pinyin_kana_tangos, reverse=reverse)
self._pinyin_kanji_kana_tangos = sort_nested_dict(self._pinyin_kanji_kana_tangos, reverse=reverse)
self._pinyin_kana_kanji_tangos = sort_nested_dict(self._pinyin_kana_kanji_tangos, reverse=reverse)

View File

@ -0,0 +1,57 @@
"""
数据模型Kanji汉字-假名-拼音Tango单词-假名
"""
from pydantic import BaseModel
class Kanji(BaseModel):
"""单个汉字的三要素:汉字、假名读音、拼音"""
kanji: str = ""
kana: str = ""
pinyin: str = ""
@classmethod
def parse_string(cls, s: str, splitter: str = '->') -> 'Kanji':
"""'中->zhong->ちゅう' 格式的字符串解析为 Kanji 对象"""
if splitter not in s:
raise ValueError("字符串格式应为 '汉字->拼音->假名' ")
kanji, pinyin, kana = s.split(splitter, maxsplit=2)
return cls(kanji=kanji.strip(), pinyin=pinyin.strip(), kana=kana.strip())
def to_json(self, indent=None) -> str:
"""将对象转为 JSON 字符串"""
return self.model_dump_json(indent=indent)
def format(self, splitter: str = '->') -> str:
"""格式化输出"""
return f"{self.kanji}{splitter}{self.pinyin}{splitter}{self.kana}"
class Tango(BaseModel):
"""一个日语单词:单词本身 + 假名读音"""
tango: str
kana: str
@classmethod
def parse_string(cls, s: str, splitter: str = '->') -> 'Tango':
"""'日本語->にほんご' 格式的字符串解析为 Tango 对象"""
if splitter not in s:
raise ValueError("字符串格式应为 '日语单词->假名' ")
tango, kana = s.split(splitter, maxsplit=1)
return cls(tango=tango.strip(), kana=kana.strip())
def to_json(self, indent=None) -> str:
"""将对象转为 JSON 字符串"""
return self.model_dump_json(indent=indent)
def format(self, splitter: str = '->') -> str:
"""格式化输出"""
return f"{self.tango}{splitter}{self.kana}"
def __eq__(self, other: object) -> bool:
if not isinstance(other, Tango):
return False
return (self.tango, self.kana) == (other.tango, other.kana)
def __hash__(self) -> int:
return hash((self.tango, self.kana))

View File

@ -0,0 +1,70 @@
"""
解析器 '汉字|分段:假名|分段:拼音|分段' 格式解析为 Tango + Kanji 列表
"""
from typing import List, Tuple, Optional
from loguru import logger
from .models import Kanji, Tango
def string_to_tango_kanjis(
s: str,
main_splitter: str = ':',
sub_splitter: str = '|',
strict: bool = True
) -> Tuple[Optional[Tango], List[Kanji]]:
"""
解析字符串为 Tango Kanji 列表
Args:
s: 输入字符串格式 '汉字部分:假名部分:拼音部分'
main_splitter: 主分隔符默认':'
sub_splitter: 子分隔符默认'|'
strict: 严格模式True时长度不匹配报错False时跳过并返回 None
Returns:
Tuple[Optional[Tango], List[Kanji]]
Raises:
ValueError: 严格模式下格式错误时抛出
"""
s = s.strip()
if not s:
raise ValueError("输入字符串不能为空")
# 分割主部分
parts = [part.strip() for part in s.split(main_splitter)]
if len(parts) != 3:
raise ValueError(
f"{s} 需要恰好2个主分隔符'{main_splitter}',实际得到{len(parts)-1}"
)
# 分割子部分
tango_parts = parts[0].split(sub_splitter) if parts[0] else []
kana_parts = parts[1].split(sub_splitter) if parts[1] else []
pinyin_parts = parts[2].split(sub_splitter) if parts[2] else []
# 长度校验
if len({len(tango_parts), len(kana_parts), len(pinyin_parts)}) != 1:
if strict:
raise ValueError(
f"{s} 子部分长度不匹配: 汉字{len(tango_parts)}个, "
f"假名{len(kana_parts)}个, 拼音{len(pinyin_parts)}"
)
else:
logger.warning(f"不匹配项: {s} ")
return None, []
min_len = min(len(tango_parts), len(kana_parts), len(pinyin_parts))
# 构建 Tango
tango = Tango(tango=''.join(tango_parts), kana=''.join(kana_parts))
# 构建 Kanji 列表(跳过空拼音的项)
kanji_list = [
Kanji(kanji=tango_parts[i], pinyin=pinyin_parts[i], kana=kana_parts[i])
for i in range(min_len)
if pinyin_parts[i]
]
return tango, kanji_list

View File

@ -0,0 +1,102 @@
飲|料:いん|りょう:yin|liao
簡|化|24|式|太|極|拳:かん|か|にじゅうよん|しき|たい|きょく|けん:jian|hua||shi|tai|ji|quan
賞:しょう:shang
層:そう:ceng
茶:ちゃ:cha
赤:あか:chi
料:りょう:liao
語:ご:yu
語:ご:yu
語:ご:yu
語:ご:yu
語:ご:yu
歯:は:chi
症|候|群:しょう|こう|ぐん:zheng|hou|qun
保|険:ほ|けん:bao|xian
H2|型:エイチツー|がた:|xing
100|万|の|夜|景:ひゃく|まん|の|や|けい:|wan||ye|jing
金:きん:jin
人:じん:ren
富|士:ふ|じ:fu|shi
語:ご:yu
生:なま:sheng
省:しょう:sheng
温|室|効|果:おん|しつ|こう|か:wen|shi|xiao|guo
省:しょう:sheng
屋:や:wu
漫|才:まん|ざい:man|cai
共|和|国:きょう|わ|こく:gong|he|guo
語:ご:yu
映|画|祭:えい|が|さい:ying|hua|ji
とり|肉|の|炒|め:とり|にく|の|いた|め:|rou||chao|
島:とう:dao
洋:よう:yang
用|紙:よう|し:yong|zhi
東:ひがし:dong
陝|西|救|護|飼|養:せん|せい|きゅう|ご|し|よう:shan|xi|jiu|hu|si|yang
中|国|保|護|支|援|基|金:ちゅう|ごく|ほ|ご|し|えん|き|きん:zhong|guo|bao|hu|zhi|yuan|ji|jin
中|国|保|護|観|察|団:ちゅう|ごく|ほ|ご|かん|さつ|だん:zhong|guo|bao|hu|guan|cha|tuan
佐|渡|保|護:さ|ど|ほ|ご:zuo|du|bao|hu
玉:たま:yu
粉:こな:fen
老|人:ろう|じん:lao|ren
袋:ぶくろ:dai
靴:ぐつ:xue
巻|き:ま|き:juan|
非|常:ひ|じょう:fei|chang
賞:しょう:shang
さくら|文|化:さくら|ぶん|か:|wen|hua
初|恋|の|来|た|道:はつ|こい|の|き|た|みち:chu|lian||lai||dao
の|森:の|もり:|sen
弦|原:げん|げん:xian|yuan
下|弦:か|げん:xia|xian
華:はな:hua
平|原|の|風:へい|げん|の|かぜ:ping|yuan||feng
花|咲|くころ」:はな|さ|くころ:hua|xiao|
人:じん:ren
戦:せん:zhan
回|し:まわ|し:hui|
王|立|科|学:おう|りつ|か|がく:wang|li|ke|xue
化|学|賞:か|がく|しょう:hua|xue|shang
大|学:だい|がく:da|xue
触|媒|反|応:しょく|ばい|はん|のう:chu|mei|fan|ying
1|番|打|者:いち|ばん|だ|しゃ:|fan|da|zhe
塀:べい:ping
村:むら:cun
缶:かん:fou
再|生|可|能:さい|せい|か|のう:zai|sheng|ke|neng
工|業:こう|ぎょう:gong|ye
天|然:てん|ねん:tian|ran
育:いく:yu
食|品:しょく|ひん:shi|pin
太|陽|光:たい|よう|こう:tai|yang|guang
車:しゃ:che
地:ち:di
川:がわ:chuan
東|京:とう|きょう:dong|jing
営|業:えい|ぎょう:ying|ye
発|光:はっ|こう:fa|guang
菜:な:cai
栽|培:さい|ばい:zai|pei
美|術|館:び|じゅつ|かん:mei|shu|guan
段:だん:duan
複|層:ふく|そう:fu|ceng
板:いた:ban
朝:ちょう:chao
玉:だま:yu
西:にし:xi
北:きた:bei
路:ろ:lu
新|疆|自|治|区:しん|きょう|じ|ち|く:xin|jiang|zi|zhi|qu
盆|地:ぼん|ち:pen|di
大|陸:たい|りく:da|lu
路:ろ:lu
洋:よう:yang
半|島:はん|とう:ban|dao
西|遊|記:さい|ゆう|き:xi|you|ji
宮|殿:きゅう|でん:gong|dian
広|西|族|自|治|区:こう|せい|ぞく|じ|ち|く:guang|xi|zu|zi|zhi|qu
お|子|様:お|こ|さま:|zi|yang
花|の:はな|の:hua|
東:あ:dong
東|京|白|銀:とう|きょう|はく|ぎん:dong|jing|bai|yin

View File

@ -0,0 +1,236 @@
L11 あっという|間:あっという|ま:|jian # 多音字(間),请确认
L52 浄|水|場:じょう|すい|じょう:jing|shui|chang # 多音字(場),请确认
L59 金|町|浄|水|場:かな|まち|じょう|すい|じょう:jin|ting|jing|shui|chang # 多音字(場),请确认
L80 議|会:ぎ|かい:yi|hui # 多音字(会),请确认
L90 責|任:せき|にん:ze|ren # 多音字(責),请确认
L132 見|直|し:み|なお|し:jian|zhi| # 多音字(見),请确认
L157 ごみ|捨|て|場:ごみ|す|て|ば:|she||chang # 多音字(場),请确认
L176 広|場:ひろ|ば:guang|chang # 多音字(場),请确认
L190 市|長:し|ちょう:shi|zhang # 多音字(長),请确认
L218 間|違|う:ま|ちが|う:jian|wei| # 多音字(間),请确认
L219 苦|難:く|なん:ku|nan # 多音字(難),请确认
L231 社|会:しゃ|かい:she|hui # 多音字(会),请确认
乾|燥:かん|そう:gan|zao
L300 学|会:がっ|かい:xue|hui # 多音字(会),请确认
L389 見|積|もり:み|つ|もり:jian|ji| # 多音字(見),请确认
乾|電|池:かん|でん|ち:gan|dian|chi
L429 避|難:ひ|なん:bi|nan # 含~+多音字(難)
L432 校|長|先|生:こう|ちょう|せん|せい:xiao|zhang|xian|sheng # 多音字(長),请确认
L446 場|面:ば|めん:chang|mian # 多音字(場),请确认
L454 実|行:じっ|こう:shi|xing # 含~+多音字(行)
L479 要|塞:よう|さい:yao|sai # 多音字(要),请确认
L485 見|どころ:み|どころ:jian| # 多音字(見),请确认
長|江:ちょう|こう:chang|jiang
L35 必|見:ひっ|けん:bi|jian # 多音字(見),请确认
L43 見|張|る:み|は|る:jian|zhang| # 多音字(見),请确认
L46 見|飽|きる:み|あ|きる:jian|bao| # 多音字(見),请确认
L54 目|を|見|張|る:め|を|み|は|る:mu||jian|zhang| # 多音字(見),请确认
L72 求|人|難:きゅう|じん|なん:qiu|ren|nan # 多音字(難),请确认
L84 見|物:けん|ぶつ:jian|wu # 多音字(見),请确认
L104 飲|み|会:の|み|かい:yin||hui # 多音字(会),请确认
L105 当|てはまる:あ|てはまる:dang| # 多音字(当),请确认
L106 責|任|を|取|る:せき|にん|を|と|る:ze|ren||qu| # 多音字(責),请确认
L120 夢|を|見|る:ゆめ|を|み|る:meng||jian| # 多音字(見),请确认
L141 手|間:て|ま:shou|jian # 多音字(間),请确认
L144 行|為:こう|い:xing|wei # 多音字(行),请确认
L146 郵|便|受|け:ゆう|びん|う|け:you|bian|shou| # 多音字(便),请确认
L149 便|秘:べん|ぴ:bian|mi # 多音字(便),请确认
L158 長|女:ちょう|じょ:zhang|nv # 多音字(長),请确认
L159 団|体|行|動:だん|たい|こう|どう:tuan|ti|xing|dong # 多音字(行),请确认
L167 見|本:み|ほん:jian|ben # 多音字(見),请确认
還|暦:かん|れき:huan|li
L179 総|料|理|長:そう|りょう|り|ちょう:zong|liao|li|zhang # 多音字(長),请确认
L208 展|示|会:てん|じ|かい:zhan|shi|hui # 多音字(会),请确认
乾|燥|機:かん|そう|き:gan|zao|ji
L246 当|初:とう|しょ:dang|chu # 多音字(当),请确认
L273 重|役:じゅう|やく:zhong|yi # 多音字(重),请确认
L295 干|ばつ:かん|ばつ:gan| # 多音字(干),请确认
L298 大|気:た|いき:da|qi # 多解,已取第一个
L311 要|因:よう|いん:yao|yin # 多音字(要),请确认
L320 重|み:おも|み:zhong| # 多音字(重),请确认
L332 要|求:よう|きゅう:yao|qiu # 多音字(要),请确认
L348 客|間:きゃく|ま:ke|jian # 多音字(間),请确认
L350 国|際|社|会:こく|さい|しゃ|かい:guo|ji|she|hui # 多音字(会),请确认
L355 当|人:とう|にん:dang|ren # 多音字(当),请确认
L373 納|得:なっ|とく:na|de # 多音字(得),请确认
L384 当|てはめる:あ|てはめる:dang| # 多音字(当),请确认
L392 花|見:はな|み:hua|jian # 多音字(見),请确认
L393 職|場:しょく|ば:zhi|chang # 多音字(場),请确认
L414 災|難:さい|なん:zai|nan # 多音字(難),请确认
L453 委|員|会:い|いん|かい:wei|yuan|hui # 多音字(会),请确认
L468 晩|餐|会:ばん|さん|かい:wan|can|hui # 多音字(会),请确认
細|長|い:ほそ|なが|い:xi|chang|
長|寿|国:ちょう|じゅ|こく:chang|shou|guo
L496 来|場|者:らい|じょう|しゃ:lai|chang|zhe # 多音字(場),请确认
L3 得|点:とく|てん:de|dian # 含~+多音字(得)
L26 宴|会:えん|かい:yan|hui # 多音字(会),请确认
L39 支|店|長:し|てん|ちょう:zhi|dian|zhang # 多音字(長),请确认
L44 重|力:じゅう|りょく:zhong|li # 多音字(重),请确认
L55 遂|行:すい|こう:sui|xing # 多音字(行),请确认
L61 難|題:なん|だい:nan|ti # 多音字(難),请确认
L70 会|長:かい|ちょう:hui|zhang # 多音字(会,長),请确认
L73 困|難:こん|なん:kun|nan # 多音字(難),请确认
L79 忘|年|会:ぼう|ねん|かい:wang|nian|hui # 多音字(会),请确认
L89 見|捨|てる:み|す|てる:jian|she| # 多音字(見),请确认
L95 見|慣|れる:み|な|れる:jian|guan| # 多音字(見),请确认
L107 百|薬|の|長:ひゃく|やく|の|ちょう:bai|yao||zhang # 多音字(長),请确认
L117 当|館:とう|かん:dang|guan # 多音字(当),请确认
L118 館|長:かん|ちょう:guan|zhang # 多音字(長),请确认
重|ねる:かさ|ねる:chong|
L189 開|会:かい|かい:kai|hui # 多音字(会),请确认
L206 世|間:せ|けん:shi|jian # 多音字(間),请确认
L226 難|しさ:むずか|しさ:nan| # 多音字(難),请确认
L242 研|究|会:けん|きゅう|かい:yan|jiu|hui # 多音字(会),请确认
L243 有|人|宇|宙|飛|行:ゆう|じん|う|ちゅう|ひ|こう:you|ren|yu|zhou|fei|xing # 多音字(行),请确认
L393 適|当:てき|とう:shi|dang # 多音字(当),请确认
帰|還:き|かん:gui|huan
L441 会|話:かい|わ:hui|hua # 多音字(会),请确认
L459 便|り:たよ|り:bian| # 多音字(便),请确认
L497 現|場:げん|ば:xian|chang # 多音字(場),请确认
L8 新|婚|旅|行:しん|こん|りょ|こう:xin|hun|lv|xing # 多音字(行),请确认
L25 同|窓|会:どう|そう|かい:tong|chuang|hui # 多音字(会),请确认
L34 当|事|者:とう|じ|しゃ:dang|shi|zhe # 多音字(当),请确认
L47 重|量:じゅう|りょう:zhong|liang # 多音字(重),请确认
L103 刊|行:かん|こう:kan|xing # 含~+多音字(行)
L126 よそ|見:よそ|み:|jian # 多音字(見),请确认
L189 間|に|合|わせる:ま|に|あ|わせる:jian||he| # 多音字(間),请确认
L190 間:ま:jian # 多音字(間),请确认
特|長:とく|ちょう:te|chang
L211 瞬|間:しゅん|かん:shun|jian # 多音字(間),请确认
L242 競|技|場:きょう|ぎ|じょう:jing|ji|chang # 多音字(場),请确认
L264 見|届|ける:み|とど|ける:jian|jie| # 多音字(見),请确认
L266 要|素:よう|そ:yao|su # 多音字(要),请确认
L277 当|てる:あ|てる:dang| # 多音字(当),请确认
L350 要|約:よう|やく:yao|yue # 多音字(要),请确认
L366 当|分:とう|ぶん:dang|fen # 多音字(当),请确认
L412 開|場:かい|じょう:kai|chang # 多音字(場),请确认
L432 夢|見|る:ゆめ|み|る:meng|jian| # 多音字(見),请确认
L435 中|国|会|館:ちゅう|ごく|かい|かん:zhong|guo|hui|guan # 多音字(会),请确认
L448 首|を|長|くする:くび|を|なが|くする:shou||chang| # 多音字(長),请确认
L34 当|校:とう|こう:dang|xiao # 多音字(当),请确认
L59 夜|間|学|校:や|かん|がっ|こう:ye|jian|xue|xiao # 多音字(間),请确认
L67 全|国|高|等|学|校|野|球|選|手|権|大|会:ぜん|こく|こう|とう|がっ|こう|や|きゅう|せん|しゅ|けん|たい|かい:quan|guo|gao|deng|xue|xiao|ye|qiu|xuan|shou|quan|da|hui # 多音字(会),请确认
L68 全|国|大|会:ぜん|こく|たい|かい:quan|guo|da|hui # 多音字(会),请确认
L70 阪|神|甲|子|園|球|場:はん|しん|こう|し|えん|きゅう|じょう:ban|shen|jia|zi|yuan|qiu|chang # 多音字(場),请确认
L90 全|国|高|等|学|校|総|合|体|育|大|会:ぜん|こく|こう|とう|がっ|こう|そう|ごう|たい|いく|たい|かい:quan|guo|gao|deng|xue|xiao|zong|he|ti|yu|da|hui # 多音字(会),请确认
L111 体|育|会|系:たい|いく|かい|けい:ti|yu|hui|xi # 多音字(会),请确认
L147 国|会|議|事|堂:こっ|かい|ぎ|じ|どう:guo|hui|yi|shi|tang # 多音字(会),请确认
L156 人|間|万|事|塞|翁|が|馬:にん|げん|ばん|じ|さい|おう|が|うま:ren|jian|wan|shi|sai|weng||ma # 多音字(間),请确认
L169 難|問:なん|もん:nan|wen # 多音字(難),请确认
延|長:えん|ちょう:yan|chang
L239 世|間|話:せ|けん|ばなし:shi|jian|hua # 多音字(間),请确认
L304 見|舞|う:み|ま|う:jian|wu| # 多音字(見),请确认
波|長:は|ちょう:bo|chang
L376 九|死|に|一|生|を|得|る:きゅう|し|に|いっ|しょう|を|え|る:jiu|si||yi|sheng||de| # 多音字(得),请确认
L377 目|の|当|たりにする:ま|の|あ|たりにする:mu||dang| # 多音字(当),请确认
L398 見|入|る:み|い|る:jian|ru| # 多音字(見),请确认
最|長:さい|ちょう:zui|chang
L409 遭|難:そう|なん:zao|nan # 含~+多音字(難)
L413 重|傷:じゅう|しょう:zhong|shang # 多音字(重),请确认
L434 座|談|会:ざ|だん|かい:zuo|tan|hui # 多音字(会),请确认
L452 走|行:そう|こう:zou|xing # 多音字(行),请确认
L461 見|守|る:み|まも|る:jian|shou| # 多音字(見),请确认
L478 間|接|的:かん|せつ|てき:jian|jie|de # 多音字(間),请确认
L482 需|要:じゅ|よう:xu|yao # 多音字(要),请确认
L483 見|通|し:み|とお|し:jian|tong| # 多音字(見),请确认
L22 取|得:しゅ|とく:qu|de # 含~+多音字(得)
L40 行|き|届|く:い|き|とど|く:xing||jie| # 多音字(行),请确认
L46 夜|間:や|かん:ye|jian # 多音字(間),请确认
L52 月|間:げっ|かん:yue|jian # 多音字(間),请确认
L89 通|行:つう|こう:tong|xing # 多音字(行),请确认
L97 一|方|通|行:いっ|ぽう|つう|こう:yi|fang|tong|xing # 多音字(行),请确认
L101 空|間:くう|かん:kong|jian # 多音字(間),请确认
L126 先|行:せん|こう:xian|xing # 含~+多音字(行)
L137 急|行:きゅう|こう:ji|xing # 多音字(行),请确认
L175 工|場|長:こう|じょう|ちょう:gong|chang|zhang # 多音字(場,長),请确认
L221 社|会|保|障|費:しゃ|かい|ほ|しょう|ひ:she|hui|bao|zhang|fei # 多音字(会),请确认
L223 博|覧|会:はく|らん|かい:bo|lan|hui # 多音字(会),请确认
L228 間|柄:あいだ|がら:jian|bing # 多音字(間),请确认
L245 株|式|会|社|東|進:かぶ|しき|がい|しゃ|とう|しん:zhu|shi|hui|she|dong|jin # 多音字(会),请确认
L246 概|要:がい|よう:gai|yao # 多音字(要),请确认
L256 見|合|わせる:み|あ|わせる:jian|he| # 多音字(見),请确认
L259 見|ごたえ:み|ごたえ:jian| # 多音字(見),请确认
L275 中|間:ちゅう|かん:zhong|jian # 多音字(間),请确认
L277 中|間|管|理|職:ちゅう|かん|かん|り|しょく:zhong|jian|guan|li|zhi # 多音字(間),请确认
L304 市|場:いち|ば:shi|chang # 多音字(場),请确认
L319 手|間|をかける:て|ま|をかける:shou|jian| # 多音字(間),请确认
L335 当|機:とう|き:dang|ji # 多音字(当),请确认
L340 断|行:だん|こう:duan|xing # 含~+多音字(行)
L343 同|好|会:どう|こう|かい:tong|hao|hui # 多音字(会),请确认
L361 当|選:とう|せん:dang|xuan # 含~+多音字(当)
L367 町|会|議|員:ちょう|かい|ぎ|いん:ting|hui|yi|yuan # 多音字(会),请确认
L369 国|会|議|員:こっ|かい|ぎ|いん:guo|hui|yi|yuan # 多音字(会),请确认
L375 行|政:ぎょう|せい:xing|zheng # 多音字(行),请确认
L381 行|き|過|ぎ:い|き|す|ぎ:xing||guo| # 多音字(行),请确认
L405 要|件:よう|けん:yao|jian # 多音字(要),请确认
L410 長:ちょう:zhang # 多音字(長),请确认
L430 当|該:とう|がい:dang|gai # 多音字(当),请确认
L436 長|官:ちょう|かん:zhang|guan # 多音字(長),请确认
L451 見|かけ:み|かけ:jian| # 多音字(見),请确认
L464 東|京|都|議|会|議|員:とう|きょう|と|ぎ|かい|ぎ|いん:dong|jing|dou|yi|hui|yi|yuan # 多音字(会),请确认
L473 会|則:かい|そく:hui|ze # 多音字(会),请确认
L484 無|責|任:む|せき|にん:wu|ze|ren # 多音字(責),请确认
L485 見|当|たる:み|あ|たる:jian|dang| # 多音字(見,当),请确认
L487 見|つめる:み|つめる:jian| # 多音字(見),请确认
L12 行|事:ぎょう|じ:xing|shi # 多音字(行),请确认
L13 会|合:かい|ごう:hui|he # 多音字(会),请确认
L37 進|行:しん|こう:jin|xing # 多音字(行),请确认
L40 お|買|い|得:お|か|い|どく:|mai||de # 多音字(得),请确认
L43 裁|判|長:さい|ばん|ちょう:cai|pan|zhang # 多音字(長),请确认
L61 該|当:がい|とう:gai|dang # 多音字(当),请确认
L77 若|干:じゃっ|かん:ruo|gan # 多音字(干),请确认
L78 当|方:とう|ほう:dang|fang # 多音字(当),请确认
L131 夜|行:や|こう:ye|xing # 多音字(行),请确认
五|重|の|塔:ご|じゅう|の|とう:wu|chong||ta
L144 見|聞|録:けん|ぶん|ろく:jian|wen|lu # 多音字(見),请确认
L145 見|聞:けん|ぶん:jian|wen # 多音字(見),请确认
三|重|の|塔:さん|じゅう|の|とう:san|chong||ta
L155 一|見:いっ|けん:yi|jian # 多音字(見),请确认
積|み|重|ねる:つ|み|かさ|ねる:ji||chong|
L163 すき|間:すき|ま:|jian # 多音字(間),请确认
重|なる:かさ|なる:chong|
L178 間|近:ま|ぢか:jian|jin # 多音字(間),请确认
L227 総|会:そう|かい:zong|hui # 多音字(会),请确认
L277 重|症:じゅう|しょう:zhong|zheng # 多音字(重),请确认
L310 見|方:み|かた:jian|fang # 多音字(見),请确认
L323 便:べん:bian # 多音字(便),请确认
L330 排|便:はい|べん:pai|bian # 含~+多音字(便)
L337 見|回|す:み|まわ|す:jian|hui| # 多音字(見),请确认
L338 工|場|野|菜:こう|じょう|や|さい:gong|chang|ye|cai # 多音字(場),请确认
L364 植|物|工|場:しょく|ぶつ|こう|じょう:zhi|wu|gong|chang # 多音字(場),请确认
L381 長|い|目:なが|い|め:chang||mu # 多音字(長),请确认
二|重:に|じゅう:er|chong
L385 挙|行:きょ|こう:ju|xing # 含~+多音字(行)
L413 非|難:ひ|なん:fei|nan # 多音字(難),请确认
L472 冷|気:れ|いき:leng|qi # 多解,已取第一个
L494 長|芋:なが|いも:chang|yu # 多音字(長),请确认
L15 口|が|重|い:くち|が|おも|い:kou||zhong| # 多音字(重),请确认
L22 足|が|重|い:あし|が|おも|い:zu||zhong| # 多音字(重),请确认
L24 心|行|くまで:こころ|ゆ|くまで:xin|xing| # 多音字(行),请确认
L84 随|行:ずい|こう:sui|xing # 含~+多音字(行)
L122 会|期:かい|き:hui|qi # 多音字(会),请确认
L146 理|事|会:り|じ|かい:li|shi|hui # 多音字(会),请确认
L154 記|者|会|見:き|しゃ|かい|けん:ji|zhe|hui|jian # 多音字(会,見),请确认
L155 会|見:かい|けん:hui|jian # 多音字(会,見),请确认
L157 見|解:けん|かい:jian|jie # 多音字(見),请确认
L223 難|儀:なん|ぎ:nan|yi # 含~+多音字(難)
L231 見|世|物:み|せ|もの:jian|shi|wu # 多音字(見),请确认
L239 手|間|がかかる:て|ま|がかかる:shou|jian| # 多音字(間),请确认
還|元:かん|げん:huan|yuan
L298 習|得:しゅう|とく:xi|de # 含~+多音字(得)
L310 仲|間|入|り:なか|ま|い|り:zhong|jian|ru| # 多音字(間),请确认
L342 才|気:さ|いき:cai|qi # 多解,已取第一个
L347 行|きずり:ゆ|きずり:xing| # 多音字(行),请确认
L369 のど|自|慢|大|会:のど|じ|まん|たい|かい:|zi|man|da|hui # 多音字(会),请确认
L406 要|項:よう|こう:yao|xiang # 多音字(要),请确认
L443 実|社|会:じっ|しゃ|かい:shi|she|hui # 多音字(会),请确认
L444 便|宜:べん|ぎ:bian|yi # 多音字(便),请确认
L455 見|計|らう:み|はか|らう:jian|ji| # 多音字(見),请确认
L468 南|方|株|式|会|社:なん|ぽう|かぶ|しき|がい|しゃ:nan|fang|zhu|shi|hui|she # 多音字(会),请确认
L471 見|よう|見|まね:み|よう|み|まね:jian||jian| # 多音字(見,見),请确认
干|し:ほ|し:gan| # 含~+多音字(干),请确认
人|間:にん|げん:ren|jian # 含~+多音字(間),请确认
旅|行:りょ|こう:lv|xing # 含~+多音字(行),请确认
二|足|歩|行:に|そく|ほ|こう:er|zu|bu|xing # 含~+多音字(行),请确认
『あの|子|を|探|して』:あの|こ|を|さ|がして:|zi||tan| # 多解,已取第一个
至|福|のとき』:し|ふ|くのとき:zhi|fu| # 多解,已取第一个

View File

@ -0,0 +1,11 @@
L379 CS|貿|易:シーエス|ぼう|えき:|mao|yi # 含字母,请确认
L418 A|型:エー|がた:|xing # 含字母,请确认
L183 ランク|付|け:ランク|づ|け:|fu| # 含片假名,请确认
L367 |小|説:エスエフ|しょう|せつ:|xiao|shuo # 含字母,请确认
L476 |蒼|竜:アイアールエス|そう|りゅう:|cang|long # 含字母,请确认
L124 阪|神|・|淡|路|大|震|災:はん|しん|・|あわ|じ|だい|しん|さい:ban|shen||dan|lu|da|zhen|zai # 含片假名,请确认
L251 |商|事:エスシー|しょう|じ:|shang|shi # 含字母,请确认
L26 三|ツ|村|電|機:み|つ|むら|でん|き:san||cun|dian|ji # 含片假名,请确认
L227 ゴールを|決|める:ゴールを|き|める:|jue| # 含片假名,请确认
L491 |放|送|文|化|研|究|所:エヌエイチケー|ほう|そう|ぶん|か|けん|きゅう|じょ:|fang|song|wen|hua|yan|jiu|suo # 含字母,请确认
Aソ|連|型:エーソ|れん|がた:|lian|xing

View File

@ -0,0 +1,93 @@
葛飾区:かつしかく:
吹雪:ふぶき:
中国体育委员会:ちゅうごくたいいくいいんかい”:
博士:はかせ:
漬物:つけもの:
成田:なりた:
損失:そうしつ:
棒球:ピッチャー:
立場:たちば:
両替:りょうがえ:
缶詰:かんづめ:
霞ヶ浦:かすみがうら:
茨城県:いばらきけん:
お父様:おとうさま:
日比谷公園:ひびやこうえん:
宮保鶏丁:ゴンバオジーディン:
乗組員:のりくみいん:
魚香肉絲:ユイシャンロウスー:
物語:ものがたり:
芥川龍之介:あくたがわりゅうのすけ:
蜘蛛の糸:くものいと:
佐渡島:さどがしま:
友友:ヨウヨウ:
洋洋:ヤンヤン:
社員割引制度:しゃいんわりびきせいど:
割引:わりびき:
お兄ちゃん:おにいちゃん:
灰汁:あく:
織田秀忠:おだひでただ:
徳川信長:とくがわのぶなが:
王府井:ワンフーチン:
物置:ものおき:
植木:うえき:
望:のぞみ:
お義母さん:おかあさん:
奈良時代:ならじだい:
大海原:おおうなばら:
世論:せろん/よろん:
張芸謀:チャン・イーモウ:
『赤い~』:あかいコーリャン:
陜西省:せんせいしょう:
敷地:しきち:
朱芳麗:チュウ・ファンリー:
後:のち/あと:
結子:ゆいこ:
寛:ひろし:
紅葉狩り:もみじがり:
神戸~:こうべアート・エンタープライズ:
親父:おやじ:
黒澤明:くろさわあきら:
迷子:まいご:
小栗圭祐:おぐりけいすけ:
為替~:かわせレート:
章:あきら:
茨城:いばらき:
行方:ゆくえ:
行き来:いきき/ゆきき:
川越:かわごえ:
名残:なごり:
所以:ゆえん:
山羊肉:やぎにく:
三越:みつこし:
融通~:ゆうずうする:
対馬海峡:つしまかいきょう:
大久保勇:おおくぼいさむ:
上海博物館:シャンハイはくぶつかん:
お義父さん:おとうさん:
安曇野:あづみの:
清々しい:すがすがしい:
良男:よしお:
浪速大学:なにわだいがく:
勇人:はやと:
武蔵:むさし:
A香港型:エーホンコンがた:
有:あり:
農作物:のうさくぶつ/のうさくもつ:
理:おさむ:
聖武天皇:しょうむてんのう:
天皇:てんのう:
螺鈿紫檀五弦琵琶:らでんしたんのごげんびわ:
杯:はい/さかずき:
西域:さいいき/せいいき:
寧波:ニンポー:
奈良国立博物館:ならこくりつはくぶつかん:
尾道:おのみち:
種種/種々:しゅじゅ:
端端/端々:はしばし:
あり処:ありか:
学びて思わざれば,則ち罔し。思いて学ばざれば,則ち殆うし:まなびておもわざれば,すなわちくらし。おもいてまなばざれば,すなわちあやうし:
今道友信:いまみちとものぶ:
金詰り:かねづまり:
入江:いりえ:
台詞:せりふ:

View File

@ -0,0 +1 @@
L211 [原] 心を1つにする:こころをひとつにする: [需人工确认] # 完整表达/敬语

View File

View File

@ -0,0 +1,43 @@
{
"task_id": "legacy_batch1",
"name": "遗留批次1(tobe处理中)",
"task_dir": "tasks\\legacy_batch1",
"config": {
"source": "data/backup/xinbiaori_tobe.backup.txt",
"start_line": 1,
"count": 3971,
"auto_done": "tasks\\legacy_batch1\\auto_done.txt",
"skip": "tasks\\legacy_batch1\\skip.txt",
"review_pinyin": "tasks\\legacy_batch1\\review_pinyin.txt",
"review_split": "tasks\\legacy_batch1\\review_split.txt",
"review_verb": "tasks\\legacy_batch1\\review_verb.txt",
"review_special": "tasks\\legacy_batch1\\review_special.txt",
"main": "data/db/vocabulary.txt",
"skipped": "data/db/skipped.txt"
},
"state": {
"status": "reviewing",
"bucket_counts": {
"auto_done": 102,
"skip": 0,
"review_pinyin": 236,
"review_split": 93,
"review_verb": 1,
"review_special": 11
},
"input_valid": 3971,
"validation_ok": false,
"created_at": "2026-08-19T14:03:30.346722",
"updated_at": "2026-08-19T14:03:51.949812",
"history": [
{
"time": "2026-08-19T14:03:30.346722",
"event": "created"
},
{
"time": "2026-08-19T14:03:51.944754",
"event": "migrated existing single-batch data"
}
]
}
}

0
tests/__init__.py Normal file
View File

38
tests/conftest.py Normal file
View File

@ -0,0 +1,38 @@
from pathlib import Path
import pytest
@pytest.fixture
def get_test_file_path():
"""
获取测试数据文件的绝对路径
Args:
relative_path (str): 相对于 tests/data/ 的文件路径
Returns:
Path: 绝对路径对象
"""
def _get_path(relative_path):
return (Path(__file__).parent / "data" / relative_path).resolve()
return _get_path
@pytest.fixture
def get_data_file_path():
"""
获取项目 data/ 目录下数据文件的绝对路径
Args:
relative_path (str): 相对于项目根 data/ 的文件路径 "db/vocabulary.txt"
Returns:
Path: 绝对路径对象
"""
def _get_path(relative_path):
return (Path(__file__).parent.parent / "data" / relative_path).resolve()
return _get_path

141
tests/data/nihon_kanjis.txt Normal file
View File

@ -0,0 +1,141 @@
日|本|語:に|ほん|ご:ri|ben|yu
中|国|語:ちゅう|ごく|ご:zhong|guo|yu
中|学|生:ちゅう|がく|せい:zhong|xue|sheng
中:なか:zhong
着|る:き|る:zhuo|
斬|る:き|る:zhan|
切|る:き|る:qie|
物|価:ぶっ|か:wu|jia
高|い:たか|い:gao|
調|節:ちょう|せつ:tiao|jie
安|定:あん|てい:an|ding
上|げる:あ|げる:shang|
下|げる:さ|げる:xia|
凍|結:とう|けつ:dong|jie
天|井:てん|じょう:tian|jing
知|る:し|る:zhi|
上|がる:あ|がる:shang|
変|動:へん|どう:bian|dong
賃|金:ちん|きん:lin|jin
製:せい:zhi
東|京:とう|きょう:dong|jing
他:ほか:ta
地|域:ちい|き:di|yu
生|活:せい|かつ:sheng|huo
高|橋:たか|はし:gao|qiao
社|長:しゃ|ちょう:she|zhang
暮|らし:く|らし:mu|
影|響:えい|きょう:ying|xiang
見|極|める:み|きわ|める:jian|ji|
現|在:げん|ざい:xian|zai
価|格:か|かく:jia|ge
幅:はば:fu
変|更:へん|こう:bian|geng
検|討:けん|とう:jian|tao
夫|婦:ふう|ふ:fu|fu
訪|れる:おとず|れる:fang|
代:だい:dai
女|性:じょ|せい:nv|xing
円|安:えん|やす:en|an
避|ける:さ|ける:bi|
安|い:やす|い:an|
選|ぶ:えら|ぶ:xuan|
過|ごす:す|ごす:guo|
話|す:はな|す:hua|
消|しゴム:け|しごむ:xiao|
当|たる:あ|たる:dang|
同|じ:おな|じ:tong|
品:ひん:pin
安|売|り:やす|う|り:an|mai|
内:うち:nei
人:ひと:ren
探|す:さが|す:tan|
来|る:く|る:lai|
応|援:おう|えん:ying|yuan
頼|む:たの|む:lai|
誰:だれ:shui
店:みせ:dian
行|く:い|く:xing|
国:くに:guo
何:なに:he
質|問:しつ|もん:zhi|wen
飲|み|物:の|み|もの:ying||wu
計|画:けい|かく:ji|hua
達|成:たっ|せい:da|cheng
君:きみ:jun
適|任|しゃ:てき|にん|しゃ:shi|ren|zhe
部|屋:へ|や:bu|wu
机:つくえ:ji
椅:いす:yi
小|さい:ち|いさい:xiao|
家:いえ:jia
財|産:ざい|さん:cai|chan
ご|飯:ご|はん:|fan
食|べる:た|べる:shi|
毎|週:まい|しゅう:mei|zhou
隔|週:かく|しゅう:ge|zhou
日|曜|日:にち|よう|び:ri|yao|ri
月|曜|日:げつ|よう|び:yue|yao|ri
火|曜|日:か|よう|び:huo|yao|ri
水|曜|日:すい|よう|び:shui|yao|ri
木|曜|日:もく|よう|び:mu|yao|ri
金|曜|日:きん|よう|び:jin|yao|ri
土|曜|日:ど|よう|び:tu|yao|ri
定|休:てい|きゅう:ding|xiu
彼|女:かの|じょ:bi|nv
英|語:えい|ご:ying|
堪|能:かん|のう:kan|neng
買|う:か|う:mai|
飼|う:か|う:si|
味|噌|汁:み|そ|しる:wei|zeng|zhi
待|つ:ま|つ:dai|
食|う:く|う:shi|
明|らか:あき|らか:ming|
壁:かべ:bi
壁|紙:かべ|かみ:bi|zhi
見|る:み|る:jian|
言|う:い|う:yan|
本|当:ほん|とう:ben|dang
以|上:い|じょう:yi|shang
時|間:じ|かん:shi|jian
歳:さい:sui
小|児:しょう|に:xiao|er
子:こ:zi
数|える:かぞ|える:shu|
箱:はこ:xiang
方:ほう:fang
応|募:おう|ぼ:ying|mu
彼:かれ:bi
時|速:じ|そく:shi|su
出|す:だ|す:chu|
収|入:しゅう|にゅう:shou|ru
惨|状:さん|じょう:can|zhuang
想|像:そう|ぞう:xiang|xiang
予|想:よ|そう:yu|xiang
現|状:げん|じょう:xian|zhuang
分|析:ぶん|せき:fen|xi
引|き|受|ける:ひ|き|うけ|る:yin||shou|
責|任:せき|にん:ze|ren
持|つ:も|つ:chi|
店|員:てん|いん:dian|yuan
男|子:だん|し:nan|zi
学|生:がく|せい:xue|sheng
仕|方:し|かた:shi|fang
踏|む:ふ|む:ta|
足:あし:zu
釘:くぎ:ding
外|国:がい|こく:wai|guo
地:ち:di
舞|台:ぶ|たい:wu|tai
初|舞|台:はつ|ぶ|たい:chu|wu|tai
場|数:ば|かず:chang|shu
千|円:せん|えん:qian|en
議|員:ぎ|いん:yi|yuan
程|度:てい|ど:cheng|du
実|現:じつ|げん:shi|xian
不|可|能:ふ|か|のう:bu|ke|neng
床:ゆか:chuang
音:おと:yin
聞|こえる:き|こえる:wen|
底:そこ:di
草:くさ:cao

104
tests/data/rules.md Normal file
View File

@ -0,0 +1,104 @@
# 日语词表处理规则(铁律)
本文件记录 xinbiaori_tobe.txt → xinbiaori.txt 的所有处理约定,流水线和人工都遵循。
## 数据格式
每行三段,用 `:` 分隔,段内用 `|` 对齐:
```
汉字|分段:假名|分段:拼音|分段
```
示例:`中|国|人:ちゅう|ごく|じん:zhong|guo|ren`
- 汉字段:每个汉字单独一段;连续假名(送り仮名/前后缀)合并为一段
- 假名段:与汉字段一一对应
- 拼音段:汉字对应拼音,假名段留空
## 处理规则
### 1. 词中没有汉字的 → 跳过不处理
汉字段不含任何汉字(纯英文、纯符号、纯片假名外来语等),直接跳过,放入单批忽略文件 `skip.txt`
- 例:`IT:アイティー:``WTO:...``Japan Railways:...``:パーセント:`
- 判断标准:汉字段用正则 `[\u4e00-\u9fff]` 匹配不到任何字符
### 2. 含字母+汉字的混合词 → 正常处理
字母作为独立段,拼音填小写字母本身。
- 例:`阿|Q|正|伝:あ|キュー|せい|でん:a|q|zheng|zhuan`
- 例:`|自|動|車:ジェーシー|じ|どう|しゃ:jc|zi|dong|che`
### 3. `~` 标记 → 作为任意长通配符对齐,~ 对应部分不输出
`~`(全角 或半角 ~)表示"省略的、无对应汉字的假名部分"。处理时把 `~` 当作
任意长通配符,与汉字 token 一起对齐,`~` 吸收到的假名段和纯标点段都从输出中删除,
只保留汉字段和与汉字紧邻的送り仮名。
- する 动词:`発明~:はつめいします:``発|明:はつ|めい:fa|ming`~ 吸收「します」)
- 名词省略前半:`~賞:ノーベルしょう:``賞:しょう:shang`~ 吸收「ノーベル」)
- 名词省略后半:`飲料~:いんりょうメーカー:``飲|料:いん|りょう:yin|liao`
- 送り仮名保留:`干し~:ほしブドウ:``干|し:ほ|し:gan|`(し 保留,~ 吸收「ブドウ」)
- 对齐成功 → auto无多音字或 review_pinyin含多音字
- 对齐失败/多解无法定夺 → review_split人工确认
### 4. ます形动词 → 转辞書形(原型)
- 五段:ます前 i段音 → u段音かり→かる、き→く、し→す
- 一段:ます → る(げます→げる)
- 用 jamdict 词典验证动词身份
- **词典唯一解** → 直接处理
- **词典歧义**(如 にます→にる/ぬ)→ 放入 review_verb人工查证
- 例:`預かります:あずかります:``預|かる:あず|かる:yu|`
### 5. 完整表达/惯用句 → 保留原样(不砍词尾)
带 お/に/ください 等构成完整寒暄的整句,保留 します/でした。
- 例:`お先に失礼します``お|先|に|失|礼|します:お|さき|に|しつ|れい|します:|xian||shi|li|`
### 6. 々(同字重复符号)→ 展开为前一个字
- 例:`我々:われわれ:``我|我:われ|われ:wo|wo`
- 例:`佐々木:ささき:``佐|佐|木:さ|さ|き:zuo|zuo|mu`
### 7. 熟字训/特殊读音 → 词典无法分割的放 review_split 人工处理
- 例:田舎(いなか)、明日(あす)、お父さん(おとうさん)
## 拼音规则
- 多音字:只标记高频多音字黑名单(行/长/重/传等),其他直接用最常见读音
- 日本新字体按对应的中文字标拼音楽→le、伝→chuan、価→jia
## 条数校验(铁律)
**输入条数 = 各输出文件条数之和**。条数不对,结果一定不对,必须排查。
去掉纯英文等跳过项后,剩余条数也要能对上。
## 工作流(任务化架构)
每次清洗是一个**任务**Task有独立配置、独立临时文件、状态机。
详见 `WORKFLOW.md`。核心流程:
1. **创建任务**:指定数据源 + 处理范围,任务独立目录 `tasks/{id}/`
`jclean create <id> --source data.txt --start 1 --count 300`
2. **处理一批**数据源只读分流到单批文件auto_done/skip/review_*
`jclean process <id>`
3. **逐个 review**:你指出问题 → AI 只记录 → 单文件说完后 AI 汇总
4. **改代码重跑**AI 改字典/规则(不手改输出文件),重跑单个 review 桶
`jclean reprocess <id> --bucket pinyin`
5. **核对总数**review 全清零后auto_done + skip == 原始输入有效条数
6. **你说合并**AI 才合并auto_done→vocabulary、skip→skipped去重
`jclean merge <id>`
> 命令行等价写法:`jclean` = `python -m pl_japanese.cleaner.cli` = `python scripts/clean.py`
> 流程需人工介入,推荐用 `tests/test_cleaner_workflow.py` 测试驱动协作。
### 两层文件结构
- **单批(临时,任务独立)**auto_done / skip / review_{pinyin,split,verb,special}
- **最终权威(累积,只增去重)**xinbiaori.txt / skipped_total.txt
### 铁律
- 数据源只读,绝不修改原文件
- review 是中间态,确认完必须清零
- 改代码而非手改输出文件
- 合并需人工授权,最终库只增不删
- 所有路径都是任务配置项,可覆盖
## 编码注意
- 所有文件用 UTF-8 无 BOM换行 \n
- 写文件时禁止引入 BOMEF BB BF
- 控制台是 GBK脚本输出避免使用非 ASCII 符号(如 ✅⚠️)

99
tests/test_cleaner.py Normal file
View File

@ -0,0 +1,99 @@
"""
Cleaner 模块单元测试
"""
import pytest
from pl_japanese.cleaner import Classifier, Aligner, PinyinMaker
@pytest.fixture
def classifier():
"""创建分类器实例"""
aligner = Aligner()
pinyin_maker = PinyinMaker()
return Classifier(aligner, pinyin_maker)
class TestRules:
"""规则测试"""
def test_skip_no_hanzi(self, classifier):
"""规则1无汉字词跳过"""
bucket, out, note = classifier.classify("IT", "アイティー")
assert bucket == 'skip'
assert "跳过" in note
bucket, out, note = classifier.classify("Japan Railways", "ジャパンレールウェイズ")
assert bucket == 'skip'
bucket, out, note = classifier.classify("", "パーセント")
assert bucket == 'skip'
def test_mixed_letter_kanji(self, classifier):
"""规则2含字母+汉字的混合词正常处理"""
bucket, out, note = classifier.classify("JC自動車", "ジェーシーじどうしゃ")
assert bucket in ('auto', 'pinyin', 'special')
assert "|自|動|車" in out or "JC|自|動|車" in out
def test_tilde_handling(self, classifier):
"""规则3含~自动处理"""
# 成功分割
bucket, out, note = classifier.classify("経営~", "けいえいします")
assert bucket in ('auto', 'pinyin')
assert "経|営" in out
assert "けい|えい" in out
# ~作为通配符,吸收非汉字部分(おせち),汉字部分正常对齐
bucket, out, note = classifier.classify("~料理", "おせちりょうり")
assert bucket in ('auto', 'pinyin')
assert "料|理" in out
assert "りょう|り" in out
def test_repeat_mark_expansion(self, classifier):
"""规则6々展开"""
bucket, out, note = classifier.classify("我々", "われわれ")
assert "我|我" in out or bucket != 'skip'
bucket, out, note = classifier.classify("佐々木", "ささき")
assert "佐|佐|木" in out or bucket != 'skip'
class TestAlignment:
"""对齐测试"""
def test_simple_alignment(self, classifier):
"""简单对齐"""
bucket, out, note = classifier.classify("中国", "ちゅうごく")
assert bucket in ('auto', 'pinyin')
assert "中|国" in out
assert "ちゅう|ごく" in out
def test_mixed_kana(self, classifier):
"""含假名送り仮名"""
bucket, out, note = classifier.classify("お父さん", "おとうさん")
# 熟字训,可能分割失败
assert bucket in ('auto', 'pinyin', 'split')
class TestPinyin:
"""拼音测试"""
def test_polyphone_detection(self, classifier):
"""多音字检测"""
bucket, out, note = classifier.classify("行動", "こうどう")
# '行' 是多音字
if bucket == 'pinyin':
assert "多音字" in note or "" in note
class TestTokenSplit:
"""Token 切分测试"""
def test_token_split(self, classifier):
"""测试 token 切分"""
tokens = classifier.split_kanji_tokens("お父さん")
# 预期: ['お', '父', 'さん']
assert len(tokens) == 3
tokens = classifier.split_kanji_tokens("JC自動車")
# 预期: ['', '自', '動', '車'] 或类似
assert '' in tokens
assert '' in tokens
if __name__ == '__main__':
pytest.main([__file__, '-v'])

View File

@ -0,0 +1,230 @@
"""
清洗工作流测试任务驱动 + 人工介入协作
本文件把"处理 → review → 重跑 → 合并"协作流程写成可运行的测试
每个测试用隔离的临时任务目录tmp_path不碰真实数据legacy_batch1 / data/db
设计要点体现工作流本质
- 工作流需要人工介入review 桶里的条目由人核对测试用"模拟人工确认"占位
- merge 有授权门禁review 未清零时 merge_all 必须拒绝
- 条数铁律输入有效行数 == 各桶输出之和
- 状态机流转created processing reviewing ready merged
日常协作真实数据请直接用 driver 函数在这里加临时测试驱动
或用 `python -m pl_japanese.cleaner.cli` / `jclean` 命令行
"""
from pathlib import Path
import pytest
from pl_japanese.cleaner import BatchProcessor, TaskManager, Task
from pl_japanese.cleaner.task import (
STATUS_CREATED, STATUS_REVIEWING, STATUS_READY, STATUS_MERGED,
)
# --------------------------------------------------------------------------
# 夹具:隔离的任务环境
# --------------------------------------------------------------------------
@pytest.fixture
def isolated_env(tmp_path):
"""
构建一个完全隔离的任务环境
- tasks_root tmp_path
- 权威库main/skipped也在 tmp_path 不碰 data/db
- 数据源是临时写入的小样本
返回 (tasks_root, main, skipped, make_source)
"""
tasks_root = tmp_path / "tasks"
main = tmp_path / "db" / "vocabulary.txt"
skipped = tmp_path / "db" / "skipped.txt"
main.parent.mkdir(parents=True, exist_ok=True)
main.write_text("", encoding="utf-8", newline="\n")
skipped.write_text("", encoding="utf-8", newline="\n")
def make_source(lines, name="source.txt"):
p = tmp_path / name
p.write_text("\n".join(lines) + "\n", encoding="utf-8", newline="\n")
return p
return tasks_root, main, skipped, make_source
def _new_task(isolated_env, source_lines, count=100):
tasks_root, main, skipped, make_source = isolated_env
src = make_source(source_lines)
tm = TaskManager(tasks_root=str(tasks_root))
task = tm.create_task(
task_id="t1",
source=str(src),
name="测试任务",
start_line=1,
count=count,
main=str(main),
skipped=str(skipped),
)
return tm, task
# --------------------------------------------------------------------------
# 任务生命周期
# --------------------------------------------------------------------------
def test_task_create_and_load(isolated_env):
"""创建任务后可从磁盘重新加载,配置一致"""
tm, task = _new_task(isolated_env, ["日本語:にほんご:"])
assert task.state.status == STATUS_CREATED
reloaded = tm.load_task("t1")
assert reloaded.task_id == "t1"
assert reloaded.config.source == task.config.source
assert reloaded.config.main == task.config.main
assert reloaded.state.status == STATUS_CREATED
def test_multi_task_isolation(isolated_env):
"""多任务并存:各自独立目录,互不干扰"""
tasks_root, main, skipped, make_source = isolated_env
tm = TaskManager(tasks_root=str(tasks_root))
s1 = make_source(["学生:がくせい:"], "s1.txt")
s2 = make_source(["先生:せんせい:"], "s2.txt")
t1 = tm.create_task("task_a", str(s1), main=str(main), skipped=str(skipped))
t2 = tm.create_task("task_b", str(s2), main=str(main), skipped=str(skipped))
assert Path(t1.task_dir) != Path(t2.task_dir)
ids = {s["task_id"] for s in tm.list_task_summaries()}
assert ids == {"task_a", "task_b"}
def test_create_duplicate_rejected(isolated_env):
"""重复 task_id 创建被拒绝"""
tm, task = _new_task(isolated_env, ["日本:にほん:"])
with pytest.raises(FileExistsError):
tm.create_task("t1", source=task.config.source)
# --------------------------------------------------------------------------
# 处理与状态流转
# --------------------------------------------------------------------------
def test_process_count_invariant(isolated_env):
"""条数铁律:有效输入行数 == 各桶输出之和"""
lines = [
"中国人:ちゅうごくじん:",
"日本人:にほんじん:",
"学生:がくせい:",
"", # 空行不计
"先生:せんせい:",
]
tm, task = _new_task(isolated_env, lines)
bp = BatchProcessor(task)
result = bp.process_batch()
assert result["valid_lines"] == 4
assert result["output_total"] == 4
assert result["validation"] is True
def test_process_all_auto_goes_ready(isolated_env):
"""全部自动处理(无 review→ 任务状态直接 ready"""
lines = ["日本人:にほんじん:", "中国人:ちゅうごくじん:"]
tm, task = _new_task(isolated_env, lines)
bp = BatchProcessor(task)
result = bp.process_batch()
assert result["buckets"]["auto"] == 2
assert result["status"] == STATUS_READY
# 从磁盘复核状态已持久化
assert tm.load_task("t1").state.status == STATUS_READY
def test_process_with_review_goes_reviewing(isolated_env):
"""产生 review 桶 → 任务状态 reviewing"""
# 多音字/字母混合等会进 review用一个含字母的确保进 special
lines = ["日本人:にほんじん:", "IT:アイティー:"]
tm, task = _new_task(isolated_env, lines)
bp = BatchProcessor(task)
result = bp.process_batch()
review_total = sum(
result["buckets"][b] for b in ("pinyin", "split", "verb", "special")
)
# skip 也可能吃掉纯字母词;只要不是全部 auto 即可能有 review
if review_total > 0:
assert result["status"] == STATUS_REVIEWING
else:
assert result["status"] == STATUS_READY
# --------------------------------------------------------------------------
# 合并授权门禁
# --------------------------------------------------------------------------
def test_merge_rejected_when_review_pending(isolated_env):
"""review 未清零时 merge_all 必须拒绝(授权门禁的前置约束)"""
tm, task = _new_task(isolated_env, ["日本人:にほんじん:"])
bp = BatchProcessor(task)
bp.process_batch()
# 人为往 review 桶塞一条,模拟待确认
bp.review_files["pinyin"].write_text("行|列:こう|れつ:hang|lie\n",
encoding="utf-8", newline="\n")
result = bp.merge_all(dry_run=True)
assert "error" in result
assert result["total_review"] > 0
def test_merge_final_dedup_and_status(isolated_env):
"""review 清零后合并到最终库:去重 + 状态变 merged + 单批清空"""
lines = ["日本人:にほんじん:", "中国人:ちゅうごくじん:"]
tm, task = _new_task(isolated_env, lines)
bp = BatchProcessor(task)
bp.process_batch()
assert bp.get_status()["ready_to_merge"] is True
# 首次合并
r1 = bp.merge_all(dry_run=False)
assert r1["merged_auto"] == 2
assert r1["single_batch_cleared"] is True
assert tm.load_task("t1").state.status == STATUS_MERGED
assert r1["main_total"] == 2
def test_merge_idempotent(isolated_env):
"""幂等:同一批数据再次进入主库不会重复"""
lines = ["日本人:にほんじん:"]
tm, task = _new_task(isolated_env, lines)
bp = BatchProcessor(task)
bp.process_batch()
bp.merge_all(dry_run=False)
# 再处理同样的数据 + 合并,主库不应增长
bp.process_batch()
r2 = bp.merge_all(dry_run=False)
assert r2["merged_auto"] == 0
assert r2["duplicated_auto"] == 1
assert r2["main_total"] == 1
# --------------------------------------------------------------------------
# review 重跑(改代码后重新分流)
# --------------------------------------------------------------------------
def test_reprocess_review_reclassifies(isolated_env):
"""重跑 review 桶:条目重新分类,原桶清零"""
tm, task = _new_task(isolated_env, ["日本人:にほんじん:"])
bp = BatchProcessor(task)
bp.process_batch()
# 手动放一条可自动处理的词进 pinyin 桶,模拟"修正后应归入 auto"
bp.review_files["pinyin"].write_text("中国人:ちゅうごくじん:\n",
encoding="utf-8", newline="\n")
result = bp.reprocess_review("pinyin")
assert result["reprocessed"] == 1
# 原 pinyin 桶已清零
assert len(bp._read_clean_lines(bp.review_files["pinyin"])) == 0
def test_reprocess_unknown_bucket_raises(isolated_env):
"""重跑未知桶名报错"""
tm, task = _new_task(isolated_env, ["日本:にほん:"])
bp = BatchProcessor(task)
with pytest.raises(ValueError):
bp.reprocess_review("nonexistent")

100
tests/test_tango.py Normal file
View File

@ -0,0 +1,100 @@
from pathlib import Path
from jamdict import Jamdict
from loguru import logger
import pytest
from pl_japanese.tango import Kanji, Tango, TangoKanji
from pl_japanese import JAM_DICT_DB_PATH
@pytest.mark.parametrize("input_str, expected", [
("中->zhong->ちゅう", {"kanji": "","pinyi":"zhong", "kana": "ちゅう"}),
("日->ri->にち", {"kanji": "","pinyi":"ri", "kana": "にち"}),
])
def test_kanji_parse_string(input_str, expected):
result = Kanji.parse_string(input_str)
assert result.kanji == expected["kanji"]
assert result.kana == expected["kana"]
assert result.pinyin == expected["pinyi"]
@pytest.mark.parametrize("input_str, expected", [
("日本語->にほんご", {"tango": "日本語","kana": "にほんご"}),
])
def test_tango_parse_string(input_str, expected):
result = Tango.parse_string(input_str)
assert result.tango == expected["tango"]
assert result.kana == expected["kana"]
@pytest.mark.parametrize("s,expected", [
('日|本|語:に|ほん|ご:ri|ben|yu',
{"tango":{"tango":"日本語","kana":"にほんご"},
"kanji_list":[{"kanji":"","pinyi":"ri","kana":""},
{"kanji":"","pinyi":"ben","kana":"ほん"},\
{"kanji":"","pinyi":"yu","kana":""}]}),
])
def test_tango_kanjis(s: str,expected):
result = TangoKanji()
added = result.add_tango_kanjis(s)
assert added is True
tango_kanjis = result.get_tango_kanjis()
# 应恰好添加一个单词
assert len(tango_kanjis) == 1
(tango, kanji_list), = tango_kanjis.items()
assert tango.tango == expected["tango"]["tango"]
assert tango.kana == expected["tango"]["kana"]
assert len(kanji_list) == len(expected["kanji_list"])
for k, expected_k in zip(kanji_list, expected["kanji_list"]):
assert k.kanji == expected_k["kanji"]
assert k.kana == expected_k["kana"]
assert k.pinyin == expected_k.get("pinyi", "")
logger.debug(f"Kanji-Kana-Tangos:\n{result.get_kanji_kana_tangos_str()}")
logger.debug(f"Pinyin-Kana-Tangos:\n{result.get_pinyin_kana_tangos_str()}")
logger.debug(f"Kanji-Pinyin-Kana-Tangos:\n{result.get_kanji_pinyin_kana_tangos_str()}")
logger.debug(f"Pinyin-Kanji-Kana-Tangos:\n{result.get_pinyin_kanji_kana_tangos_str()}")
@pytest.mark.parametrize("path", [
# ('nihon_kanjis.txt'),
('db/vocabulary.txt'),
])
def test_tango_kanjis_file(get_data_file_path,path: str):
absolute_path = get_data_file_path(path)
logger.debug(f"测试文件绝对路径: {absolute_path}")
tango_kanji = TangoKanji()
success_count = 0
failure_count = 0
for r in tango_kanji.add_from_file_stream(file_path=absolute_path, strict=False):
if r:
success_count += 1
else:
failure_count += 1
tango_kanji.sort_all()
logger.debug(f"Pinyin-Kana-Kanji-Tangos:\n{tango_kanji.get_pinyin_kana_kanji_tangos_str()}")
logger.debug(f"成功: {success_count} 条 | 失败: {failure_count}")
# 至少应成功导入若干条单词
assert success_count > 0
tango_kanjis = tango_kanji.get_tango_kanjis()
assert len(tango_kanjis) == success_count
def test_jmd():
if not Path(JAM_DICT_DB_PATH).is_file():
pytest.skip(f"词典数据库不存在,跳过: {JAM_DICT_DB_PATH}")
jmd = Jamdict(db_file=JAM_DICT_DB_PATH)
result = jmd.lookup('はな')
logger.debug(f"result {result}")
assert result.entries, "查询 'はな' 应至少返回一个词条"
for word in result.entries:
logger.debug(f"word {word}")

76
tests/test_validator.py Normal file
View File

@ -0,0 +1,76 @@
"""
格式校验器单元测试
确保脏数据被拦截干净数据通过
"""
import pytest
from pl_japanese.cleaner.validator import clean_line, validate_line
class TestCleanLine:
"""行清理:去行号前缀、注释后缀"""
def test_strip_line_number(self):
assert clean_line("L123 中|国:ちゅう|ごく:zhong|guo") == "中|国:ちゅう|ごく:zhong|guo"
def test_strip_comment(self):
assert clean_line("中|国:ちゅう|ごく:zhong|guo # 备注") == "中|国:ちゅう|ごく:zhong|guo"
def test_strip_both(self):
assert clean_line("L5 中|国:ちゅう|ごく:zhong|guo # x") == "中|国:ちゅう|ごく:zhong|guo"
def test_plain_line(self):
assert clean_line("中|国:ちゅう|ごく:zhong|guo") == "中|国:ちゅう|ごく:zhong|guo"
class TestValidateValid:
"""合格数据应通过"""
@pytest.mark.parametrize("s", [
"中|国:ちゅう|ごく:zhong|guo",
"食|べる:た|べる:shi|", # 假名段拼音留空
"U|ターン:ユー|ターン:u|", # 字母+片假名
"日|本|語:に|ほん|ご:ri|ben|yu",
])
def test_valid_lines(self, s):
ok, reason = validate_line(s)
assert ok, f"应通过但被拒绝: {reason}"
class TestValidateReject:
"""脏数据应被拒绝"""
def test_reject_question_mark(self):
ok, reason = validate_line("青|島:チンタオビール:?")
assert not ok
assert "?" in reason
def test_reject_comment_marker(self):
ok, reason = validate_line("[原] これから:これから:")
assert not ok
def test_reject_wrong_colon_count(self):
ok, reason = validate_line("CS公司:シーエス|コン|ス:|gong|si")
assert not ok
def test_reject_length_mismatch(self):
# 3 个冒号段但分段数不一致
ok, reason = validate_line("大|量:たい:da|liang")
assert not ok
assert "长度" in reason
def test_reject_empty(self):
ok, reason = validate_line("")
assert not ok
def test_reject_empty_kanji(self):
ok, reason = validate_line(":ちゅう:zhong")
assert not ok
def test_reject_empty_kana(self):
ok, reason = validate_line("中::zhong")
assert not ok
if __name__ == '__main__':
pytest.main([__file__, '-v'])

234
validate_data.py Normal file
View File

@ -0,0 +1,234 @@
"""
校验手工维护的日语词表数据
- 拼音校验 pypinyin 检查每个汉字拼音是否在其合法读音集合内考虑多音字
- 日语读音校验
1) 整词级 jamdict 真词典查整词看假名是否为该词已知读音
2) 单字级 kanjidic 的音读/训读检查每个汉字的假名是否合理
- 结构校验三段是否齐全汉字数/假名数/拼音数是否对齐
用法: python validate_data.py <file1> [file2 ...]
结果写入 validation_report.txtUTF-8避免控制台乱码
"""
import sys
import re
from pathlib import Path
from collections import defaultdict
from pypinyin import pinyin, Style
from jamdict import Jamdict
import os
# 优先用本地副本(网络路径上的 sqlite 查询极慢)
_LOCAL_DB = r"C:\Users\panli\jamdict_local.db"
_NET_DB = r"\\192.168.3.200\work\workspace\python\japanese\src\pl_japanese\jamdict.db\jamdict.db"
DB = _LOCAL_DB if os.path.isfile(_LOCAL_DB) else _NET_DB
jam = Jamdict(db_file=DB)
HANZI = re.compile(r'[\u4e00-\u9fff]')
# 片假名 -> 平假名
def kata_to_hira(s: str) -> str:
out = []
for ch in s:
code = ord(ch)
if 0x30A1 <= code <= 0x30F6:
out.append(chr(code - 0x60))
else:
out.append(ch)
return ''.join(out)
def clean_reading(s: str) -> str:
# 去掉 kanjidic 训读里的送り仮名标记:- . 及其后缀
s = s.replace('-', '')
if '.' in s:
s = s.split('.')[0]
return kata_to_hira(s).strip()
# ---- 缓存 ----
_pinyin_cache = {}
_kanji_reading_cache = {}
_word_cache = {}
def valid_pinyins(char: str):
if char in _pinyin_cache:
return _pinyin_cache[char]
res = pinyin(char, style=Style.NORMAL, heteronym=True)
s = set(res[0]) if res else set()
_pinyin_cache[char] = s
return s
def kanji_readings(char: str):
"""返回该汉字所有合法假名读音(平假名,含音读+训读,含常见连浊近似)"""
if char in _kanji_reading_cache:
return _kanji_reading_cache[char]
readings = set()
try:
r = jam.lookup(char)
for c in r.chars:
for g in c.rm_groups:
for on in g.on_readings:
readings.add(clean_reading(str(on)))
for kun in g.kun_readings:
readings.add(clean_reading(str(kun)))
except Exception:
pass
readings.discard('')
_kanji_reading_cache[char] = readings
return readings
def word_readings(word: str):
"""整词在词典里的已知假名读音集合"""
if word in _word_cache:
return _word_cache[word]
readings = set()
try:
r = jam.lookup(word)
for e in r.entries:
for k in e.kana_forms:
readings.add(kata_to_hira(k.text))
except Exception:
pass
_word_cache[word] = readings
return readings
# 连浊/促音的宽松匹配:允许清浊互换、结尾促音
DAKUTEN = {
'':'','':'','':'','':'','':'',
'':'','':'','':'','':'','':'',
'':'','':'','':'','':'','':'',
'':'','':'','':'','':'','':'',
}
HANDAKU = {'':'','':'','':'','':'','':''}
def reading_variants(r: str):
"""给一个字典读音生成宽松变体:首音连浊/半浊、结尾长音、结尾促音化"""
vs = {r}
if r:
first = r[0]
if first in DAKUTEN:
vs.add(DAKUTEN[first] + r[1:])
if first in HANDAKU:
vs.add(HANDAKU[first] + r[1:])
# 结尾促音(っ):如 いち->いっ
if len(r) >= 2:
vs.add(r[:-1] + '')
# 结尾长音:こう 之类,允许省略う
if r.endswith(''):
vs.add(r[:-1])
return vs
def kana_matches_kanji(kana_seg: str, char: str) -> bool:
"""该假名片段是否匹配该汉字的某个合法读音(含宽松变体)"""
valid = kanji_readings(char)
if not valid:
return None # 词典无此字信息,无法判断
candidates = set()
for r in valid:
candidates |= reading_variants(r)
return kana_seg in candidates
def analyze_file(path: str, issues: dict):
p = Path(path)
name = p.name
with open(p, 'r', encoding='utf-8') as f:
for lineno, raw in enumerate(f, 1):
line = raw.rstrip('\n').strip()
if not line or line.startswith('#'):
continue
# 归一全角冒号
norm = line.replace('', ':')
parts = norm.split(':')
if len(parts) < 2:
continue
kanji_part = parts[0]
kana_part = parts[1]
pinyin_part = parts[2] if len(parts) >= 3 else ''
kanji_tokens = kanji_part.split('|')
kana_tokens = kana_part.split('|')
pinyin_tokens = pinyin_part.split('|') if pinyin_part else []
word = ''.join(kanji_tokens)
word_kana = ''.join(kana_tokens)
# ---- 结构校验 ----
if pinyin_tokens and len({len(kanji_tokens), len(kana_tokens), len(pinyin_tokens)}) != 1:
issues['struct'].append(
f"{name}:{lineno} 分段数不齐 汉字{len(kanji_tokens)}/假名{len(kana_tokens)}/拼音{len(pinyin_tokens)} | {line}")
# ---- 拼音校验(逐字,仅汉字)----
if pinyin_tokens and len(kanji_tokens) == len(pinyin_tokens):
for ch, py in zip(kanji_tokens, pinyin_tokens):
if not HANZI.search(ch):
if py.strip():
issues['pinyin'].append(
f"{name}:{lineno} 非汉字'{ch}'却标了拼音'{py}' | {line}")
continue
if not py.strip():
issues['pinyin'].append(
f"{name}:{lineno} 汉字'{ch}'缺拼音 | {line}")
continue
valid = valid_pinyins(ch)
if valid and py.strip().lower() not in {v.lower() for v in valid}:
issues['pinyin'].append(
f"{name}:{lineno} '{ch}' 拼音 '{py}' 疑误,正确应为 {sorted(valid)} | {line}")
# ---- 日语整词校验 ----
wr = word_readings(word)
if wr and word_kana not in wr:
issues['word'].append(
f"{name}:{lineno}'{word}'读音'{word_kana}'不在词典 {sorted(wr)} | {line}")
# ---- 日语单字校验(逐字)----
if len(kanji_tokens) == len(kana_tokens):
for ch, ka in zip(kanji_tokens, kana_tokens):
if not HANZI.search(ch):
continue # 假名对假名,跳过
m = kana_matches_kanji(ka, ch)
if m is False:
valid = sorted(kanji_readings(ch))
issues['kanji'].append(
f"{name}:{lineno} '{ch}''{ka}'疑误,音训读为 {valid} | {line}")
def main():
files = sys.argv[1:]
if not files:
print("usage: python validate_data.py <file...>")
return
issues = defaultdict(list)
for f in files:
analyze_file(f, issues)
out = Path(r"\\192.168.3.200\work\workspace\python\japanese\validation_report.txt")
lines = []
lines.append("=" * 70)
lines.append("日语词表数据校验报告")
lines.append("=" * 70)
lines.append("")
lines.append("说明:以下为疑似错误,需人工确认。词典(jamdict/kanjidic)未必收录所有")
lines.append("专有名词、姓氏、古语读法,故'疑误'不等于一定错,请结合语境判断。")
lines.append("")
order = [
('struct', '一、结构问题(汉字/假名/拼音分段数不一致)'),
('pinyin', '二、汉语拼音疑似错误'),
('word', '三、整词日语读音疑似错误(词典有该词但读音不符)'),
('kanji', '四、单字日语读音疑似错误(音读/训读均不匹配)'),
]
for key, title in order:
lst = issues[key]
lines.append("-" * 70)
lines.append(f"{title}】 共 {len(lst)}")
lines.append("-" * 70)
if not lst:
lines.append(" (无)")
else:
lines.extend(" " + x for x in lst)
lines.append("")
out.write_text('\n'.join(lines), encoding='utf-8')
print(f"done. issues: struct={len(issues['struct'])} pinyin={len(issues['pinyin'])} word={len(issues['word'])} kanji={len(issues['kanji'])}")
print(f"report -> {out}")
if __name__ == '__main__':
main()

185
validation_report.txt Normal file
View File

@ -0,0 +1,185 @@
======================================================================
日语词表数据校验报告
======================================================================
说明:以下为疑似错误,需人工确认。词典(jamdict/kanjidic)未必收录所有
专有名词、姓氏、古语读法,故'疑误'不等于一定错,请结合语境判断。
----------------------------------------------------------------------
【一、结构问题(汉字/假名/拼音分段数不一致)】 共 0 条
----------------------------------------------------------------------
(无)
----------------------------------------------------------------------
【二、汉语拼音疑似错误】 共 70 条
----------------------------------------------------------------------
xinbiaori.txt:28 '太' 拼音 'tao' 疑误,正确应为 ['ta', 'tai'] | 太|郎:た|ろう:tao|lang
xinbiaori.txt:52 '土産' 拼音 'tuchan' 疑误,正确应为 ['cha', 'du', 'tu'] | お|土産:お|みやげ:|tuchan
xinbiaori.txt:83 '今日' 拼音 'jinri' 疑误,正确应为 ['jin'] | 今日:きょう:jinri
xinbiaori.txt:89 '居間' 拼音 'jujian' 疑误,正确应为 ['ji', 'ju'] | 居間:いま:jujian
xinbiaori.txt:121 '一人' 拼音 'yiren' 疑误,正确应为 ['yi'] | 一人|暮|らし:ひとり|ぐ|らし:yiren|mu|
xinbiaori.txt:130 '昨日' 拼音 'zuori' 疑误,正确应为 ['zuo'] | 昨日:きのう:zuori
xinbiaori.txt:131 '明日' 拼音 'jinri' 疑误,正确应为 ['meng', 'ming'] | 明日:あした:jinri
xinbiaori.txt:143 '今朝' 拼音 'jinzhao' 疑误,正确应为 ['jin'] | 今朝:けさ:jinzhao
xinbiaori.txt:178 '美' 拼音 'nei' 疑误,正确应为 ['mei'] | 美|術|館:び|じゅつ|かん:nei|shu|guan
xinbiaori.txt:186 '国' 拼音 'huo' 疑误,正确应为 ['guo'] | 韓|国:かん|こく:han|huo
xinbiaori.txt:189 '海' 拼音 'gai' 疑误,正确应为 ['hai'] | 北|海|道:ほっ|かい|どう:bei|gai|dao
xinbiaori.txt:192 '渋' 拼音 'she' 疑误,正确应为 ['se'] | 渋|谷:しぶ|や:she|gu
xinbiaori.txt:206 '込' 拼音 'ru' 疑误,正确应为 ['yu'] | 申|込|書:もうし|こみ|しょ:shen|ru|shu
xinbiaori.txt:208 '楽' 拼音 'yue' 疑误,正确应为 ['le'] | 音|楽:おん|がく:yin|yue
xinbiaori.txt:236 汉字'紙'缺拼音 | 新|聞|紙:しん|ぶん|し:xin|wen|
xinbiaori.txt:247 '出' 拼音 'da' 疑误,正确应为 ['chu'] | 出|ま:だ|ま:da|
xinbiaori.txt:322 '菓子' 拼音 'guozi' 疑误,正确应为 ['guo'] | お|菓子:お|かし:|guozi
xinbiaori.txt:334 '曇' 拼音 'yun' 疑误,正确应为 ['tan'] | 曇|り:くも|り:yun|
xinbiaori.txt:362 '写' 拼音 'xue' 疑误,正确应为 ['xie'] | 写|真|展:しゃ|しん|てん:xue|zhen|zhan
xinbiaori.txt:364 '荘' 拼音 'su' 疑误,正确应为 ['zhuang'] | 別|荘:べっ|そう:bie|su
xinbiaori.txt:378 '下手' 拼音 'xiashou' 疑误,正确应为 ['xia'] | 下手:へた:xiashou
xinbiaori.txt:380 '時々' 拼音 'shishi' 疑误,正确应为 ['shi'] | 時々:ときどき:shishi
xinbiaori.txt:388 '酎' 拼音 'zhuo' 疑误,正确应为 ['zhou'] | 焼|酎:しょう|ちゅう:shao|zhuo
xinbiaori.txt:418 '咲' 拼音 'kai' 疑误,正确应为 ['xiao'] | 咲|きます:さ|きます:kai|
xinbiaori.txt:469 '風邪' 拼音 'fengxie' 疑误,正确应为 ['feng'] | 風邪:かぜ:fengxie
xinbiaori.txt:472 '風呂' 拼音 'fenglv' 疑误,正确应为 ['feng'] | お|風呂:お|ふろ:|fenglv
xinbiaori.txt:481 '撮' 拼音 'she' 疑误,正确应为 ['chua', 'cuo', 'zuan', 'zui', 'zuo'] | 撮|影|禁|止:さつ|えい|きん|し:she|ying|jin|zhi
xinbiaori.txt:488 '伝' 拼音 'chuan' 疑误,正确应为 ['yun'] | 伝|える:つた|える:chuan|
xinbiaori.txt:503 '計' 拼音 'jia' 疑误,正确应为 ['ji'] | 設|計:せっ|けい:she|jia
xinbiaori.txt:571 '香港' 拼音 'xianggang' 疑误,正确应为 ['xiang'] | 香港:ほんこん:xianggang
xinbiaori.txt:577 '嬢' 拼音 'nang' 疑误,正确应为 ['niang'] | お|嬢|さん:お|じょう|さん:|nang|
xinbiaori.txt:583 '価' 拼音 'jia' 疑误,正确应为 ['si'] | 定|価:てい|か:ding|jia
xinbiaori.txt:586 '色' 拼音 'sen' 疑误,正确应为 ['se', 'shai'] | 色:いろ:sen
xinbiaori.txt:612 '伝' 拼音 'chuan' 疑误,正确应为 ['yun'] | 手|伝|う:て|つだ|う:shou|chuan|
xinbiaori.txt:616 '払' 拼音 'shi' 疑误,正确应为 ['fan'] | 払|う:はら|う:shi|
xinbiaori.txt:644 '濯' 拼音 'di' 疑误,正确应为 ['shuo', 'zhao', 'zhuo'] | 洗|濯|機:せん|たく|き:xi|di|ji
xinbiaori.txt:653 '弾' 拼音 'tan' 疑误,正确应为 ['dan'] | 弾|く:ひ|く:tan|
xinbiaori.txt:676 '濯' 拼音 'di' 疑误,正确应为 ['shuo', 'zhao', 'zhuo'] | 洗|濯:せん|たく:xi|di
xinbiaori.txt:727 '絶' 拼音 'hue' 疑误,正确应为 ['jue'] | 絶|対|に:ぜっ|たい|に:hue|dui|
xinbiaori.txt:730 '伝' 拼音 'chuan' 疑误,正确应为 ['yun'] | よろしくお|伝|えください:よろしくお|つた|えください:|chuan|
xinbiaori.txt:742 '道' 拼音 'gong' 疑误,正确应为 ['dao'] | 高|速|道|路:こう|そく|どう|ろ:gao|su|gong|lu
xinbiaori.txt:754 '渋' 拼音 'she' 疑误,正确应为 ['se'] | 渋|滞:じゅう|たい:she|zhi
xinbiaori.txt:791 '束' 拼音 'su' 疑误,正确应为 ['shu'] | 約|束:やく|そく:yue|su
xinbiaori.txt:819 '姉' 拼音 'jie' 疑误,正确应为 ['zi'] | 姉:あね:jie
xinbiaori.txt:837 '雰' 拼音 'wu' 疑误,正确应为 ['fen'] | 雰|囲|気:ふん|い|き:wu|wei|qi
xinbiaori.txt:837 '囲' 拼音 'wei' 疑误,正确应为 ['tong'] | 雰|囲|気:ふん|い|き:wu|wei|qi
xinbiaori.txt:845 '孫' 拼音 'suan' 疑误,正确应为 ['sun', 'xun'] | 孫:まご:suan
xinbiaori.txt:899 '風邪' 拼音 'fengxie' 疑误,正确应为 ['feng'] | 風邪|を|引|きます:かぜ|を|ひ|きます:fengxie||yin|
xinbiaori.txt:902 '梅雨' 拼音 'meiyu' 疑误,正确应为 ['mei'] | 梅雨:つゆ:meiyu
xinbiaori.txt:909 '殻' 拼音 'ke' 疑误,正确应为 ['qiao'] | 吸|殻:すい|がら:xi|ke
xinbiaori.txt:955 '伝' 拼音 'chuan' 疑误,正确应为 ['yun'] | 宣|伝:せん|でん:xuan|chuan
xinbiaori.txt:1030 '戻' 拼音 'li' 疑误,正确应为 ['ti'] | 戻|す:もど|す:li|
xinbiaori.txt:1034 汉字'金'缺拼音 | 貯|金:ちょ|きん:zhu|
xinbiaori.txt:1048 '大人' 拼音 'daren' 疑误,正确应为 ['da'] | 大人:おとな:daren
xinbiaori.txt:1083 '凧' 拼音 'fengzheng' 疑误,正确应为 ['zheng'] | 凧:たこ:fengzheng
xinbiaori.txt:1090 '交' 拼音 'ijao' 疑误,正确应为 ['jiao'] | 交|通|事|故:こう|つう|じ|こ:ijao|tong|shi|gu
xinbiaori.txt:1120 '則' 拼音 'zhe' 疑误,正确应为 ['ze'] | 規|則:き|そく:gui|zhe
xinbiaori.txt:1145 '駅' 拼音 'zhan' 疑误,正确应为 ['yi'] | 北|京|北|駅:ペ|きん|きた|えき:bei|jing|bei|zhan
xinbiaori.txt:1156 '具' 拼音 'u' 疑误,正确应为 ['ju'] | 具|合:ぐ|あい:u|he
xinbiaori.txt:1157 '仮' 拼音 'jia' 疑误,正确应为 ['fan'] | 平|仮|名:ひら|が|な:ping|jia|ming
xinbiaori.txt:1164 '伝' 拼音 'chuan' 疑误,正确应为 ['yun'] | 伝|統|的:でん|とう|てき:chuan|tong|de
xinbiaori.txt:1189 '楽' 拼音 'yue' 疑误,正确应为 ['le'] | 音|楽|会:おん|がく|かい:yin|yue|hui
xinbiaori.txt:1191 '円' 拼音 'en' 疑误,正确应为 ['yuan'] | 円|高:えん|だか:en|gao
xinbiaori.txt:1208 '欠' 拼音 'qina' 疑误,正确应为 ['qian'] | 欠|席:けっ|せき:qina|xi
xinbiaori.txt:1243 '査' 拼音 'cha' 疑误,正确应为 ['zha'] | 調|査:ちょう|さ:diao|cha
xinbiaori.txt:1247 '価' 拼音 'jia' 疑误,正确应为 ['si'] | 低|価|格:てい|か|かく:di|jia|ge
xinbiaori.txt:1254 '査' 拼音 'cha' 疑误,正确应为 ['zha'] | 市|場|調|査:し|じょう|ちょう|さ:shi|chang|diao|cha
xinbiaori.txt:1256 '弁' 拼音 'bing' 疑误,正确应为 ['bian', 'pan'] | 合|弁|会|社:ごう|べん|がい|しゃ:he|bing|hui|she
xinbiaori.txt:1258 '込' 拼音 'ru' 疑误,正确应为 ['yu'] | 巻|き|込|む:ま|き|こ|む:juan||ru|
xinbiaori.txt:1263 '込' 拼音 'ru' 疑误,正确应为 ['yu'] | 見|込|む:み|こ|む:jian|ru|
----------------------------------------------------------------------
【三、整词日语读音疑似错误(词典有该词但读音不符)】 共 13 条
----------------------------------------------------------------------
xinbiaori.txt:19 词'李'读音'り'不在词典 ['すもも'] | 李:り:li
xinbiaori.txt:185 词'北京'读音'ペきん'不在词典 ['ぺいちん', 'ぺきん'] | 北|京:ペ|きん:bei|jing
xinbiaori.txt:399 词'降る'读音'ふりる'不在词典 ['くだる', 'ふる'] | 降|る:ふり|る:jiang|
xinbiaori.txt:411 词'生ビール'读音'なまビール'不在词典 ['なまびーる'] | 生|ビール:なま|ビール:sheng|
xinbiaori.txt:458 词'降る'读音'おる'不在词典 ['くだる', 'ふる'] | 降|る:お|る:jiang|
xinbiaori.txt:525 词'オーストラリア人'读音'オーストラリアじん'不在词典 ['おーすとらりあじん'] | オーストラリア|人:オーストラリア|じん:|ren
xinbiaori.txt:647 词'フランス語'读音'フランスご'不在词典 ['ふらんすご'] | フランス|語:フランス|ご:|yu
xinbiaori.txt:680 词'スキー場'读音'スキーじょう'不在词典 ['すきーじょう'] | スキー|場:スキー|じょう:|chang
xinbiaori.txt:728 词'馬'读音'ば'不在词典 ['いま', 'うま', 'おま'] | 馬:ば:ma
xinbiaori.txt:793 词'楊'读音'よう'不在词典 ['やなぎ', 'ようりゅう'] | 楊:よう:yang
xinbiaori.txt:1055 词'ビタミン剤'读音'ビタミンざい'不在词典 ['びたみんざい'] | ビタミン|剤:ビタミン|ざい:|ji
xinbiaori.txt:1223 词'コピー機'读音'コピーき'不在词典 ['こぴーき'] | コピー|機:コピー|き:|ji
xinbiaori.txt:1255 词'交通事情'读音'こうつじじょう'不在词典 ['こうつうじじょう'] | 交|通|事|情:こう|つ|じ|じょう:jiao|tong|shi|qing
----------------------------------------------------------------------
【四、单字日语读音疑似错误(音读/训读均不匹配)】 共 79 条
----------------------------------------------------------------------
xinbiaori.txt:2 '日'读'に'疑误,音训读为 ['か', 'じつ', 'にち', 'ひ', 'び'] | 日|本|人:に|ほん|じん:ri|ben|ren
xinbiaori.txt:17 '迎'读'む'疑误,音训读为 ['げい', 'むか'] | 出|迎|える:で|む|かえる:chu|ying|
xinbiaori.txt:32 '北'读'ペ'疑误,音训读为 ['きた', 'ほく'] | 北|京|大|学:ペ|きん|だい|がく:bei|jing|da|xue
xinbiaori.txt:34 '北'读'ペ'疑误,音训读为 ['きた', 'ほく'] | 北|京|旅|行|社:ペ|きん|りょ|こう|しゃ:bei|jing|lv|xing|she
xinbiaori.txt:47 '時'读'と'疑误,音训读为 ['あき', 'きざ', 'こく', 'しゅう', 'じ', 'とき', 'どき'] | 時|計:と|けい:shi|ji
xinbiaori.txt:52 '土産'读'みやげ'疑误,音训读为 ['う', 'うぶ', 'さん', 'つち', 'と', 'ど', 'む'] | お|土産:お|みやげ:|tuchan
xinbiaori.txt:59 '母'读'かあ'疑误,音训读为 ['はは', 'ぼ', 'も'] | お|母|さん:お|かあ|さん:|mu|
xinbiaori.txt:60 '日'读'に'疑误,音训读为 ['か', 'じつ', 'にち', 'ひ', 'び'] | 日|本|語:に|ほん|ご:ri|ben|yu
xinbiaori.txt:64 '日'读'に'疑误,音训读为 ['か', 'じつ', 'にち', 'ひ', 'び'] | 日|本:に|ほん:ri|ben
xinbiaori.txt:72 '建'读'たて'疑误,音训读为 ['きょう', 'けん', 'こん', 'すい', 'た', 'だ', 'ふくろう'] | 建|物:たて|もの:jian|wu
xinbiaori.txt:76 '受'读'うけ'疑误,音训读为 ['う', 'じゅ'] | 受|付:うけ|つけ:shou|fu
xinbiaori.txt:76 '付'读'つけ'疑误,音训读为 ['つ', 'つき', 'づ', 'づき', 'づけ', 'ふ'] | 受|付:うけ|つけ:shou|fu
xinbiaori.txt:83 '今日'读'きょう'疑误,音训读为 ['いま', 'か', 'きん', 'こん', 'じつ', 'にち', 'ひ', 'び'] | 今日:きょう:jinri
xinbiaori.txt:86 '部'读'へ'疑误,音训读为 ['ぶ', 'べ'] | 部|屋:へ|や:bu|wu
xinbiaori.txt:89 '居間'读'いま'疑误,音训读为 ['あい', 'あいだ', 'い', 'お', 'かん', 'きょ', 'けん', 'こ', 'ま'] | 居間:いま:jujian
xinbiaori.txt:96 '鏡'读'がね'疑误,音训读为 ['かがみ', 'きょう', 'けい'] | 眼|鏡:め|がね:yan|jing
xinbiaori.txt:121 '一人'读'ひとり'疑误,音训读为 ['いち', 'いつ', 'じん', 'と', 'とく', 'どく', 'にん', 'ひと', 'り'] | 一人|暮|らし:ひとり|ぐ|らし:yiren|mu|
xinbiaori.txt:130 '昨日'读'きのう'疑误,音训读为 ['か', 'さく', 'じつ', 'にち', 'ひ', 'び'] | 昨日:きのう:zuori
xinbiaori.txt:131 '明日'读'あした'疑误,音训读为 ['あ', 'あか', 'あき', 'か', 'じつ', 'にち', 'ひ', 'び', 'みょう', 'みん', 'めい'] | 明日:あした:jinri
xinbiaori.txt:143 '今朝'读'けさ'疑误,音训读为 ['あさ', 'いま', 'きん', 'こん', 'ちょう'] | 今朝:けさ:jinzhao
xinbiaori.txt:166 '北'读'ペ'疑误,音训读为 ['きた', 'ほく'] | 北|京|支|社:ペ|きん|し|しゃ:bei|jing|zhi|she
xinbiaori.txt:167 '戸'读'べ'疑误,音训读为 ['かど', 'こ', 'と', 'もん'] | 神|戸:こう|べ:shen|hu
xinbiaori.txt:185 '北'读'ペ'疑误,音训读为 ['きた', 'ほく'] | 北|京:ペ|きん:bei|jing
xinbiaori.txt:192 '谷'读'や'疑误,音训读为 ['きわ', 'けい', 'こく', 'たに', 'たにがわ'] | 渋|谷:しぶ|や:she|gu
xinbiaori.txt:206 '申'读'もうし'疑误,音训读为 ['さる', 'しん', 'もう'] | 申|込|書:もうし|こみ|しょ:shen|ru|shu
xinbiaori.txt:206 '込'读'こみ'疑误,音训读为 ['こ'] | 申|込|書:もうし|こみ|しょ:shen|ru|shu
xinbiaori.txt:239 '兄'读'にい'疑误,音训读为 ['あ', 'あに', 'うえ', 'うじ', 'うわ', 'かみ', 'きょう', 'くび', 'けい', 'し', 'しゃん', 'しゅ', 'しょう', 'じょう', 'たてまつ', 'のぼ'] | お|兄|さん:お|にい|さん:|xiong|
xinbiaori.txt:264 '浴'读'ゆ'疑误,音训读为 ['あ', 'よく'] | 浴|衣:ゆ|かた:yu|yi
xinbiaori.txt:264 '衣'读'かた'疑误,音训读为 ['い', 'え', 'きぬ', 'ぎ', 'ころも'] | 浴|衣:ゆ|かた:yu|yi
xinbiaori.txt:310 '紅'读'も'疑误,音训读为 ['あか', 'あけ', 'く', 'くれない', 'こう', 'しゃく', 'しゅ', 'せき', 'ひ', 'べに'] | 紅|葉:も|みじ:hong|ye
xinbiaori.txt:310 '葉'读'みじ'疑误,音训读为 ['は', 'よう'] | 紅|葉:も|みじ:hong|ye
xinbiaori.txt:322 '菓子'读'かし'疑误,音训读为 ['か', 'こ', 'し', 'す', 'つ', 'ね'] | お|菓子:お|かし:|guozi
xinbiaori.txt:347 '良'读'ら'疑误,音训读为 ['い', 'よ', 'りょう'] | 奈|良:な|ら:nai|liang
xinbiaori.txt:359 '果'读'くだ'疑误,音训读为 ['か', 'き', 'きょく', 'きわ', 'ぎ', 'けい', 'ことごと', 'ごく', 'さん', 'じん', 'ず', 'ちょく', 'つ', 'づ', 'は', 'はか', 'はかど', 'はた', 'ほ', 'りょう'] | 果|物:くだ|もの:guo|wu
xinbiaori.txt:365 '木'读'せ'疑误,音训读为 ['き', 'こ', 'じゅ', 'ぼく', 'もく'] | 寄|木|細|工:よ|せ|ぎざ|いく:ji|mu|xi|gong
xinbiaori.txt:365 '細'读'ぎざ'疑误,音训读为 ['いささか', 'お', 'こ', 'こま', 'さ', 'さい', 'しゃ', 'しょう', 'ち', 'ちい', 'ほそ'] | 寄|木|細|工:よ|せ|ぎざ|いく:ji|mu|xi|gong
xinbiaori.txt:365 '工'读'いく'疑误,音训读为 ['く', 'ぐ', 'こう', 'しょう', 'たくみ'] | 寄|木|細|工:よ|せ|ぎざ|いく:ji|mu|xi|gong
xinbiaori.txt:378 '下手'读'へた'疑误,音训读为 ['お', 'か', 'くだ', 'げ', 'さ', 'した', 'しも', 'しゅ', 'ず', 'た', 'て', 'もと'] | 下手:へた:xiashou
xinbiaori.txt:380 '時々'读'ときどき'疑误,音训读为 ['じ', 'とき', 'どき'] | 時々:ときどき:shishi
xinbiaori.txt:386 '日'读'に'疑误,音训读为 ['か', 'じつ', 'にち', 'ひ', 'び'] | 日|本|料|理:に|ほん|りょう|り:ri|ben|liao|li
xinbiaori.txt:389 '日'读'に'疑误,音训读为 ['か', 'じつ', 'にち', 'ひ', 'び'] | 日|本|酒:に|ほん|しゅ:ri|ben|jiu
xinbiaori.txt:399 '降'读'ふり'疑误,音训读为 ['お', 'くだ', 'こう', 'ご', 'ふ'] | 降|る:ふり|る:jiang|
xinbiaori.txt:406 '切'读'きっ'疑误,音训读为 ['かぎ', 'き', 'ぎ', 'げん', 'さい', 'せつ'] | 切|手:きっ|て:qie|shou
xinbiaori.txt:410 '居'读'いざ'疑误,音训读为 ['い', 'お', 'きょ', 'こ'] | 居|酒|屋:いざ|か|や:ju|jiu|wu
xinbiaori.txt:410 '酒'读'か'疑误,音训读为 ['さか', 'さけ', 'しゅ'] | 居|酒|屋:いざ|か|や:ju|jiu|wu
xinbiaori.txt:447 '下ろ'读'おろ'疑误,音训读为 ['お', 'か', 'くだ', 'げ', 'さ', 'した', 'しも', 'もと'] | 下ろ|す:おろ|す:xia|
xinbiaori.txt:472 '風呂'读'ふろ'疑误,音训读为 ['かざ', 'かぜ', 'せぼね', 'ふ', 'ふう', 'りょ', 'ろ'] | お|風呂:お|ふろ:|fenglv
xinbiaori.txt:479 '立'读'たち'疑误,音训读为 ['た', 'たて', 'だ', 'りっとる', 'りつ', 'りゅう'] | 立|入|禁|止:たち|いり|きん|し:li|ru|jin|zhi
xinbiaori.txt:479 '入'读'いり'疑误,音训读为 ['い', 'じゅ', 'にゅう', 'はい'] | 立|入|禁|止:たち|いり|きん|し:li|ru|jin|zhi
xinbiaori.txt:542 '細'读'ほ'疑误,音训读为 ['いささか', 'お', 'こ', 'こま', 'さ', 'さい', 'しゃ', 'しょう', 'ち', 'ちい', 'ほそ'] | 細|い:ほ|そい:xi|
xinbiaori.txt:555 '詣'读'もうで'疑误,音训读为 ['いた', 'けい', 'げい', 'まい', 'もう'] | 初|詣:はつ|もうで:chu|yi
xinbiaori.txt:564 '今'读'こ'疑误,音训读为 ['いま', 'きん', 'こん'] | 今|年:こ|とし:jin|nian
xinbiaori.txt:571 '香港'读'ほんこん'疑误,音训读为 ['か', 'かお', 'きょう', 'こう', 'みなと'] | 香港:ほんこん:xianggang
xinbiaori.txt:575 '息'读'むす'疑误,音训读为 ['いき', 'こ', 'し', 'す', 'そく', 'つ', 'ね'] | 息|子:むす|こ:xi|zi
xinbiaori.txt:582 '引'读'びき'疑误,音训读为 ['いん', 'ひ'] | 割|引:わり|びき:ge|yin
xinbiaori.txt:639 '餃'读'ぎょー'疑误,音训读为 ['きょう', 'ぎょう', 'こう'] | 餃|子:ぎょー|ざ:jiao|zi
xinbiaori.txt:639 '子'读'ざ'疑误,音训读为 ['あるじ', 'おも', 'げい', 'こ', 'し', 'しつ', 'しゅ', 'しゅう', 'じ', 'じつ', 'す', 'っこ', 'つ', 'に', 'ぬし', 'ね', 'まこと', 'み', 'みち', 'みの'] | 餃|子:ぎょー|ざ:jiao|zi
xinbiaori.txt:663 '切'读'きっ'疑误,音训读为 ['かぎ', 'き', 'ぎ', 'げん', 'さい', 'せつ'] | 切|符:きっ|ぷ:qie|fu
xinbiaori.txt:694 '清'读'し'疑误,音训读为 ['あ', 'あか', 'あき', 'きよ', 'しょう', 'しん', 'せい', 'みょう', 'みん', 'めい'] | 清|水:し|みず:qing|shui
xinbiaori.txt:695 '太'读'おお'疑误,音训读为 ['た', 'たい', 'ふと'] | 太|田:おお|た:tai|tian
xinbiaori.txt:733 '立'读'たち'疑误,音训读为 ['た', 'たて', 'だ', 'りっとる', 'りつ', 'りゅう'] | 役|に|立|ちます:やく|に|たち|ます:yi||li|
xinbiaori.txt:897 '谷'读'がや'疑误,音训读为 ['きわ', 'けい', 'こく', 'たに', 'たにがわ'] | 世|田|谷:せ|た|がや:shi|tian|gu
xinbiaori.txt:902 '梅雨'读'つゆ'疑误,音训读为 ['あま', 'あめ', 'う', 'うめ', 'か', 'かび', 'さめ', 'ばい', 'び', 'まい', 'み'] | 梅雨:つゆ:meiyu
xinbiaori.txt:906 '待'读'まち'疑误,音训读为 ['たい', 'ま'] | 待|合|室:まち|あい|しつ:dai|he|shi
xinbiaori.txt:909 '吸'读'すい'疑误,音训读为 ['きゅう', 'す'] | 吸|殻:すい|がら:xi|ke
xinbiaori.txt:925 '北'读'ペ'疑误,音训读为 ['きた', 'ほく'] | 北|京|植|物|園:ペ|きん|しょく|ぶつ|えん:bei|jing|zhi|wu|yuan
xinbiaori.txt:968 '換'读'かえ'疑误,音训读为 ['か', 'かん'] | 乗|り|換|る:の|り|かえ|る:cheng||huan|
xinbiaori.txt:981 '時'读'ど'疑误,音训读为 ['あき', 'きざ', 'こく', 'しゅう', 'じ', 'とき', 'どき'] | 腕|時|計:うで|ど|けい:wan|shi|ji
xinbiaori.txt:1048 '大人'读'おとな'疑误,音训读为 ['おお', 'じん', 'たい', 'だい', 'と', 'にん', 'ひと', 'り'] | 大人:おとな:daren
xinbiaori.txt:1088 '日'读'に'疑误,音训读为 ['か', 'じつ', 'にち', 'ひ', 'び'] | 日|本|文|化:に|ほん|ぶん|か:ri|ben|wen|hua
xinbiaori.txt:1145 '北'读'ペ'疑误,音训读为 ['きた', 'ほく'] | 北|京|北|駅:ペ|きん|きた|えき:bei|jing|bei|zhan
xinbiaori.txt:1148 '胡'读'ふー'疑误,音训读为 ['う', 'こ', 'ご', 'なんぞ'] | 胡|同:ふー|とん:hu|tong
xinbiaori.txt:1148 '同'读'とん'疑误,音训读为 ['おな', 'どう'] | 胡|同:ふー|とん:hu|tong
xinbiaori.txt:1150 '条'读'てぃあお'疑误,音训读为 ['えだ', 'きん', 'くだん', 'けん', 'じょう', 'すじ', 'ちょう', 'でき'] | 油|条:ゆう|てぃあお:you|tiao
xinbiaori.txt:1151 '日'读'に'疑误,音训读为 ['か', 'じつ', 'にち', 'ひ', 'び'] | 日|本|食:に|ほん|しょく:ri|ben|shi
xinbiaori.txt:1173 '火'读'や'疑误,音训读为 ['か', 'ひ', 'び', 'ほ'] | 火|傷:や|けど:huo|shang
xinbiaori.txt:1173 '傷'读'けど'疑误,音训读为 ['あら', 'いた', 'か', 'きず', 'けず', 'し', 'しょう', 'そう', 'つく', 'なんぞ', 'はじ'] | 火|傷:や|けど:huo|shang
xinbiaori.txt:1183 '璃'读'りが'疑误,音训读为 ['り'] | 瑠|璃|瓦:る|りが|わら:liu|li|wa
xinbiaori.txt:1183 '瓦'读'わら'疑误,音训读为 ['かわら', 'が', 'ぐらむ'] | 瑠|璃|瓦:る|りが|わら:liu|li|wa