☰
探矿多格式文档清洗实战:从TXT、Word、PDF到RAG知识库
2026/10/7 5:23:24 网站建设 项目流程

1. 探矿业务里的数据清洗为什么是个硬骨头

干了几年地质信息化,我最怕听到的一句话就是“这批资料你整理一下”。探矿行业的数据来源实在太杂了:老一辈地质队员手写的钻孔编录本扫描件、上世纪九十年代用WPS打的储量报告、合作单位发来的PDF附图、还有从各种地勘网站扒下来的矿权公示页面。这些玩意儿堆在一起,格式五花八门,编码千奇百怪,你要是直接往RAG知识库里灌,检索出来的结果能让你怀疑人生。

我接手过一个铜矿项目的资料数字化,大概两千多份文档,涉及TXT、Word、PDF和网页四种主要格式。刚开始图省事,用了个开源的RAG框架直接批量导入,结果问“ZK1201钻孔的见矿深度”,系统给我返回了一段乱码加半页化学分析表。后来花了整整三周做清洗,才把检索准确率从惨不忍睹拉到能用的水平。这篇文章就把我踩过的坑和总结出来的方法完整分享出来,不管你是做地质信息化、矿业数据分析,还是单纯想搞明白RAG知识库怎么处理多格式文档,应该都能找到能直接抄作业的东西。

核心思路其实就一句话:不同格式的文档,脏的方式不一样,清洗的策略也必须分开设计。TXT的敌人是编码和分隔符,Word的敌人是隐藏格式和嵌入对象,PDF的敌人是版面还原和表格识别,网页的敌人是导航噪音和动态加载。把这四类分开处理,再统一成RAG友好的结构化格式,后面检索才能稳。

2. 四类文档格式的脏数据特征与清洗策略拆解

2.1 TXT文档:编码问题是第一道坎

探矿业务里的TXT文件,来源特别杂。有从老式数据库导出的,有从PDF复制粘贴的,还有从各种转换工具生成的。最常见的问题就是编码混乱。我遇到过一份1998年的钻孔数据,用GB2312存的,里面还混了几个GBK扩展字符,用UTF-8打开直接乱码。更麻烦的是有些文件里同时存在多种编码的段落,你根本没法用一个统一的编码去读。

处理这类问题,我的做法是先用chardet做编码探测,然后逐行读取,遇到解码失败的字节就用errors='replace'跳过,同时记录下出错的行号。这样至少能保证大部分内容能读出来,出错的部分单独标记出来人工核对。实测下来,两千多份TXT里大概有百分之七左右存在编码问题,其中大部分是GB2312和UTF-8混用。

另一个坑是分隔符不统一。同样是钻孔数据,有的用制表符分隔,有的用逗号,还有的用多个空格。更离谱的是有些文件里不同段落的列数都不一样。我的处理方式是先做分隔符统计,找出出现频率最高的分隔模式,然后按这个模式切分,列数不匹配的行单独拎出来。这里有个经验:不要试图自动修复所有异常行,标记出来让人工确认比强行解析更靠谱,因为地质数据一旦解析错了,后面所有分析都是错的。

2.2 Word文档:隐藏格式比正文更麻烦

Word文档在探矿业务里主要用于储量报告、地质说明书这类正式文档。表面上看Word比TXT规整,实际上坑更多。首先是隐藏格式,比如修订记录、批注、文本框里的内容,这些用常规的python-docx读取时很容易漏掉。我遇到过一份报告,关键的地层厚度数据放在文本框里,直接读段落根本读不到。

处理Word文档,我一般用python-docx配合lxml直接解析XML结构。具体做法是先把docx解压,然后遍历word/document.xml,把所有w:t标签里的文本提取出来,同时记录每个文本块所在的段落样式和层级。这样能保证不遗漏任何可见文本。对于表格,单独用docx的表格接口读取,保留行列结构。

还有一个容易被忽略的点是公式。地质报告里经常有品位计算公式、储量估算公式,这些公式在Word里可能是OMML格式或者嵌入的图片。OMML格式的公式可以用latex2mathml反向转换,图片格式的公式就只能走OCR了。我的建议是:公式内容单独存一个字段,不要和正文混在一起,因为RAG检索时公式的匹配逻辑和普通文本完全不同。

2.3 PDF文档:版面还原是核心难题

PDF是探矿资料里最让人头疼的格式。地勘报告通常有复杂的版面:双栏排版、跨页表格、图注混排、页眉页脚。用普通的PDF解析库直接抽文本,出来的结果往往是乱的。我试过用PyPDF2直接抽,结果表格里的数据全串行了,根本没法用。

后来换成了pdfplumber,情况好一些,但还是要做大量后处理。我的策略是分三步走:第一步用pdfplumber提取每个页面的文本块和坐标信息;第二步根据坐标判断文本块的阅读顺序,重建段落;第三步单独处理表格,用pdfplumber的表格提取功能,配合自定义的合并单元格逻辑。

这里有个关键参数需要根据实际文档调整:pdfplumber的table_settings里有个snap_tolerance,默认是3,对于扫描件或者低质量PDF,可能需要调到5甚至8才能正确识别表格线。我处理过一批老报告,表格线断断续续的,调到8之后识别率明显提升。

对于扫描件PDF,就必须上OCR了。我一般用PaddleOCR,对中文的识别效果比Tesseract好不少。但OCR有个问题:它会把页眉页脚也识别进去,而且对表格的支持有限。我的做法是先用OCR拿到全文,然后用正则匹配页眉页脚的特征(比如重复出现的页码、报告名称),批量删除。

2.4 网页文档:导航噪音和动态内容是主要障碍

探矿业务需要的网页数据主要是矿权公示、地质资料目录、行业标准页面。这些页面的共同特点是:正文占比低,导航栏、侧边栏、广告位占了大半。直接抓下来往知识库里灌,检索时全是噪音。

我的处理流程是:先用BeautifulSoup或lxml解析HTML,然后根据标签的语义和位置做正文提取。具体来说,优先保留article、main、content这类语义标签内的内容,删除nav、aside、footer、header里的内容。对于没有语义标签的页面,就用密度算法:统计每个div里的文本长度和链接密度,文本长且链接少的div大概率是正文。

动态加载的内容是另一个坑。有些矿权公示页面用JavaScript渲染表格,直接抓HTML只能拿到空壳。这种情况我一般用Playwright模拟浏览器行为,等页面加载完成后再提取内容。但要注意控制请求频率,避免对目标网站造成压力。

3. 从原始文档到RAG友好格式的完整实操流程

3.1 环境准备与工具选型

先把工具链列一下,这些都是我实际用过的,版本以我写这篇文章时的稳定版为准:

pip install chardet python-docx pdfplumber paddleocr beautifulsoup4 lxml playwright playwright install chromium

选型理由简单说一下。chardet做编码探测够用,虽然偶尔会误判,但配合人工复核问题不大。python-docx是处理Word文档的标准选择,配合lxml能覆盖绝大多数场景。pdfplumber在文本和表格提取上比PyPDF2强太多,虽然速度慢一些,但探矿资料对准确性要求高,慢点可以接受。PaddleOCR对中文和表格的识别效果目前是开源方案里最好的。Playwright处理动态网页比Selenium更稳定,而且API更简洁。

3.2 TXT文档清洗实操

先看编码处理的核心代码:

import chardet def detect_encoding(file_path): with open(file_path, 'rb') as f: raw = f.read(10000) result = chardet.detect(raw) return result['encoding'], result['confidence'] def read_txt_safely(file_path): encoding, confidence = detect_encoding(file_path) if confidence < 0.7: encoding = 'utf-8' lines = [] with open(file_path, 'r', encoding=encoding, errors='replace') as f: for i, line in enumerate(f): if '\ufffd' in line: lines.append((i, line, 'decode_error')) else: lines.append((i, line, 'ok')) return lines

这段代码的关键在于errors='replace',它会把无法解码的字节替换成\ufffd,这样至少不会中断读取。然后我们标记出所有包含替换字符的行,后续人工核对。

分隔符处理我一般用csv模块配合自定义的dialect:

import csv def detect_delimiter(sample_lines): delimiters = ['\t', ',', ';', '|'] counts = {d: 0 for d in delimiters} for line in sample_lines[:50]: for d in delimiters: counts[d] += line.count(d) return max(counts, key=counts.get)

实测下来,探矿TXT数据里制表符和逗号各占一半左右,分号偶尔出现,竖线很少见。检测出分隔符后,用csv.reader读取,列数不匹配的行单独输出到error_rows.txt。

3.3 Word文档结构化提取

Word文档的处理重点是保留层级结构。地质报告通常有章、节、子节三级标题,这些层级信息对RAG检索很重要,因为用户问“第二章第三节的储量计算方法”时,你需要能定位到具体章节。

from docx import Document from docx.table import Table from docx.text.paragraph import Paragraph def extract_word_content(doc_path): doc = Document(doc_path) content_blocks = [] for element in doc.element.body: if element.tag.endswith('p'): para = Paragraph(element, doc) style = para.style.name if para.style else 'Normal' text = para.text.strip() if text: content_blocks.append({ 'type': 'paragraph', 'style': style, 'text': text }) elif element.tag.endswith('tbl'): table = Table(element, doc) table_data = [] for row in table.rows: row_data = [cell.text.strip() for cell in row.cells] table_data.append(row_data) content_blocks.append({ 'type': 'table', 'data': table_data }) return content_blocks

这段代码遍历文档body里的所有元素,区分段落和表格。段落保留样式名,这样后续可以根据样式名判断标题层级。表格保留二维数组结构,方便后续转成Markdown表格。

对于文本框里的内容,需要额外处理:

from lxml import etree def extract_textboxes(doc_path): ns = {'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'} tree = etree.parse(doc_path) textboxes = tree.xpath('//w:txbxContent', namespaces=ns) results = [] for tb in textboxes: texts = tb.xpath('.//w:t/text()', namespaces=ns) results.append(''.join(texts)) return results

3.4 PDF解析与表格重建

PDF解析我分文本型和扫描型两种情况处理。文本型PDF直接用pdfplumber:

import pdfplumber def extract_pdf_text(pdf_path): with pdfplumber.open(pdf_path) as pdf: all_content = [] for page_num, page in enumerate(pdf.pages): text = page.extract_text() tables = page.extract_tables(table_settings={ 'snap_tolerance': 5, 'join_tolerance': 3, 'edge_min_length': 10 }) all_content.append({ 'page': page_num + 1, 'text': text, 'tables': tables }) return all_content

snap_tolerance控制表格线的吸附距离,值越大越容易把断开的线连起来。join_tolerance控制文本块的合并距离,对于字间距较大的文档可以适当调大。edge_min_length过滤掉太短的线段,避免把下划线误判为表格线。

扫描型PDF走OCR流程:

from paddleocr import PaddleOCR ocr = PaddleOCR(use_angle_cls=True, lang='ch') def ocr_pdf_page(image_path): result = ocr.ocr(image_path, cls=True) lines = [] for line in result[0]: text = line[1][0] confidence = line[1][1] if confidence > 0.8: lines.append(text) return '\n'.join(lines)

置信度阈值设0.8是个经验值,低于这个值的识别结果错误率明显上升。对于地质报告里的专业术语,OCR经常出错,比如把“矽卡岩”识别成“砂卡岩”,这种就需要建一个专业词典做后处理校正。

3.5 网页正文提取与清洗

网页处理我用Playwright加BeautifulSoup的组合:

from playwright.sync_api import sync_playwright from bs4 import BeautifulSoup def extract_web_content(url): with sync_playwright() as p: browser = p.chromium.launch() page = browser.new_page() page.goto(url, wait_until='networkidle') html = page.content() browser.close() soup = BeautifulSoup(html, 'lxml') for tag in soup(['nav', 'aside', 'footer', 'header', 'script', 'style']): tag.decompose() main_content = soup.find('article') or soup.find('main') or soup.find('div', class_='content') if main_content: text = main_content.get_text(separator='\n', strip=True) else: text = soup.get_text(separator='\n', strip=True) lines = [line.strip() for line in text.split('\n') if line.strip()] return '\n'.join(lines)

wait_until='networkidle'确保动态内容加载完成。删除nav、aside等标签是第一步过滤,然后优先找语义化的正文容器。如果找不到,就退而求其次用全文,但后续还要用密度算法进一步过滤。

4. 统一输出格式与RAG知识库对接

4.1 结构化输出格式设计

四类文档清洗完之后,需要统一成一种格式才能往RAG知识库里灌。我用的是一种简化的JSON结构:

{ "doc_id": "zk1201_2023", "source_type": "pdf", "source_path": "/data/reports/zk1201.pdf", "metadata": { "project": "某铜矿勘探", "year": 2023, "author": "某地质队" }, "blocks": [ { "type": "heading", "level": 1, "text": "第一章 矿区地质概况" }, { "type": "paragraph", "text": "矿区位于... " }, { "type": "table", "headers": ["钻孔编号", "见矿深度", "品位"], "rows": [["ZK1201", "125.3m", "0.85%"]] } ] }

这个结构的好处是保留了文档的层级和类型信息。RAG检索时,可以根据type字段决定是否参与检索,比如表格数据单独建索引,段落文本建全文索引。metadata字段用于过滤,比如只检索某个项目或某个年份的资料。

4.2 分块策略与检索优化

分块是RAG知识库的关键环节。探矿资料的分块不能简单按字数切,因为地质描述的逻辑单元往往是一个完整的段落或一个表格。我的策略是:

  • 标题单独成块,并作为后续段落的上下文
  • 段落按语义完整性切分,一般不超过500字
  • 表格整体作为一个块,如果太大就按行切分,但保留表头
  • 公式单独成块,附带前后文说明

具体实现时,我用langchain的RecursiveCharacterTextSplitter做基础切分,然后根据type字段做二次调整。表格块不参与文本切分,直接整体存入。标题块会和后续段落做关联,检索时如果命中段落,可以把所属标题一起返回,帮助用户理解上下文。

4.3 元数据提取与过滤

元数据对探矿资料的检索特别重要。用户经常需要按矿区、按年份、按矿种过滤。我的做法是在清洗阶段就提取好这些信息:

  • 从文件名提取:ZK1201_2023_铜矿.pdf→ 钻孔号、年份、矿种
  • 从文档内容提取:用正则匹配“矿区位于”、“勘探年份”、“矿种类型”等关键词
  • 从网页URL提取:矿权公示页面的URL通常包含地区代码和公示年份

这些元数据存入知识库后,检索时可以先做元数据过滤,再做向量检索,能大幅提升准确率。我实测下来,加了元数据过滤后,检索准确率从百分之六十多提升到了百分之八十五以上。

5. 常见问题排查与避坑经验实录

5.1 编码问题速查表

现象可能原因解决方法
中文显示为乱码文件是GB2312/GBK编码用chardet探测后指定编码读取
部分字符显示为问号编码不兼容或字节损坏用errors='replace'跳过,标记出错行
文件开头有BOMUTF-8 with BOM用utf-8-sig编码读取
混合编码文件由多段不同编码内容拼接逐段探测编码,分段读取

5.2 PDF表格识别失败的排查思路

PDF表格识别失败是最常见的问题。我的排查顺序是:

  1. 先看PDF是不是扫描件。用pdfplumber打开,如果extract_text()返回空或极少文本,基本就是扫描件,走OCR流程。
  2. 如果是文本型PDF但表格识别乱,调整table_settings参数。先调大snap_tolerance,再调join_tolerance。
  3. 如果表格线是虚线或点线,pdfplumber可能识别不到。这种情况用camelot的lattice模式试试,它对虚线表格的支持更好。
  4. 如果表格跨页,需要手动合并。我的做法是检测连续两页的表格列数是否一致,一致就合并。

5.3 Word文档内容遗漏的检查清单

  • 检查文本框:用lxml遍历w:txbxContent
  • 检查页眉页脚:遍历sectPr里的headerReference和footerReference
  • 检查批注:遍历w:comment标签
  • 检查修订:遍历w:ins和w:del标签
  • 检查嵌入对象:遍历w:object和w:pict标签

5.4 网页抓取的反爬应对

矿权公示网站通常有反爬机制。我的经验是:

  • 控制请求频率,每次请求间隔至少3秒
  • 使用真实的User-Agent,不要用默认的
  • 对于需要登录的页面,用Playwright保存登录状态
  • 如果遇到验证码,不要强行绕过,换其他数据源

注意:网页抓取要遵守目标网站的robots.txt规则,控制抓取频率,避免对目标网站造成负担。

5.5 RAG检索效果差的排查方向

如果清洗完灌入知识库后检索效果还是差,按这个顺序排查:

  1. 检查分块是否合理。用几个典型问题测试,看返回的块是否包含答案。
  2. 检查元数据是否完整。尝试按矿区、年份过滤,看是否能缩小范围。
  3. 检查向量模型是否适合中文地质文本。通用模型对专业术语的语义理解可能不够,考虑用地质领域语料微调。
  4. 检查是否有重复内容。同一份报告的不同版本可能都被灌进去了,导致检索结果重复。

6. 一些实操心得和后续扩展思路

这套清洗流程我前后迭代了三个版本,第一版图快,结果返工了两周。第二版加了人工复核环节,准确率上来了但效率低。第三版把人工复核集中在编码错误和表格识别这两个高风险环节,其他环节自动化,整体效率和质量都达到了可用水平。

有个小技巧分享:在清洗阶段就建立术语词典。把探矿业务里常见的专业术语、矿物名称、地层代号整理成一个词典,OCR后处理时用这个词典做校正,能显著降低错误率。我整理了一个大概两千词的词典,OCR识别准确率从百分之八十五提升到了百分之九十三左右。

后续如果还要扩展,我打算做两件事:一是把清洗流程做成可配置的管道,不同项目只需要改配置文件就能适配;二是把元数据提取做得更智能,用命名实体识别自动抽取矿区、矿种、品位这些关键信息,减少人工标注的工作量。

这个方向上的坑还有很多,比如多语言混排的文档、手写体扫描件、老式打字机打印的报告,每一种都有特殊的处理技巧。后面有机会再单独写。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询