8.0 KiB
8.0 KiB
DESIGN — 订单批量识别(OCR 元信息提取 + 批量一键处理)
一、整体架构
graph TB
A[扫描 data/input/*.png/jpg] --> B[主流程入口]
B --> C[OCRService.process_image]
C --> D[TableOCR 返回 Excel 数据]
D --> E[metadata_extractor 识别三字段]
E --> F[product_db.save_order_metadata]
F --> G[重命名 input 原图片]
G --> H[OrderService.process_excel]
H --> I[ExcelProcessor.fill_template → 新文件名]
I --> J[汇总日志]
二、模块设计
1. app/core/ocr/metadata_extractor.py(新增)
核心类 OrderMetadataExtractor
class OrderMetadataExtractor:
def extract(ocr_text: str, ocr_rows: list[list[str]]) -> OrderMetadata:
"""从 OCR 原始文本和解析后的二维数组提取三字段。
Returns:
OrderMetadata(supplier, bill_date, total_amount, raw_supplier_text)
"""
提取算法:
Supplier(供应商)
- 取前 20 行(OCR 顶部)
- 行内关键词匹配:
供货单、供应商、供货方、批发、酒行、商行、经销、专卖店、配送单、送货单 - 命中后取该行清理(去数字、空白、特殊字符)后的文本
- 多行匹配时取最长非空行
- 兜底:取非空前 3 行中第一个中文 ≥ 4 字的行(启发式)
BillDate(单据日期)
- 全文正则匹配,按优先级:
(\d{4})[-./年](\d{1,2})[-./月](\d{1,2})日?→ 标准化 YYYYMMDD(\d{4})(\d{2})(\d{2})→ YYYYMMDD(紧邻的 8 位数字)
- 多个匹配时取第一个
- 校验:年 1900
2100,月 112,日 1~31
TotalAmount(应付金额)
- 关键词优先:
应付金额、实付金额、金额合计、应付合计、合计金额、总计 - 取关键词后最近一个
数字.数字或数字 - 兜底:取所有金额中最大值(排除单价列)
2. app/core/db/product_db.py 扩展
新增表 order_metadata:
CREATE TABLE IF NOT EXISTS order_metadata (
file_hash TEXT PRIMARY KEY, -- OCR 输出 xlsx 的 hash,与原图同 hash
supplier TEXT DEFAULT '', -- 供应商
bill_date TEXT DEFAULT '', -- 单据日期 YYYYMMDD
total_amount REAL DEFAULT 0, -- 应付金额
raw_supplier_text TEXT DEFAULT '', -- 原始文本,便于回查
source_image TEXT DEFAULT '', -- 原图路径
created_at TEXT DEFAULT '',
updated_at TEXT DEFAULT ''
);
新增方法:
save_order_metadata(file_hash, supplier, bill_date, total_amount, raw_supplier_text, source_image)get_order_metadata(file_hash) -> dict | Nonelist_all_metadata() -> list[dict](用于批量处理后的日志汇总)
3. app/services/order_service.py 扩展
修改 process_excel(excel_path) 流程:
def process_excel(self, excel_path):
# ... 现有 fill_template 流程 ...
# 1. 从 output/xxx.xlsx 的文件名提取 hash(现有逻辑已用)
file_hash = Path(excel_path).stem
# 2. 读 OCR 原始文本(来自 OCRService 缓存或从 xlsx 头部回读)
ocr_text = self._read_ocr_text(file_hash) # 新增
# 3. 提取元信息
meta = self.extractor.extract(ocr_text, ocr_rows)
# 4. 落库
self.product_db.save_order_metadata(file_hash, ...)
# 5. 应用新文件名(result + 原图)
new_result_name = f"采购单_{meta.bill_date or '未知'}_{meta.supplier or '未知供应商'}_{file_hash}.xls"
# 替换 result_path
# 原图重命名:找到 data/input/ 下对应的图(hash 一致)
return new_result_path, meta
OCR 文本获取策略:
- OCR 写入
data/output/{hash}.xlsx时,同步写data/output/{hash}.meta.json(含 OCR 原始 text) - 新增
app/core/ocr/table_ocr.py写入 metadata 时 dump 原始 text - 如 meta.json 不存在(历史文件),回退:从 xlsx 顶部 cell 拼文本
4. app/services/processor_service.py 扩展
新增方法:
class ProcessorService:
def process_all_inputs(self) -> dict:
"""扫描 data/input/ 下所有图片,串行处理。
Returns:
{
'total': int,
'success': int,
'failed': int,
'results': [
{'image': '...', 'hash': '...', 'status': 'success',
'supplier': '...', 'bill_date': '...', 'total_amount': '...',
'result_file': '...', 'error': None},
...
]
}
"""
流程:
- 扫描
data/input/{*.png,*.jpg,*.jpeg,*.bmp,*.pdf} - 对每个图片:
- 调
OCRService.process_image(已有) - 拿到
data/output/{hash}.xlsx - 调
OrderService.process_excel(已扩展) - 收集结果
- 调
- 整体串行(避免百度 API 限流和数据库锁)
- 返回汇总 dict
5. app/ui/action_handlers.py 扩展
新增方法 batch_process_all_inputs:
- 弹进度对话框(不可关闭,tk.Toplevel + progressbar + 实时日志 Text)
- 在线程中调
ProcessorService.process_all_inputs - 完成后:
- 在主窗口日志区显示汇总
- 弹出 MessageBox 列出失败清单
- 自动打开
data/result/文件夹(可选)
新增按钮:"一键处理全部图片" 放在 GUI 主流程区,与现有"一键处理"按钮并列
三、关键接口契约
OrderMetadata dataclass
@dataclass
class OrderMetadata:
supplier: str = ''
bill_date: str = '' # YYYYMMDD
total_amount: float = 0.0
raw_supplier_text: str = ''
def is_complete(self) -> bool:
return bool(self.supplier and self.bill_date)
文件命名
- result xls:
采购单_{YYYYMMDD}_{供应商}_{hash}.xls- 供应商名清理:
/、\、空格、*、?、:、"、<、>、|替换为_ - hash 长度 32(与现有 md5 一致)
- 供应商名清理:
- 原图片:
{原stem}_{YYYYMMDD}_{供应商}_{hash}.{原扩展名}- 不覆盖已重命名的图片
数据库迁移
ProductDatabase._connect 中初始化时检查表存在并创建。新增方法:
def _init_order_metadata_table(self, conn):
conn.execute("""
CREATE TABLE IF NOT EXISTS order_metadata (...)
""")
四、依赖与时序
调用时序
main_window 点击 "一键处理全部图片"
↓
action_handlers.batch_process_all_inputs (UI 线程)
↓
processor_service.process_all_inputs (后台线程)
├─ for each image:
│ ├─ ocr_service.process_image
│ ├─ ocr_text 写到 data/output/{hash}.meta.json ← 新增
│ ├─ order_service.process_excel
│ │ ├─ 读 {hash}.meta.json → OrderMetadata
│ │ ├─ product_db.save_order_metadata
│ │ ├─ 重命名 input 原图
│ │ ├─ fill_template 到 临时名
│ │ └─ 重命名到 采购单_YYYYMMDD_供应商_hash.xls
│ └─ 收集结果
└─ 返回汇总 dict
↓
action_handlers 显示日志汇总 + MessageBox
五、异常处理
| 场景 | 处理 |
|---|---|
data/input/ 为空 |
UI 提示"无待处理图片",不报错 |
| OCR 失败(网络/凭据) | 单张失败跳过,结果记 error,继续下一张 |
| process_excel 失败 | 同上 |
| 元信息识别失败 | supplier/bill_date 填空为"未知",继续 |
| 原图重命名冲突 | 加 _{N} 后缀 |
| 数据库写入失败 | log 警告,不阻断流程 |
六、性能预期
- 单张图 OCR 1
3 秒 + 处理 0.5 秒 = 1.53.5 秒 - 10 张串行:15~35 秒(UI 显示进度条)
- 数据库写入 < 10ms/张,可忽略
七、风险与缓解
| 风险 | 缓解 |
|---|---|
| 供应商名称过长或包含特殊字符 | 清理规则 + 截断到 50 字符 |
| 日期格式五花八门 | 正则覆盖 4 种主流格式 + 校验范围 |
| OCR 文本被错误识别 | 保留 raw_supplier_text 便于回查修正 |
| 批量处理中程序崩溃 | 串行处理 + 已处理的不重做(processed_files.json 已记录) |