02-断词机制详解
断词机制详解 (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
代码逐段解析
入口过滤:短词不处理
// 第242-244行
const hyphenateWord = (word: string) => {
const lower = word.toLowerCase().replace(/[.,;:!?"'""''—–-]/g, "");
// 移除标点符号后转小写,用于匹配词典和词缀
if (lower.length < 5) return [word];
// 长度小于5的单词不拆分,避免过度断裂
为什么要移除标点?
词典键是纯单词形式,标点只是输入文本的修饰,不应影响匹配。
为什么阈值是 5?
过短的单词(如 "the", "is")即使拆分也可能造成更多问题而非优化。
第一层:异常词典匹配
// 第245-255行
const exc = HYPHEN_EXCEPTIONS[lower as keyof typeof HYPHEN_EXCEPTIONS];
if (exc) {
const parts: string[] = [];
let pos = 0;
// 逐片段累加位置指针
for (const part of exc) {
parts.push(word.slice(pos, pos + part.length));
// 按词典定义的长度切分,保持原词大小写
pos += part.length;
}
if (pos < word.length)
parts[parts.length - 1] += word.slice(pos);
// 处理边界情况:词典未完整覆盖(如末尾有多余字符)
return parts.filter((p) => p.length > 0);
}
词典数据结构设计:
// 示例:relationship 的词典定义
relationship: ["re", "la", "tion", "ship"]
// re + la + tion + ship
// 2 + 2 + 4 + 4 = 12 ✓
边界容错逻辑:
- 如果词典片段总长度 < 原词,会把多出部分追加到最后一个片段
- 这确保即使词典定义有微小偏差,也能"勉强"工作
第二层:前缀匹配
// 第256-260行
for (const prefix of PREFIXES) {
if (lower.startsWith(prefix) && lower.length - prefix.length >= 3) {
// 必须匹配前缀,且剩余部分 ≥ 3 个字符
return [word.slice(0, prefix.length), word.slice(prefix.length)];
// 前缀保持原样,后缀也保持原样(不断开)
}
}
为什么 >= 3?
后缀部分至少保留3个字符,避免过短的尾部导致排版问题。
返回结构:
- 返回数组长度 = 2(分为两部分)
- 软连字符可以在两部分的交界处插入
第三层:后缀匹配
// 第261-266行
for (const suffix of SUFFIXES) {
if (lower.endsWith(suffix) && lower.length - suffix.length >= 3) {
const cut = word.length - suffix.length;
return [word.slice(0, cut), word.slice(cut)];
// 前半部分 + 后缀(后半部分)
}
}
前后缀互斥:代码先检查前缀,再检查后缀
这意味着对于 "reading":
- 不在异常词典
- 不以前缀开头
- 但以后缀 "ing" 结尾 → 返回 ["read", "ing"]
词缀数据规模
pie title 词缀库统计
"前缀数量 PREFIXES" : 55
"后缀数量 SUFFIXES" : 37
"异常词典词条" : ~130
前缀列表片段
const PREFIXES = [
"anti", "auto", "be", "bi", "co", "com", "con",
"contra", "counter", "de", "dis", "en", "em",
"ex", "extra", "fore", "hyper", "il", "im", "in",
"inter", "intra", "ir", "macro", "mal", "micro",
// ... 共55个
];
后缀列表片段
const SUFFIXES = [
"able", "ible", "tion", "sion", "ment", "ness",
"ous", "ious", "eous", "ful", "less", "ive",
"ative", "itive", "al", "ial", "ical", "ing",
// ... 共37个
];
断词后的后续处理
断词结果会传递给 prepareWithSegments 添加软连字符:
// 第428-434行(renderCanvas 中)
const hyphenated = tokens
.map((t) => {
if (/^\s+$/.test(t)) return t; // 空格保留
const parts = hyphenateWord(t);
return parts.length <= 1 ? t : parts.join("");
// 拆分后用软连字符 U+00AD 连接
})
.join("");
软连字符作用:
- 在 Canvas 渲染时,某些实现会在此处断裂
- 或者被
prepareWithSegments识别为可断裂点
扩展讨论:如何增强断词能力
方案一:引入词典库
可接入 hypher 或 hyphenation 等专业断词库:
import Hypher from 'hypher';
import english from 'hyphenation.en-us';
const hypher = new Hypher(english);
const parts = hypher.hyphenate('extensively');
// → ['ex', 'ten', 'sive', 'ly']
方案二:添加语言检测
当前实现只针对英文,可扩展:
const hyphenate = (word: string, lang: string) => {
switch(lang) {
case 'en': return hyphenateEnglish(word);
case 'de': return hyphenateGerman(word); // 德语规则不同
case 'fr': return hyphenateFrench(word);
default: return [word];
}
};
方案三:学习式断词
基于 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 移除标点 | 匹配时不考虑标点 |