从“脏文本”到“干净数据”:一份可直接落地的 Java 文本清洗工程笔记

242 阅读4分钟

1. 背景:为什么又要造轮子?

做算法的朋友都懂,80% 的精力耗在洗数据。
我们最近对接了 3 个 OCR 供应商、4 个 PDF 解析工具,拿到的文本五花八门:

  • 零宽空白(\u200B)导致 ES 索引 500;
  • 全角半角混写,MySQL 唯一键冲突;
  • 手机号、身份证直接明文落地,法务天天敲门;
  • OCR 把 “clear” 识别成 “c1ear”,搜索直接挂掉。

开源工具(Apache Tika、OpenRefine)固然强大,但:

  1. 重,依赖多,在边缘节点跑不动;
  2. 规则不可定制,遇到中文标点就抓瞎;
  3. 不能打分,不知道洗得干不干净。

于是撸了一个轻量级、零依赖、可嵌入的 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→mvv→w0O→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. 如何在项目中引用

  1. 直接复制 TextCleaningService.java 到工程;
  2. 零依赖,无需额外 Jar;
  3. 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% 的源码。