OCR输出垃圾字符清理优化及关键内容保护最佳实践
生产级OCR文本归一化方案与问题解答
背景与现有问题
我有一套本地部署的OCR管道,提取的文本以JSON格式返回。解析LLM响应后,会调用本地归一化器处理文本再返回给调用方,调用逻辑如下:
result = client.chat.completions.create( model=self.image_llm_model, messages=messages, max_tokens=max_tokens, response_format={"type": "json_object"}, ) parsed = json.loads(result.choices[0].message.content) info_text = parsed.get("info") return self._normalize_ocr_text(info_text) if info_text else info_text
OCR输出常存在以下问题:
- 大量冗余可见标点
- mojibake乱码(如
’序列) - 零宽空格等不可见/格式控制字符
- OCR引入的重复字符
原归一化器依赖手动编写的mojibake匹配规则和正则,覆盖范围有限且容易误改有效文本,核心逻辑片段如下:
def _normalize_ocr_text(self, text: str) -> str: if not text or not isinstance(text, str): return text mojibake_indicators = ['â€', '´', 'µ', '±', '³', 'º'] has_mojibake = any(indicator in text for indicator in mojibake_indicators) # repetitive pattern detection... repetitive_pattern = re.compile(r'(\S)\s*\1{49,}') has_repetitive = bool(repetitive_pattern.search(text)) if not has_mojibake and not has_repetitive: return text # manual replacements mojibake_fixes = { '•': '•', '€': '₹', '´': '', # ... } # replacements and repeats-handling... return normalized
改进后的归一化实现
针对上述问题,规划的生产级改进方案覆盖了核心需求,具体实现代码如下:
import re import unicodedata from ftfy import fix_text def _remove_control_chars(s: str) -> str: return re.sub(r'[\x00-\x1F\x7F-\x9F]', '', s) def _collapse_repetitions(s: str, threshold: int = 50) -> str: pattern = re.compile(r'(\S)(\s*\1){' + str(threshold-1) + r',}') def repl(m): char = m.group(1) matched = m.group(0) if ' ' in matched or '\n' in matched: return f"{char} {char} {char}..." return char * 3 + "..." return pattern.sub(repl, s) def _normalize_ocr_text(self, text: str) -> str: if not text or not isinstance(text, str): return text original_len = len(text) try: text = fix_text(text) # 自动修复mojibake except Exception: pass text = unicodedata.normalize("NFKC", text) # 归一化兼容字符与连字 text = _remove_control_chars(text) # 移除不可见/控制字符 text = _collapse_repetitions(text, threshold=50) # 合并大量重复字符 text = re.sub(r' {10,}', ' ', text) text = re.sub(r'\n{3,}', '\n\n', text) text = text.strip(' \t\n') if len(text) != original_len: self._stage_print("ocr_text_normalized", f"Text normalized: {original_len} -> {len(text)}") return text
问题解答
1. 关键内容保护的最佳实践
优先选择提前保护子串的方案,而非字段级白名单。原因如下:
- OCR输出的文本往往是非结构化的,字段级白名单依赖明确的结构化字段划分,实际场景中很难精准覆盖所有关键内容的位置;
- 提前通过正则匹配识别邮箱、URL、金额等关键模式,将其替换为临时占位符(如
{{EMAIL_0}}、{{URL_1}}),完成归一化流程后再还原为原内容,能从根本上避免归一化步骤(如mojibake修复、字符归一化)误改这些信息; - 举例来说:先匹配所有符合邮箱格式的子串,存储到字典中并替换占位符,处理完文本后遍历字典把占位符替换回原邮箱,既不影响整体归一化,也能确保关键内容完整。
2. 无法更新模型时,当前方案是否足够
当前方案已经能满足生产级核心需求,是足够可靠的:
- 覆盖了所有预设目标:
ftfy.fix_text()自动修复绝大多数mojibake,NFKC归一化处理兼容字符与连字(如fl转fl),正则移除控制字符,重复字符合并逻辑处理OCR引入的冗余重复; - 额外的空格、换行归一化进一步提升了文本整洁度;
- 容错机制(
try-except包裹fix_text)避免了异常中断流程。
如果要进一步优化,可以针对业务场景补充以下细节:
- 针对金额类内容,可提前识别货币符号+数字的模式并保护,避免NFKC归一化误改特殊货币符号;
- 调整重复字符的阈值(如从50降到20),或添加重复单词的处理逻辑(如
hello hello hello...合并为hello...); - 补充常见OCR字符错误映射(如
O与0、l与1的修正规则),但需注意避免误改有效内容; - 可添加归一化前后的文本对比日志,便于排查偶发的误改问题。
内容的提问来源于stack exchange,提问作者agaonsindhe
相关产品推荐
相关产品推荐

