文章目录
- [1 前言:数据采回来,只算完成了一半](#1 前言:数据采回来,只算完成了一半)
- [2 文档解析:先把杂货铺改成标准间](#2 文档解析:先把杂货铺改成标准间)
-
- [2.1 MinerU:挖矿界的清道夫](#2.1 MinerU:挖矿界的清道夫)
- [2.2 图片内容处理:让大模型看图说话](#2.2 图片内容处理:让大模型看图说话)
-
- [2.2.1 单张图片转成描述](#2.2.1 单张图片转成描述)
- [2.2.2 批量处理:一张张来,手会先报废](#2.2.2 批量处理:一张张来,手会先报废)
- [2.2.3 别忘了图文一致性检验](#2.2.3 别忘了图文一致性检验)
- [3 数据清洗流水线:半成品到成品的三道工序](#3 数据清洗流水线:半成品到成品的三道工序)
-
- [3.1 去重:同一个通告,十家网站转](#3.1 去重:同一个通告,十家网站转)
-
- [3.1.1 精确去重:MD5 一锤子买卖](#3.1.1 精确去重:MD5 一锤子买卖)
- [3.1.2 近似去重:MinHash + LSH 认亲大会](#3.1.2 近似去重:MinHash + LSH 认亲大会)
- [3.2 质量过滤:把噪声挡在训练集门外](#3.2 质量过滤:把噪声挡在训练集门外)
- [3.3 隐私脱敏:安全领域的安全课](#3.3 隐私脱敏:安全领域的安全课)
- [4 总结:两大阶段,五道工序](#4 总结:两大阶段,五道工序)

P.S. 无意间发现了一个巨牛的人工智能教程,非常通俗易懂,对AI感兴趣的朋友强烈推荐去看看, 传送门https://blog.csdn.net/qq_34419312
1 前言:数据采回来,只算完成了一半
上回我们把数据从各个犄角旮旯捞了回来------论文、电子书、漏洞库、开源数据集,一个都没落下。当时觉得自己富可敌国。
现在冷静下来看一眼手里的东西:论文有双栏有单栏,电子书图文混排,漏洞库清一色 JSON,文章里还夹着截图。这哪是数据,这是杂货铺刚进完货。
你要敢把这堆东西直接喂给模型,模型还没学会说话,先学会了看相------因为它得先猜你这份 PDF 里哪一行才是正文。
所以中间的加工环节省不了。流程不复杂,就三步:先统一格式,再清洗,再切块导出。今天咱们把这条流水线从头跑到尾。
2 文档解析:先把杂货铺改成标准间
异构文档怎么统一?我的答案是 Markdown。标题层级天然带结构,分块和清洗都能顺着章节语义来,属于中间格式里的万金油。
2.1 MinerU:挖矿界的清道夫
之前聊 OCR 的时候提过 DeepSeek-OCR,今天主角换一个:上海人工智能实验室开源的 MinerU。名字里带个"矿",干的就是挖数据的活。
它对学术论文、固定版式 PDF、docx、网页数据都挺能打,公式和表格还原得比我手打还完整,而且部分模式纯 CPU 也能跑------这个对穷人非常友好。
安装就三条命令,先给它单独开个 Conda 环境。理由很简单:环境隔离做好,将来炸了也不连累别人。
bash
conda create -n mineru python=3.13 #创建名为mineru的conda虚拟环境
conda activate mineru #激活mineru环境
pip install -U "mineru[all]" #安装mineru依赖
装完直接用。假设原始文档都躺在 raw_docs 里,一行命令开工:
bash
mineru -p ./raw_docs -o ./raw_output
-p 指定输入,-o 指定输出。跑完之后 MinerU 会自动做版面分析、文本提取、公式识别,最后吐给你一份结构化的 Markdown。
GPU 不够用?换 pipeline 模式,它调用更小的模型来干版式识别和文本提取。道理和点外卖一样:预算不够就换个小份的,饭照样能吃饱。
bash
mineru -p ./raw_docs -o ./raw_output -b pipeline
解析完你会发现,每个 PDF 都生成一个同名目录,里面躺着一堆文件:.md 是最终成果,_layout.pdf 是版面还原,几个 JSON 是中间过程,还有一个 images 文件夹。咱们重点盯两个:Markdown 文件和图片文件夹。
打开 .md 一看,双栏论文的阅读顺序、章节标题、参考文献全给还原了,公式也变成了 LaTeX。说实话,它还原双栏论文的阅读顺序,比我当年看论文时翻页的顺序靠谱多了。
但这里有个问题:纯文本模型读不了图。Markdown 里的 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KLSDpxXD-1789106617242)(images/xxx.jpg) 只是立了个"此地有图"的牌子,图里的信息------攻击链、网络拓扑、流量截图------全藏在牌子后面。
2.2 图片内容处理:让大模型看图说话
思路其实之前就埋过伏笔:找多模态大模型给每张图生成一段语义描述,把图片"翻译"成文字。图没了,知识还在,这叫精神续作。
2.2.1 单张图片转成描述
我这次用的是阿里云百炼平台的 qwen3.8-flash。别问我为什么选它,问就是便宜大碗。
先用起来:登录百炼平台,在 API Key 管理页面创建一把 key。记住,key 只在创建那一刻给你看一次,之后想找回?没门。比前女友还绝情。
然后通过 OpenAI 兼容接口访问。注意,图片要先转成 base64 才能发,代码如下:
python
import base64
from openai import OpenAI
client = OpenAI(
api_key='', # 百炼平台申请的 API Key
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)
def describe_image(image_path: str) -> str:
with open(image_path, "rb") as f:
b64 = base64.b64encode(f.read()).decode()
completion = client.chat.completions.create(
model="qwen3.8-flash",
messages=[{"role": "user", "content": [
{"type": "image_url",
"image_url": {"url": f"data:image/png;base64,{b64}"}},
{"type": "text",
"text": "你是一名网络安全专家。请用一段话详细描述这张图片的内容,"
"若图中包含文字标注,请一并准确转录。"}
]}],
)
return completion.choices[0].message.content
if __name__ == "__main__":
print(describe_image("./images/f9534223ea3d6c1cd740555708cab1a64f3286c4005d78fae12b11709187d4ac.jpg"))
跑一下,模型会给你一段像模像样的描述,连图里的文字标注都能转录出来。那一刻你会觉得,它比我认识的大多数同事会写周报。
2.2.2 批量处理:一张张来,手会先报废
单张能跑通,但现实里动辄成百上千张图,一张张手动调?那模型还没训练完,先废掉的是我的手。
常规做法三步走:先用正则把 Markdown 里所有图片引用扒出来,再批量调模型生成描述,最后用描述文本把原引用替换掉。代码如下:
python
# ---------- 批量处理逻辑 ----------
MD_PATH = "./面向大模型分布式微调的选择性聚合轻量化后门防御方法.md"
IMAGE_DIR = "./images"
OUTPUT_PATH = "./面向大模型分布式微调的选择性聚合轻量化后门防御方法_new.md" # 直接覆盖原文件
# 第一步:用正则提取 Markdown 中实际引用的图片
IMG_PATTERN = re.compile(r"!\[([^\]]*)\]\(images/([^)]+)\)")
with open(MD_PATH, encoding="utf-8") as f:
md_text = f.read()
referenced = [(m.group(0), m.group(2)) for m in IMG_PATTERN.finditer(md_text)]
print(f"Markdown 中引用的图片数量: {len(referenced)}")
# 第二步:逐张生成描述并直接替换
for i, (full_match, fname) in enumerate(referenced, 1):
path = os.path.join(IMAGE_DIR, fname)
if not os.path.exists(path):
print(f"[{i}/{len(referenced)}] {fname} 文件不存在,跳过")
continue
try:
desc = describe_image(path)
# 用【图片内容:...】包裹描述,替换原图片引用
replacement = f"【图片内容:{desc}】"
md_text = md_text.replace(full_match, replacement, 1)
print(f"[{i}/{len(referenced)}] {fname} 完成")
except Exception as e:
print(f"[{i}/{len(referenced)}] {fname} 失败: {e}")
time.sleep(0.5) # 简单限速,避免触发 API 限流
# 第三步:写回文件
with open(OUTPUT_PATH, "w", encoding="utf-8") as f:
f.write(md_text)
print("全部完成,结果已写回", OUTPUT_PATH)
跑完一看,图片引用全变成了【图片内容:......】,图没了,但信息都在,读起来居然毫无违和感。像极了把前任的照片换成文字备注------人走了,备注还在。
2.2.3 别忘了图文一致性检验
这里有个坑,我替你们踩过了:多模态模型偶尔会"看图说话说过头",编造图里根本不存在的细节。这毛病我外婆也有,看个背影能给你脑补出一整部连续剧。
所以批量生成完之后,按 5%~10% 的比例抽检,重点看描述跟上下文逻辑对不对得上;要是关键报告(比如威胁分析里的攻击链图),那就全量人工过一遍。
记住一句话:宁可少几张图的描述,也别让错误信息混进训练集。模型学错东西,比你教错孩子还难掰回来。
3 数据清洗流水线:半成品到成品的三道工序
图片描述替换完,这份 Markdown 就能直接拿去训练了吗?想得美。
刚才处理的是论文,素质本身就高。换成爬来的社区数据试试?转载重复、广告导流、真实 IP 和邮箱,比小区门口的小广告还全。
从半成品到能下锅的饭,中间隔着三道工序:去重、质量过滤、隐私脱敏。咱们一道一道来。
3.1 去重:同一个通告,十家网站转
同一个漏洞通告,经常被十几个网站转载,措辞还略有不同。你以为是看到了十份新知识,其实是同一个笑话换了十个封面。
不去重的后果很实在:模型把这类内容反复学好多遍,挤占了其他知识的"学习配额",还容易过拟合。说得直白点,模型把"重复"当成了"重要"。
实践里通常上两级策略:先精确去重,再近似去重。
3.1.1 精确去重:MD5 一锤子买卖
完全一样的文档------转载时原样复制的很常见------直接用哈希判定,快到你还没反应过来就结束了:
python
import hashlib
def exact_dedup(texts: dict) -> dict:
"""texts: {文件名: 文本},返回去重后的字典"""
seen, result = set(), {}
for name, text in texts.items():
h = hashlib.md5(text.encode("utf-8")).hexdigest()
if h not in seen:
seen.add(h)
result[name] = text
return result
但是!更多转载不是原样复制,人家改个标题、换两段导语,MD5 就失效了。这帮人别的本事没有,规避检测的直觉比安全工程师还敏锐。
3.1.2 近似去重:MinHash + LSH 认亲大会
这时候就要近似去重登场:把文本切成 n-gram 词集合,用 MinHash 生成签名,再用 LSH 快速找出签名高度相似的文档对。
原理我用大白话讲:MinHash 把每篇文章的分词集合压成一串固定长度的整数签名,压缩完还能估算两篇文章的相似度,属于"压缩饼干"级别的技术;LSH 再把签名切成好几块分别哈希,保证相似文章至少在一块上撞车------像极了相亲软件,总有一个共同好友能把你们凑到一起。
工具不用自己造,datasketch 库拿来就用。先装依赖:
bash
pip install jieba datasketch
完整代码长这样,注意看并查集那部分,那是处理"重复的传染性"的关键:
python
import jieba
from datasketch import MinHash, MinHashLSH
# 1. 模拟文章数据(ID -> 正文)
articles = {
1: "人工智能是计算机科学的一个分支,致力于开发能够执行人类智能任务的系统。",
2: "人工智能是计算机科学的分支,专注于创建能完成人类智能活动的系统。", # 近似重复
3: "今天天气很好,适合出门散步和运动。"
}
# 2. 分词与预处理
def tokenize(text):
return [w for w in jieba.lcut(text.lower()) if len(w) > 1] # 过滤单字
# 3. 生成 MinHash 签名(perm=128)
def get_minhash(text, num_perm=128):
mh = MinHash(num_perm=num_perm)
for w in tokenize(text):
mh.update(w.encode('utf-8'))
return mh
# 4. 构建 LSH 索引(相似度阈值 0.8)
lsh = MinHashLSH(threshold=0.8, num_perm=128)
for aid, txt in articles.items():
lsh.insert(aid, get_minhash(txt))
# 5. 查询相似并标记重复(使用并查集处理传递性)
parent = {aid: aid for aid in articles}
def find(x):
while parent[x] != x:
parent[x] = parent[parent[x]]
x = parent[x]
return x
def union(x, y):
rx, ry = find(x), find(y)
if rx != ry:
parent[ry] = rx
for aid, txt in articles.items():
mh = get_minhash(txt)
candidates = list(lsh.query(mh)) # 0.4.6+ 返回生成器
for cid in candidates:
if cid != aid:
# 可选:二次精确验证(提高准确率)
# if mh.jaccard(get_minhash(articles[cid])) >= 0.8:
union(aid, cid)
# 6. 提取去重后的文章(每个并查集簇保留一个代表)
unique_ids = []
seen_roots = set()
for aid in articles:
root = find(aid)
if root not in seen_roots:
seen_roots.add(root)
unique_ids.append(aid)
print("保留的文章ID:", unique_ids) # 输出 [1, 3](2 被去重)
为什么必须用并查集?因为重复会传染:A 和 B 像,B 和 C 像,那 A 和 C 虽然长得不直接像,也得归到一家。不处理传递性,你会在半个月后惊讶地发现,同一篇文章在你的数据集里出现了三个版本。
参数上提两嘴:num_perm 是签名长度,越大精度越高但内存吃紧;threshold 控制灵敏度,建议先在小样本上试,0.7~0.9 是常见区间。百万级数据就换 MinHashLSHForest 提速。还有,LSH 是近似算法,召回候选后最好用 mh.jaccard() 精确算一遍相似度兜底,别全信相亲软件的"灵魂契合度"。
中文分词质量直接决定去重效果,停用词表和自定义词典该上就上。这套组合拳打下来,洗稿、局部改写统统逃不掉------它俩在我们这行,相当于学术界的降重。
3.2 质量过滤:把噪声挡在训练集门外
社区爬来的数据里,广告、导航残留、标题党是标配。这些低质数据模型是真消化不了,训练集里混进一条广告,模型就能学会在正经回答里给你安利公众号。
质量过滤的目标很简单粗暴:宁可错杀,不可放过。先上规则过滤,覆盖绝大多数明显噪声:
python
AD_KEYWORDS = ["扫码关注", "点击阅读全文", "加微信", "领取资料"]
def quality_filter(text: str) -> bool:
"""返回 True 表示文本通过过滤(保留)"""
# 1. 有效字数:去掉 Markdown 符号和空白后统计
plain = re.sub(r"[#>\\-|`!()\\[\\]]", "", text)
plain = re.sub(r"\\s", "", plain)
if len(plain) < 100:
return False
# 2. 特殊字符占比:乱码、OCR 残留的典型特征
special_ratio = len(re.findall(r"[^\\w\\s\u4e00-\u9fa5]", text)) / max(len(text), 1)
if special_ratio > 0.3:
return False
# 3. 广告关键词命中
if any(kw in text for kw in AD_KEYWORDS):
return False
return True
三条规则各管一片天:
| 规则 | 针对的噪声 | 示例 |
|---|---|---|
| 有效字数下限 | 导航页、目录页、摘要碎片 | "首页 / 安全资讯 / 正文" |
| 特殊字符占比 | OCR 乱码、编码损坏文本 | 大段的 锟斤拷 或符号串 |
| 广告关键词 | 导流话术 | "扫码关注公众号领取资料" |
字数都凑不满 100 的,别叫碎片,叫渣渣,直接扔。特殊字符占比超 30% 的,基本是 OCR 把字认成了符号------这文本拿去训练,模型会以为世界的通用语是乱码。广告关键词更不用说了,它比你自己还懂读者想要什么。
实际工程里规则可不止这三条,我见过最多的公司光过滤规则就上百种。规则嘛,跟袜子一样,多多益善,就是别穿反。
那规则拿不准怎么办?老祖宗的智慧:万事不绝问大模型。如果公司有本地部署的大模型,写个脚本调 API,用提示词让模型判断文本有没有干货:
python
from openai import OpenAI
client = OpenAI(
api_key=os.environ["DASHSCOPE_API_KEY"],
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)
def model_quality_filter(text: str) -> bool:
"""调用大模型判断文本是否包含有效安全知识"""
prompt = f"""你是一名网络安全数据质量评估专家。请判断以下文本是否包含有效的网络安全知识。
判断标准:
1. 文本是否包含实质性的安全技术内容(如漏洞分析、攻防技术、安全规范等)
2. 文本是否有明确的技术价值,而非广告、导航、无意义内容
3. 文本是否适合作为大模型预训练语料
请仅返回一个数字:1表示通过(有有效知识),0表示不通过(无有效知识)。
待评估文本:
---
{text}
---
请判断(仅返回0或1):"""
completion = client.chat.completions.create(
model="qwen-plus",
messages=[{"role": "user", "content": prompt}],
)
result = completion.choices[0].message.content.strip()
return result.startswith("1")
注意,模型过滤又慢又贵,别全量硬上。正确姿势是:规则先过滤一轮,剩下拿不准的才交给模型二判。规则当保安,模型当法官,各司其职。
3.3 隐私脱敏:安全领域的安全课
脱敏不是网络安全语料的专属需求,但安全文章里这个需求最扎眼:真实 IP、域名、邮箱、内网主机名,尤其是事件复盘和应急响应报告,受害单位的家底全在里面。
你要敢原样喂给模型,模型将来回答问题时能直接给你"背"出别人公司的内网地址。安全模型自己先泄密,这说出去,甲方爸爸的脸往哪搁。
日常脱敏也是两段式。先用正则做基础脱敏,覆盖三类最常见的敏感信息:
python
def desensitize(text: str) -> str:
# IP 地址 -> 占位符
text = re.sub(r"\b(?:\d{1,3}\.){3}\d{1,3}\b", "[IP已脱敏]", text)
# 邮箱地址 -> 占位符
text = re.sub(r"[\w.-]+@[\w.-]+\.\w+", "[邮箱已脱敏]", text)
# 口令类字段(password=xxx 等)
text = re.sub(r"(password|passwd|pwd)\s*[=:]\s*\S+",
r"\1=[已脱敏]", text, flags=re.I)
return text
但正则不是万能的。叙述句里的"运维人员账号 admin_zhang"、内部系统名,正则根本抓不到------人家藏得比你对象藏私房钱还深。对高敏感的报告类语料,还得让大模型再过一遍:
python
from openai import OpenAI
client = OpenAI(
api_key=os.environ["DASHSCOPE_API_KEY"],
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)
def model_desensitize(text: str) -> str:
"""调用大模型识别并替换敏感实体"""
prompt = f"""你是一名数据安全脱敏专家。请识别并替换以下文本中的敏感信息。
需要脱敏的敏感信息类型:
1. 人名(如:张三、admin_zhang)-> 替换为 [人名已脱敏]
2. 账号/用户名(如:admin、root、user123)-> 替换为 [账号已脱敏]
3. 内部系统名/主机名(如:server-01、db-master)-> 替换为 [主机名已脱敏]
4. 手机号(如:13800138000)-> 替换为 [手机号已脱敏]
5. 身份证号 -> 替换为 [身份证号已脱敏]
6. 银行卡号 -> 替换为 [银行卡号已脱敏]
注意:
- 保留 CVE 编号、版本号、技术术语等非敏感技术信息
- 仅替换明确的敏感实体,不要过度脱敏
- 直接返回脱敏后的文本,不要添加额外说明
待脱敏文本:
---
{text}
---
请返回脱敏后的文本:"""
completion = client.chat.completions.create(
model="qwen3.7-plus",
messages=[{"role": "user", "content": prompt}],
)
return completion.choices[0].message.content.strip()
脱敏完照样要抽检 5%~10%,确认没有漏网之鱼,同时别误伤了正常技术内容。别问为什么又是 5%~10%,问就是经验,就是玄学,就是工程界的黄金比例。
到这儿,三道工序走完:去重、质量过滤、隐私脱敏。Markdown 文档也完成了从"原料"到"有效数据集"的蜕变------就像菜从带泥的萝卜,变成了码得整整齐齐的净菜。
4 总结:两大阶段,五道工序
来,把今天的流水线复盘一遍。全过程两大阶段:
**阶段一:结构化解析。**先用 MinerU 把 PDF、docx 这类异构文档统一解析成 Markdown,再调 Qwen-VL 多模态模型给图片批量生成语义描述、替换掉原始引用。从"异构原料"到"结构化半成品"。
**阶段二:清洗与脱敏。**按"去重 → 质量过滤 → 隐私脱敏"的顺序跑三道工序,去掉重复、滤掉噪声、替换敏感实体。半成品正式转正。
数一数:两大阶段,五道工序。听起来不多?等你真去洗一千份安全报告的时候,你会回来点赞的。
手里有了这份干净的结构化语料库,下一步就是把它转成预训练格式、丢进 LLaMAFactory 做增量预训练。咱们下篇接着唠。
P.S. 无意间发现了一个巨牛的人工智能教程,非常通俗易懂,对AI感兴趣的朋友强烈推荐去看看,传送门https://blog.csdn.net/qq_34419312