feat: 百度通用文字识别提取供应商/日期(config api_key 持久化)

This commit is contained in:
2026-07-19 19:23:38 +08:00
parent cc66448327
commit 056e7d8e75
14 changed files with 1198 additions and 17 deletions
+163 -2
View File
@@ -5,6 +5,11 @@
"""
import os
import json
import re
import shutil
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional, Tuple, Union, Any, Callable
from ..config.settings import ConfigManager
@@ -12,6 +17,8 @@ from ..core.utils.log_utils import get_logger
from ..core.excel.processor import ExcelProcessor
from ..core.excel.merger import PurchaseOrderMerger
from ..core.db.product_db import ProductDatabase
from ..core.db.order_metadata_db import OrderMetadataDB
from ..core.ocr.metadata_extractor import OrderMetadataExtractor, sanitize_for_filename
logger = get_logger(__name__)
@@ -40,7 +47,11 @@ class OrderService:
# 创建Excel处理器和采购单合并器
self.excel_processor = ExcelProcessor(self.config, product_db=self.product_db)
self.order_merger = PurchaseOrderMerger(self.config)
# 元信息识别器 + 单据元信息库
self.extractor = OrderMetadataExtractor()
self.metadata_db = OrderMetadataDB(db_path)
logger.info("OrderService初始化完成")
def get_latest_excel(self) -> Optional[str]:
@@ -89,7 +100,21 @@ class OrderService:
except Exception as e:
logger.error(f"检查特殊预处理时出错: {e}")
return self.excel_processor.process_specific_file(file_path, progress_cb=progress_cb)
result_path = self.excel_processor.process_specific_file(file_path, progress_cb=progress_cb)
if not result_path:
return None
# 应用单据元信息识别 + 重命名 result 与原图
try:
meta = self._extract_and_save_metadata(file_path)
if meta:
result_path = self._apply_metadata_to_filenames(
result_path, file_path, meta
)
except Exception as e:
logger.error(f"应用单据元信息失败(不影响 result 文件): {e}")
return result_path
def _check_special_preprocess(self, file_path: str) -> Optional[str]:
"""检查并执行特殊的预处理(支持杨碧月、烟草公司、蓉城易购)"""
@@ -243,3 +268,139 @@ class OrderService:
except Exception as e:
logger.error(f"单价校验过程中发生错误: {e}")
return []
# ══════════════════════════════════════════════════════════════
# 单据元信息识别 + 文件重命名
# ══════════════════════════════════════════════════════════════
def _extract_and_save_metadata(self, ocr_excel_path: str) -> Optional[Any]:
"""从 OCR 输出的 xlsx 同目录的 .meta.json 提取元信息,写入 SQLite。
Returns:
OrderMetadata 或 None(失败时)
"""
try:
base = Path(ocr_excel_path)
file_hash = base.stem
meta_path = base.with_suffix('.meta.json')
ocr_text = ''
ocr_rows: List[List[str]] = []
source_image = ''
if meta_path.exists():
try:
payload = json.loads(meta_path.read_text(encoding='utf-8'))
# 优先用通用识别文本(含手写抬头/日期)
ocr_text = payload.get('general_text') or payload.get('ocr_text', '') or ''
ocr_rows = payload.get('ocr_rows', []) or []
source_image = payload.get('image_path', '') or ''
except Exception as e:
logger.warning(f"读 meta.json 失败: {e}")
# 兜底:从 xlsx 拼文本(与 OCRService._write_meta_json 的兜底一致)
if not ocr_text:
try:
import xlrd
rb = xlrd.open_workbook(str(ocr_excel_path))
ws = rb.sheet_by_index(0)
lines = []
for r in range(ws.nrows):
row_vals = [str(ws.cell_value(r, c)) for c in range(ws.ncols)]
ocr_rows.append(row_vals)
lines.append(' '.join(row_vals))
ocr_text = '\n'.join(lines)
except Exception as e:
logger.debug(f"从 xlsx 拼 OCR 文本失败: {e}")
meta = self.extractor.extract(ocr_text, ocr_rows)
self.metadata_db.save(
file_hash=file_hash,
supplier=meta.supplier,
bill_date=meta.bill_date,
total_amount=meta.total_amount,
raw_supplier_text=meta.raw_supplier_text,
source_image=source_image,
)
logger.info(
f"元信息识别: hash={file_hash} supplier={meta.supplier!r} "
f"bill_date={meta.bill_date!r} total_amount={meta.total_amount:.2f}"
)
return meta
except Exception as e:
logger.error(f"_extract_and_save_metadata 失败: {e}")
return None
def _apply_metadata_to_filenames(self, result_path: str,
ocr_excel_path: str,
meta) -> str:
"""应用新文件名规则:
- result: 采购单_{YYYYMMDD}_{供应商}_{hash}.xls
- 原图: {原stem}_{YYYYMMDD}_{供应商}_{hash}.{ext}
任一步骤失败不影响 result 文件本身。
Returns:
新 result 路径(无论重命名是否成功都返回;失败时返回原路径)
"""
try:
file_hash = Path(ocr_excel_path).stem
supplier_clean = sanitize_for_filename(meta.supplier) or '未知供应商'
date_part = meta.bill_date or '未知日期'
# ── 1. result 重命名 ──
new_result_name = f"采购单_{date_part}_{supplier_clean}_{file_hash}.xls"
result_dir = Path(result_path).parent
new_result_path = result_dir / new_result_name
try:
# 冲突时加 _N
if new_result_path.exists() and str(new_result_path) != str(result_path):
new_result_path = self._dedup_path(new_result_path)
if str(new_result_path) != str(result_path):
os.rename(result_path, str(new_result_path))
logger.info(f"result 重命名: {result_path} -> {new_result_path}")
result_path = str(new_result_path)
except Exception as e:
logger.warning(f"result 重命名失败: {e}")
# ── 2. 原图重命名 ──
try:
meta_row = self.metadata_db.get(file_hash)
src_image = (meta_row or {}).get('source_image', '')
if src_image and os.path.exists(src_image):
src_p = Path(src_image)
stem = src_p.stem
ext = src_p.suffix
new_image_name = f"{stem}_{date_part}_{supplier_clean}_{file_hash}{ext}"
new_image_path = src_p.parent / new_image_name
# 不覆盖已重命名的图片
if str(new_image_path) != str(src_p) and not new_image_path.exists():
os.rename(src_p, new_image_path)
logger.info(f"原图重命名: {src_p.name} -> {new_image_path.name}")
# 更新 source_image 路径
self.metadata_db.save(
file_hash=file_hash,
supplier=meta.supplier,
bill_date=meta.bill_date,
total_amount=meta.total_amount,
raw_supplier_text=meta.raw_supplier_text,
source_image=str(new_image_path),
)
except Exception as e:
logger.warning(f"原图重命名失败: {e}")
return result_path
except Exception as e:
logger.error(f"_apply_metadata_to_filenames 失败: {e}")
return result_path
@staticmethod
def _dedup_path(p: Path) -> Path:
"""路径冲突时加 _N 后缀。"""
stem, suffix = p.stem, p.suffix
parent = p.parent
n = 1
while True:
cand = parent / f"{stem}_{n}{suffix}"
if not cand.exists():
return cand
n += 1