资讯详情

探矿RAG数据清洗实战:TXT、Word、PDF、网页高精度结构化处理

📅 2026/10/9 14:32:20 | 华诺云谱 👁 阅读
探矿RAG数据清洗实战:TXT、Word、PDF、网页高精度结构化处理
1. 探矿数据清洗到底难在哪从一堆乱码说起地质探矿行业的数据外行看着就是一堆文件内行才知道这里面的水有多深。我接触过的探矿项目里资料员发过来的压缩包解压之后往往是几十个TXT、上百个Word文档、几百兆的PDF扫描件外加几个从内部系统导出的网页存档。这些文件来自不同年代、不同软件、不同人手里格式混乱程度堪称灾难现场。先说TXT。探矿业务里的TXT文件很多是早期地质队员用记事本直接记录的钻孔编录数据编码格式五花八门。GBK、GB2312、UTF-8混着来有的甚至是从老式设备导出的二进制转文本打开就是满屏问号。更麻烦的是同一个项目里不同人用的分隔符都不一样有人用逗号有人用制表符还有人用连续空格。你直接拿去做RAG检索向量化出来的结果基本没法用。再说Word。地质报告、勘探设计方案、储量核实报告这些文档动辄几十页上百页里面夹杂着大量表格、公式、图片。表格里是品位数据、厚度数据公式是储量计算公式图片是钻孔柱状图。普通解析库遇到这些内容要么直接跳过要么把表格拍扁成一行乱码。我见过最离谱的情况一个Word文档里的表格被解析成了“孔号品位厚度”连在一起的一串字符完全丢失了行列对应关系。PDF的问题更突出。探矿行业大量历史资料是扫描件尤其是上世纪八九十年代的地质报告很多只有纸质版后来扫描成了PDF。这些PDF本质上是图片没有文字层直接解析出来就是空白。还有一些PDF虽然有文字层但排版极其复杂多栏、脚注、页眉页脚混在一起解析出来的文字顺序完全是乱的。网页数据相对好一点但探矿业务里的网页来源很杂。有内部OA系统导出的HTML有地质云平台的数据页面还有一些行业论坛的技术讨论帖。这些网页的DOM结构千差万别正文和导航栏、广告、评论区混在一起不做清洗直接抽取噪声比有效信息还多。这些问题的本质是什么是数据源头的异构性和非标准化。探矿业务链条长、参与方多、历史跨度大数据从产生那一刻起就没有统一的规范。而RAG系统的核心逻辑是“检索增强生成”检索的质量直接决定了生成的质量。如果喂进去的是乱码和噪声检索出来的片段就是垃圾大模型再强也生成不出靠谱的答案。所以做探矿业务的RAG知识库清洗环节不是可选项而是生死线。我个人的经验是清洗环节投入的时间应该占到整个RAG项目周期的百分之四十到五十。很多人一上来就急着搭向量数据库、调大模型参数结果发现检索效果怎么调都不行回头一看源头数据根本没洗干净。这篇文章要聊的就是怎么把探矿业务里这四类典型数据源——TXT、Word、PDF、网页——从乱码状态清洗成高精度检索可用的结构化文本。我会把每个环节的实操步骤、参数选择、踩过的坑都摊开来讲你照着做就能复现。2. 清洗方案的整体设计思路2.1 为什么不能一套方案打天下很多人做RAG清洗喜欢找一个万能工具指望它能把所有格式都处理好。我试过不少开源方案结论是不存在这样的工具。TXT、Word、PDF、网页这四类数据底层结构完全不同清洗策略必须分开设计。TXT的核心问题是编码和分隔符处理重点是字符集检测和格式归一化。Word的核心问题是嵌套结构表格、公式、图片、文本框层层嵌套处理重点是结构化抽取和语义保留。PDF的核心问题是文字层的有无和版面还原处理重点是OCR识别和阅读顺序重建。网页的核心问题是噪声过滤和正文定位处理重点是DOM解析和内容密度计算。这四类问题的技术栈重叠度很低。你用一个PDF解析库去处理TXT纯属杀鸡用牛刀还杀不好。反过来你用正则表达式去处理PDF版面基本是自讨苦吃。所以我的方案设计原则是分而治之统一输出。每一类数据源用专门的清洗管道处理最终输出统一的Markdown格式文本再进入后续的分块和向量化环节。2.2 清洗管道的分层架构我把整个清洗流程分成四层从下到上依次是第一层格式识别与路由层。这一层负责判断输入文件到底是什么格式。听起来简单实际上坑很多。比如一个文件后缀是.txt但内容可能是HTML代码一个文件后缀是.pdf但里面全是图片没有文字层。我的做法是结合后缀名和文件头魔数双重判断再对PDF做一次文字层探测决定走文本解析还是OCR通道。第二层内容抽取层。这一层针对不同格式调用不同的解析引擎。TXT用chardet做编码检测后直接读取Word用python-docx或docx2python做结构化抽取PDF用PyMuPDF做文字层抽取无文字层的走PaddleOCR网页用trafilatura或readability-lxml做正文抽取。第三层语义清洗层。抽取出来的原始文本还需要进一步清洗。包括去除页眉页脚、合并断行、修复乱码字符、统一标点符号、处理表格的Markdown化转换、公式的LaTeX化转换等。这一层是决定最终检索精度的关键。第四层质量校验层。清洗完的文本不能直接入库需要做质量抽检。我通常会统计几个指标有效字符占比、乱码字符数量、平均段落长度、表格转换成功率。低于阈值的文本打回重洗或人工介入。这个分层架构的好处是每一层的问题可以独立排查。检索效果不好的时候你可以快速定位是抽取环节丢了信息还是清洗环节引入了噪声还是分块策略不合理。2.3 输出格式为什么选Markdown清洗后的文本输出成什么格式这个决策影响很大。我对比过纯文本、JSON、HTML、Markdown四种方案最终选择Markdown理由有三条。第一Markdown对表格的支持足够好。探矿数据里大量品位表、厚度表、坐标表用Markdown表格能保留行列结构向量化的时候语义损失小。纯文本会把表格拍扁JSON虽然结构完整但可读性差HTML标签太多会干扰分词。第二Markdown的标题层级天然适合做分块。RAG分块最怕的就是把一个完整语义单元切碎。Markdown的##和###标题可以作为天然的分块边界保证每个块内部的语义完整性。第三Markdown对大模型友好。现在主流大模型在预训练阶段都见过大量Markdown格式的文本对Markdown结构的理解能力很强。你把清洗后的Markdown喂给大模型做生成它更容易识别出哪里是标题、哪里是正文、哪里是表格。注意Markdown转换过程中要特别小心表格里的合并单元格。探矿报告里的表格经常有跨行跨列的合并单元格直接转Markdown会丢失合并信息。我的做法是把合并单元格展开成重复值虽然冗余但保证了语义完整。3. TXT文件清洗编码检测与格式归一化实操3.1 编码检测的坑与解决方案TXT文件清洗的第一步是搞清楚它到底是什么编码。这个问题看似简单实际上我踩过的坑能写满一页纸。最常见的错误是用open(file, r)直接读Python默认用系统编码在Windows上通常是GBK在Linux上通常是UTF-8。同一个文件在不同机器上读出来的结果完全不一样。更隐蔽的问题是有些文件是混合编码的前面几行是GBK后面几行是UTF-8这是早期地质队员用不同软件编辑同一文件留下的历史遗留问题。我的解决方案是三步走。第一步用chardet或charset-normalizer做编码探测拿到一个置信度最高的编码。第二步用探测到的编码尝试读取如果读取过程中抛出UnicodeDecodeError说明文件是混合编码。第三步对混合编码文件用errorsreplace先读进来然后逐行做编码修复。import chardet from charset_normalizer import from_bytes def detect_and_read_txt(file_path): with open(file_path, rb) as f: raw f.read() # 先用charset-normalizer做检测 result from_bytes(raw).best() if result and result.encoding: encoding result.encoding else: # 回退到chardet detection chardet.detect(raw) encoding detection[encoding] or utf-8 # 尝试解码 try: text raw.decode(encoding) except UnicodeDecodeError: # 混合编码处理逐行解码 lines raw.split(b\n) decoded_lines [] for line in lines: try: decoded_lines.append(line.decode(encoding)) except UnicodeDecodeError: # 尝试用其他常见编码 for fallback in [utf-8, gbk, gb2312, latin-1]: try: decoded_lines.append(line.decode(fallback)) break except UnicodeDecodeError: continue else: decoded_lines.append(line.decode(utf-8, errorsreplace)) text \n.join(decoded_lines) return text这段代码的关键在于errorsreplace的兜底策略。有些乱码字符确实无法还原与其让程序崩溃不如用替换字符占位后续在语义清洗层再处理。3.2 分隔符归一化与结构化解析编码问题解决之后下一个问题是分隔符。探矿TXT数据常见的分隔方式有逗号分隔、制表符分隔、连续空格分隔、固定宽度分隔。固定宽度是最麻烦的因为不同文件的列宽定义不一样需要根据表头来推断。我的做法是先做分隔符探测。统计每一行中逗号、制表符、连续空格的出现次数如果某一种分隔符在多数行中出现的次数一致就判定为分隔符。对于固定宽度分隔用表头的位置信息来切分数据行。import re from collections import Counter def detect_delimiter(text): lines [l for l in text.split(\n) if l.strip()][:50] if not lines: return None candidates { comma: [l.count(,) for l in lines], tab: [l.count(\t) for l in lines], space: [len(re.findall(r\s{2,}, l)) for l in lines], } for name, counts in candidates.items(): if len(set(counts)) 1 and counts[0] 0: return name # 固定宽度检测 if all(len(l) len(lines[0]) for l in lines): return fixed_width return None分隔符归一化之后把TXT数据转成Markdown表格。这里有个细节探矿数据里经常有缺失值用空字符串或“-”表示。转Markdown表格的时候缺失值统一用空单元格表示不要用“N/A”之类的占位符因为向量化的时候这些占位符会引入噪声。3.3 数值字段的清洗与单位统一探矿TXT数据里大量是数值字段品位、厚度、坐标、高程。这些数值的格式也很乱有的用科学计数法有的带单位后缀有的用中文全角数字。我通常会做以下几件事。第一全角数字转半角。第二去除数值后面的单位后缀把单位信息提取到单独的列或元数据里。第三统一小数位数品位数据通常保留两位小数坐标数据保留六位小数。第四处理特殊值比如“未检出”统一转成“0”或空值“大于”转成对应的数值上限。def clean_numeric_field(value): if not value or value.strip() in [-, —, N/A, 无]: return # 全角转半角 value value.translate(str.maketrans(, 0123456789.)) # 提取数值和单位 match re.match(r([]?)\s*([\d.])\s*([a-zA-Z%‰]*)$, value.strip()) if match: prefix, num, unit match.groups() try: num float(num) if prefix : num num / 2 # 小于某值的处理策略 elif prefix : num num * 1.5 # 大于某值的处理策略 return f{num:.2f} except ValueError: return value return value提示小于和大于的处理策略要根据业务场景来定。品位数据里“0.01”通常意味着低于检出限我一般直接转成0.005或者0。但如果是厚度数据“50”可能意味着厚度超过测量上限这时候转成50还是保留原样需要跟业务方确认。4. Word文档清洗表格、公式与图片的语义保留4.1 表格抽取的三种策略对比Word文档里的表格是探矿数据的核心载体。一个钻孔编录表可能包含孔号、孔深、岩性、品位、厚度等十几列数据跨页是常态。表格抽取的质量直接决定了后续检索的精度。我试过三种策略各有优劣。策略一python-docx直接读取。这是最直接的方法通过document.tables遍历所有表格逐行逐列读取单元格文本。优点是速度快、依赖少。缺点是遇到合并单元格会重复读取或丢失数据遇到嵌套表格直接歇菜。策略二docx2python转换。这个库会把Word文档转成嵌套的Python列表结构表格的行列关系保留得比较好。合并单元格会展开成重复值嵌套表格也能处理。缺点是转换后的结构比较深需要写递归函数来遍历。策略三LibreOffice转HTML再解析。先用LibreOffice命令行把Word转成HTML然后用BeautifulSoup解析HTML表格。优点是表格结构保留得最完整合并单元格用rowspan和colspan表示。缺点是需要安装LibreOffice转换速度慢而且HTML标签会引入额外噪声。我的选择是普通表格用docx2python复杂表格用LibreOffice转HTML嵌套表格用python-docx手动递归处理。三种策略组合使用覆盖所有场景。from docx2python import docx2python def extract_tables_with_docx2python(file_path): result docx2python(file_path) tables [] def find_tables(obj): if isinstance(obj, list): for item in obj: find_tables(item) elif isinstance(obj, tuple): # docx2python的表格结构 tables.append(obj) find_tables(result.body) return tables4.2 公式处理从OMML到LaTeX的转换探矿报告里的公式主要是储量计算公式、品位加权平均公式、坐标转换公式。这些公式在Word里通常是用公式编辑器插入的底层是OMML格式。直接读取会得到一堆XML标签完全不可读。我的处理方案是用pandoc做OMML到LaTeX的转换。Pandoc对Word公式的支持相当好转换出来的LaTeX公式可以直接嵌入Markdown。pandoc input.docx -t markdown -o output.md --extract-media./media这条命令会把Word文档转成Markdown公式转成LaTeX图片提取到media目录。但pandoc的问题是它对表格的处理不如docx2python精细复杂表格会丢失结构。所以我的实际工作流是先用pandoc做整体转换拿到公式和正文再用docx2python单独抽取表格最后把两者合并。合并的时候要注意公式在正文中的位置pandoc转换后的Markdown里公式位置是准确的表格位置需要根据上下文来对齐。注意MathType公式和Word自带公式编辑器的OMML格式不一样。MathType公式在Word里是以OLE对象嵌入的pandoc无法直接转换。遇到MathType公式我的做法是用MathType的“转换公式”功能批量转成OMML然后再用pandoc处理。如果公式数量少手动截图用OCR识别也是一种办法但精度不保证。4.3 图片与图注的关联处理探矿报告里的图片主要是钻孔柱状图、剖面图、等值线图。这些图片本身包含大量信息但RAG系统目前对图片的处理能力有限。我的策略是图片本身不做OCR识别但把图注和图片周围的文字描述抽取出来作为图片的语义代理。具体做法是用python-docx遍历文档的段落遇到包含图片的段落时记录图片的位置然后向前和向后各找三个段落把其中的文字作为图片的上下文描述。图注通常在图片下方格式是“图1 某某矿区钻孔柱状图”这个直接抽取。from docx import Document def extract_images_with_captions(file_path): doc Document(file_path) images_info [] for i, para in enumerate(doc.paragraphs): if para._element.findall(.//{http://schemas.openxmlformats.org/drawingml/2006/main}blip): # 这是一个包含图片的段落 caption # 向后找图注 for j in range(i1, min(i4, len(doc.paragraphs))): text doc.paragraphs[j].text.strip() if text.startswith(图) or text.startswith(Figure): caption text break # 向前找上下文 context [] for j in range(max(0, i-3), i): text doc.paragraphs[j].text.strip() if text: context.append(text) images_info.append({ index: i, caption: caption, context: .join(context) }) return images_info这样处理之后图片虽然不能直接被检索但图片的图注和上下文描述可以被检索到。用户搜索“某某矿区钻孔柱状图”的时候能定位到图片所在的位置然后人工去查看原图。5. PDF清洗OCR与版面还原的实战细节5.1 文字层探测与OCR触发条件PDF清洗的第一步是判断这个PDF有没有文字层。有文字层的直接抽取没有文字层的走OCR。判断方法很简单用PyMuPDF打开PDF随机抽取几页看page.get_text()返回的文本长度。如果平均每页文本长度小于50个字符基本可以判定是扫描件。import fitz # PyMuPDF def has_text_layer(pdf_path, sample_pages5): doc fitz.open(pdf_path) total_pages len(doc) sample min(sample_pages, total_pages) total_chars 0 for i in range(sample): page doc[i] text page.get_text() total_chars len(text.strip()) avg_chars total_chars / sample doc.close() return avg_chars 50这个阈值50是我根据探矿报告的特点调的。有些PDF虽然有文字层但文字层是OCR软件后期加的质量很差错字连篇。这种情况我建议还是走OCR重新识别用更好的OCR引擎。5.2 版面分析与阅读顺序重建有文字层的PDF抽取文字不难难的是还原正确的阅读顺序。探矿报告常见双栏排版直接get_text()会把左右两栏的文字交错在一起读起来完全不通。我的解决方案是用PyMuPDF的get_text(dict)拿到每个文本块的坐标信息然后根据坐标做排序。双栏排版的判断逻辑是如果文本块的x坐标集中在两个区间就是双栏如果x坐标分布连续就是单栏。def extract_text_with_layout(page): blocks page.get_text(dict)[blocks] text_blocks [] for block in blocks: if block[type] 0: # 文本块 bbox block[bbox] text for line in block[lines]: for span in line[spans]: text span[text] text \n text_blocks.append({ bbox: bbox, text: text.strip() }) # 判断单栏还是双栏 x_centers [(b[bbox][0] b[bbox][2]) / 2 for b in text_blocks] page_width page.rect.width left_blocks [b for b in text_blocks if (b[bbox][0] b[bbox][2]) / 2 page_width / 2] right_blocks [b for b in text_blocks if (b[bbox][0] b[bbox][2]) / 2 page_width / 2] if len(left_blocks) 3 and len(right_blocks) 3: # 双栏排版 left_blocks.sort(keylambda b: b[bbox][1]) right_blocks.sort(keylambda b: b[bbox][1]) ordered left_blocks right_blocks else: # 单栏排版 ordered sorted(text_blocks, keylambda b: (b[bbox][1], b[bbox][0])) return \n.join([b[text] for b in ordered])这个逻辑对大多数探矿报告有效但遇到三栏排版或者不规则排版会失效。遇到这种情况我建议用pdfplumber的extract_text(layoutTrue)它内置了版面分析算法效果比手写排序好。5.3 扫描件OCR的参数调优扫描件OCR是PDF清洗里最耗时的环节。我用的是PaddleOCR原因是它对中文的识别精度比Tesseract高不少尤其是对表格和公式的识别。OCR的参数调优有几个关键点。第一det_db_thresh控制文本检测的阈值默认0.3对于字迹模糊的老报告可以调到0.2。第二rec_batch_num控制识别批大小GPU环境下可以调到16或32CPU环境保持8。第三use_angle_cls开启角度分类对于有倾斜的扫描件很有用。from paddleocr import PaddleOCR ocr PaddleOCR( use_angle_clsTrue, langch, det_db_thresh0.2, rec_batch_num16, show_logFalse ) def ocr_pdf_page(image_path): result ocr.ocr(image_path, clsTrue) lines [] for line in result[0]: text line[1][0] confidence line[1][1] if confidence 0.6: # 置信度过滤 lines.append(text) return \n.join(lines)提示OCR结果一定要做置信度过滤。低于0.6的识别结果大概率是错的与其让错误信息进入知识库不如直接丢弃。丢弃的文本可以在元数据里标记“OCR低置信度”方便后续人工复核。5.4 表格与图件的特殊处理PDF里的表格处理比Word更麻烦因为PDF没有表格结构信息只有文字和线条的坐标。我的做法是用pdfplumber的extract_tables()方法它基于线条和文字对齐来推断表格结构。import pdfplumber def extract_pdf_tables(pdf_path): tables [] with pdfplumber.open(pdf_path) as pdf: for page in pdf.pages: page_tables page.extract_tables() for table in page_tables: # 清理表格数据 cleaned [] for row in table: cleaned_row [cell.strip() if cell else for cell in row] cleaned.append(cleaned_row) tables.append(cleaned) return tables对于图件PDF里的图件通常是矢量图或位图。矢量图可以用page.get_drawings()拿到线条信息但重建图件意义不大。我的策略和Word一样抽取图注和上下文图件本身不做处理。6. 网页数据清洗正文抽取与噪声过滤6.1 正文抽取工具选型对比探矿业务的网页数据来源主要有三类内部OA系统导出的HTML、地质云平台的数据页面、行业论坛的技术帖。这三类页面的DOM结构差异很大正文抽取策略也需要调整。我对比过四个工具trafilatura、readability-lxml、newspaper3k、goose3。实测下来trafilatura的综合表现最好对中文网页的支持也最到位。readability-lxml速度快但有时候会漏掉正文newspaper3k对中文分词有问题goose3已经很久没维护了。import trafilatura def extract_web_content(html_content): # trafilatura的抽取 text trafilatura.extract( html_content, include_commentsFalse, include_tablesTrue, include_imagesFalse, output_formatmarkdown, favor_precisionTrue ) return textfavor_precisionTrue这个参数很关键。默认情况下trafilatura会尽量多抽取内容但探矿业务的网页里导航栏、侧边栏、评论区都是噪声宁可少抽一点也要保证精度。6.2 动态网页的抓取策略有些地质云平台的数据页面是JavaScript动态渲染的直接请求HTML拿不到数据。这种情况需要用Playwright或Selenium做浏览器渲染。from playwright.sync_api import sync_playwright def fetch_dynamic_page(url): with sync_playwright() as p: browser p.chromium.launch(headlessTrue) page browser.new_page() page.goto(url, wait_untilnetworkidle) # 等待特定元素加载 page.wait_for_selector(.data-table, timeout10000) html page.content() browser.close() return htmlwait_untilnetworkidle表示等待网络请求空闲后再抓取wait_for_selector确保关键数据元素已经渲染。这两个条件配合使用基本能拿到完整的动态内容。注意动态网页抓取要控制频率避免对目标服务器造成压力。我的做法是每次请求间隔2到3秒批量抓取时用队列控制并发数不超过3。6.3 表格与列表的结构化保留网页里的表格用trafilatura的include_tablesTrue可以保留成Markdown表格。但网页表格经常有合并单元格、嵌套表格、表头表尾重复等问题转换效果不如Word和PDF。我的补充方案是用BeautifulSoup单独解析HTML表格处理合并单元格后再转Markdown。from bs4 import BeautifulSoup def parse_html_table(table_html): soup BeautifulSoup(table_html, html.parser) table soup.find(table) if not table: return rows [] for tr in table.find_all(tr): cells [] for td in tr.find_all([td, th]): text td.get_text(stripTrue) colspan int(td.get(colspan, 1)) rowspan int(td.get(rowspan, 1)) cells.append({ text: text, colspan: colspan, rowspan: rowspan }) rows.append(cells) # 展开合并单元格 expanded expand_merged_cells(rows) # 转Markdown md_lines [] for i, row in enumerate(expanded): md_lines.append(| | .join(row) |) if i 0: md_lines.append(| | .join([---] * len(row)) |) return \n.join(md_lines)合并单元格的展开逻辑是遇到colspan大于1的单元格复制成多个相同值的单元格遇到rowspan大于1的单元格在后续行对应位置填充相同值。这样虽然冗余但保证了每一行的列数一致Markdown表格不会错位。7. 常见问题与排查技巧实录7.1 清洗效果自查清单清洗完一批数据之后我通常会做一轮快速自查。下面这张表是我总结的检查项和判断标准你可以直接拿去用。检查项判断标准不达标时的处理有效字符占比大于85%检查是否有大量乱码或空白乱码字符数量每万字少于10个回溯编码检测环节表格转换成功率大于90%检查合并单元格处理逻辑公式转换成功率大于80%检查OMML转LaTeX流程平均段落长度50到500字之间过短说明断行没合并过长说明分块有问题标题层级完整性有明确的章节结构检查Word/PDF的标题样式是否被正确识别7.2 典型问题速查表问题现象可能原因排查方法解决方案TXT读取全是问号编码检测错误用chardet重新检测手动指定编码或逐行解码Word表格数据错位合并单元格处理不当打印表格行列数用docx2python或LibreOffice转HTMLPDF文字顺序混乱双栏排版未识别查看文本块坐标用pdfplumber的layout模式OCR识别率低扫描件质量差查看原始图片分辨率提高扫描分辨率或调整OCR阈值网页正文抽取不全动态渲染未等待查看HTML源码用Playwright等待元素加载公式转LaTeX失败MathType格式不支持检查公式类型先转OMML再用pandoc表格转Markdown错位列数不一致检查每行列数展开合并单元格清洗后文本过短抽取环节丢内容对比原始文件检查解析库的配置参数7.3 我踩过的三个大坑第一个坑用默认编码读TXT。早期我图省事直接用open(file, r)读TXT结果在Windows上读GBK文件正常在Linux上读同样的文件全是乱码。后来改成二进制读取加编码检测问题解决。这个坑的本质是不要依赖系统默认值所有编码相关的操作都要显式指定。第二个坑用PDF解析库处理扫描件。有一次拿到一批PDF用PyMuPDF抽取文字结果全是空白。我以为是库的问题换了好几个库都一样。后来才发现这些PDF是扫描件根本没有文字层。这个坑教会我处理PDF之前一定要先探测文字层有文字层和没文字层是两条完全不同的技术路线。第三个坑网页表格直接转Markdown。网页表格里的合并单元格直接转Markdown会错位我一开始没注意导致检索出来的表格数据行列对不上。后来加了合并单元格展开逻辑问题解决。这个坑的教训是HTML表格和Markdown表格的结构模型不一样转换的时候必须做结构适配。7.4 性能优化的几个实用技巧清洗大批量数据的时候性能是个绕不开的问题。我总结了几条实用技巧。第一批量处理用多进程而不是多线程。文本清洗是CPU密集型任务Python的GIL会限制多线程的并行效果。用multiprocessing.Pool可以充分利用多核CPU。第二OCR结果做缓存。同一批扫描件如果重复清洗OCR结果可以缓存到本地避免重复计算。我用的是joblib.Memory简单好用。第三大文件分块读取。有些TXT文件几百兆一次性读进内存会爆。用生成器逐行读取内存占用可以控制在几十兆以内。第四PDF按页并行处理。PyMuPDF支持按页打开可以把不同页分配给不同进程处理最后合并结果。对于几百页的报告速度提升很明显。from multiprocessing import Pool import fitz def process_pdf_page(args): pdf_path, page_num args doc fitz.open(pdf_path) page doc[page_num] text page.get_text() doc.close() return page_num, text def parallel_pdf_extract(pdf_path, num_workers4): doc fitz.open(pdf_path) total_pages len(doc) doc.close() with Pool(num_workers) as pool: args [(pdf_path, i) for i in range(total_pages)] results pool.map(process_pdf_page, args) # 按页码排序 results.sort(keylambda x: x[0]) return \n.join([text for _, text in results])提示多进程处理PDF的时候每个进程都要重新打开PDF文件这会带来额外的IO开销。如果PDF文件不大可以把整个文件读进内存再传给子进程。如果文件很大按页打开是更稳妥的做法。8. 清洗后的分块策略与检索精度验证8.1 分块大小与重叠度的选择清洗完的Markdown文本不能直接整篇向量化需要分块。分块大小和重叠度的选择直接影响检索精度。我的经验值是分块大小500到800个中文字符重叠度100到150个字符。这个范围是经过多轮测试得出的。分块太小语义不完整检索出来的片段缺乏上下文分块太大噪声比例上升向量化的语义焦点模糊。探矿数据的特殊性在于表格和公式比较多。表格分块的时候要保证一个表格不被切断如果表格超过分块大小按行拆分并在每个分块里重复表头。公式分块的时候要保证公式和它的解释文字在同一个块里。def chunk_markdown(text, chunk_size600, overlap120): # 按标题层级优先分块 sections re.split(r\n(?#{1,3}\s), text) chunks [] for section in sections: if len(section) chunk_size: chunks.append(section) else: # 长段落按句子边界切分 sentences re.split(r(?[。]), section) current_chunk for sentence in sentences: if len(current_chunk) len(sentence) chunk_size: current_chunk sentence else: if current_chunk: chunks.append(current_chunk) current_chunk sentence if current_chunk: chunks.append(current_chunk) # 添加重叠 overlapped_chunks [] for i, chunk in enumerate(chunks): if i 0: prev_tail chunks[i-1][-overlap:] chunk prev_tail chunk overlapped_chunks.append(chunk) return overlapped_chunks8.2 检索精度验证方法清洗和分块做完之后怎么验证效果我通常用三个指标召回率、精确率、MRR平均倒数排名。召回率衡量的是在所有相关片段中检索系统能找回多少。精确率衡量的是检索回来的片段中有多少是真正相关的。MRR衡量的是第一个相关片段出现在检索结果的第几位。验证方法是人工构造一批测试查询每个查询标注好应该匹配的片段。然后跑检索统计指标。召回率低于80%说明清洗或分块有问题精确率低于70%说明噪声太多MRR低于0.6说明排序算法需要调整。def evaluate_retrieval(queries, ground_truth, retriever, top_k10): recall_sum 0 precision_sum 0 mrr_sum 0 for query, relevant_ids in queries.items(): results retriever.search(query, top_ktop_k) retrieved_ids [r[id] for r in results] # 召回率 hits set(retrieved_ids) set(relevant_ids) recall len(hits) / len(relevant_ids) if relevant_ids else 0 # 精确率 precision len(hits) / len(retrieved_ids) if retrieved_ids else 0 # MRR mrr 0 for i, rid in enumerate(retrieved_ids): if rid in relevant_ids: mrr 1 / (i 1) break recall_sum recall precision_sum precision mrr_sum mrr n len(queries) return { recall: recall_sum / n, precision: precision_sum / n, mrr: mrr_sum / n }8.3 清洗质量对检索效果的影响分析我做过一组对比实验同一批探矿数据一组做完整清洗一组只做简单清洗然后跑同样的检索测试。结果差异非常明显。完整清洗的召回率是87%简单清洗的召回率只有52%。差距主要来自三个方面表格数据在简单清洗中被拍扁导致品位和厚度的对应关系丢失公式在简单清洗中变成乱码导致储量计算相关的查询完全无法匹配页眉页脚在简单清洗中没有去除导致大量噪声片段被检索出来。这个实验说明一个道理RAG系统的检索精度上限是由清洗质量决定的。你后面用再好的向量模型、再精细的排序算法如果源头数据是脏的效果提升空间非常有限。与其在检索环节反复调参不如回头把清洗环节做扎实。提示清洗质量验证不需要等到整个知识库建完再做。每清洗完一批数据抽10到20个查询做一轮快速验证发现问题及时调整清洗策略。这样比全部做完再返工要高效得多。8.4 持续迭代的清洗管道维护探矿业务的数据是持续产生的清洗管道不是一次性的项目而是需要持续维护的基础设施。我的做法是把清洗管道脚本化、配置化每次新数据进来只需要改配置不需要改代码。具体来说把编码检测规则、分隔符规则、表格处理规则、OCR参数都抽成配置文件。不同项目的数据特点不一样通过配置文件来适配代码保持稳定。# config.yaml txt: encoding_fallback: [utf-8, gbk, gb2312] delimiter_candidates: [,, \t, ] numeric_precision: 2 word: table_engine: docx2python formula_converter: pandoc image_caption_pattern: ^图\d pdf: text_layer_threshold: 50 ocr_engine: paddleocr ocr_confidence_threshold: 0.6 layout_mode: auto web: extractor: trafilatura favor_precision: true dynamic_wait: networkidle配置文件的好处是换一个探矿项目只需要调整配置参数清洗管道的核心逻辑不用动。我维护的清洗管道已经跑了三年多处理了十几个探矿项目的数据代码主体基本没变过改的都是配置。这个内容后续还可以这样扩展把清洗管道和RAG知识库的更新流程打通新数据自动触发清洗、分块、向量化、入库的全流程。再进一步可以加一个清洗质量监控面板实时显示每批数据的清洗指标低于阈值自动告警。这些是我下一步打算做的事等跑通了再写一篇分享。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑