ARTICLE DETAIL

资讯详情

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

探矿RAG知识库构建:TXT、Word、PDF与网页多格式文档清洗实战

探矿RAG知识库构建:TXT、Word、PDF与网页多格式文档清洗实战 1. 探矿数据清洗到底难在哪从一堆“乱码”说起搞探矿这行的人都有一个共同的痛数据来源太杂了。地质队的原始记录可能是手写的TXT台账钻探报告是Word文档化验分析单是PDF历史资料还散落在各种内部网页上。你想把这些东西喂给RAG知识库做智能检索第一步就卡住了——文件打开全是乱码表格错位公式变成一堆问号。我接手过一个探矿项目的知识库搭建光是数据清洗就花了整整三周。当时拿到手的资料包括127个TXT文件编码从GBK到UTF-8都有还有几个是GB2312混着BOM头、89份Word报告里面嵌了AxMath和MathType的公式、200多份PDF有扫描件也有原生电子版、以及从内部系统导出的HTML页面。这些东西如果不做清洗直接入库检索出来的结果基本没法看——你搜“铜品位”它给你返回一段乱码你搜“钻孔倾角”它把表格里的数字全拼成了一行。这篇文章就是把我踩过的坑和最终跑通的方案完整拆开讲。核心关键词是RAG、TXT、Word、PDF、网页但我不打算讲那些泛泛的“RAG框架怎么选”而是聚焦在一个具体场景探矿业务中多格式文档的清洗与结构化入库。适合谁看如果你正在搭建地质、矿业、勘探相关的知识库或者你手头有一堆格式混乱的技术文档要处理这篇内容可以直接抄作业。先说清楚一个基本认知RAG的效果上限在清洗阶段就已经决定了。很多人把精力花在调模型、换向量库上结果检索精度死活上不去回头一看入库的文本本身就是垃圾。Garbage in, garbage out这句话在RAG项目里是铁律。2. 整体清洗架构为什么我不推荐一把梭2.1 分而治之按格式拆管道而不是统一转换我见过不少团队的做法是不管什么格式先统一转成TXT然后再做后续处理。这个思路听起来简单但实际跑下来问题很大。PDF转TXT会丢失表格结构Word转TXT会把公式变成乱码网页转TXT会把导航栏和正文混在一起。你后面再想从这堆纯文本里恢复结构成本比一开始就分格式处理要高得多。我的方案是按文件格式拆成四条独立管道每条管道有自己的解析策略和输出规范最后汇入统一的结构化中间层。具体来说TXT管道重点解决编码识别和字段切分Word管道重点解决公式提取和表格还原PDF管道重点区分原生电子版和扫描件走不同路径网页管道重点做正文抽取和噪声去除这四条管道的输出统一为带元数据的结构化JSON包含三个核心字段content清洗后的正文、metadata来源、日期、作者、文件类型、structure标题层级、表格、公式的定位信息。这样做的好处是后面不管你是入向量库还是入图数据库都有干净的原料可用。2.2 清洗深度的取舍不是越干净越好这里有一个容易被忽略的问题清洗到什么程度算合适我一开始追求“极致干净”把所有特殊字符、所有格式标记全删了结果发现检索效果反而变差了。为什么因为探矿文档里很多关键信息恰恰藏在那些“看起来像噪声”的东西里——比如化学式里的下标数字、坐标数据里的度分秒符号、岩性描述里的特殊代号。后来我调整了策略保留语义相关的特殊字符只清除真正的噪声。具体判断标准是内容类型处理方式理由化学式下标/上标保留并标准化Cu²⁺、Fe₂O₃等是检索高频词坐标度分秒符号保留原格式°′″是地质数据核心标识页眉页脚清除纯噪声干扰检索表格边框线转为结构化数据保留行列关系公式图片OCRLaTeX双路提取纯文本化会丢失语义乱码字符按编码修复后保留可能是编码问题而非真乱码注意清洗的目标是“可检索、可理解”不是“看起来整洁”。这两个目标有时候是冲突的遇到冲突时优先保检索。3. TXT文件清洗编码问题是第一道坎3.1 编码检测为什么chardet经常不靠谱TXT文件看起来最简单实际上编码问题最让人头疼。我拿到的127个TXT文件里用chardet检测的结果有将近三成是错的。原因很简单探矿领域的TXT文件通常很短而且包含大量专业术语和数字chardet的统计模型在这种文本上表现很差。我的做法是多引擎交叉验证业务规则兜底。具体流程import chardet import cchardet def detect_encoding(file_path): with open(file_path, rb) as f: raw f.read() # 多引擎检测 results [] results.append(chardet.detect(raw)) results.append(cchardet.detect(raw)) # 业务规则探矿TXT常见编码优先级 priority [utf-8, gbk, gb2312, gb18030, utf-16] # 如果多引擎结果一致且置信度高直接采用 if len(set(r[encoding] for r in results)) 1 and results[0][confidence] 0.9: return results[0][encoding] # 否则按优先级逐个尝试解码 for enc in priority: try: raw.decode(enc) return enc except UnicodeDecodeError: continue return utf-8 # 兜底这里有个关键点GB18030是GBK的超集如果GBK解不出来试试GB18030往往能成。另外有些TXT文件开头有BOM头utf-8-sig能自动处理但如果你用utf-8去读就会在开头多出一个\ufeff字符这个字符进入向量库后会污染检索结果。3.2 字段切分从自由文本到结构化记录探矿TXT文件的内容通常是有固定格式的比如钻孔记录可能是这样的ZK001 钻孔 开孔日期:2023-05-12 终孔深度:156.3m 0-5m 第四系覆盖层 黄色粘土 5-12m 强风化花岗岩 褐灰色 岩芯采取率85% 12-45m 中风化花岗岩 灰白色 见黄铜矿化 ...这种文本如果不做切分直接入库检索“黄铜矿化”会返回整段文本用户还得自己找。我的做法是用正则规则模板做字段抽取import re def parse_drill_log(text): records [] # 匹配钻孔头部信息 header_pattern r(ZK\d)\s钻孔\s开孔日期:(\S)\s终孔深度:(\S) header re.search(header_pattern, text) # 匹配分层记录 layer_pattern r(\d-\dm)\s(\S)\s(\S)\s(.) for match in re.finditer(layer_pattern, text): records.append({ depth_range: match.group(1), layer_name: match.group(2), color: match.group(3), description: match.group(4) }) return { hole_id: header.group(1) if header else None, date: header.group(2) if header else None, depth: header.group(3) if header else None, layers: records }这样切分之后每条分层记录都是独立的检索单元检索“黄铜矿化”直接命中对应层位精度提升非常明显。3.3 实操心得TXT清洗的三个坑第一个坑换行符不统一。Windows的\r\n、Linux的\n、老Mac的\r混在一起如果不统一处理后面按行切分会出问题。我一般先用text.replace(\r\n, \n).replace(\r, \n)统一。第二个坑全角半角混用。探矿文档里经常出现全角数字和半角数字混用的情况比如“深度.”和“深度156.3m”。检索时如果不做归一化用户搜“156.3”可能匹配不到全角的版本。我的做法是在清洗阶段统一转为半角但保留原始文本在metadata里。第三个坑空行和空格的处理。有些TXT文件用大量空行做视觉分隔有些用空格对齐表格。我的策略是连续空行压缩为一个行首行尾空格去除但行内多个空格如果是对齐用途则保留通过检测列对齐模式判断。4. Word文档清洗公式和表格是两大难关4.1 公式提取AxMath、MathType与OMML的三角关系Word文档里的公式是最让人头疼的部分。探矿报告里大量使用AxMath和MathType插入公式这两种工具生成的公式在Word里存储为OLE对象直接用python-docx读取只能拿到一个占位符拿不到公式内容。我试过几种方案最终跑通的是OMML转换图片OCR兜底的组合拳from docx import Document from lxml import etree def extract_formulas(docx_path): doc Document(docx_path) formulas [] # 方法1提取OMML公式Word原生公式 nsmap {m: http://schemas.openxmlformats.org/officeDocument/2006/math} for para in doc.paragraphs: omml_elements para._element.findall(.//m:oMath, nsmap) for omml in omml_elements: # OMML转LaTeX latex omml_to_latex(omml) formulas.append({type: omml, latex: latex}) # 方法2提取嵌入的OLE对象AxMath/MathType for rel in doc.part.rels.values(): if oleObject in rel.reltype: # 提取OLE对象转为图片后OCR ole_data rel.target_part.blob img ole_to_image(ole_data) latex ocr_formula(img) # 使用公式OCR模型 formulas.append({type: ole, latex: latex}) return formulas这里的关键是OMML转LaTeX有现成的XSLT样式表可以直接用。而AxMath和MathType的OLE对象需要先转成图片再用公式OCR模型识别。我实测下来OMML转换的准确率接近100%OLEOCR的准确率在85%左右对于复杂的积分和矩阵公式可能会出错需要人工校验。提示如果你发现Word里同时装了AxMath和MathType插入公式时可能会跳转到MathType。这不是bug是MathType抢占了默认公式编辑器。在AxMath设置里把“设为默认公式编辑器”勾上就能解决。4.2 表格还原合并单元格是最大的坑Word表格的清洗难点不在简单表格而在合并单元格。探矿报告里的钻孔数据表经常有这样的结构钻孔编号深度区间岩性品位ZK0010-5m粘土-5-12m花岗岩0.3%12-45m花岗岩0.8%ZK001这个单元格是跨行合并的。如果你用python-docx直接遍历单元格会发现合并单元格的内容只在第一个位置出现后面的是空的。我的处理方式是先检测合并模式再填充def parse_word_table(table): rows [] for i, row in enumerate(table.rows): row_data [] for j, cell in enumerate(row.cells): # 检测是否是合并单元格的延续 if cell._tc is row.cells[j-1]._tc if j 0 else False: row_data.append(row_data[-1]) # 沿用上一个单元格的值 else: row_data.append(cell.text.strip()) rows.append(row_data) return rows这个逻辑的核心是通过比较底层XML元素判断是否是同一个单元格。如果当前单元格和左边的是同一个tc元素说明是横向合并沿用左边的值。纵向合并类似处理。4.3 样式与层级标题级别决定了检索粒度Word文档的标题样式Heading 1/2/3是天然的层级结构这个信息在清洗时一定要保留。我的做法是把标题层级转成面包屑路径附加到每个段落的metadata里def extract_with_hierarchy(docx_path): doc Document(docx_path) sections [] current_path [] for para in doc.paragraphs: if para.style.name.startswith(Heading): level int(para.style.name.split()[-1]) # 更新面包屑路径 current_path current_path[:level-1] [para.text] else: if para.text.strip(): sections.append({ content: para.text, path: .join(current_path), style: para.style.name }) return sections这样检索“ZK001钻孔的岩芯采取率”时可以精确定位到“第三章 钻探工程 3.2 钻孔质量 ZK001”这个路径下的内容而不是全文模糊匹配。5. PDF解析原生电子版和扫描件要分开处理5.1 先判断PDF类型这一步不能省PDF文件分两种原生电子版文字可选和扫描件文字是图片。这两种的处理路径完全不同如果不做判断直接上OCR原生电子版会浪费大量时间且准确率反而下降。判断方法很简单import fitz # PyMuPDF def is_scanned_pdf(pdf_path, sample_pages5): doc fitz.open(pdf_path) total_text 0 total_pages min(len(doc), sample_pages) for i in range(total_pages): page doc[i] text page.get_text() total_text len(text.strip()) # 如果平均每页文字少于50个字符判定为扫描件 return total_text / total_pages 505.2 原生电子版表格和公式的提取策略原生电子版PDF的表格提取我推荐用pdfplumber它对表格线的识别比PyMuPDF更准。但探矿报告里很多表格没有明确的边框线这时候需要用camelot的stream模式基于文字对齐来推断表格结构。公式提取更麻烦。PDF里的公式通常是嵌入的字体或者图片。如果是字体PyMuPDF能提取出文字但会丢失数学结构如果是图片就需要走OCR。我的策略是先用PyMuPDF提取所有文本块检测文本块中是否包含数学符号密集区域对疑似公式区域截图走公式OCR其余文本正常提取5.3 扫描件OCR之后的清洗更重要扫描件走OCR是必然的但OCR出来的文本噪声很大。探矿报告扫描件常见的问题包括表格线被识别成字符、页眉页脚混入正文、手写批注干扰。我的清洗流程是版面分析用PaddleOCR的版面分析功能先区分正文、表格、图片区域表格重建对表格区域单独做OCR用PP-Structure重建表格结构文本后处理用正则清除OCR常见的噪声模式比如连续的点号、孤立的单字符行人工抽检随机抽10%的页面人工校验发现系统性问题及时调整实操心得扫描件OCR的准确率再高也建议保留原始图片的引用。在metadata里存一个source_image字段指向原始扫描页的截图。这样当用户对检索结果存疑时可以回溯到原图核对。6. 网页数据清洗正文抽取与噪声去除6.1 正文抽取Readability算法与自定义规则探矿业务相关的网页数据来源包括内部地质资料系统、在线地质词典、矿业新闻网站。这些网页的正文抽取不能用同一套规则。我一般用readability-lxml做第一轮抽取然后针对特定站点补充自定义规则from readability import Document import requests def extract_web_content(url): resp requests.get(url, timeout10) doc Document(resp.text) # readability抽取正文 content doc.summary() # 针对特定站点的补充清洗 if geology.com in url: # 移除广告和推荐阅读 content remove_ads(content) return { title: doc.title(), content: content, url: url }6.2 动态网页什么时候该用浏览器渲染有些地质资料系统是前后端分离的直接请求HTML拿不到数据需要等JavaScript渲染完成。这时候就得上Playwright或Selenium。但我的原则是能不用浏览器就不用因为浏览器渲染的速度比直接请求慢一个数量级。判断标准很简单先用requests请求一次如果返回的HTML里没有目标内容比如表格数据是空的再考虑上浏览器。另外有些网站有反爬机制这时候需要设置合理的请求头、控制请求频率但这些都是常规操作不展开讲。6.3 网页清洗的特殊问题导航栏和评论区网页数据最烦人的是噪声太多。导航栏、侧边栏、评论区、相关推荐这些内容如果不清理会严重干扰检索。我的做法是维护一个噪声选择器黑名单NOISE_SELECTORS [ nav, header, footer, aside, .sidebar, .comment, .related-posts, #nav, #footer, #comments ] def remove_noise(soup): for selector in NOISE_SELECTORS: for element in soup.select(selector): element.decompose() return soup这个黑名单需要根据实际抓取的站点不断补充。我一般会先抓10个页面人工看一下有哪些噪声区域把对应的选择器加进去。7. 常见问题与排查技巧实录7.1 编码问题速查表现象可能原因解决方法中文显示为乱码编码检测错误尝试GB18030解码开头多出\ufeffUTF-8 BOM头用utf-8-sig读取部分字符显示为?编码不支持该字符转用GB18030或UTF-8全文都是方块字体缺失或编码完全错误检查原始文件编码数字和字母正常但中文乱码典型的GBK/UTF-8混淆用GBK重新解码7.2 公式提取失败排查公式提取失败通常有三种情况情况一OMML元素找不到。检查命名空间是否正确。Word的OMML命名空间是http://schemas.openxmlformats.org/officeDocument/2006/math但有些文档可能用了不同的命名空间前缀。情况二OLE对象无法转换。AxMath和MathType的OLE对象需要对应的解析器。如果没有安装对应的软件可以尝试用LibreOffice的命令行模式转换。情况三OCR识别率低。公式OCR对图片质量要求很高。如果原始图片分辨率低于150dpi识别率会急剧下降。建议在OCR前先做图像增强二值化、去噪、放大。7.3 表格结构错乱的处理表格清洗最常见的问题是行列错位。排查步骤检查原始文件是否有合并单元格检查是否有隐藏的行或列检查表格是否跨页检查是否有嵌套表格对于跨页表格我的处理方式是先合并再解析。用pdfplumber的extract_tables()时设置page参数为多页它会自动处理跨页表格。8. 清洗后的数据如何入RAG知识库8.1 分块策略按语义分块而不是按字数清洗完的文本不能直接整篇入库需要分块。很多人按固定字数分块比如每500字一块这在探矿文档上效果很差因为一个钻孔的描述可能刚好被切到两块里。我的分块策略是按语义单元分块TXT钻孔记录每个钻孔一个块Word章节每个三级标题下的内容一个块PDF表格每个表格一个块表格的标题和表头作为metadata网页正文按段落分块但保持段落完整性如果语义单元太长超过1000字再按句子边界切分。如果太短少于50字和相邻块合并。8.2 元数据设计检索精度的隐形推手元数据在RAG检索中的作用经常被低估。我设计的元数据字段包括{ source_file: ZK001钻孔记录.txt, file_type: txt, hole_id: ZK001, date: 2023-05-12, depth_range: 12-45m, content_type: drill_log, hierarchy: 钻探工程 钻孔记录 ZK001 }这些元数据在检索时可以做预过滤。比如用户搜“ZK001的铜品位”可以先按hole_idZK001过滤再在过滤结果里做向量检索精度和速度都会大幅提升。8.3 向量化模型的选择中文探矿领域要选对向量化模型的选择直接影响检索效果。我实测下来在中文探矿领域BGE-large-zh和M3E-base的表现比较均衡。如果追求更高精度可以用BGE-M3它支持多语言和长文本但计算成本更高。注意不要用OpenAI的text-embedding模型处理中文探矿文本它在中文专业术语上的表现明显不如国产模型。这不是崇洋媚外是实测结果。9. 我在实际项目中的几点体会清洗这活儿说起来都是细节但真正决定成败的往往不是技术方案而是对业务的理解。我一开始用通用的文档清洗流程去处理探矿资料效果很一般。后来跟着地质队的人跑了几天现场看了他们怎么记录、怎么查资料、怎么用检索结果才明白哪些信息是关键的、哪些噪声是可以忽略的。举个例子探矿报告里的“岩芯采取率”这个指标通用清洗流程可能会把它当成普通文本处理。但实际上这个指标在检索时经常需要做数值比较比如“采取率大于80%的钻孔”所以我在清洗时会把数值和单位分开存储数值字段用float类型这样后面做范围检索就方便很多。另一个体会是清洗规则要可配置、可迭代。我一开始把规则写死在代码里后来业务方说“这个字段也要提取”“那个噪声也要去掉”每次都要改代码重新部署。后来我把规则抽成YAML配置文件业务方自己就能改效率高了很多。最后分享一个小技巧建立清洗质量的自动化评估机制。我写了一个脚本随机抽取清洗后的数据用正则检查是否有残留的乱码、是否有明显的格式错误、是否有空字段。每次清洗流程跑完自动运行发现问题及时告警。这个机制帮我省了很多人工检查的时间。这个内容后续还可以这样扩展把清洗后的结构化数据入图数据库构建探矿知识图谱支持更复杂的关联检索。比如“查找所有与ZK001钻孔相邻且铜品位大于0.5%的钻孔”这种查询用向量检索很难做好但用图数据库就很自然。
返回列表