1. 背景:为什么又要造轮子?
做算法的朋友都懂,80% 的精力耗在洗数据。
我们最近对接了 3 个 OCR 供应商、4 个 PDF 解析工具,拿到的文本五花八门:
- 零宽空白(
\u200B)导致 ES 索引 500; - 全角半角混写,MySQL 唯一键冲突;
- 手机号、身份证直接明文落地,法务天天敲门;
- OCR 把 “clear” 识别成 “c1ear”,搜索直接挂掉。
开源工具(Apache Tika、OpenRefine)固然强大,但:
- 重,依赖多,在边缘节点跑不动;
- 规则不可定制,遇到中文标点就抓瞎;
- 不能打分,不知道洗得干不干净。
于是撸了一个轻量级、零依赖、可嵌入的 TextCleaningService,几百行代码,直接复制就能用。今天把设计思路与踩坑笔记分享出来,希望大家“少走弯路,多睡觉”。
2. 整体架构:6 步流水线
代码只有一个类,但内部拆成 6 个微步骤,方便按需开关:
复制
rawText
├─ ① 移除不可见字符 & BOM
├─ ② 脱敏(URL、邮箱、手机、身份证)
├─ ③ 标准化空白字符
├─ ④ 处理特殊符号(引号、Emoji、奇怪箭头)
├─ ⑤ 全角半角互转 & 中文标点对齐
├─ ⑥ 合并多余空行
└─ cleanText
每一步都返回 String,可单独调用,方便单元测试。
3. 亮点拆解
3.1 零宽字符“大屠杀”
零宽空白(\u200B、\uFEFF)复制不到,却真实存在,曾导致日志切割错位。
用正则一刀切除:
java
复制
text = text.replaceAll("[\u200B-\u200F\uFEFF]", "");
3.2 脱敏不“误杀”
正则写不好,会把 “C++11” 里的 +11 当成手机号。
我们采用“分段+边界”策略:
- 手机号:
1[3-9]\d{9}加\b词界; - 身份证:年份限定
18|19|20世纪,2 月不出现 31 日; - 替换为语义标签
[PHONE]、[ID],后续业务方可选择是否加密存储。
3.3 中文标点“智能”全角化
英文逗号 , 在半角场景要保留,例如 “i, j 两个变量”;
只有前后出现中文字符才转全角,避免 “Hello,World” 这种奇怪写法。
实现技巧:滑动窗口判断 UnicodeBlock:
java
复制
private boolean isInChineseContext(char[] chars, int idx) {
boolean prevIsChinese = idx > 0 && isChinese(chars[idx - 1]);
boolean nextIsChinese = idx < chars.length - 1 && isChinese(chars[idx + 1]);
return prevIsChinese || nextIsChinese;
}
3.4 OCR 常见错误字典
把 rn→m、vv→w、0O→O 做成 Map<String, String>,全词替换前先跑一遍,F1 提升 7%。
别小看这一点,搜索“招商银行”搜不到“招0商银行”的痛点你懂的。
3.5 文本质量“健康分”
清洗完不知道效果?直接打分:
- 长度分:>100 字符给 0.3;
- 中文占比:>10% 给 0.3;
- 标点完整性:句末是否有句号、引号是否成对;
- 行结构:空行比例 <20% 给满分。
返回 0~1 的 double,方便下游路由:
score < 0.5 → 走人工复核;score > 0.8 → 直接入索引。
4. 性能压测
环境:
JDK 17 + G1 GC + 2.4 GHz Intel i7
数据:
500 MB 单文件,平均 2.5k 字/篇,共 20 万篇。
结果:
表格
复制
| 指标 | 数值 |
|---|---|
| 平均单篇耗时 | 0.83 ms |
| 内存峰值 | 120 MB(正则预编译+char[] 复用) |
| 脱敏准确率 | 99.3%(人工抽样 1 万条) |
| 打分与人工一致性 | κ=0.87(几乎完美) |
CPU 热点主要在 Pattern.matcher,已预编译 static final,无额外优化空间。
5. 如何在项目中引用
- 直接复制
TextCleaningService.java到工程; - 零依赖,无需额外 Jar;
- Spring 用户可
@Bean单例注入,线程安全(无共享可变状态)。
示例:
java
复制
TextCleaningService cleaner = new TextCleaningService();
String nice = cleaner.cleanText(raw);
double score = cleaner.calculateTextQuality(nice);
if (score < 0.5) {
log.warn("低质量文本,进入人工审核:{}", nice);
}
6. 踩坑 & FAQ
Q1:正则把“C++11”替换成“C[PHONE]11”怎么办?
A:把“++11”加入白名单字典,清洗前先跑白名单替换。
Q2:想保留部分 URL,只脱敏图片域名?
A:把 URL_PATTERN 拆成两段,先替换 *.jpg/*.png,其余保持不动。
Q3:繁体中文会“误杀”吗?
A:UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS 已覆盖繁体,标点转换规则同样适用。
7. 未来规划
- 引入 ICU4J 做 Unicode 正规化(NFKC);
- 用 Aho-Corasick 替换多模式匹配,百万级关键词脱敏;
- 把打分模型改成小型 LR,支持自定义特征;
- 开源到 GitHub,欢迎提 PR。
8. 结语
文本清洗是“脏活累活”,却直接决定算法天花板。
希望这份“可直接复制粘贴”的代码笔记,能让你在下一个凌晨两点少掉两根头发。
源码就在上方,拿去用吧,记得点赞/收藏/转发三连!
文末彩蛋:在公众号后台回复【清洗】,获取完整 Maven Demo + 单元测试覆盖率 95% 的源码。