ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

探矿文档清洗实战:从乱码到结构化,提升RAG检索精度

探矿文档清洗实战:从乱码到结构化,提升RAG检索精度 去年接了一个矿区历史资料整理的项目第一批TXT打开全是乱码扫描版PDF用通用OCR跑完ZK301孔深285.6m变成了ZK30l孔深285.6m检索ZK301怎么都匹配不上。当时第一反应是Embedding模型不行向量库不行折腾了两周才发现问题根本不在RAG链路的中后段而在这批文档压根没洗干净。这篇就把我在探矿业务里和TXT、Word、PDF、网页这四种格式死磕出来的清洗经验完整写出来从乱码根源到切块策略再到上线之后的维护坑一次性说清楚。1. 探矿文档的脏数据画像为什么这套清洗管线非做不可先讲一个反直觉的结论RAG的检索精度天花板不是由Embedding模型决定的而是由进向量库之前的文档质量决定的。模型再强喂进去的是乱码和断裂表格召回结果就是一堆语义噪音。探矿这类传统行业资料尤其严重文档格式跨度大、历史跨度长、专业符号密集如果不先把清洗管线做扎实后面每一步都是在给错误数据做美化。1.1 一次真实的翻车乱码文档进向量库之后我当时拿到的是某铅锌矿区几十年的历史勘探资料包括早年用老式文字处理软件存盘的TXT、大量扫描成PDF的地质报告以及一部分从行业网站抓下来的公告网页。最开始图省事写了个脚本把TXT按UTF-8读出来PDF直接交给OCR工具识别网页用正则粗暴去标签然后统统灌进向量库。结果线上检索一测就出问题用户问ZK303孔浅部矿层的铅品位系统返回的是完全不相关的段落因为原始文档里ZK303被OCR成了ZK3O3数字0和字母O混淆Embedding模型根本不知道该把这两个Token关联起来。更离谱的是一份Word格式的地层对比表在提取时被拼成了一长段纯文本原本的顶板深度-底板深度-岩性描述结构全部丢失检索蚀变带厚度的时候模型把整张表搅在一起返回了一段谁也看不懂的文字。这次翻车让我意识到探矿文档的脏是有共性的而且远比想象中严重。直接把原始文件切块再Embedding等于拿一堆半成品去做菜。1.2 探矿RAG的四大格式难点我梳理了探矿业务中最高频的四种来源格式它们的病根各不相同必须分开对症下药。格式典型脏数据问题检索影响TXT编码混乱GBK、GB2312、UTF-8混用字节截断产生乱码生僻地名和早期简化字关键词直接匹配不上乱码字符污染Embedding向量Worddoc/docx两种格式表格密集上下标和公式符号提取后丢失表格行列关系消失品位、深度等关键数据无法精确召回PDF文本层缺失或字体映射错误扫描件噪声大多栏和跨页表格破坏阅读顺序大片乱码或OCR错字结构化信息彻底丢失网页导航标签、版权信息、备案号混杂表格被粗暴转成文本同一公告多版本重复检索结果里混入大量噪音片段重复内容稀释向量检索精度这四种格式在真实项目里往往是混合存在的一个矿区的资料包既有老TXT也有扫描PDF还有从矿权公示网站抓下来的公告。清洗管线必须同时覆盖所有路径任何一条短路都会拖垮整体检索效果。1.3 清洗管线在整个RAG链路里的定位整个RAG链路可以粗略划分为采集、清洗、切块、Embedding、向量检索、生成。很多团队把清洗当成一种不做也行的预处理但实际上它是决定检索精度的核心环节。你可以把清洗理解成浏览器显示网页时的编码设置网页本身是GBK编码浏览器当成UTF-8解析显示出来全是乱码这时候你不会觉得是浏览器渲染引擎不行而是会先改编码。RAG也一样原始文档的编码、排版、OCR结果出了问题Embedding模型再优秀也无能为力。所以清洗做的事情有两个层面一个是把乱码和OCR错误修掉保证文本可读另一个是把表格、公式、段落结构还原出来保证文本语义完整。这两点做不到检索精度就是空中楼阁。2. TXT与Word清洗编码判定和排版残留是两座山TXT和Word是Office时代最基础的两种格式看上去简单实际坑最深。TXT的坑集中在编码判定Word的坑集中在格式残留和结构丢失。这两个格式处理好了清洗管线就算打下了半壁江山。2.1 TXT乱码的根因编码识别顺序不能错TXT本身没有编码元信息打开时用什么编码去解完全取决于读取方的猜测。早年地质队员在Windows和DOS环境下存的文件绝大多数是GBK或GB2312编码但很多现代工具默认按UTF-8读取中文字符一旦用错编码解码就会变成锟斤拷这一类的经典乱码。我的处理顺序是这样的以二进制方式读取文件先检查头部有没有BOM。有BOM的话直接用BOM指定的编码UTF-8、UTF-16解码这一步能解决一部分问题。没有BOM就用charset-normalizer或chardet做编码探测输出概率最高的候选编码。解码时优先尝试GB18030而非GBK。GB18030是GBK的超集能覆盖更多生僻字和冷门字符探矿资料里经常出现的地名生僻字靠GBK是解不出来的。无论解出来什么编码统一转成UTF-8落到清洗中间件里后续所有环节都只用UTF-8。import chardet from charset_normalizer import from_bytes def safe_decode(raw: bytes) - str: # 1. 有BOM就按BOM解码 if raw.startswith(b\xef\xbb\xbf): return raw.decode(utf-8-sig, errorsreplace) if raw.startswith(b\xff\xfe) or raw.startswith(b\xfe\xff): return raw.decode(utf-16, errorsreplace) # 2. 无BOM用chardet探测 guess chardet.detect(raw[:10000]) encoding guess.get(encoding, utf-8) try: return raw.decode(encoding, errorsreplace) except LookupError: pass # 3. chardet搞不定时退回charset_normalizer result from_bytes(raw).best() if result is not None: return str(result) return raw.decode(gb18030, errorsreplace)这里有一个非常容易踩的坑文件在采集或传输过程中被截断最后一个汉字只剩一半字节解码后会产生UFFFD这个替换符。这些替换符如果不清理会原样进入Embedding模型。所以解码之后要专门做一轮非法字符清理把\ufffd、控制字符、零宽字符全部去掉并且把所有全角空格统一成半角。TXT清洗完成后还要做一次人工抽检随机打开几份文件确认内容语义通顺。编码探测算法不是万能的遇到极短文本或纯数字文本经常猜错抽检能兜底。2.2 从docx里保住表格结构地质报告最值钱的部分不能丢Word清洗很多人用一个python-docx把paragraphs拼起来就结束了但探矿报告里最值钱的信息恰恰在表格里地层对比表、样品化验单、储量估算表。这些表格一旦被拍平成一长段文本行列对应关系就没了检索阶段面对某钻孔见矿深度是多少这类精确数据问题基本是瘫痪的。我的做法是把段落和表格分路解析再按文档顺序合并。段落直接取文本表格则逐行读取单元格之间用制表符或竖线分隔并给表格整体打上附表编号作为元数据标记。这样后续切块时可以把表格单独作为一个结构化块检索时用户问到表格内容可以直接命中对应行。from docx import Document from docx.table import Table from docx.text.paragraph import Paragraph def iter_block_items(parent): from docx.oxml.ns import qn for child in parent.element.body.iterchildren(): if child.tag qn(w:p): yield Paragraph(child, parent) elif child.tag qn(w:tbl): yield Table(child, parent) doc Document(某矿区详查报告.docx) for block in iter_block_items(doc): if isinstance(block, Paragraph): print(block.text) else: for row in block.rows: cells [cell.text.strip() for cell in row.cells] print( | .join(cells))旧版.doc格式没有直接可用的Python解析库我的经验是优先用LibreOfficeheadless模式转成docx再继续清洗转换后要随机抽几页对比转换前后内容防止字体和表格样式在转换中丢失。这里有个细节早期地质报告里大量使用宋体仿宋黑体混排转docx后字体信息会留在styles里清洗时不要丢弃字体可以辅助判断标题层级和正文顺序。2.3 公式、符号与生僻字Word清洗里的隐藏炸弹探矿报告里到处都是上下标和化学符号Fe₂O₃、ZnO、ΣREE、Ag品位等。直接提取纯文本时上下标信息会丢失变成Fe2O3这种容易被误读成普通数字的文本。我处理这类内容的标准做法是解析时检测上下标用markdown式的下划线或插入可读文本表达例如把Fe₂O₃写成Fe_2O_3把ΣREE写成稀土总量ΣREE。这样既保住了原意又不影响Embedding模型对语义的理解。Word里更麻烦的是公式。老报告里用MathType或AxMath插入的公式原始结构是OMML格式直接转纯文本就是一大段乱码。如果公式本身是图片还得单独走OCR公式识别链路。对检索场景来说我的建议是能转LaTeX就转LaTeX保留语义转不了的至少保留公式编号和一句话说明例如公式3-2储量计算加权平均公式避免切块时把公式图片当乱码处理同时让大模型在生成回答时知道这里有个公式。生僻字和地名异体字也是一大类。GB18030能解决一部分但还有一部分是历史写法差异比如异体字峆和简写地名用字。这种问题最有效的方案是建立行业自定义词典把高频错字、异体字、旧称映射到规范写法在清洗阶段做一次规则替换。词典要基于语料持续更新这也是为什么后面要强调清洗日志和样本库的重要性。3. PDF清洗文本层、版面还原与OCR兜底的三层防线PDF是探矿业务里最复杂的格式没有之一。同样是PDF有的有文本层有的是纯扫描件有的是扫描件套了一个透明文本层处理路径完全不同。我的经验是把PDF清洗拆成三层防线先判断文本层可用性再做版面还原最后才是OCR兜底。3.1 先判断PDF有没有可用的文本层很多人拿到PDF就急着调OCR结果把一个明明有文本层的文件用OCR重新跑一遍反而引入一堆识别错误。正确的第一步是判断文本层是否可用。用PyMuPDF提取前几页文本统计有效中文字符在总字符数里的占比如果可读字符占比太低或者提取出来的字符流和肉眼看到的内容完全对不上就说明文本层不可用或字体映射有问题。字体映射错误在国产软件导出的PDF里很常见文本层存在但ToUnicode映射表是错的提取出来一堆乱码或方框。这种PDF靠修文本层很难直接走OCR更实际。判断标准很简单抽样提取三页看字符流里是否存在大量口、乱码、方框或者单词之间没有任何空格出现这些特征就转OCR管线。import fitz def check_text_layer(pdf_path): doc fitz.open(pdf_path) sample_text for page in doc.pages(start0, stopmin(3, len(doc))): sample_text page.get_text() if not sample_text.strip(): return no_text_layer # 统计中文字符占比 zh_chars sum(1 for ch in sample_text if \u4e00 ch \u9fff) total len(sample_text.replace( , ).replace(\n, )) ratio zh_chars / total if total else 0 if ratio 0.1 and total 20: return no_text_layer return usable有文本层的PDF也不是直接提取就完事页眉页脚、页码、交叉引用要单独处理。探矿报告经常每页顶部都有项目名称和共几页的字样这些内容进了切块就是纯噪音。我用一个规则通过坐标位置过滤掉页面上方和下方的固定区域同时用正则把第X页、共X页这类模式删除。3.2 扫描件OCR选型对比与行业词表纠错如果文档确实没有可用的文本层OCR就是必经之路。我的选型经历比较曲折最开始用开源Tesseract中文识别效果在清晰印刷体上还行但老扫描件模糊、背景有噪声时就完全不行了数字和字母经常混淆。后来换成PaddleOCR中文识别效果和表格结构识别能力都明显强一截。方案部署成本中文效果表格结构数据安全Tesseract低一般模糊文档差不支持本地PaddleOCR中较好支持PP-Structure版面分析支持本地商用云OCR低按量最好支持数据出域需评估企业数据安全要求探矿数据通常在企业内网处理我的建议是优先本地化部署PaddleOCR。CPU模式跑起来确实慢但胜在可控几百页报告中午挂上跑睡个午觉起来就完事了。真正要关心的不是速度而是OCR之后的错字怎么修。OCR错字最致命的地方是专业实体钻孔编号ZK301识别成ZK30l坐标纬度和经度里的0和O混在一起化学符号Zn变成Zr。这些错字直接用Embedding模型根本救不回来因为模型看到的是完全不同的Token。我的做法是建一个行业词表把所有常见的易混淆字符对和关键词列表放进纠错器里做后处理数字与字母混淆0/O、1/l、8/B单位符号g/t识别成g/1或g|t、m变成rn化学元素符号Zn与Zr、Pb与P、As与A5常见专业词品位识别成品仕、岩芯识别成岩心行业术语保留原样、矿化识别成矿他每次OCR完先跑一轮纠错再抽几页人眼看把新的错例补充进映射表。OCR不可能做到100%正确关键是把检索时最依赖的字段级信息钻孔编号、深度、品位、坐标控制到可接受范围。3.3 表格和多栏版面从视觉排版回到语义顺序扫描版PDF经过OCR之后得到的是散落的文本框它们的位置是视觉坐标不一定是阅读顺序。探矿报告最常见的版面是两栏正文加跨页表格如果直接按坐标排序提取文本会出现表格和正文交错、列顺序错乱的问题。这一步我推荐用版面分析模型自动识别页面里的标题、正文、表格、图片区域再按阅读顺序重组。PaddleOCR的PP-Structure就带这个能力识别完会把每个区域标注类型和坐标我再根据坐标从左到右、从上到下重排正文顺序表格单独提取并按行合并。跨页表格尤其要小心一个表格可能在第5页底部被截断第6页顶部续排我清洗时遇到续表字样会把两个表格片段按表头拼接成一张完整表。版面还原的目的是让文本从视觉上的块变成语义上通顺的段落这一步做得越细后续切块时越不容易把一句话拆成两半。3.4 混合型PDF的双轨处理实际项目里还有一类更磨人的PDF同一份文件有几十页扫描件中间夹了几页文字版或者每一页既有扫描图片又有文本层。我的处理方式是按页判断逐页决定走文本提取还是OCR最后按页码顺序合并成一个完整文档。这里要强调必须给每页打一个处理路径标记。比如文本层可用的页面标记为text走OCR的标记为ocr_zh后面做质量评估时能清楚看到哪些页面是OCR结果、哪些是原生文本排查问题时也能快速定位。这类标记信息我会原样保存在文档的元数据里进了向量库之后依然可以用于检索过滤。4. 网页资料清洗从HTML表格到结构化字段探矿业务里大量公开资料来自矿权公示和出让公告网站这些网页的清洗和普通文章完全不同。普通文章清洗的目标是提取通顺正文而公告类页面的核心价值在表格里的结构化字段探矿权人、许可证号、有效期限、勘查面积、坐标范围。4.1 网页正文提取公告页与普通文章要分开处理用通用正文提取算法处理公告页是常见翻车点。Readability这类算法对新闻文章效果好但遇到全是表格的公告页要么丢表格要么把表格内容横七竖八地塞进正文。我的经验是分两套方案普通介绍性文章用通用正文提取公告、公示、备案类页面用XPath模板化抽取。模板化抽取的意思是针对固定来源的网站手工分析页面结构用XPath把正文容器、表格容器、字段位置指出来。这样做的好处是稳定坏处是要维护。对高频来源站点维护十几个模板换来的是公告字段几乎零丢失我觉得非常值得。from lxml import html import requests resp requests.get(https://example-mining-announcement.gov.cn/detail/12345, timeout10) doc html.fromstring(resp.text) # 假设公告详情页正文在 #main-content 下表格在 .table-announcement 下 main_content doc.xpath(//div[idmain-content]//text()) tables doc.xpath(//table[contains(class,table-announcement)])网页本身的标签噪音也要处理干净导航栏、版权信息、备案号、上一篇/下一篇链接、翻页碎片这些都要在模板层就滤掉。有时候公告页面是JS动态渲染的直接requests拿不到正文需要headless浏览器渲染后再抓但清洗逻辑保持不变。4.2 字段抽取把人看的表格变成机器读的记录矿权公示信息里最核心的字段包括项目名称、探矿权人、勘查单位、许可证号、勘查矿种、有效期限、勘查面积、坐标范围。这些内容在网页上以表格形式呈现我的处理方式是先用BeautifulSoup解析表格结构转成DataFrame再把DataFrame按行变成结构化记录写入PostgreSQL或SQLite。这样做和把整个表格拍平成文本的本质区别在于结构化记录既能进RAG向量库做语义召回又能支持精确SQL查询。比如用户问某省近三年出让的铅锌矿探矿权我先用结构化字段过滤再走向量语义检索效果和只靠向量检索完全不在一个量级。坐标范围是一类特殊字段网页上经常是东经99°00′00″—99°30′00″北纬35°00′00″—35°30′00″这种文本。我清洗时会解析成数值区间并存成结构化字段这样检索时能支持空间范围过滤。这个功能在探矿场景很实用用户问某某坐标附近有没有正在公示的探矿权可以直接命中。4.3 去重与版本管理别让向量库存三份一样的公告同一份矿权公告经常被多个网站转载同项目的PDF和网页版也可能并存。我做过去重统计一个矿区的资料包里网页公告和PDF报告的重叠率能到15%以上。这部分数据如果不清理向量库里会堆满近似重复的chunk检索时同一份内容占掉好几个Top位置严重稀释检索质量。去重方案我从简到繁试过几种最基础的是内容哈希整段文本算MD5完全重复的直接扔掉。但网页转载经常带不同模板、不同页眉完全一样的哈希几乎见不到所以实际用的是SimHash或MinHash做近似去重相似度超过阈值的文档只保留一份。保留哪一份有讲究我的规则是优先保留来源权威、文本更完整、清洗错误更少的版本同时把source_url和抓取时间记录进元数据。网页版和PDF版并存时如果PDF版是官方盖章扫描件保留PDF版如果是内容一样的网页转载保留网页版就够了因为文本更干净。5. 清洗之后的最后一公里切块、元数据与质量验证文档清洗完之后还有一关直接决定检索精度切块和元数据注入。这一步做不好前面所有的清洗努力都会在最终效果上打折扣。探矿业务的问题类型和通用知识库不一样切块策略也要跟着调整。5.1 切块策略不能脱离问题类型探矿场景的提问大体分两类。第一类是段落型问题这个矿区的区域地质背景是什么答案是一段通顺描述适合用常规段落块。第二类是字段型问题ZK303孔浅部矿层铅品位多少答案来自表格里某一行的某个单元格这时候普通段落块根本没法精确命中。我的做法是让切块尊重文档结构段落按标题层级切表格直接整体作为一个块字段型内容单独切出一个结构化摘要块。探矿报告里的表格经常跨页清洗阶段我已经把表格合并复原了切块时整个表格入一个chunk不硬拆。固定字数硬切是最省事的方案但遇到表格和公式会切得支离破碎检索精度直线下降。参数上我习惯用300~500 token的窗口重叠80~100 token但每个项目都要拿验证集跑一遍再调没有放之四海皆准的固定值。中文字符在LLMTokenizer里的占比和英文不同300 token大概对应三百多个汉字但对表格类内容半个表就可能超过这个数所以结构化块和段落块分开设参数更合理。5.2 元数据与过滤把高精度检索变成先筛后搜给每个chunk打元数据是提升检索精度的性价比之王。我维护的元数据字段包括文档编号对应原始文件名方便溯源矿区名称直接决定检索范围资料类型报告、公告、规范标准、新闻格式来源TXT、Word、PDF、网页页码/表格编号用于精确定位清洗路径标记text还是ocr_zh检索时先根据用户问题做一轮元数据过滤比如用户问的是某矿区详查报告里钻探情况系统先过滤出该矿区且资料类型为报告、字段内容包含钻探的chunk再走向量召回。这样既缩小了语义搜索范围又天然避免了跨矿区、跨资料类型带来的噪音。实际链路顺序是查询意图分析 - 元数据过滤 - 向量召回 - 重排。这个顺序不要反过来。先召回再过滤的问题是向量搜索已经把所有相似内容都混在一起了过滤只能去掉一部分噪音精度损失已经造成。5.3 用标注问题集验证清洗效果从乱码率到命中率清洗做没做到位不能靠肉眼感觉要靠量化指标。我建了一套标注问题集大概四十到五十条覆盖不同类型问题每条都标注了正确答案所在的源文档和页码。清洗前和清洗后各跑一遍RAG统计Top5/Top10命中率用数据说话。指标清洗前清洗后文本乱码率抽样200条23.6%0.4%表格结构完整保留率31.0%92.0%Top5命中率标注问题集38.0%68.0%Top10命中率标注问题集52.0%79.0%这个结果很直观清洗前后Top5命中率提升了30个百分点。除了命中率我还会随机抽20到30个chunk人工检查文字准确率、段落是否断裂、表格是否完整。这一步能发现自动化指标发现不了的问题比如某个段落被分成两半、某张表格的表头和表体错位。6. 清洗管线上线后的三条保命经验前面讲的是技术方案最后分享几条在真实项目里被现实毒打出来的经验。这些经验不属于教科书但在生产环境里每一条都救过我的时间。6.1 没有清洗日志后面调优就是盲人摸象清洗管线一定要记录每份文档的处理日志检测到的编码、乱码率、走了文本提取还是OCR、OCR置信度、版面分析结果、丢弃的页数和原因。这些日志有两个作用一是线上出问题时能快速定位是哪一步洗坏了二是新OCR模型上线或词表更新时能用同一批历史文档做回归对比看看准确率到底提升了多少。我之前有一版清洗脚本跑完没留日志后来一份报告的表格全被拼乱排查半天也不知道是版面分析模型的问题还是Word解析路径的问题。加日志之后这类问题定位从小时级缩短到分钟级。6.2 部署形态决定OCR方案别一上来就上GPUOCR方案的部署形态要和数据量匹配。如果只是一个矿区的历史资料总计几千页扫描件单机CPU跑PaddleOCR完全可以接受挂一晚上跑完第二天看结果。为了提速上一台GPU服务器后续的驱动维护、环境兼容、内网部署成本反而可能比收益还高。但如果知识库要持续扩容每个月都有新资料进来那OCR就是常态化任务GPU的投入就划算。我见过太多项目在起步阶段就搞了一套重部署结果维护人员离职后整套环境没人敢动。先评估数据增量和处理频率再决定部署形态比一开始就追求最优性能稳得多。另外云OCR效果好但探矿资料属于企业内部敏感数据数据出不出域、交给第三方处理是否符合企业数据安全要求这个合规问题必须在选型阶段就想清楚不能等数据传上去才意识到。6.3 把清洗做成管道不是脚本最后一条清洗逻辑一定要设计成可配置的管道而不是每次手工改脚本。解析器、清洗器、校验器做成独立组件用一套配置描述这个来源走什么解析器、套什么清洗规则、用什么校验器新增一种格式时只加配置不写新脚本。我早期每次拿到新格式就复制一个脚本改改维护了几个月就乱成一锅粥。后来把所有逻辑收拢成一条管道TXT、Word、PDF、网页都是插拔式组件清洗规则和行业词表外置成配置文件整个系统的可维护性一下子上来。这也是知识库能长期运行、资料持续扩充的根基。这条管线我前后调了快两个月才稳定下来回头看最值的投入不是换Embedding模型而是把清洗层做实。探矿业务里很多历史资料都是不可再生的清洗一次之后后面建索引、做问答、出报告都建立在干净数据上。希望这篇能帮你少走点弯路如果后续有新格式的坑我也打算继续整理出来。
返回列表