首页 / 资讯中心 / 文章详情

探矿资料RAG清洗实战:从乱码到结构化知识库

探矿资料RAG清洗实战:从乱码到结构化知识库 ★ FEATURED ARTICLE
1. 探矿资料为什么必须做 RAG 清洗1.1 从一堆“打不开的乱码”说起做过探矿项目的人都有一个共同体会资料不是没有而是“有但没法用”。一个中型勘探项目从立项到结题积累下来的文档动辄上千份——地质报告是 Word钻孔柱状图是 PDF 扫描件化验数据是 TXT历史资料是某个老系统导出的网页快照甚至还有从老地质队档案室里翻拍的照片。这些资料堆在硬盘里文件名五花八门编码格式各不相同打开之后经常是一屏乱码。我接手过一个铜矿的补勘项目甲方给过来的资料包解压之后有 3.2GB里面 40% 的 TXT 文件用记事本打开全是问号和方块。一开始我以为是文件损坏后来用十六进制工具一看是 GBK 编码被当成了 UTF-8 读取。这种问题在探矿行业太常见了因为很多资料是十几年前用老版本软件生成的编码标准不统一加上中间经过多次拷贝、压缩、转存编码信息早就丢了。RAG检索增强生成在探矿业务里的价值说白了就是让这些“死资料”变成“活知识”。你问一个问题比如“XX 矿区 3 号勘探线 ZK302 钻孔在 450 米深处的铜品位是多少”系统能直接从 PDF 报告里把那段表格抠出来而不是让你自己去翻 200 页的文档。但前提是这些文档必须先经过清洗变成机器能理解的结构化文本。清洗没做好后面检索出来的全是乱码片段RAG 就成了“垃圾进、垃圾出”的典型。1.2 探矿资料的四种典型形态与清洗难点探矿业务的资料形态我大致归为四类每一类的清洗逻辑完全不同。TXT 文本看起来最简单实际上坑最多。探矿行业的 TXT 来源很杂——有化验室仪器导出的原始数据有老式测井仪生成的日志有从其他系统导出的 CSV 改后缀来的。编码问题首当其冲GBK、GB2312、UTF-8、UTF-8 with BOM、甚至 ISO-8859-1 都可能出现。更麻烦的是有些 TXT 里混着二进制控制字符比如测井数据里的换页符、退格符直接读进来会污染整个文本流。Word 文档地质报告的主力格式。难点不在读取而在结构还原。一份典型的勘探报告有封面、目录、章节标题、正文、表格、图注、附录Word 里的样式层级如果没被正确解析清洗出来的就是一大坨没有层次的文字。表格更是重灾区合并单元格、嵌套表格、跨页表格用 python-docx 直接读经常错位。还有公式探矿报告里的品位计算公式、储量估算公式很多是用 MathType 或 AxMath 插入的 OLE 对象普通解析器根本读不出来。PDF 文档分两种一种是原生 PDF文字可选一种是扫描件图片。原生 PDF 的清洗相对可控用 pdfplumber 或 PyMuPDF 能提取文字和表格但遇到多栏排版、页眉页脚、水印就会乱。扫描件 PDF 必须走 OCR而探矿报告里的 OCR 难度极高——地质图件上的标注字体小、背景复杂化验数据表格的线条密集通用 OCR 的准确率可能只有 70% 左右必须做后处理校正。网页资料探矿行业的历史资料很多是从老地质网站、内部 OA 系统、甚至论坛上采集的。网页清洗的核心是去噪——导航栏、广告、版权声明、相关链接这些都要去掉只保留正文。但探矿网页经常有表格和图片混排正文边界不好判断。另外网页的编码声明可能和实际编码不一致需要做编码探测。1.3 清洗质量直接决定 RAG 检索精度我做过一个对比实验同一批 200 份探矿报告一份不做清洗直接切块入库一份做完整清洗后入库。问同样 50 个问题不清洗的那组检索命中率只有 38%而且经常返回乱码片段清洗后的那组命中率到了 82%返回的内容可以直接引用。差距就是这么明显。清洗的核心目标有三个编码统一全部转成 UTF-8、结构还原标题、段落、表格、图注各归其位、噪声去除页眉页脚、水印、乱码字符。这三个目标做到了RAG 的检索精度才有保障。下面我按资料类型把每一类的清洗实操拆开讲。2. TXT 文件清洗编码探测与结构化预处理2.1 编码探测的三种可靠方法TXT 清洗的第一步永远是编码探测。我试过很多方案最后稳定下来的是“chardet charset-normalizer 人工规则”三层兜底。chardet 是老牌库速度快但对短文本和混合编码的准确率一般。charset-normalizer 是 requests 作者做的对中文编码的识别更准尤其是 GBK 和 GB18030 的区分。我的做法是先用 charset-normalizer 探测如果置信度低于 0.8再用 chardet 交叉验证两者不一致时用规则判断——比如文件里如果出现“锟斤拷”这种典型乱码基本可以确定是 GBK 被误读。from charset_normalizer import from_path import chardet def detect_encoding(file_path): # 第一层charset-normalizer result from_path(file_path).best() if result and result.chaos 0.2: return result.encoding # 第二层chardet 交叉验证 with open(file_path, rb) as f: raw f.read(100000) chardet_result chardet.detect(raw) if chardet_result[confidence] 0.7: return chardet_result[encoding] # 第三层规则兜底 for enc in [utf-8, gb18030, gbk, big5]: try: raw.decode(enc) return enc except UnicodeDecodeError: continue return utf-8注意gb18030 是 gbk 的超集优先用 gb18030 解码能覆盖更多生僻字。探矿报告里经常出现地名生僻字用 gbk 会报错gb18030 基本不会。2.2 控制字符与乱码的清理策略编码转对之后下一步是清理控制字符。探矿 TXT 里常见的控制字符有\x00空字符、\x0c换页符、\x08退格符、\x1aDOS 文件结束符。这些字符如果不清掉切块的时候会把一个完整的段落切断检索出来就是半截话。我的清理规则是保留\n、\t、\r其余 C0 控制字符全部替换为空格。另外连续的空格和换行要压缩——探矿数据里经常有大量连续空格用来对齐表格清洗时要保留结构但去掉冗余。import re def clean_control_chars(text): # 保留换行、制表符其余控制字符替换为空格 text re.sub(r[\x00-\x08\x0b\x0c\x0e-\x1f\x7f], , text) # 压缩连续空格保留单个空格用于分词 text re.sub(r[ \t]{3,}, , text) # 压缩连续空行最多保留两个换行 text re.sub(r\n{3,}, \n\n, text) return text.strip()乱码的清理要分情况。如果是“锟斤拷”这种编码转换错误产生的乱码基本不可逆只能整段丢弃或标记。如果是“?”这种替换字符说明原始编码信息已丢失需要结合上下文判断——比如“?矿”可能是“铜矿”这种要靠领域词典做模糊匹配修复。我在实际项目里维护了一个探矿领域词典包含常见矿物名、地层名、钻孔编号格式用来辅助修复。2.3 化验数据 TXT 的结构化提取化验数据 TXT 是探矿业务里最有价值的一类通常是仪器导出的固定格式。比如某化验室的输出格式是样品编号 Cu(%) Pb(%) Zn(%) Ag(g/t) ZK302-01 0.85 0.12 0.45 12.3 ZK302-02 1.23 0.08 0.67 15.8这种数据的清洗关键是识别列分隔符和表头。我一般用正则先匹配表头行确定列名和列数然后逐行解析。注意有些仪器导出的 TXT 用固定宽度对齐列之间是多个空格不是单个制表符这时候要用split()不带参数自动按任意空白分割而不是split(\t)。def parse_assay_txt(text): lines text.strip().split(\n) # 找到表头行包含 Cu、Pb 等元素符号 header_idx None for i, line in enumerate(lines): if re.search(rCu|Pb|Zn|Ag|Au, line): header_idx i break if header_idx is None: return None headers lines[header_idx].split() records [] for line in lines[header_idx1:]: if not line.strip(): continue values line.split() if len(values) len(headers): records.append(dict(zip(headers, values))) return records实操心得化验数据 TXT 里经常有“0.01”这种检出限表示解析时要保留原样不要转成数字否则会丢失“低于检出限”的语义。RAG 检索时用户问“有没有低于检出限的样品”这个信息很关键。3. Word 文档清洗样式解析与表格还原3.1 用 python-docx 还原文档层级Word 文档清洗的核心是还原层级结构。一份勘探报告标题层级通常是一级标题章、二级标题节、三级标题小节、正文、表格、图注。python-docx 可以通过paragraph.style.name拿到样式名比如“Heading 1”“Heading 2”“Normal”“Caption”。但实际项目里很多报告的样式是乱的——有人用加粗大字号模拟标题而不是用 Heading 样式。这时候需要结合字体大小、加粗、居中等属性做启发式判断。我的规则是如果段落居中、加粗、字号大于正文 2pt 以上判定为标题如果段落以“图”“表”开头且居中判定为图注或表注。from docx import Document from docx.shared import Pt def extract_word_structure(doc_path): doc Document(doc_path) structure [] body_font_size 12 # 默认正文大小后续动态调整 for para in doc.paragraphs: text para.text.strip() if not text: continue style_name para.style.name if para.style else is_heading Heading in style_name # 启发式判断 if not is_heading: runs para.runs if runs: font_size runs[0].font.size is_bold runs[0].font.bold is_center para.alignment 1 # CENTER if is_bold and is_center and font_size and font_size Pt(body_font_size 2): is_heading True if is_heading: level 1 if Heading in style_name: try: level int(style_name.split()[-1]) except: level 1 structure.append({type: heading, level: level, text: text}) elif text.startswith(图) or text.startswith(表): structure.append({type: caption, text: text}) else: structure.append({type: paragraph, text: text}) return structure3.2 表格提取的合并单元格处理Word 表格是清洗里最头疼的部分。python-docx 的table.rows和table.columns对合并单元格的支持不好合并单元格在遍历时会重复出现。我的做法是先构建一个二维网格记录每个单元格的实际位置和跨度然后按网格输出。def extract_table(table): # 构建网格 grid [] for row in table.rows: grid_row [] for cell in row.cells: grid_row.append(cell.text.strip()) grid.append(grid_row) # 去重合并单元格相邻相同内容且属于同一合并区域的 # 简化处理如果一行中连续多个单元格内容相同保留第一个 cleaned [] for row in grid: new_row [] prev None for cell in row: if cell ! prev: new_row.append(cell) prev cell cleaned.append(new_row) return cleaned注意这个方法对简单的横向合并有效纵向合并需要额外处理。探矿报告里的钻孔数据表经常有纵向合并一个钻孔多个样品清洗时要保留钻孔编号的归属关系否则检索“ZK302 的所有样品”会漏掉。更稳妥的方案是用docx2python库它对合并单元格的处理更完善能直接输出带行列索引的表格结构。我现在的项目里Word 表格提取基本都用 docx2python 打底python-docx 做补充。3.3 公式与特殊对象的处理探矿报告里的公式主要是品位计算、储量估算、坐标转换这几类。MathType 和 AxMath 插入的公式是 OLE 对象python-docx 读出来是空字符串。我的处理方案是先用docx2python提取如果发现公式位置为空再用olefile读取 OLE 对象的二进制流尝试提取 MathType 的 MTEF 数据转成 LaTeX。这个方案实现起来比较复杂实际项目中我更多是用“占位符人工补录”的方式——清洗时把公式位置标记为[FORMULA]然后让地质工程师在后期审核时补录。如果公式数量大可以考虑用 Mathpix 的 API 做批量转换但成本较高。另一个坑是 Word 里的嵌入图片。探矿报告里的图件地质图、剖面图、柱状图很多是嵌入的图片清洗时要把图片单独提取出来存到指定目录并在文本中保留图片引用位置。这样 RAG 检索到相关段落时可以同时返回图片路径方便用户查看。4. PDF 文档清洗原生解析与 OCR 校正4.1 原生 PDF 的文字与表格提取原生 PDF 的清洗我首选 PyMuPDFfitz它的文字提取精度高速度快对多栏排版的支持也比 pdfplumber 好。表格提取则用 pdfplumber它的extract_tables()对有线表格的识别很准。import fitz import pdfplumber def extract_pdf_text(pdf_path): doc fitz.open(pdf_path) full_text [] for page in doc: # 按块提取保留段落结构 blocks page.get_text(blocks) blocks.sort(keylambda b: (b[1], b[0])) # 按纵坐标、横坐标排序 for block in blocks: text block[4].strip() if text: full_text.append(text) return \n\n.join(full_text) 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: tables.append(table) return tables多栏排版的 PDF 是难点。PyMuPDF 的get_text(blocks)会把不同栏的文字混在一起。我的处理方式是先用page.get_text(dict)拿到每个文字块的坐标然后根据 x 坐标聚类判断栏数再按栏分别提取。一般探矿报告是双栏排版x 坐标会明显分成两组。4.2 扫描件 PDF 的 OCR 方案与后处理扫描件 PDF 必须走 OCR。我试过 Tesseract、PaddleOCR、EasyOCR最后在探矿场景下稳定用的是 PaddleOCR。原因很简单PaddleOCR 对中文的识别准确率明显高于 Tesseract而且对表格线条的干扰有一定的鲁棒性。但即使 PaddleOCR直接跑探矿报告的扫描件准确率也只有 75% 左右。问题主要出在地质图件的标注字体小、背景有纹理化验数据表格的线条密集OCR 会把线条识别成字符页眉页脚的水印干扰。我的后处理流程是图像预处理先做二值化、去噪、倾斜校正。探矿扫描件经常有倾斜用cv2.minAreaRect做倾斜校正能提升 5-10% 的准确率。区域分割用版面分析模型如 PP-Structure把页面分成文字区、表格区、图片区分别处理。表格校正OCR 出来的表格数据用正则匹配数字模式如品位值通常是 0.xx 到 xx.xx 之间把明显不是数字的识别结果标记出来人工复核。领域词典纠错用探矿领域词典做模糊匹配比如“铜矿”被识别成“同矿”“品位”被识别成“品住”自动纠正。from paddleocr import PaddleOCR import cv2 import numpy as np ocr PaddleOCR(use_angle_clsTrue, langch) def ocr_pdf_page(image_path): img cv2.imread(image_path) # 倾斜校正 gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) coords np.column_stack(np.where(gray 128)) angle cv2.minAreaRect(coords)[-1] if angle -45: angle 90 angle if abs(angle) 0.5: h, w img.shape[:2] M cv2.getRotationMatrix2D((w//2, h//2), angle, 1.0) img cv2.warpAffine(img, M, (w, h), flagscv2.INTER_CUBIC, borderModecv2.BORDER_REPLICATE) result ocr.ocr(img, 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 后的文本一定要做人工抽检。我的做法是每 50 页抽 1 页逐字核对统计错误率。如果错误率超过 10%就要调整预处理参数或换 OCR 引擎。探矿报告里的关键数据品位、坐标、储量错一个数字后果可能很严重。4.3 PDF 清洗中的页眉页脚与水印去除页眉页脚和水印是 PDF 清洗的常见噪声。页眉通常是报告名称或章节名页脚是页码和日期。这些内容如果不清掉会在每个切块里重复出现严重影响检索精度。我的去除策略是先统计每页顶部和底部 10% 区域的文字如果某个字符串在超过 80% 的页面中出现就判定为页眉或页脚从正文中剔除。水印的处理类似但水印通常是倾斜的、半透明的PyMuPDF 提取时可能会混入正文。这时候可以用page.get_text(dict)检查文字的方向和透明度属性把倾斜角度大于 30 度且颜色浅的文字块标记为水印。def remove_header_footer(doc): page_texts [] for page in doc: blocks page.get_text(blocks) page_texts.append(blocks) # 统计顶部和底部区域的重复文本 top_texts {} bottom_texts {} for blocks in page_texts: page_height max(b[3] for b in blocks) if blocks else 1 for b in blocks: text b[4].strip() if not text: continue if b[1] page_height * 0.1: top_texts[text] top_texts.get(text, 0) 1 elif b[3] page_height * 0.9: bottom_texts[text] bottom_texts.get(text, 0) 1 threshold len(page_texts) * 0.8 headers {t for t, c in top_texts.items() if c threshold} footers {t for t, c in bottom_texts.items() if c threshold} return headers, footers5. 网页资料清洗正文提取与编码统一5.1 网页正文提取的三种方案对比探矿行业的网页资料来源包括老地质网站、内部 OA、行业论坛、新闻页面。网页清洗的核心是正文提取把导航、广告、侧边栏、评论区去掉。我试过三种方案方案原理优点缺点适用场景正则规则按 HTML 标签和 class 名匹配快可控维护成本高网站改版就失效固定几个来源readability-lxml基于文本密度和链接密度通用性好对中文网页偶尔误判通用新闻、博客trafilatura多特征融合支持多语言准确率高支持元数据速度稍慢批量采集我现在的项目里主力是 trafilatura它对中文网页的正文识别准确率明显高于 readability。如果 trafilatura 提取失败再用 readability 兜底。import trafilatura from readability import Document import requests def extract_web_content(url): try: downloaded trafilatura.fetch_url(url) if downloaded: text trafilatura.extract(downloaded, include_tablesTrue, include_commentsFalse) if text and len(text) 200: return text except Exception as e: print(ftrafilatura failed: {e}) # 兜底readability try: resp requests.get(url, timeout10) resp.encoding resp.apparent_encoding doc Document(resp.text) return doc.summary() except Exception as e: print(freadability failed: {e}) return None5.2 网页表格与图片的处理探矿网页里的表格很多是化验数据或钻孔坐标。trafilatura 的include_tablesTrue能把表格转成文本但格式可能不理想。我的做法是先用 BeautifulSoup 单独提取所有table标签转成 Markdown 表格然后替换回正文。from bs4 import BeautifulSoup def html_table_to_markdown(html): soup BeautifulSoup(html, html.parser) tables soup.find_all(table) markdown_tables [] for table in tables: rows table.find_all(tr) md_rows [] for row in rows: cells row.find_all([td, th]) md_cells [cell.get_text(stripTrue) for cell in cells] md_rows.append(| | .join(md_cells) |) if md_rows: # 添加分隔行 header_sep | | .join([---] * len(md_rows[0].split(|)[1:-1])) | md_rows.insert(1, header_sep) markdown_tables.append(\n.join(md_rows)) return markdown_tables网页图片的处理主要是保留图片 URL 和 alt 文本。探矿网页里的地质图、剖面图alt 文本往往包含关键信息比如“XX 矿区 3 号勘探线剖面图”。清洗时要把图片位置标记为[IMAGE: url, alt: xxx]这样 RAG 检索到相关段落时可以同时返回图片链接。5.3 编码声明与实际编码不一致的修复网页编码问题比 TXT 更复杂因为 HTML 里有meta charsetxxx声明但实际编码可能不一致。比如声明是 UTF-8实际是 GBK浏览器可能能自动纠正但程序读取就会乱码。我的处理流程是先用requests获取响应检查resp.encoding如果resp.encoding是 ISO-8859-1requests 的默认值说明服务器没返回正确的编码这时候用resp.apparent_encoding重新设置。如果apparent_encoding也不对就用charset-normalizer对resp.content做探测。import requests from charset_normalizer import from_bytes def fetch_with_correct_encoding(url): resp requests.get(url, timeout10) if resp.encoding ISO-8859-1: # 尝试从 HTML meta 标签提取编码 import re match re.search(rbcharset[\]?([\w-]), resp.content[:2000]) if match: resp.encoding match.group(1).decode(ascii) else: # 用 charset-normalizer 探测 result from_bytes(resp.content).best() if result: resp.encoding result.encoding return resp.text注意有些老地质网站的编码是 GB2312但实际内容包含 GBK 的生僻字用 GB2312 解码会报错。这时候统一用 GB18030 解码它是 GB2312 和 GBK 的超集兼容性最好。6. 清洗后的切块与入库策略6.1 按语义切块而不是按字数切块清洗完的文本下一步是切块入库。很多人习惯按固定字数切块比如每 500 字一块。这在探矿业务里是灾难——一个钻孔的描述可能跨了 800 字按字数切会把“ZK302 在 450 米处见矿”和“品位 0.85%”切到两个块里检索时只能命中一个信息不完整。我的切块策略是按语义边界切块优先按标题切标题下的内容如果超过 800 字再按段落切表格单独成块不拆分图注和正文关联的合并成一块。切块时保留标题作为元数据这样检索时可以用标题做过滤。def semantic_chunk(structure, max_chunk_size800): chunks [] current_chunk [] current_size 0 current_heading None for item in structure: if item[type] heading: # 遇到新标题先保存当前块 if current_chunk: chunks.append({ heading: current_heading, text: \n.join(current_chunk) }) current_heading item[text] current_chunk [item[text]] current_size len(item[text]) elif item[type] table: # 表格单独成块 if current_chunk: chunks.append({ heading: current_heading, text: \n.join(current_chunk) }) chunks.append({ heading: current_heading, text: item[text], type: table }) current_chunk [] current_size 0 else: text item[text] if current_size len(text) max_chunk_size and current_chunk: chunks.append({ heading: current_heading, text: \n.join(current_chunk) }) current_chunk [text] current_size len(text) else: current_chunk.append(text) current_size len(text) if current_chunk: chunks.append({ heading: current_heading, text: \n.join(current_chunk) }) return chunks6.2 元数据设计让检索更精准切块之后每个块要附带元数据。探矿业务的元数据至少包括来源文件、文件类型、矿区名称、勘探线号、钻孔编号、章节标题、页码。这些元数据在检索时可以做过滤比如用户问“XX 矿区 ZK302 的化验数据”系统可以先用元数据过滤出 ZK302 相关的块再做向量检索精度会高很多。元数据的提取一部分可以从文件名和路径推断一部分要从正文里用正则抽取。比如钻孔编号通常是“ZK数字”的格式矿区名称通常在报告标题或页眉里。import re def extract_metadata(text, file_path): metadata { source: file_path, file_type: file_path.split(.)[-1].lower() } # 钻孔编号 drill_holes re.findall(rZK\d, text) if drill_holes: metadata[drill_holes] list(set(drill_holes)) # 勘探线号 lines re.findall(r\d号勘探线, text) if lines: metadata[exploration_lines] list(set(lines)) # 品位数据 grades re.findall(r(\d\.?\d*)\s*%, text) if grades: metadata[has_grade_data] True return metadata6.3 向量化与检索的配合切块和元数据准备好之后就是向量化。探矿业务的文本我建议用支持中文的 embedding 模型比如 BGE 或 M3E。向量化时把标题和正文拼在一起做 embedding这样标题的语义也能被检索到。检索时先用元数据做粗筛再用向量做精排。比如用户问“XX 矿区铜品位大于 1% 的样品”先用元数据过滤出 XX 矿区的块再用向量检索“铜品位大于 1%”的语义最后用正则从检索结果里提取具体的品位数值做精确过滤。这种“元数据向量规则”的三层检索比单纯向量检索的精度高很多。实操心得探矿业务的检索数值精确性很重要。用户问“品位 0.85%”向量检索可能返回 0.83%、0.87% 的块因为语义相近。这时候要在检索后加一层数值过滤把不匹配的块剔除。我一般用正则从块里提取数值和查询条件做精确比对。7. 常见问题与排查技巧实录7.1 编码问题速查表现象可能原因排查方法解决方案中文显示为“锟斤拷”UTF-8 被 GBK 解码用十六进制查看字节用 UTF-8 重新解码中文显示为“???”编码信息丢失检查文件头是否有 BOM用 charset-normalizer 探测部分生僻字乱码GBK 不支持该字定位乱码位置改用 GB18030 解码网页中文乱码meta 声明与实际不符对比 resp.encoding 和 apparent_encoding用 charset-normalizer 探测PDF 提取文字乱码PDF 内嵌字体编码问题用 PyMuPDF 查看字体信息改用 OCR 或换解析库7.2 表格提取错位的排查思路表格提取错位是最高频的问题。我的排查顺序是检查表格是否有合并单元格用 docx2python 或 pdfplumber 的可视化功能看表格结构是否被正确识别。检查是否有跨页表格跨页表格在 PDF 里会被分成两个表需要手动合并。我的做法是检测连续两页的表头是否相同如果相同就合并。检查是否有嵌套表格Word 里的嵌套表格python-docx 读出来会错位。这时候要用 docx2python它能正确处理嵌套。检查列分隔符有些表格用空格对齐不是制表符。解析时用split()不带参数而不是split(\t)。7.3 OCR 准确率提升的五个实操技巧图像分辨率至少 300 DPI低于 300 DPI 的扫描件OCR 准确率会明显下降。如果原始扫描件分辨率低先用超分辨率模型放大。二值化阈值动态调整探矿扫描件经常有深浅不一的背景固定阈值效果差。用 Otsu 自适应阈值。表格线去除OCR 前用形态学操作把表格线去掉避免线条被识别成字符。领域词典纠错维护一个探矿领域词典OCR 后用编辑距离做模糊匹配自动纠正。人工抽检每 50 页抽 1 页逐字核对统计错误率超过 10% 就要调整方案。7.4 清洗流程的自动化与监控清洗流程一旦跑起来就要做监控。我一般会记录每个文件的清洗状态成功、失败、部分成功。失败的文件要记录失败原因比如“编码探测失败”“OCR 置信度过低”“表格解析异常”。这些日志对于后续优化很重要。另外清洗后的文本要做质量抽检。我写了一个简单的质量评分函数从编码正确率、乱码比例、表格完整度、段落连贯性四个维度打分低于 60 分的文件自动标记人工复核。def quality_score(text): score 100 # 乱码比例 garbled len(re.findall(r[锟斤拷\ufffd], text)) if len(text) 0: garbled_ratio garbled / len(text) score - min(30, garbled_ratio * 1000) # 控制字符比例 control len(re.findall(r[\x00-\x08\x0b\x0c\x0e-\x1f], text)) if len(text) 0: control_ratio control / len(text) score - min(20, control_ratio * 500) # 段落连贯性平均段落长度 paragraphs [p for p in text.split(\n) if p.strip()] if paragraphs: avg_len sum(len(p) for p in paragraphs) / len(paragraphs) if avg_len 20: score - 20 elif avg_len 500: score - 10 return max(0, score)这套清洗流程我在三个探矿项目里跑过累计处理了 5000 多份文档。最深的体会是清洗不是一次性的工作而是一个持续迭代的过程。每接入一批新资料都可能遇到新的编码格式、新的表格样式、新的 OCR 难点。关键是要把清洗流程模块化每个环节都可以单独替换和优化同时做好日志和监控出了问题能快速定位。另外探矿业务的 RAG 清洗一定要有地质工程师参与。很多清洗规则比如“什么样的表格结构是合理的”“哪些数值范围是正常的”只有领域专家才能判断。纯技术团队做清洗很容易把关键信息洗掉或者把错误数据保留下来。我现在的做法是清洗规则由技术团队实现但规则的定义和验收由地质工程师把关每批数据清洗完抽检 5% 让工程师确认确认通过才入库。
阅读完成 · 觉得有帮助?
咨询建站