断词机制详解 (hyphenateWord)
概述
hyphenateWord() 是文本预处理的第一环,负责将英文单词拆分为可断裂的片段。其设计采用三层级联匹配策略:异常词典 → 前缀匹配 → 后缀匹配。
完整流程图
flowchart TD
A[hyphenateWord word] --> B{word.length < 5}
B -->|是| C[return word<br/>不拆分短词]
B -->|否| D{lowerCase in<br/>HYPHEN_EXCEPTIONS}
D -->|命中| E[遍历词典片段<br/>parts数组累积]
D -->|未命中| F{lowerCase<br/>startsWith PREFIX}
E --> J{pos < word.length<br/>完整覆盖?}
J -->|否| K[最后片段追加<br/>剩余字符]
J -->|是| L[filter空片段<br/>return parts]
F -->|匹配且残余≥3| G[slice前缀<br/>slice后缀]
F -->|不匹配| H{lowerCase<br/>endsWith SUFFIX}
G --> L
H -->|匹配且残余≥3| I[slice前半<br/>slice后半]
H -->|不匹配| M[return 原词]
I --> L
M --> L
style A fill:#e3f2fd
style E fill:#fff3e0
style G fill:#f3e5f5
style I fill:#e8f5e9
style M fill:#ffecb3
代码逐段解析
入口过滤:短词不处理
为什么要移除标点?
词典键是纯单词形式,标点只是输入文本的修饰,不应影响匹配。
为什么阈值是 5?
过短的单词(如 "the", "is")即使拆分也可能造成更多问题而非优化。
第一层:异常词典匹配
词典数据结构设计:
边界容错逻辑:
- 如果词典片段总长度 < 原词,会把多出部分追加到最后一个片段
- 这确保即使词典定义有微小偏差,也能"勉强"工作
第二层:前缀匹配
为什么 >= 3?
后缀部分至少保留3个字符,避免过短的尾部导致排版问题。
返回结构:
- 返回数组长度 = 2(分为两部分)
- 软连字符可以在两部分的交界处插入
第三层:后缀匹配
前后缀互斥:代码先检查前缀,再检查后缀
这意味着对于 "reading":
- 不在异常词典
- 不以前缀开头
- 但以后缀 "ing" 结尾 → 返回 ["read", "ing"]
词缀数据规模
pie title 词缀库统计
"前缀数量 PREFIXES" : 55
"后缀数量 SUFFIXES" : 37
"异常词典词条" : ~130
前缀列表片段
后缀列表片段
断词后的后续处理
断词结果会传递给 prepareWithSegments 添加软连字符:
软连字符作用:
- 在 Canvas 渲染时,某些实现会在此处断裂
- 或者被
prepareWithSegments识别为可断裂点
扩展讨论:如何增强断词能力
方案一:引入词典库
可接入 hypher 或 hyphenation 等专业断词库:
方案二:添加语言检测
当前实现只针对英文,可扩展:
方案三:学习式断词
基于 syllable detection 算法自动判断音节边界,不依赖词典。
与 OptimalLayout 的衔接
sequenceDiagram
participant H as hyphenateWord()
participant P as prepareWithSegments
participant OL as optimalLayout()
H->>P: 返回拆分片段数组
P->>P: 用连接片段
P->>P: 调用pretext库
P->>OL: {segments, widths}
OL->>OL: 构建breakCandidates
OL->>OL: DP计算最优断点
设计亮点
| 特性 | 实现方式 | 价值 |
|---|---|---|
| 优先级设计 | 词典 > 前缀 > 后缀 | 精确控制重要词汇 |
| 边界容错 | pos < word.length 追加 | 容忍词典不完整 |
| 长度保护 | < 5 不拆分 / ≥3 残余 | 避免过度断裂 |
| 标点透明 | replace 移除标点 | 匹配时不考虑标点 |