大模型知识库建设前置之数据清洗

Day09|大模型知识库建设前置之数据清洗——数据是 AI 的粮食,垃圾进则垃圾出
前言:教程里的语料是干净 txt,企业里是一锅粥
先甩一个可能反直觉的数字。
多年业界调研都指向同一件事:数据团队 / 数据科学家 60%~80% 的时间,花在"找数据、洗数据"上,而不是建模。AI 项目失败,根因也大多落在数据质量,而不是模型不够强。
这个数字我第一次看到时是不信的。直到我第一次给公司搭企业 RAG,自信满满上了线——结果用户一问,AI 一本正经地引用了一份 2018 年的过期表格,而那份表格,藏在一个扫描件 PDF 的第三页。
从那以后我学会一件事:先洗数据,再谈模型。
教程里的语料,永远是干干净净的 txt。企业里的语料呢?是 Word 批注、Excel 合并单元格、双栏 PDF、PPT 截图、扫描件 jpg 一锅粥。十几种格式,每种脏法都不一样。
这就引出 AI 领域最古老也最狠的一条铁律——Garbage In, Garbage Out(垃圾进,垃圾出)。
很多人觉得大模型这么强,数据脏一点无所谓吧?恰恰相反。模型越强,对数据越敏感。 弱模型遇到脏数据,输出平庸;强模型遇到脏数据,输出的是"自信的胡说"——危害大十倍。
Day06 我讲过 RAG 检索。今天补上拼图里缺的那一块:为什么你 RAG 效果差,八成不是检索算法的锅,是语料没洗干净。
这篇文章按企业真实场景走一遍:
- 先建诊断框架——脏数据的"五宗罪"
- 按文件类型分类实战——纯文本、办公文档、PDF 与图片,各怎么洗
- 治本——如何从源头避免脏数据,而不是事后擦屁股

PART 01:脏数据的五宗罪——先学会体检,再动刀
动手洗之前,先搞清楚"脏"有哪些种类。我把企业里最常见的脏数据归成五宗罪:
- ① 缺失与空值:字段为空、只有标题没有正文、只有模板没有内容
- ② 重复数据:同一份文档被爬 10 次,严重污染检索结果和向量库
- ③ 格式混乱:PDF / 网页提取后的换行错乱、表格散架、段落粘连
- ④ 编码与噪音:乱码、HTML 标签残留、不可见字符、全角半角混用
- ⑤ 隐私与合规:手机号、身份证、邮箱、地址直接进语料——最容易忽略,也最致命
记住这五条。后面不管你处理什么文件类型,都是在跟这五种脏打交道,只是每种格式的"重灾区"不一样。
一个心法:清洗前先体检,别上来就套模板。 统计一下缺失率、重复率,随机抽几条看看长什么样,再决定怎么洗。

PART 02:纯文本与结构化数据(txt / markdown / html / json / csv)——别以为最简单就最干净
从最简单的说起。但最简单,不等于最干净。
txt:第一杀手是编码
txt 的头号问题是编码。同一份文件,可能是 GBK,可能是 UTF-8,还可能是带 BOM 的 UTF-8。读错编码,就是满屏乱码。
import re
import unicodedata
from charset_normalizer import from_path
def read_txt_clean(path: str) -> str:
# 1. 自动探测编码,统一读成 utf-8
raw = from_path(path).best().bytes.decode("utf-8", errors="ignore")
# 2. NFKC 规范化:全角转半角、干掉零宽字符、统一兼容字符
text = unicodedata.normalize("NFKC", raw)
# 3. 折叠多余空白和连续空行
text = re.sub(r"[ \t]+", " ", text)
text = re.sub(r"\n{3,}", "\n\n", text)
return text.strip()
这里有个坑我踩过无数次:全角逗号","和半角逗号",",看起来一样,其实是两个字符。 它们会让去重完全失效——你以为去重了,结果同一句话因为一个标点存了两份。
NFKC 规范化就是救场的。它会把全角字符统一转成半角,让"看起来一样"的东西"真的变一样"。
markdown / html:导航栏和广告是噪音重灾区
网页抓取下来的内容,正文往往混着导航栏、页脚、广告、JS 代码。直接喂给模型,等于让 AI 读一份贴满小广告的报纸。
别自己写正则去抠。用 trafilatura,专门做网页正文提取,一行搞定:
import trafilatura
def extract_main_content(html: str) -> str:
# 一行干掉导航栏、广告、页脚,只留正文
text = trafilatura.extract(
html, include_comments=False, include_tables=True
)
return text or ""
json / csv:schema 不一致是慢性毒药
结构化数据看似规整,真正的杀手是字段对不上:这一批 title 是字符串,下一批变成了 null;这行的 date 是 2024-01-01,下一行是 2024年1月1日。
别等数据进了向量库才发现问题。入库前就用 schema 校验卡住它,像代码 CI 一样:
import pandas as pd
import pandera as pa
from pandera import Column, DataFrameSchema, Check
schema = DataFrameSchema({
"title": Column(str, checks=Check.str_length(min_value=1)), # 非空
"content": Column(str, checks=Check.str_length(min_value=10)), # 正文至少10字
"date": Column(pa.DateTime, nullable=True),
})
df = pd.read_csv("articles.csv")
try:
schema.validate(df, lazy=True)
print("✅ 数据合规")
except pa.errors.SchemaErrors as err:
print(f"❌ 拦截 {len(err.failure_cases)} 行脏数据")
脏数据在门口就被拦下,总好过它溜进模型后再让你 debug 一整夜。
PART 03:办公文档三兄弟(Word / Excel / PPT)——企业语料的主战场
企业里的知识,八成躺在办公文档里。这一节是真正的实战。

Word:批注和修订会"复活"脏内容
Word 文档最坑的地方,是修订和批注。你以为删掉的内容,其实还躺在文档里,只是被标记成"删除态"。如果你的提取代码不处理,已经删掉的脏内容会"复活",重新进语料。
from docx import Document
def extract_docx_text(path: str) -> str:
doc = Document(path)
paragraphs = []
for p in doc.paragraphs:
text = p.text.strip()
if text: # python-docx 的 .text 默认不含已删除修订,但仍建议先"接受所有修订"
paragraphs.append(text)
return "\n\n".join(paragraphs)
实操建议:批量处理前,先用脚本或 Word 自带功能"接受所有修订",再提取。别带着满篇红线就上。
Excel:合并单元格和公式是两个地雷
Excel 有两个经典地雷:
地雷一:公式 vs 公式结果。 一个单元格存的是 =A1+B1 这个公式字符串,还是它算出来的 42?读错就是灾难——下游拿到的全是公式,根本不是数据。
地雷二:合并单元格。 看起来一个值占了好几格,实际只有左上角有值,其它是空。直接读会丢一大片数据。
import openpyxl
def read_excel_values(path: str):
# data_only=True:取公式计算结果,不取公式字符串
wb = openpyxl.load_workbook(path, data_only=True)
rows = []
for ws in wb.worksheets:
for row in ws.iter_rows(values_only=True):
# 过滤整行全空的废行
if any(c is not None and str(c).strip() for c in row):
rows.append(row)
return rows
data_only=True 这一个参数,能帮你省掉无数个"为什么我读出来全是 None"的深夜。
PPT:每页都碎,备注里藏着金子
PPT 的特点是碎片化——每页就那么几个字,单独看几乎没有信息量。但有个地方常被忽略:备注栏。演讲者把真正想说的、详细的解释,都写在备注里。丢掉备注,等于丢掉一半内容。
from pptx import Presentation
def extract_pptx(path: str):
prs = Presentation(path)
pages = []
for i, slide in enumerate(prs.slides, 1):
texts = []
for shape in slide.shapes:
if shape.has_text_frame:
texts.append(shape.text_frame.text)
note = slide.notes_slide.notes_text_frame.text if slide.has_notes_slide else ""
pages.append({"page": i, "content": "\n".join(texts), "note": note})
return pages
办公文档的"脏",往往不在字面,藏在格式结构里。所以你必须按结构去解析,而不是当纯文本一把梭。
PART 04:最硬的两块骨头——PDF 与图片(pdf / png / jpg / 扫描件)
终于来到企业最痛的地方。前面那些都算好洗的,PDF 和图片才是真正费时费力的。

PDF:第一步永远先判断"它有没有文本层"
PDF 分两种,清洗路线完全不同:
- 文本层 PDF(数字原版导出的):可以直接提取文字
- 扫描件 PDF(纸质扫描 / 拍照转的):没有文本层,必须走 OCR
怎么判断?提一下试试,提不出字,就是扫描件:
import pdfplumber
def is_scanned_pdf(path: str) -> bool:
with pdfplumber.open(path) as pdf:
text = pdf.pages[0].extract_text() or ""
return len(text.strip()) < 20 # 提不出字 → 大概率扫描件
文本层 PDF 用 pdfplumber,还能把表格还原成结构化数据:
def extract_pdf(path: str):
all_text, tables = [], []
with pdfplumber.open(path) as pdf:
for page in pdf.pages:
all_text.append(page.extract_text() or "")
tables.extend(page.extract_tables()) # 表格单独还原,保留行列结构
return "\n".join(all_text), tables
PDF 的坑还有一堆:双栏排版(按行读会跨栏串台)、跨页断句(一句话被切成两半)、页眉页脚噪音。这些没有银弹,得针对你的文档类型调参。
复杂 PDF 别硬啃——上 MinerU
pdfplumber 提文本、自己再接 OCR,是"拼积木"。简单 PDF 够用,但企业里的 PDF 常常是双栏论文、嵌套表格、数学公式、图文混排——每个坑都得自己填,填到怀疑人生。
这种场景别硬拼,直接上 MinerU——上海人工智能实验室开源的文档解析工具,一个工具把 PDF(也支持 Word / PPT)变成结构化 Markdown,专治各种"硬骨头"。
它一个工具干完四件事:
- 版面分析:自动识别标题、段落、图、表、页眉页脚,恢复正确阅读顺序——双栏不再串台
- 表格还原:复杂表格直接输出 Markdown 表格,行列结构完整
- 公式识别:数学公式转成 LaTeX——这是 pdfplumber 想都别想的能力
- OCR:扫描件、图片也能解析,不用你手动分流文本层
输出是干干净净的 Markdown,直接喂 RAG。前面"文本层还是扫描件"的判断,MinerU 内部自动处理,你根本不用操心。
用法也很省心(参数以官方仓库 opendatalab/MinerU 为准):
pip install -U "mineru[core]"
mineru -p report.pdf -o output_dir/
那什么时候用 pdfplumber,什么时候上 MinerU?一句话:简单文本 PDF 用 pdfplumber(轻、快),复杂或扫描件 PDF 用 MinerU(重,但省心)。 当你手里有一堆合同、报告、论文 PDF 要清洗进 RAG,MinerU 基本是当下绕不开的选择。
图片 / 扫描件:OCR 的老路,和多模态大模型的新路
图片(png / jpg)和扫描件,本质都得把图变成字。有两条路:
老路:OCR。 pytesseract 是通用选择,中文场景 PaddleOCR 更准。
新路(2026 主流):直接丢给多模态大模型读图。 GPT-4o、Claude 这类模型能直接"看懂"图片,连图表、公式、手写都能读。省掉一整套 OCR 管线:
import base64
from anthropic import Anthropic
client = Anthropic()
def image_to_text(image_path: str) -> str:
with open(image_path, "rb") as f:
b64 = base64.b64encode(f.read()).decode()
resp = client.messages.create(
model="claude-opus-4-8", # 替换成你在用的多模态模型
max_tokens=2000,
messages=[{
"role": "user",
"content": [
{"type": "image",
"source": {"type": "base64", "media_type": "image/png", "data": b64}},
{"type": "text",
"text": "把这张图里的文字、表格、公式结构化提取出来,保留原始顺序。"}
]
}]
)
return resp.content[0].text
这条路还有一个隐形好处:多模态模型理解内容,而 OCR 只是"识别"字符。遇到模糊图、复杂表格,差距立现。
最后提一个隐私坑:图片的 EXIF 信息可能带 GPS 定位、拍摄设备、时间。上传图片给模型前,先把 EXIF 剥掉,别让你的训练数据泄漏公司坐标。
PART 05:治本——如何从源头避免脏数据
到这里你可能会想:清洗这么麻烦,能不能别让它脏?
能。清洗是治标,规范是治本。 源头少制造一份脏数据,下游就少洗十份。

我总结成五件事:
① 采集就要立规矩。 统一文档模板、字段必填、定好 schema。别让每个人按自己习惯填,那是制造脏数据的流水线。
② 入库即校验(数据 CI)。 上一节的 Pandera 就是干这个的。让脏数据在进库那一刻就被拦下,像代码过 CI 一样——进不了库的脏数据,永远不会污染你的模型。
③ 隐私前置,别事后补救。 采集 / 上传那一刻就脱敏,而不是等数据堆成山了再全库扫一遍:
import re
PATTERNS = {
"phone": (re.compile(r"1[3-9]\d{9}"), "[手机号]"),
"id_card": (re.compile(r"\d{17}[\dXx]"), "[身份证]"),
"email": (re.compile(r"[\w.-]+@[\w.-]+\.\w+"), "[邮箱]"),
}
def mask_pii(text: str) -> str:
for name, (pat, rep) in PATTERNS.items():
text = pat.sub(rep, text)
return text
# mask_pii("联系我:13800138000,邮箱 abc@x.com")
# → "联系我:[手机号],邮箱 [邮箱]"
④ 数据血缘与版本管理。 每份数据能追溯来源、能回滚版本(工具如 DVC、LakeFS)。哪天模型突然变蠢,你能定位到是哪一批脏数据混进来的。
⑤ 组织层面定 SLO、定 owner。 工具再全,没人对数据质量负责也白搭。脏数据有没有人兜底,比有没有工具更关键。
记住这句话:最好的清洗,是让数据从一开始就不脏。
结尾:模型决定上限,数据决定下限
回头盘点这篇的核心:
- 脏数据有五宗罪:缺失、重复、格式、编码噪音、隐私泄漏
- 按文件类型对症下药:纯文本治编码、办公文档治结构、PDF 与图片先判文本层
- 清洗是治标,规范是治本——采集规范、入库校验、隐私前置、血缘版本、质量 SLO
我见过太多团队,模型换了三代,效果还是上不去。一查,语料还是三年前那堆没洗过的扫描件。
模型决定上限,数据决定下限——而大多数人卡在下限上。
你喂给 AI 什么,它就还你什么。粮食干净了,AI 才吃得香。
互动时间:你做 AI 项目时,被哪种格式的脏数据坑得最惨?是合并单元格、双栏 PDF,还是别的?来评论区聊聊,我挨个帮你拆。
下一篇 Day10 预告:大模型微调入门——数据洗干净了(Day09),下一步就是拿这批干净数据,训练一个属于你自己的模型。LoRA 和全参微调怎么选、什么场景才该微调。关注小刘檀木,不错过每一篇。
— END —
小刘檀木 · 帮普通人把 AI 学进简历
更多推荐




所有评论(0)