Initial commit: 日语词表清洗工具(任务化架构)
- 任务化清洗流程:Task/TaskManager + BatchProcessor 三方法
- 数据目录规范化:data/{db,sources,backup}
- CLI 入口移进包,注册 jclean 命令
- 工作流测试驱动(tests/test_cleaner_workflow.py)
- 40 测试通过
This commit is contained in:
commit
1f64c87d0e
15
.gitattributes
vendored
Normal file
15
.gitattributes
vendored
Normal 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
37
.gitignore
vendored
Normal 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
|
||||
192
README_cleaner.md
Normal file
192
README_cleaner.md
Normal 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
115
REVIEW_ANALYSIS.md
Normal file
@ -0,0 +1,115 @@
|
||||
# Review 桶模式分析报告
|
||||
|
||||
总计 443 条待确认。以下按桶分析常见模式和处理建议。
|
||||
|
||||
---
|
||||
|
||||
## 1. review_pinyin(230 条)— 多音字
|
||||
|
||||
### 多音字频率 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_split(201 条)— 分割/对齐失败
|
||||
|
||||
### 三大类别
|
||||
| 类别 | 数量 | 特征 | 处理方式 |
|
||||
|------|------|------|----------|
|
||||
| A. 熟字训读 | 89 | 纯汉字但假名对不齐 | 整词不分割 |
|
||||
| B. 汉字+片假名 | 11 | 汉字配片假名读音 | 整词不分割 |
|
||||
| C. 含~省略标记 | 101 | 带~的省略词 | 去~后处理 |
|
||||
|
||||
### A. 熟字训读(89条)— 汉字与假名无法逐字对应
|
||||
按你的规则:**整词不分割,拼音连写**
|
||||
- `葛飾区:かつしかく:` → `葛飾区:かつしかく:geshiquku`(地名)
|
||||
- `吹雪:ふぶき:` → `吹雪:ふぶき:chuixue`
|
||||
- `博士:はかせ:` → `博士:はかせ:boshi`
|
||||
- `漬物:つけもの:` → `漬物:つけもの:zewu`
|
||||
- `成田:なりた:` → `成田:なりた:chengtian`(地名)
|
||||
|
||||
### B. 汉字+片假名(11条)— 多为中文人名/菜名的日语音译
|
||||
- `宮保鶏丁:ゴンバオジーディン:` → 宫保鸡丁
|
||||
- `魚香肉絲:ユイシャンロウスー:` → 鱼香肉丝
|
||||
- `張芸謀:チャン・イーモウ:` → 张艺谋(导演)
|
||||
- `王府井:ワンフーチン:` → 王府井(地名)
|
||||
这些片假名是**中文发音的日语音译**,整词处理。
|
||||
|
||||
### C. 含~省略标记(101条)— 词表用~表示省略部分
|
||||
- `~賞:ノーベルしょう:` → 诺贝尔奖(~代表具体名称)
|
||||
- `~茶:プーアルちゃ:` → 普洱茶
|
||||
- `飲料~:いんりょうメーカー:` → 饮料厂商
|
||||
这类需要决定:**保留~还是补全**。多数片假名部分是外来词。
|
||||
|
||||
---
|
||||
|
||||
## 3. review_special(11 条)— 含字母/片假名混合
|
||||
|
||||
### 含字母(拼音段已按规则留空)
|
||||
- `CS|貿|易:シーエス|ぼう|えき:|mao|yi`(CS贸易)
|
||||
- `A|型:エー|がた:|xing`(A型)
|
||||
- `SF|小|説:エスエフ|しょう|せつ:|xiao|shuo`(SF小说=科幻)
|
||||
- `NHK|放|送...`(NHK广播)
|
||||
|
||||
这些**格式已正确**(字母拼音留空),可直接合并。
|
||||
|
||||
### 含特殊符号
|
||||
- `阪|神|・|淡|路|大|震|災`(阪神·淡路大地震)含中点·
|
||||
- `三|ツ|村|電|機`(三ツ村電機)含片假名ツ
|
||||
|
||||
---
|
||||
|
||||
## 4. review_verb(1 条)— 完整表达
|
||||
|
||||
- `心を1つにする:こころをひとつにする:` — 含数字1和完整句
|
||||
建议:`心|を|1つ|にする:こころ|を|ひとつ|にする:xin||...` 或整句处理
|
||||
|
||||
---
|
||||
|
||||
## 处理优先级建议
|
||||
|
||||
1. **先合并 special(11条)** — 格式基本正确,字母已留空
|
||||
2. **再处理 pinyin(230条)** — 大部分正确,重点改 `長(なが)`、`重(かさ)`
|
||||
3. **split 分三批**:
|
||||
- A类熟字训读(89):整词+连写拼音
|
||||
- B类中文音译(11):整词处理
|
||||
- C类~省略(101):统一决定~的处理方式
|
||||
4. **verb(1条)** — 单独处理
|
||||
|
||||
---
|
||||
|
||||
## 可考虑的规则改进
|
||||
|
||||
1. **假名读音辅助拼音**:建立 `汉字+假名→拼音` 映射表
|
||||
- `長+なが→chang`、`重+かさ→chong`
|
||||
- 可自动修正部分 pinyin 桶条目
|
||||
2. **熟字训读词典**:常见熟字训读(吹雪、博士、成田等)直接查表
|
||||
3. **~处理规则**:统一决定省略标记的展开或保留策略
|
||||
191
WORKFLOW.md
Normal file
191
WORKFLOW.md
Normal 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
194
analyze_phonetics.py
Normal 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
77
apply_corrections.py
Normal 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()
|
||||
15
data/backup/dirty_backup.txt
Normal file
15
data/backup/dirty_backup.txt
Normal 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| [建议砍词尾] よろしくお|願|い|申|し|上|げます:よろしくおねがいもうしあげ:?
|
||||
2698
data/backup/xinbiaori.backup.txt
Normal file
2698
data/backup/xinbiaori.backup.txt
Normal file
File diff suppressed because it is too large
Load Diff
3971
data/backup/xinbiaori_tobe.backup.txt
Normal file
3971
data/backup/xinbiaori_tobe.backup.txt
Normal file
File diff suppressed because it is too large
Load Diff
42
data/db/skipped.txt
Normal file
42
data/db/skipped.txt
Normal file
@ -0,0 +1,42 @@
|
||||
IT:アイティー:it
|
||||
WTO:ダブリューティーオー:wto
|
||||
Japan Railways:ジャパンレールウェイズ:
|
||||
%:パーセント:
|
||||
CM:シーエム:
|
||||
PR:ピーアール:
|
||||
ことはない:そんな:
|
||||
GDP:ジーディーピー:
|
||||
べん:~弁:
|
||||
DVD:ディーブイディー:
|
||||
NGO:エヌジーオー:
|
||||
MIT-7900:エムアイティーななせんきゅうひゃく:
|
||||
CM~:シーエムソング:
|
||||
SUSHI:すし:
|
||||
IC~:アイシータグ:
|
||||
ASIMO:アシモ:
|
||||
R.U.R.:エル・ウー・エル:
|
||||
BBK~:ビービーケーラジオ:
|
||||
EU:イーユー:
|
||||
『1Q84』:いちきゅうはちよん:
|
||||
FIFA:フィファ:
|
||||
CD~:シーディーアルバム:
|
||||
RIVERS:リバーズ:
|
||||
「YASAKA」:やさか:
|
||||
BGM:ビージーエム:
|
||||
NBA:エヌビーエー:
|
||||
~A:ビタミンエー:
|
||||
J~:ジェーリーガー:
|
||||
J~:ジェーリーグ:
|
||||
SP:エスピー:
|
||||
GPS:ジーピーエス:
|
||||
USB~:ユーエスビーメモリ:
|
||||
CO2:シーオーツー:
|
||||
CSR:シーエスアール:
|
||||
4R:フォーアール:
|
||||
ATM:エーティーエム:
|
||||
PTA:ピーティーエー:
|
||||
RNA:アールエヌエー:
|
||||
LED:エルイーディー:
|
||||
アクトロイド- F:アクトロイドエフ:
|
||||
1つ1つ:ひとつひとつ:
|
||||
VCD:ブイシーディー:
|
||||
6161
data/db/vocabulary.txt
Normal file
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
7088
data/sources/xinbiaori_1.txt
Normal file
File diff suppressed because it is too large
Load Diff
131
phonetics_report.txt
Normal file
131
phonetics_report.txt
Normal 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
443
pipeline.py
Normal 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-zA-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 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz':
|
||||
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
32
pyproject.toml
Normal 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
0
requirements.txt
Normal file
16
scripts/clean.py
Normal file
16
scripts/clean.py
Normal 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()
|
||||
6
src/pl_japanese/__init__.py
Normal file
6
src/pl_japanese/__init__.py
Normal 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")
|
||||
33
src/pl_japanese/cleaner/__init__.py
Normal file
33
src/pl_japanese/cleaner/__init__.py
Normal 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',
|
||||
]
|
||||
5
src/pl_japanese/cleaner/__main__.py
Normal file
5
src/pl_japanese/cleaner/__main__.py
Normal file
@ -0,0 +1,5 @@
|
||||
"""模块入口:python -m pl_japanese.cleaner ..."""
|
||||
from .cli import main
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
196
src/pl_japanese/cleaner/aligner.py
Normal file
196
src/pl_japanese/cleaner/aligner.py
Normal 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-zA-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)
|
||||
396
src/pl_japanese/cleaner/batch_processor.py
Normal file
396
src/pl_japanese/cleaner/batch_processor.py
Normal 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')
|
||||
244
src/pl_japanese/cleaner/classifier.py
Normal file
244
src/pl_japanese/cleaner/classifier.py
Normal 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, ""
|
||||
208
src/pl_japanese/cleaner/cli.py
Normal file
208
src/pl_japanese/cleaner/cli.py
Normal 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()
|
||||
71
src/pl_japanese/cleaner/pinyin_maker.py
Normal file
71
src/pl_japanese/cleaner/pinyin_maker.py
Normal 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
|
||||
119
src/pl_japanese/cleaner/pinyin_overrides.py
Normal file
119
src/pl_japanese/cleaner/pinyin_overrides.py
Normal 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
|
||||
70
src/pl_japanese/cleaner/rules.py
Normal file
70
src/pl_japanese/cleaner/rules.py
Normal 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-zA-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('~', '')
|
||||
215
src/pl_japanese/cleaner/task.py
Normal file
215
src/pl_japanese/cleaner/task.py
Normal 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}
|
||||
86
src/pl_japanese/cleaner/task_manager.py
Normal file
86
src/pl_japanese/cleaner/task_manager.py
Normal 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
|
||||
59
src/pl_japanese/cleaner/validator.py
Normal file
59
src/pl_japanese/cleaner/validator.py
Normal 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
|
||||
150
src/pl_japanese/dict_utils.py
Normal file
150
src/pl_japanese/dict_utils.py
Normal 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)
|
||||
18
src/pl_japanese/tango/__init__.py
Normal file
18
src/pl_japanese/tango/__init__.py
Normal 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',
|
||||
]
|
||||
143
src/pl_japanese/tango/index.py
Normal file
143
src/pl_japanese/tango/index.py
Normal 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)
|
||||
57
src/pl_japanese/tango/models.py
Normal file
57
src/pl_japanese/tango/models.py
Normal 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))
|
||||
70
src/pl_japanese/tango/parser.py
Normal file
70
src/pl_japanese/tango/parser.py
Normal 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
|
||||
102
tasks/legacy_batch1/auto_done.txt
Normal file
102
tasks/legacy_batch1/auto_done.txt
Normal 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
|
||||
236
tasks/legacy_batch1/review_pinyin.txt
Normal file
236
tasks/legacy_batch1/review_pinyin.txt
Normal 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| # 多解,已取第一个
|
||||
11
tasks/legacy_batch1/review_special.txt
Normal file
11
tasks/legacy_batch1/review_special.txt
Normal file
@ -0,0 +1,11 @@
|
||||
L379 CS|貿|易:シーエス|ぼう|えき:|mao|yi # 含字母,请确认
|
||||
L418 A|型:エー|がた:|xing # 含字母,请确认
|
||||
L183 ランク|付|け:ランク|づ|け:|fu| # 含片假名,请确认
|
||||
L367 SF|小|説:エスエフ|しょう|せつ:|xiao|shuo # 含字母,请确认
|
||||
L476 IRS|蒼|竜:アイアールエス|そう|りゅう:|cang|long # 含字母,请确认
|
||||
L124 阪|神|・|淡|路|大|震|災:はん|しん|・|あわ|じ|だい|しん|さい:ban|shen||dan|lu|da|zhen|zai # 含片假名,请确认
|
||||
L251 SC|商|事:エスシー|しょう|じ:|shang|shi # 含字母,请确认
|
||||
L26 三|ツ|村|電|機:み|つ|むら|でん|き:san||cun|dian|ji # 含片假名,请确认
|
||||
L227 ゴールを|決|める:ゴールを|き|める:|jue| # 含片假名,请确认
|
||||
L491 NHK|放|送|文|化|研|究|所:エヌエイチケー|ほう|そう|ぶん|か|けん|きゅう|じょ:|fang|song|wen|hua|yan|jiu|suo # 含字母,请确认
|
||||
Aソ|連|型:エーソ|れん|がた:|lian|xing
|
||||
93
tasks/legacy_batch1/review_split.txt
Normal file
93
tasks/legacy_batch1/review_split.txt
Normal file
@ -0,0 +1,93 @@
|
||||
葛飾区:かつしかく:
|
||||
吹雪:ふぶき:
|
||||
中国体育委员会:ちゅうごくたいいくいいんかい”:
|
||||
博士:はかせ:
|
||||
漬物:つけもの:
|
||||
成田:なりた:
|
||||
損失:そうしつ:
|
||||
棒球:ピッチャー:
|
||||
立場:たちば:
|
||||
両替:りょうがえ:
|
||||
缶詰:かんづめ:
|
||||
霞ヶ浦:かすみがうら:
|
||||
茨城県:いばらきけん:
|
||||
お父様:おとうさま:
|
||||
日比谷公園:ひびやこうえん:
|
||||
宮保鶏丁:ゴンバオジーディン:
|
||||
乗組員:のりくみいん:
|
||||
魚香肉絲:ユイシャンロウスー:
|
||||
物語:ものがたり:
|
||||
芥川龍之介:あくたがわりゅうのすけ:
|
||||
蜘蛛の糸:くものいと:
|
||||
佐渡島:さどがしま:
|
||||
友友:ヨウヨウ:
|
||||
洋洋:ヤンヤン:
|
||||
社員割引制度:しゃいんわりびきせいど:
|
||||
割引:わりびき:
|
||||
お兄ちゃん:おにいちゃん:
|
||||
灰汁:あく:
|
||||
織田秀忠:おだひでただ:
|
||||
徳川信長:とくがわのぶなが:
|
||||
王府井:ワンフーチン:
|
||||
物置:ものおき:
|
||||
植木:うえき:
|
||||
望:のぞみ:
|
||||
お義母さん:おかあさん:
|
||||
奈良時代:ならじだい:
|
||||
大海原:おおうなばら:
|
||||
世論:せろん/よろん:
|
||||
張芸謀:チャン・イーモウ:
|
||||
『赤い~』:あかいコーリャン:
|
||||
陜西省:せんせいしょう:
|
||||
敷地:しきち:
|
||||
朱芳麗:チュウ・ファンリー:
|
||||
後:のち/あと:
|
||||
結子:ゆいこ:
|
||||
寛:ひろし:
|
||||
紅葉狩り:もみじがり:
|
||||
神戸~:こうべアート・エンタープライズ:
|
||||
親父:おやじ:
|
||||
黒澤明:くろさわあきら:
|
||||
迷子:まいご:
|
||||
小栗圭祐:おぐりけいすけ:
|
||||
為替~:かわせレート:
|
||||
章:あきら:
|
||||
茨城:いばらき:
|
||||
行方:ゆくえ:
|
||||
行き来:いきき/ゆきき:
|
||||
川越:かわごえ:
|
||||
名残:なごり:
|
||||
所以:ゆえん:
|
||||
山羊肉:やぎにく:
|
||||
三越:みつこし:
|
||||
融通~:ゆうずうする:
|
||||
対馬海峡:つしまかいきょう:
|
||||
大久保勇:おおくぼいさむ:
|
||||
上海博物館:シャンハイはくぶつかん:
|
||||
お義父さん:おとうさん:
|
||||
安曇野:あづみの:
|
||||
清々しい:すがすがしい:
|
||||
良男:よしお:
|
||||
浪速大学:なにわだいがく:
|
||||
勇人:はやと:
|
||||
武蔵:むさし:
|
||||
A香港型:エーホンコンがた:
|
||||
有:あり:
|
||||
農作物:のうさくぶつ/のうさくもつ:
|
||||
理:おさむ:
|
||||
聖武天皇:しょうむてんのう:
|
||||
天皇:てんのう:
|
||||
螺鈿紫檀五弦琵琶:らでんしたんのごげんびわ:
|
||||
杯:はい/さかずき:
|
||||
西域:さいいき/せいいき:
|
||||
寧波:ニンポー:
|
||||
奈良国立博物館:ならこくりつはくぶつかん:
|
||||
尾道:おのみち:
|
||||
種種/種々:しゅじゅ:
|
||||
端端/端々:はしばし:
|
||||
あり処:ありか:
|
||||
学びて思わざれば,則ち罔し。思いて学ばざれば,則ち殆うし:まなびておもわざれば,すなわちくらし。おもいてまなばざれば,すなわちあやうし:
|
||||
今道友信:いまみちとものぶ:
|
||||
金詰り:かねづまり:
|
||||
入江:いりえ:
|
||||
台詞:せりふ:
|
||||
1
tasks/legacy_batch1/review_verb.txt
Normal file
1
tasks/legacy_batch1/review_verb.txt
Normal file
@ -0,0 +1 @@
|
||||
L211 [原] 心を1つにする:こころをひとつにする: [需人工确认] # 完整表达/敬语
|
||||
0
tasks/legacy_batch1/skip.txt
Normal file
0
tasks/legacy_batch1/skip.txt
Normal file
43
tasks/legacy_batch1/task.json
Normal file
43
tasks/legacy_batch1/task.json
Normal 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
0
tests/__init__.py
Normal file
38
tests/conftest.py
Normal file
38
tests/conftest.py
Normal 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
141
tests/data/nihon_kanjis.txt
Normal 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
104
tests/data/rules.md
Normal 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|自|動|車:ジェーシー|じ|どう|しゃ: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
|
||||
- 写文件时禁止引入 BOM(EF BB BF)
|
||||
- 控制台是 GBK,脚本输出避免使用非 ASCII 符号(如 ✅⚠️)
|
||||
99
tests/test_cleaner.py
Normal file
99
tests/test_cleaner.py
Normal 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 "JC|自|動|車" 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自動車")
|
||||
# 预期: ['JC', '自', '動', '車'] 或类似
|
||||
assert '自' in tokens
|
||||
assert '動' in tokens
|
||||
|
||||
if __name__ == '__main__':
|
||||
pytest.main([__file__, '-v'])
|
||||
230
tests/test_cleaner_workflow.py
Normal file
230
tests/test_cleaner_workflow.py
Normal 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
100
tests/test_tango.py
Normal 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
76
tests/test_validator.py
Normal 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
234
validate_data.py
Normal file
@ -0,0 +1,234 @@
|
||||
"""
|
||||
校验手工维护的日语词表数据。
|
||||
- 拼音校验:用 pypinyin 检查每个汉字拼音是否在其合法读音集合内(考虑多音字)
|
||||
- 日语读音校验:
|
||||
1) 整词级:用 jamdict 真词典查整词,看假名是否为该词已知读音
|
||||
2) 单字级:用 kanjidic 的音读/训读,检查每个汉字的假名是否合理
|
||||
- 结构校验:三段是否齐全、汉字数/假名数/拼音数是否对齐
|
||||
|
||||
用法: python validate_data.py <file1> [file2 ...]
|
||||
结果写入 validation_report.txt(UTF-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
185
validation_report.txt
Normal 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
|
||||
Loading…
x
Reference in New Issue
Block a user