procurement-label-matcher —— 采购信息核对
场景
两类输入:
- 标签文件:图片(jpg/png)或 PDF,一张大纸/多页上有多张医疗器械标签贴纸
- 核对文件:Excel / 图片 / PDF,含多条需要核对的采购记录
- 单 PDF 同时含两者:客户经常把随货同行单 + 标签 + 出厂报告 + 注册证扫描成一整份 PDF——见 Step 1 末尾的「单 PDF 复合文件」处理
产出:
- 每个批号一张、按批号命名的标签 jpg(如
2300091386.jpg,只保存匹配到核对表批号的标签,多出的不保存) - 一份核对结果 Excel(行数以核对文件为准,跟随核对文件的列结构 + 「核对结果」列):
- 字段全部匹配 → 无颜色
- 字段不匹配 → 错误单元格标红,结果列写明差异
- 批号未找到对应标签 → 整行标红
- 标签文件里多出的标签(核对表没有的批号)→ 不单独追加行、不输出图片,只在「核对结果」列 note 里列出多出的批号
分工:脚本做机械工作(PDF 转图、贴纸裁剪、Excel 生成),AI agent 用视觉读文字做识别、匹配、校验。
安装依赖
pip install Pillow numpy openpyxl pypdf
工作流(AI agent 按此执行)
Step 1. 输入识别与预处理
| 标签文件类型 | 处理 |
|---|---|
| 图片 | 直接进入 Step 2 |
python scripts/pdf_to_images.py 标签.pdf --out ./pages/ 提取每页扫描图 |
| 核对文件类型 | 处理 |
|---|---|
| Excel | 用 openpyxl / sheet 工具直接读出表头和数据行 |
| 图片 / PDF | PDF 先 pdf_to_images.py 转图;然后 AI 用视觉读图,提取表头(headers)和每行记录(rows) |
批号字段名兼容:核对文件的表头里找「生产批号 / 批号 / 批次编号 / 批次号 / Lot / 批号(lot)」等列。
其余可校验字段通常有:商品名称、规格型号、注册证号、生产日期、失效日期、数量、单位、生产企业等。
单 PDF 复合文件(随货单 + 标签 + 其它扫描件)
客户经常把整份随货资料扫描成一份多页 PDF,里面同时包含:
- 随货同行单 / 出库单(核对单页)
- 医疗器械标签贴纸(标签页)
- 出厂检验报告 / 检验检疫证明 / 注册证 / 销售授权书等(其它页,忽略)
处理步骤:
- 先
pdf_to_images.py提取所有页 - AI 逐页视觉判定类型(核对单 / 标签 / 其它忽略)
- 取第 1 张「核对单页」用视觉提取表头 + 数据行
- 所有「标签页」作为标签图集合,进入 Step 2
- 忽略其它页(出厂报告/注册证/检疫证等)
- 之后流程和「分两个文件」完全一样
判定提示:核对单页有「表格 + 抬头/供货/收货/联系人」;标签页有「产品名称/型号/批号/注册证号/生产日期/UDI 条码」要素齐全;其它页是英文报告、注册证、CIQ 检疫证、空白页等。
单 PDF 标签页的裁剪规则(重要!避免把整页标签切坏):
- 这些标签通常是整页扫描(一张标签占一整页 A4),不要用固定 box 硬裁
- 推荐:每页标签图保存前,用 Pillow
ImageOps.invert(gray).getbbox()自动找非白边界,再加 10–20px 白边 padding - 整页就是标签的,可直接整页保存(但仍建议裁掉多余白边以减小文件尺寸)
- 用
crop_labels.py的话:用--sticker检测即可(贴纸底色是纯白,base 取 P25 通常 250+,贴纸 mask 即文字+线条)
Step 1.5. 【必做】让用户勾选要校验的字段
从核对文件识别出表头后,必须先问用户再开始校验(每家供应商列不一样,不能默认写死):
- 把核对文件里识别出的全部字段列出来(批号列除外,它固定参与匹配)
- 用 AskUserQuestion 提问,multiSelect: true
- 每题最多 4 个选项,字段数 >4 时必须拆成多组问题,把每个字段都作为独立的可选选项列出来(不能只用「常见组合」+ 自定义输入凑合)
- 拆题策略:每组 4 个自然分组(如「商品类」「证件类」「日期类」「其它」),必要时再问下一组,直到所有字段都提供过选项
- 批号列始终用于匹配,不需要用户选;用户没选的字段不参与比对,但仍照抄进报告 values 里
- 用户未回复/让自行判断时,默认比对「商品名称、规格型号、注册证号、生产日期」这 4 个字段
Step 2. 从标签文件裁出所有标签
# 先列出检测到的贴纸(不落盘)
python scripts/crop_labels.py sheet.jpg --list-stickers --out ./_tmp.jpg
# 裁第 0 张贴纸(含 18px 同色 padding,跳过免责声明)
python scripts/crop_labels.py sheet.jpg --sticker --sticker-pad 18 --skip-disclaimer --out label_0.jpg
# 换 index 逐张裁出:--sticker-index 1, 2, ...
- 标签是「独立贴纸贴在底纸上」→ 用
--sticker模式(色差 + 连通域,最常用) - 标签整齐排成网格 → 用
--rows N --cols M自动网格模式 - 自动失败 →
--box left,upper,right,lower手动框 - 每页 PDF 提出的图逐页执行上述裁剪
- 调参见文末「裁剪调参速查」
裁出的候选标签图放临时目录(如 ./_labels/),下一步识别出批号后,仅把匹配到核对表批号的重命名为 {批号}.jpg 放进输出目录;未匹配的留在临时目录即可。
Step 3. AI 视觉识别标签内容
对每张裁出的标签图,AI(多模态)读出:
- 批号(必读,用于匹配)
- 其他可核对字段:商品名称、规格型号、注册证号、生产日期、失效日期、序列号等
同时 AI 复核每张图确实是完整的一张标签(没有切到相邻标签、没有明显缺边)。
Step 4. 匹配与校验
以批号为 key 匹配两边,然后只对 Step 1.5 用户勾选的字段逐字段校验。比对规则:
- 批号:去空格、全角→半角后精确匹配
- 文本字段:去首尾空格、忽略大小写(型号代码)、全半角统一后比对
- 日期:统一为
YYYY-MM-DD或YYYYMMDD后比对 - 数量:按数值比对("1" == "1.0")
每条核对记录的 status 三选一:
ok:找到标签且所有可比字段全部一致mismatch:找到标签但至少一个字段不一致(errors 里逐字段写 expected/actual)not_found:批号在标签文件里找不到
标签文件里有、核对表里没有的批号 → 不放进 rows、不输出图片(Excel 行数、标签图数量都以核对文件为准)。把多出的批号汇总写进任意一条记录的 note 里,例如「另有3张标签批号不在核对表中:2300087898、2500042561、2400058701」。extras 字段保留但默认传空数组。
Step 5. 生成报告
构造 result.json(格式见下),然后:
python scripts/build_report.py result.json --out 输出目录/核对报告.xlsx
# 核对文件本身是 Excel 时可传模板复制表头样式:
python scripts/build_report.py result.json --out 核对报告.xlsx --template 原核对文件.xlsx
result.json 格式
{
"headers": ["序号", "商品名称", "规格型号", "生产批号", "注册证号"],
"rows": [
{"values": ["1", "金属带锁髓内钉系统-主钉", "HNAN02 7×240mm左", "2300091386", "国械注准20223130417"],
"status": "ok", "note": "标签图片: 2300091386.jpg"},
{"values": ["2", "金属带锁髓内钉系统-主钉", "HNAN03 8×240mm右", "2300091387", "国械注准20223130417"],
"status": "mismatch",
"errors": {"规格型号": {"expected": "HNAN03 8×240mm右", "actual": "HNAN02 7×240mm左"}},
"note": "标签图片: 2300091387.jpg"},
{"values": ["3", "锁定接骨板", "LB02 3.5mm", "2300091388", "国械注准20223130101"],
"status": "not_found"}
],
"extras": []
}
values 顺序与 headers 对齐,内容照抄核对文件原值(便于人工复核)。
errors/extras 里的 actual 值来自 AI 从标签图上读到的内容。
输出目录结构(最终交付)
输出目录/
├── 核对报告.xlsx # 行数=核对文件记录数,错误标红
├── 2300091386.jpg # 按批号命名的标签图(仅匹配到核对表批号的)
└── 2300091387.jpg
裁剪调参速查(crop_labels.py --sticker 模式)
| 症状 | 调整 |
|---|---|
| 检测不到贴纸 / 几张贴纸连成一块 | --sticker-color-diff 调大(12–15) |
| 底纸被误判为贴纸 | --sticker-color-diff 调小(5–8) |
| 贴纸边缘有多余噪点/残影 | --sticker-inset 调大(4–5) |
| 视觉边距不够 | --sticker-pad 调大(18–30) |
| 顶部有免责声明被带入 | 加 --skip-disclaimer |
贴纸检测算法:取图顶部 1/3 纸色像素的 P25 作底纸色 base,贴纸 mask = 纸色且亮度 ≥ base+diff,
4-邻接连通域 BFS 分离(过滤 <5000 像素噪点),按左到右、上到下排序。
文件结构
procurement-label-matcher/
├── SKILL.md
└── scripts/
├── pdf_to_images.py # PDF → 每页 PNG(提取嵌入扫描图,pypdf)
├── crop_labels.py # 标签裁剪:--sticker 贴纸色差检测 / 网格 / --box 手动 / --skip-disclaimer
├── build_report.py # result.json → 带颜色标记的核对报告 xlsx
└── generate_excel.py # (辅助)按模板 xlsx 生成同行单 Excel
注意事项
- PDF 是矢量/文本型(提取不到嵌入图)时,pdf_to_images.py 会明确报错——请用户改提供截图/扫描图
- 标签上的字段名与核对表可能不完全一致(如「规格型号」vs「规格」),AI 比对时按语义对齐列
- 核对表里批号列为空的行:跳过匹配,status 记
not_found,结果列注明「核对表批号为空」 - 模糊批号(如标签上批号被截断/疑似污损):不要强行匹配,记
not_found并在 note 里说明 - 多张贴纸共用同一批号(同批多件):裁多张,文件名加序号如
2300091386_2.jpg