diff --git a/README.md b/README.md
index bfb1a68..f80753b 100644
--- a/README.md
+++ b/README.md
@@ -293,106 +293,6 @@ python run.py <命令> [选项]
MIT License
-## 更新日志
-
-### v1.5 (2025-05-09)
-
-#### 功能改进
-- 烟草订单处理结果展示:改进烟草订单处理完成后的结果展示界面
- - 美化结果 展示界面,显示订单时间、总金额和处理条目数
- - 添加文件信息展示,包括文件大小和创建时间
- - 提供打开文件、打开所在文件夹等便捷操作按钮
- - 统一与Excel处理结果展示风格,提升用户体验
- - 增强结果文件路径解析能力,确保正确找到并显示结果文件
-- 条码映射编辑功能:
- - 添加图形化条码映射编辑工具,方便管理条码映射和特殊处理规则
- - 支持添加、修改和删除条码映射关系
- - 支持配置特殊处理规则,如乘数、目标单位、固定单价等
- - 自动保存到配置文件,便于后续使用
-
-#### 问题修复
-- 修复烟草订单处理时出现双重弹窗问题
-- 修复烟草订单处理完成后结果展示弹窗无法正常显示的问题
-- 修复ConfigParser兼容性问题,支持标准ConfigParser对象
-- 修复百度OCR客户端中getint方法调用不兼容问题
-- 修复OCRService中缺少batch_process方法的问题,确保OCR功能正常工作
-- 改进日志管理,确保所有日志正确关闭
-- 优化UI界面,统一按钮样式
-- 修复启动器中处理烟草订单按钮的显示样式
-- 修复run.py中close_logger调用缺少参数的问题
-
-#### 代码改进
-- 改进TobaccoService类对配置的处理方式,使用标准get方法
-- 添加fallback机制以增强配置健壮性
-- 优化启动器中结果预览逻辑,避免重复弹窗
-- 统一UI组件风格,提升用户体验
-- 增强错误处理,提供更清晰的错误信息
-
-### v1.4 (2025-05-09)
-
-#### 新功能
-- 烟草订单处理:新增烟草公司特定格式订单明细文件处理功能
- - 支持自动处理标准烟草订单明细格式
- - 根据烟草公司"盒码"作为条码生成银豹采购单
- - 自动将"订单量"转换为"采购量"并计算采购单价
- - 处理结果以银豹采购单格式保存,方便直接导入
-
-#### 功能优化
-- 配置兼容性:优化配置处理逻辑,兼容标准ConfigParser对象
-- 启动器优化:启动器界面增加"处理烟草订单"功能按钮
-- 代码结构优化:将烟草订单处理功能模块化,集成到整体服务架构
-
-### v1.3 (2025-07-20)
-
-#### 功能优化
-- 采购单赠品处理逻辑优化:修改了银豹采购单中赠品的处理方式
- - ~~之前:赠品数量单独填写在"赠送量"列,与正常采购量分开处理~~
- - ~~现在:将赠品数量合并到采购量中,赠送量列留空~~
- - ~~有正常商品且有赠品的情况:采购量 = 正常商品数量 + 赠品数量,单价 = 原单价 × 正常商品数量 ÷ 总数量~~
- - ~~只有赠品的情况:采购量填写赠品数量,单价为0~~
-- 更新说明:经用户反馈,赠品处理逻辑已还原为原始方式,正常商品数量和赠品数量分开填写
-
-### v1.2 (2025-07-15)
-
-#### 功能优化
-- 规格提取优化:改进了从商品名称中提取规格的逻辑,优先识别"容量*数量"格式
- - 例如从"美汁源果粒橙1.8L*8瓶"能准确提取"1.8L*8"而非错误的"1.8L*1"
-- 规格解析增强:优化`parse_specification`方法,能正确解析"1.8L*8"格式规格,确保准确提取包装数量
-- 单位推断增强:在`extract_product_info`方法中增加新逻辑,当单位为空且有条码、规格、数量、单价时,根据规格格式(如容量*数量格式或简单数量*数量格式)自动推断单位为"件"
-- 件单位处理优化:确保当设置单位为"件"时,正确触发UnitConverter单位处理逻辑,将数量乘以包装数量,单价除以包装数量,单位转为"瓶"
-- 整体改进:提高了系统处理复杂格式商品名称和规格的能力,使单位转换更加准确可靠
-- 规格提取逻辑修正:修复了在Excel中已有规格信息时仍会从商品名称推断规格的问题,现在系统会优先使用Excel中的数据,只有在规格为空时才尝试从商品名称推断
-
-### v1.1 (2025-05-07)
-
-#### 功能更新
-- 单位自动推断:当单位为空但有商品编码、规格、数量、单价等信息,且规格符合容量*数量格式时,自动将单位设置为"件"并按照件的处理规则进行转换
-- 规格解析优化:改进对容量*数量格式规格的解析,如"1.8L*8"能正确识别包装数量为8
-- 规格提取增强:从商品名称中提取"容量*数量"格式的规格时,能正确识别如"美汁源果粒橙1.8L*8瓶"中的"1.8L*8"部分
-- 条码映射功能:增加特定条码的自动映射功能,支持将特定条码自动转换为指定的目标条码
- - 6920584471055 → 6920584471017
- - 6925861571159 → 69021824
- - 6923644268923 → 6923644268480
- - 条码映射后会继续按照件/箱等单位的标准处理规则进行数量和单价的转换
-
-### v1.0 (2025-05-02)
-
-#### 主要功能
-- 图像OCR识别:支持对采购单图片进行OCR识别并生成Excel文件
-- Excel数据处理:智能处理Excel文件,提取和转换商品信息
-- 采购单生成:按照模板格式生成标准采购单Excel文件
-- 采购单合并:支持多个采购单合并为一个总单
-- 图形界面:提供简洁直观的操作界面
-- 命令行支持:支持命令行调用,方便自动化处理
-
-#### 技术改进
-- 模块化架构:重构代码为配置、核心功能、服务和CLI等模块
-- 单位智能处理:完善的单位转换规则,支持多种计量单位
-- 规格智能推断:从商品名称自动推断规格信息
-- 日志管理:完善的日志记录系统,支持终端和GUI同步显示
-- 表头智能识别:自动识别Excel中的表头位置,兼容多种格式
-- 改进用户体验:界面优化,批量处理支持,实时状态反馈
-
## 联系方式
如有问题,请提交Issue或联系开发者。
\ No newline at end of file
diff --git a/backup/v1_backup_20250502190248/.gitignore b/backup/v1_backup_20250502190248/.gitignore
deleted file mode 100644
index d9a04cd..0000000
--- a/backup/v1_backup_20250502190248/.gitignore
+++ /dev/null
@@ -1,52 +0,0 @@
-# 配置文件(可能包含敏感信息)
-config.ini
-
-# 日志文件
-*.log
-
-# 临时文件
-temp/
-~$*
-.DS_Store
-__pycache__/
-
-# 处理记录(因为通常很大且与具体环境相关)
-processed_files.json
-
-# 输入输出数据 (可以根据需要调整)
-# input/
-# output/
-
-# Python相关
-*.py[cod]
-*$py.class
-*.so
-.Python
-env/
-build/
-develop-eggs/
-dist/
-downloads/
-eggs/
-.eggs/
-lib/
-lib64/
-parts/
-sdist/
-var/
-wheels/
-*.egg-info/
-.installed.cfg
-*.egg
-
-# 虚拟环境
-venv/
-ENV/
-env/
-
-# IDE相关
-.idea/
-.vscode/
-*.swp
-*.swo
-.DS_Store
\ No newline at end of file
diff --git a/backup/v1_backup_20250502190248/README.md b/backup/v1_backup_20250502190248/README.md
deleted file mode 100644
index e436acb..0000000
--- a/backup/v1_backup_20250502190248/README.md
+++ /dev/null
@@ -1,332 +0,0 @@
-# OCR订单处理系统
-
-这是一个基于OCR技术的订单处理系统,用于自动识别和处理Excel格式的订单文件。系统支持多种格式的订单处理,包括普通订单和赠品订单的处理。
-
-## 主要功能
-
-1. **OCR识别**
- - 支持图片和PDF文件的文字识别
- - 支持表格结构识别
- - 支持多种格式的订单识别
-
-2. **Excel处理**
- - 自动处理订单数据
- - 支持赠品订单处理
- - 自动提取商品规格和数量信息
- - 从商品名称智能推断规格信息
- - 从数量字段提取单位信息
- - 支持多种格式的订单合并
-
-3. **日志管理**
- - 自动记录处理过程
- - 支持日志文件压缩
- - 自动清理过期日志
- - 日志文件自动重建
- - 支持日志大小限制
- - 活跃日志文件保护机制
-
-4. **文件管理**
- - 自动备份清理的文件
- - 支持按时间和模式清理文件
- - 文件统计和状态查看
- - 支持输入输出目录的独立管理
-
-## 系统要求
-
-- Python 3.8+
-- Windows 10/11
-
-## 安装说明
-
-1. 克隆项目到本地:
-```bash
-git clone [项目地址]
-cd orc-order
-```
-
-2. 安装依赖:
-```bash
-pip install -r requirements.txt
-```
-
-3. 配置百度OCR API:
- - 在[百度AI开放平台](https://ai.baidu.com/)注册账号
- - 创建OCR应用并获取API Key和Secret Key
- - 将密钥信息填入`config.ini`文件
-
-## 使用说明
-
-### 1. OCR处理流程
-
-1. 运行OCR识别:
-```bash
-python run_ocr.py [输入文件路径]
-```
-
-2. 使用百度OCR API:
-```bash
-python baidu_ocr.py [输入文件路径]
-```
-
-3. 处理表格OCR:
-```bash
-python baidu_table_ocr.py [输入文件路径]
-```
-
-### 2. Excel处理
-
-```bash
-python excel_processor_step2.py [输入Excel文件路径]
-```
-
-或者不指定输入文件,自动处理output目录中最新的Excel文件:
-```bash
-python excel_processor_step2.py
-```
-
-#### 2.1 Excel处理逻辑说明
-
-1. **列名识别与映射**:
- - 系统首先检查是否存在直接匹配的列(如"商品条码"列)
- - 如果没有,系统会尝试将多种可能的列名映射到标准列名
- - 支持特殊表头格式处理(如基本条码、仓库全名等)
-
-2. **条码识别与处理**:
- - 验证条码格式(确保长度在8-13位之间)
- - 对特定的错误条码进行修正(如5开头改为6开头)
- - 识别特殊条码(如"5321545613")
- - 跳过条码为"仓库"或"仓库全名"的行
-
-3. **智能规格推断**:
- - 当规格信息为空时,从商品名称自动推断规格
- - 支持多种商品命名模式:
- - 445水溶C血橙15入纸箱 → 规格推断为 1*15
- - 500-东方树叶-绿茶1*15-纸箱装 → 规格推断为 1*15
- - 12.9L桶装水 → 规格推断为 12.9L*1
- - 900树叶茉莉花茶12入纸箱 → 规格推断为 1*12
- - 500茶π蜜桃乌龙15纸箱 → 规格推断为 1*15
-
-4. **单位自动提取**:
- - 当单位信息为空时,从数量字段中自动提取单位
- - 支持格式:2箱、5桶、3件、10瓶等
- - 自动分离数字和单位部分
-
-5. **赠品识别**:
- - 通过以下条件识别赠品:
- - 商品单价为0或为空
- - 商品金额为0或为空
- - 单价非有效数字
-
-6. **数据合并与处理**:
- - 对同一条码的多个正常商品记录,累加数量
- - 对同一条码的多个赠品记录,累加赠品数量
- - 如果同一条码有不同单价,取平均值
-
-### 3. 订单合并
-
-`merge_purchase_orders.py`是专门用来合并多个采购单Excel文件的工具,可以高效处理多份采购单并去重。
-
-#### 3.1 基本用法
-
-自动合并output目录下的所有采购单文件(以"采购单_"开头的Excel文件):
-```bash
-python merge_purchase_orders.py
-```
-
-指定要合并的特定文件:
-```bash
-python merge_purchase_orders.py --input "output/采购单_1.xls,output/采购单_2.xls"
-```
-
-#### 3.2 合并逻辑说明
-
-1. **数据识别与映射**:
- - 自动识别Excel文件中的列名(支持多种表头格式)
- - 将不同格式的列名映射到标准列名(如"条码"、"条码(必填)"等)
- - 支持特殊表头结构的处理(如表头在第3行的情况)
-
-2. **相同商品的处理**:
- - 自动检测相同条码和单价的商品
- - 对相同商品进行数量累加处理
- - 保持商品名称、条码和单价不变
-
-3. **赠送量的处理**:
- - 自动检测和处理赠送量
- - 对相同商品的赠送量进行累加
- - 当原始文件中赠送量为空时,合并后保持为空(不显示为0)
-
-4. **数据格式保持**:
- - 保持条码的原始格式(不转换为小数)
- - 单价保持四位小数格式
- - 避免"nan"值的显示,空值保持为空
-
-### 4. 单位处理规则(核心规则)
-
-系统支持多种单位的智能处理,能够自动识别和转换不同的计量单位。所有开发必须严格遵循以下规则处理单位转换。
-
-#### 4.1 标准单位处理
-
-| 单位 | 处理规则 | 示例 |
-|------|----------|------|
-| 件 | 数量×包装数量
单价÷包装数量
单位转换为"瓶" | 1件(规格1*12) → 12瓶
单价108元/件 → 9元/瓶 |
-| 箱 | 数量×包装数量
单价÷包装数量
单位转换为"瓶" | 2箱(规格1*24) → 48瓶
单价120元/箱 → 5元/瓶 |
-| 包 | 保持原数量和单位不变 | 3包 → 3包 |
-| 其他单位 | 保持原数量和单位不变 | 5瓶 → 5瓶 |
-
-#### 4.2 提和盒单位特殊处理
-
-系统对"提"和"盒"单位有特殊的处理逻辑:
-
-1. 当规格是三级格式(如1*5*12)时:
- - 按照件的计算方式处理
- - 数量 = 原始数量 × 包装数量
- - 单位转换为"瓶"
- - 单价 = 原始单价 ÷ 包装数量
-
- 示例:3提(规格1*5*12) → 36瓶
-
-2. 当规格是二级格式(如1*16)时:
- - **保持原数量不变**
- - **保持原单位不变**
-
- 示例:3提(规格1*16) → 仍然是3提
-
-#### 4.3 特殊条码处理
-
-系统支持对特定条码进行特殊处理,这些条码的处理规则会覆盖上述的标准单位处理规则:
-
-1. 特殊条码配置:
- ```python
- special_barcodes = {
- '6925019900087': {
- 'multiplier': 10, # 数量乘以10
- 'target_unit': '瓶', # 目标单位
- 'description': '特殊处理:数量*10,单位转换为瓶'
- }
- # 可以添加更多特殊条码的配置
- }
- ```
-
-2. 处理规则:
- - 当遇到特殊条码时,无论规格是二级还是三级
- - 无论单位是提还是盒还是件
- - 都按照特殊条码配置进行处理
- - 数量乘以配置的倍数
- - 单位转换为配置的目标单位
- - 如果有单价,单价除以配置的倍数
-
-3. 添加新的特殊条码的正确方法:
- - 在`ExcelProcessorStep2`类的`__init__`方法中的`special_barcodes`字典中添加新的配置
- - 每个特殊条码需要配置:
- - `multiplier`: 数量乘以的倍数
- - `target_unit`: 转换后的目标单位
- - `description`: 处理规则的描述
-
-4. 注意事项:
- - 特殊条码处理优先级高于标准单位处理
- - 添加新的特殊条码前,需要确认该条码是否真的需要特殊处理,或者可以使用现有的标准规则
- - 如果商品单位是"件"且有规格信息,应首先考虑使用标准的"件"单位处理规则
-
-5. 示例:
- - 条码6925019900087,单位为"副",原始数量为2:
- - 无论规格如何
- - 最终数量 = 2 * 10 = 20,单位为"瓶"
- - 如原单价为50元/副,则转换后为5元/瓶
-
-### 5. 开发注意事项
-
-1. **遵循原有处理逻辑**:
- - 在进行任何修改前,必须理解并遵循现有的单位处理逻辑
- - 对于"件"单位,必须按照"数量×包装数量"进行处理
- - 对于"提"和"盒"单位,必须检查规格是二级还是三级格式,按相应规则处理
-
-2. **添加特殊条码处理**:
- - 只有在明确确认某条码无法使用现有规则处理的情况下,才添加特殊处理规则
- - 添加特殊条码处理前,先咨询相关负责人确认处理逻辑
-
-3. **代码更改原则**:
- - 任何代码修改都不应破坏现有的处理逻辑
- - 添加新功能时,先确保理解并保留现有功能
- - 如需修改核心处理逻辑,必须详细记录变更并更新本文档
-
-4. **文档同步更新**:
- - 当修改代码逻辑时,必须同步更新README文档
- - 确保文档准确反映当前系统的处理逻辑
- - 记录所有特殊处理规则和条码
-
-## 注意事项
-
-1. 确保输入文件格式正确
-2. 定期检查日志文件大小
-3. 及时更新百度OCR API密钥
-4. 建议定期备份重要数据
-5. 清理文件前先查看统计信息
-6. 重要文件建议手动备份后再清理
-7. 日志文件会自动重建,可以放心清理
-8. 规格推断功能适用于特定命名格式的商品
-9. 单位提取功能依赖于数量字段的格式
-
-## 常见问题
-
-1. **OCR识别失败**
- - 检查图片质量
- - 确认API密钥配置正确
- - 查看日志文件了解详细错误信息
-
-2. **Excel处理错误**
- - 确认Excel文件格式正确
- - 检查商品信息是否完整
- - 查看处理日志了解具体错误
-
-3. **规格推断失败**
- - 检查商品名称是否符合支持的格式
- - 尝试手动在Excel中填写规格信息
- - 查看日志中的推断过程信息
-
-4. **单位提取失败**
- - 检查数量字段格式是否为"数字+单位"格式
- - 确认数量字段没有额外的空格或特殊字符
- - 尝试手动在Excel中填写单位信息
-
-5. **文件清理问题**
- - 清理前使用`--test`参数预览要删除的文件
- - 清理前检查文件统计信息`--stats`
- - 对于重要文件,先使用`--test`确认后再实际删除
- - 对于被占用的文件,尝试关闭占用的程序后再清理
- - Windows下某些文件无法删除时,可以使用`--force`参数
- - 日志文件清理时可以使用`--clean-all-logs`参数
-
-## 更新日志
-
-### v1.2.1 (2024-05-04)
-- 新增日志文件活跃标记保护机制
-- 修复OCR处理器日志文件被意外删除的问题
-- 改进日志清理工具,避免删除活跃日志
-- 优化文件清理逻辑
-- 更新README文档
-
-### v1.2.0 (2024-05-03)
-- 新增对"提"单位的支持(1提=10袋)
-- 新增对三级规格格式(1*5*10)的解析支持
-- 优化单位转换逻辑
-- 改进规格解析能力
-
-### v1.1.0 (2024-05-02)
-- 新增从商品名称智能推断规格功能
-- 新增从数量字段提取单位功能
-- 优化赠品处理逻辑
-- 修复缩进错误和代码问题
-- 改进日志记录内容
-
-### v1.0.0 (2024-05-01)
-- 初始版本发布
-- 支持基本的OCR识别功能
-- 支持Excel订单处理
-- 支持日志管理功能
-- 添加文件清理工具
-- 优化文件命名和目录结构
-- 完善日志自动重建功能
-
-## 许可证
-
-MIT License
\ No newline at end of file
diff --git a/backup/v1_backup_20250502190248/baidu_ocr.py b/backup/v1_backup_20250502190248/baidu_ocr.py
deleted file mode 100644
index 10c0142..0000000
--- a/backup/v1_backup_20250502190248/baidu_ocr.py
+++ /dev/null
@@ -1,469 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-百度表格OCR识别工具
------------------
-用于将图片中的表格转换为Excel文件的工具。
-使用百度云OCR API进行识别。
-"""
-
-import os
-import sys
-import requests
-import base64
-import json
-import time
-import logging
-import datetime
-import configparser
-from pathlib import Path
-from typing import Dict, List, Optional, Any
-
-# 配置日志
-logging.basicConfig(
- level=logging.INFO,
- format='%(asctime)s - %(levelname)s - %(message)s',
- handlers=[
- logging.StreamHandler(sys.stdout)
- ]
-)
-logger = logging.getLogger(__name__)
-
-# 默认配置
-DEFAULT_CONFIG = {
- 'API': {
- 'api_key': '', # 将从配置文件中读取
- 'secret_key': '', # 将从配置文件中读取
- 'timeout': '30',
- 'max_retries': '3',
- 'retry_delay': '2'
- },
- 'Paths': {
- 'input_folder': 'input',
- 'output_folder': 'output',
- 'temp_folder': 'temp'
- },
- 'File': {
- 'allowed_extensions': '.jpg,.jpeg,.png,.bmp',
- 'excel_extension': '.xlsx'
- }
-}
-
-class ConfigManager:
- """配置管理类,负责加载和保存配置"""
-
- def __init__(self, config_file: str = 'config.ini'):
- self.config_file = config_file
- self.config = configparser.ConfigParser()
- self.load_config()
-
- def load_config(self) -> None:
- """加载配置文件,如果不存在则创建默认配置"""
- if not os.path.exists(self.config_file):
- self.create_default_config()
-
- try:
- self.config.read(self.config_file, encoding='utf-8')
- logger.info(f"已加载配置文件: {self.config_file}")
- except Exception as e:
- logger.error(f"加载配置文件时出错: {e}")
- logger.info("使用默认配置")
- self.create_default_config(save=False)
-
- def create_default_config(self, save: bool = True) -> None:
- """创建默认配置"""
- for section, options in DEFAULT_CONFIG.items():
- if not self.config.has_section(section):
- self.config.add_section(section)
-
- for option, value in options.items():
- self.config.set(section, option, value)
-
- if save:
- self.save_config()
- logger.info(f"已创建默认配置文件: {self.config_file}")
-
- def save_config(self) -> None:
- """保存配置到文件"""
- try:
- with open(self.config_file, 'w', encoding='utf-8') as f:
- self.config.write(f)
- except Exception as e:
- logger.error(f"保存配置文件时出错: {e}")
-
- def get(self, section: str, option: str, fallback: Any = None) -> Any:
- """获取配置值"""
- return self.config.get(section, option, fallback=fallback)
-
- def getint(self, section: str, option: str, fallback: int = 0) -> int:
- """获取整数配置值"""
- return self.config.getint(section, option, fallback=fallback)
-
- def getboolean(self, section: str, option: str, fallback: bool = False) -> bool:
- """获取布尔配置值"""
- return self.config.getboolean(section, option, fallback=fallback)
-
- def get_list(self, section: str, option: str, fallback: str = "", delimiter: str = ",") -> List[str]:
- """获取列表配置值"""
- value = self.get(section, option, fallback)
- return [item.strip() for item in value.split(delimiter) if item.strip()]
-
-class OCRProcessor:
- """OCR处理器,用于表格识别"""
-
- def __init__(self, config_file: str = 'config.ini'):
- """
- 初始化OCR处理器
-
- Args:
- config_file: 配置文件路径
- """
- self.config_manager = ConfigManager(config_file)
-
- # 获取配置
- self.api_key = self.config_manager.get('API', 'api_key')
- self.secret_key = self.config_manager.get('API', 'secret_key')
- self.timeout = self.config_manager.getint('API', 'timeout', 30)
- self.max_retries = self.config_manager.getint('API', 'max_retries', 3)
- self.retry_delay = self.config_manager.getint('API', 'retry_delay', 2)
-
- # 设置路径
- self.input_folder = self.config_manager.get('Paths', 'input_folder', 'input')
- self.output_folder = self.config_manager.get('Paths', 'output_folder', 'output')
- self.temp_folder = self.config_manager.get('Paths', 'temp_folder', 'temp')
-
- # 确保目录存在
- for dir_path in [self.input_folder, self.output_folder, self.temp_folder]:
- os.makedirs(dir_path, exist_ok=True)
-
- # 设置允许的文件扩展名
- self.allowed_extensions = self.config_manager.get_list('File', 'allowed_extensions')
-
- # 验证API配置
- if not self.api_key or not self.secret_key:
- logger.warning("API密钥未设置,请在配置文件中设置API密钥")
-
- def get_access_token(self) -> Optional[str]:
- """获取百度API访问令牌"""
- url = "https://aip.baidubce.com/oauth/2.0/token"
- params = {
- "grant_type": "client_credentials",
- "client_id": self.api_key,
- "client_secret": self.secret_key
- }
-
- for attempt in range(self.max_retries):
- try:
- response = requests.post(url, params=params, timeout=10)
- if response.status_code == 200:
- result = response.json()
- if "access_token" in result:
- return result["access_token"]
-
- logger.warning(f"获取访问令牌失败 (尝试 {attempt+1}/{self.max_retries}): {response.text}")
-
- except Exception as e:
- logger.warning(f"获取访问令牌时发生错误 (尝试 {attempt+1}/{self.max_retries}): {e}")
-
- # 如果不是最后一次尝试,则等待后重试
- if attempt < self.max_retries - 1:
- time.sleep(self.retry_delay * (attempt + 1))
-
- logger.error("无法获取访问令牌")
- return None
-
- def rename_image_to_timestamp(self, image_path: str) -> str:
- """将图片重命名为时间戳格式(如果需要)"""
- try:
- # 获取当前时间戳
- now = datetime.datetime.now()
- timestamp = now.strftime("%Y%m%d%H%M%S")
-
- # 构造新文件名
- dir_path = os.path.dirname(image_path)
- ext = os.path.splitext(image_path)[1]
- new_path = os.path.join(dir_path, f"{timestamp}{ext}")
-
- # 如果文件名不同,则重命名
- if image_path != new_path:
- os.rename(image_path, new_path)
- logger.info(f"已将图片重命名为: {os.path.basename(new_path)}")
- return new_path
-
- return image_path
- except Exception as e:
- logger.error(f"重命名图片时出错: {e}")
- return image_path
-
- def recognize_table(self, image_path: str) -> Optional[Dict]:
- """
- 识别图片中的表格
-
- Args:
- image_path: 图片文件路径
-
- Returns:
- Dict: 识别结果,失败返回None
- """
- try:
- # 获取access_token
- access_token = self.get_access_token()
- if not access_token:
- return None
-
- # 请求URL
- url = f"https://aip.baidubce.com/rest/2.0/solution/v1/form_ocr/request?access_token={access_token}"
-
- # 读取图片内容
- with open(image_path, 'rb') as f:
- image_data = f.read()
-
- # Base64编码
- image_base64 = base64.b64encode(image_data).decode('utf-8')
-
- # 请求参数
- headers = {
- 'Content-Type': 'application/x-www-form-urlencoded'
- }
-
- data = {
- 'image': image_base64,
- 'is_sync': 'true',
- 'request_type': 'excel'
- }
-
- # 发送请求
- response = requests.post(url, headers=headers, data=data, timeout=self.timeout)
- response.raise_for_status()
-
- # 解析结果
- result = response.json()
-
- # 检查错误码
- if 'error_code' in result:
- logger.error(f"识别表格失败: {result.get('error_msg', '未知错误')}")
- return None
-
- # 返回识别结果
- return result
-
- except Exception as e:
- logger.error(f"识别表格时出错: {e}")
- return None
-
- def get_excel_result(self, request_id: str, access_token: str) -> Optional[bytes]:
- """
- 获取Excel结果
-
- Args:
- request_id: 请求ID
- access_token: 访问令牌
-
- Returns:
- bytes: Excel文件内容,失败返回None
- """
- try:
- # 请求URL
- url = f"https://aip.baidubce.com/rest/2.0/solution/v1/form_ocr/get_request_result?access_token={access_token}"
-
- # 请求参数
- headers = {
- 'Content-Type': 'application/x-www-form-urlencoded'
- }
-
- data = {
- 'request_id': request_id,
- 'result_type': 'excel'
- }
-
- # 最大重试次数
- max_retries = 10
-
- # 循环获取结果
- for i in range(max_retries):
- # 发送请求
- response = requests.post(url, headers=headers, data=data, timeout=self.timeout)
- response.raise_for_status()
-
- # 解析结果
- result = response.json()
-
- # 检查错误码
- if 'error_code' in result:
- logger.error(f"获取Excel结果失败: {result.get('error_msg', '未知错误')}")
- return None
-
- # 检查处理状态
- result_data = result.get('result', {})
- status = result_data.get('ret_code')
-
- if status == 3: # 处理完成
- # 获取Excel文件URL
- excel_url = result_data.get('result_data')
- if not excel_url:
- logger.error("未获取到Excel结果URL")
- return None
-
- # 下载Excel文件
- excel_response = requests.get(excel_url)
- excel_response.raise_for_status()
-
- # 返回Excel文件内容
- return excel_response.content
-
- elif status == 1: # 排队中
- logger.info(f"请求排队中 ({i+1}/{max_retries}),等待后重试...")
- elif status == 2: # 处理中
- logger.info(f"正在处理 ({i+1}/{max_retries}),等待后重试...")
- else:
- logger.error(f"未知状态码: {status}")
- return None
-
- # 等待后重试
- time.sleep(2)
-
- logger.error(f"获取Excel结果超时,请稍后再试")
- return None
-
- except Exception as e:
- logger.error(f"获取Excel结果时出错: {e}")
- return None
-
- def process_image(self, image_path: str) -> Optional[str]:
- """
- 处理单个图片
-
- Args:
- image_path: 图片文件路径
-
- Returns:
- str: 生成的Excel文件路径,失败返回None
- """
- try:
- logger.info(f"开始处理图片: {image_path}")
-
- # 验证文件扩展名
- ext = os.path.splitext(image_path)[1].lower()
- if self.allowed_extensions and ext not in self.allowed_extensions:
- logger.error(f"不支持的文件类型: {ext},支持的类型: {', '.join(self.allowed_extensions)}")
- return None
-
- # 重命名图片(可选)
- renamed_path = self.rename_image_to_timestamp(image_path)
-
- # 获取文件名(不含扩展名)
- basename = os.path.basename(renamed_path)
- name_without_ext = os.path.splitext(basename)[0]
-
- # 获取access_token
- access_token = self.get_access_token()
- if not access_token:
- return None
-
- # 识别表格
- ocr_result = self.recognize_table(renamed_path)
- if not ocr_result:
- return None
-
- # 获取请求ID
- request_id = ocr_result.get('result', {}).get('request_id')
- if not request_id:
- logger.error("未获取到请求ID")
- return None
-
- # 获取Excel结果
- excel_content = self.get_excel_result(request_id, access_token)
- if not excel_content:
- return None
-
- # 保存Excel文件
- output_path = os.path.join(self.output_folder, f"{name_without_ext}.xlsx")
- with open(output_path, 'wb') as f:
- f.write(excel_content)
-
- logger.info(f"已保存Excel文件: {output_path}")
- return output_path
-
- except Exception as e:
- logger.error(f"处理图片时出错: {e}")
- return None
-
- def process_directory(self) -> List[str]:
- """
- 处理输入目录中的所有图片
-
- Returns:
- List[str]: 生成的Excel文件路径列表
- """
- results = []
-
- try:
- # 获取输入目录中的所有图片文件
- image_files = []
- for ext in self.allowed_extensions:
- image_files.extend(list(Path(self.input_folder).glob(f"*{ext}")))
- image_files.extend(list(Path(self.input_folder).glob(f"*{ext.upper()}")))
-
- if not image_files:
- logger.warning(f"输入目录 {self.input_folder} 中没有找到图片文件")
- return []
-
- logger.info(f"在 {self.input_folder} 中找到 {len(image_files)} 个图片文件")
-
- # 处理每个图片
- for image_file in image_files:
- result = self.process_image(str(image_file))
- if result:
- results.append(result)
-
- logger.info(f"处理完成,成功生成 {len(results)} 个Excel文件")
- return results
-
- except Exception as e:
- logger.error(f"处理目录时出错: {e}")
- return results
-
-def main():
- """主函数"""
- import argparse
-
- # 解析命令行参数
- parser = argparse.ArgumentParser(description='百度表格OCR识别工具')
- parser.add_argument('--config', type=str, default='config.ini', help='配置文件路径')
- parser.add_argument('--input', type=str, help='输入图片路径')
- parser.add_argument('--debug', action='store_true', help='启用调试模式')
- args = parser.parse_args()
-
- # 设置日志级别
- if args.debug:
- logging.getLogger().setLevel(logging.DEBUG)
-
- # 创建OCR处理器
- processor = OCRProcessor(args.config)
-
- # 处理单个图片或目录
- if args.input:
- if os.path.isfile(args.input):
- result = processor.process_image(args.input)
- if result:
- print(f"处理成功: {result}")
- return 0
- else:
- print("处理失败")
- return 1
- elif os.path.isdir(args.input):
- results = processor.process_directory()
- print(f"处理完成,成功生成 {len(results)} 个Excel文件")
- return 0
- else:
- print(f"输入路径不存在: {args.input}")
- return 1
- else:
- # 处理默认输入目录
- results = processor.process_directory()
- print(f"处理完成,成功生成 {len(results)} 个Excel文件")
- return 0
-
-if __name__ == "__main__":
- sys.exit(main())
\ No newline at end of file
diff --git a/backup/v1_backup_20250502190248/baidu_table_ocr.py b/backup/v1_backup_20250502190248/baidu_table_ocr.py
deleted file mode 100644
index 0c0c4a5..0000000
--- a/backup/v1_backup_20250502190248/baidu_table_ocr.py
+++ /dev/null
@@ -1,639 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-百度表格OCR识别工具
------------------
-用于将图片中的表格转换为Excel文件的工具。
-使用百度云OCR API进行识别,支持批量处理。
-"""
-
-import os
-import sys
-import requests
-import base64
-import json
-import time
-import logging
-import datetime
-import configparser
-from pathlib import Path
-from typing import Dict, List, Optional, Tuple, Union, Any
-from concurrent.futures import ThreadPoolExecutor
-
-# 配置日志
-logging.basicConfig(
- level=logging.INFO,
- format='%(asctime)s - %(levelname)s - %(message)s',
- handlers=[
- logging.FileHandler('ocr_processor.log', encoding='utf-8'),
- logging.StreamHandler(sys.stdout)
- ]
-)
-logger = logging.getLogger(__name__)
-
-# 默认配置
-DEFAULT_CONFIG = {
- 'API': {
- 'api_key': '', # 将从配置文件中读取
- 'secret_key': '', # 将从配置文件中读取
- 'timeout': '30',
- 'max_retries': '3',
- 'retry_delay': '2',
- 'api_url': 'https://aip.baidubce.com/rest/2.0/ocr/v1/table'
- },
- 'Paths': {
- 'input_folder': 'input',
- 'output_folder': 'output',
- 'temp_folder': 'temp',
- 'processed_record': 'processed_files.json'
- },
- 'Performance': {
- 'max_workers': '4',
- 'batch_size': '5',
- 'skip_existing': 'true'
- },
- 'File': {
- 'allowed_extensions': '.jpg,.jpeg,.png,.bmp',
- 'excel_extension': '.xlsx',
- 'max_file_size_mb': '4'
- }
-}
-
-class ConfigManager:
- """配置管理类,负责加载和保存配置"""
-
- def __init__(self, config_file: str = 'config.ini'):
- self.config_file = config_file
- self.config = configparser.ConfigParser()
- self.load_config()
-
- def load_config(self) -> None:
- """加载配置文件,如果不存在则创建默认配置"""
- if not os.path.exists(self.config_file):
- self.create_default_config()
-
- try:
- self.config.read(self.config_file, encoding='utf-8')
- logger.info(f"已加载配置文件: {self.config_file}")
- except Exception as e:
- logger.error(f"加载配置文件时出错: {e}")
- logger.info("使用默认配置")
- self.create_default_config(save=False)
-
- def create_default_config(self, save: bool = True) -> None:
- """创建默认配置"""
- for section, options in DEFAULT_CONFIG.items():
- if not self.config.has_section(section):
- self.config.add_section(section)
-
- for option, value in options.items():
- self.config.set(section, option, value)
-
- if save:
- self.save_config()
- logger.info(f"已创建默认配置文件: {self.config_file}")
-
- def save_config(self) -> None:
- """保存配置到文件"""
- try:
- with open(self.config_file, 'w', encoding='utf-8') as f:
- self.config.write(f)
- except Exception as e:
- logger.error(f"保存配置文件时出错: {e}")
-
- def get(self, section: str, option: str, fallback: Any = None) -> Any:
- """获取配置值"""
- return self.config.get(section, option, fallback=fallback)
-
- def getint(self, section: str, option: str, fallback: int = 0) -> int:
- """获取整数配置值"""
- return self.config.getint(section, option, fallback=fallback)
-
- def getfloat(self, section: str, option: str, fallback: float = 0.0) -> float:
- """获取浮点数配置值"""
- return self.config.getfloat(section, option, fallback=fallback)
-
- def getboolean(self, section: str, option: str, fallback: bool = False) -> bool:
- """获取布尔配置值"""
- return self.config.getboolean(section, option, fallback=fallback)
-
- def get_list(self, section: str, option: str, fallback: str = "", delimiter: str = ",") -> List[str]:
- """获取列表配置值"""
- value = self.get(section, option, fallback)
- return [item.strip() for item in value.split(delimiter) if item.strip()]
-
-class TokenManager:
- """令牌管理类,负责获取和刷新百度API访问令牌"""
-
- def __init__(self, api_key: str, secret_key: str, max_retries: int = 3, retry_delay: int = 2):
- self.api_key = api_key
- self.secret_key = secret_key
- self.max_retries = max_retries
- self.retry_delay = retry_delay
- self.access_token = None
- self.token_expiry = 0
-
- def get_token(self) -> Optional[str]:
- """获取访问令牌,如果令牌已过期则刷新"""
- if self.is_token_valid():
- return self.access_token
-
- return self.refresh_token()
-
- def is_token_valid(self) -> bool:
- """检查令牌是否有效"""
- return (
- self.access_token is not None and
- self.token_expiry > time.time() + 60 # 提前1分钟刷新
- )
-
- def refresh_token(self) -> Optional[str]:
- """刷新访问令牌"""
- url = "https://aip.baidubce.com/oauth/2.0/token"
- params = {
- "grant_type": "client_credentials",
- "client_id": self.api_key,
- "client_secret": self.secret_key
- }
-
- for attempt in range(self.max_retries):
- try:
- response = requests.post(url, params=params, timeout=10)
- if response.status_code == 200:
- result = response.json()
- if "access_token" in result:
- self.access_token = result["access_token"]
- # 设置令牌过期时间(默认30天,提前1小时过期以确保安全)
- self.token_expiry = time.time() + result.get("expires_in", 2592000) - 3600
- logger.info("成功获取访问令牌")
- return self.access_token
-
- logger.warning(f"获取访问令牌失败 (尝试 {attempt+1}/{self.max_retries}): {response.text}")
-
- except Exception as e:
- logger.warning(f"获取访问令牌时发生错误 (尝试 {attempt+1}/{self.max_retries}): {e}")
-
- # 如果不是最后一次尝试,则等待后重试
- if attempt < self.max_retries - 1:
- time.sleep(self.retry_delay * (attempt + 1)) # 指数退避
-
- logger.error("无法获取访问令牌")
- return None
-
-class ProcessedRecordManager:
- """处理记录管理器,用于跟踪已处理的文件"""
-
- def __init__(self, record_file: str):
- self.record_file = record_file
- self.processed_files = self._load_record()
-
- def _load_record(self) -> Dict[str, str]:
- """加载处理记录"""
- if os.path.exists(self.record_file):
- try:
- with open(self.record_file, 'r', encoding='utf-8') as f:
- return json.load(f)
- except Exception as e:
- logger.error(f"加载处理记录时出错: {e}")
-
- return {}
-
- def save_record(self) -> None:
- """保存处理记录"""
- try:
- with open(self.record_file, 'w', encoding='utf-8') as f:
- json.dump(self.processed_files, f, ensure_ascii=False, indent=2)
- except Exception as e:
- logger.error(f"保存处理记录时出错: {e}")
-
- def is_processed(self, image_file: str) -> bool:
- """检查文件是否已处理"""
- return image_file in self.processed_files
-
- def mark_as_processed(self, image_file: str, output_file: str) -> None:
- """标记文件为已处理"""
- self.processed_files[image_file] = output_file
- self.save_record()
-
- def get_output_file(self, image_file: str) -> Optional[str]:
- """获取已处理文件对应的输出文件"""
- return self.processed_files.get(image_file)
-
-class OCRProcessor:
- """OCR处理器核心类,用于识别表格并保存为Excel"""
-
- def __init__(self, config_manager: ConfigManager):
- self.config = config_manager
-
- # 路径配置
- self.input_folder = self.config.get('Paths', 'input_folder')
- self.output_folder = self.config.get('Paths', 'output_folder')
- self.temp_folder = self.config.get('Paths', 'temp_folder')
- self.processed_record_file = os.path.join(
- self.config.get('Paths', 'output_folder'),
- self.config.get('Paths', 'processed_record')
- )
-
- # API配置
- self.api_url = self.config.get('API', 'api_url')
- self.timeout = self.config.getint('API', 'timeout')
- self.max_retries = self.config.getint('API', 'max_retries')
- self.retry_delay = self.config.getint('API', 'retry_delay')
-
- # 文件配置
- self.allowed_extensions = self.config.get_list('File', 'allowed_extensions')
- self.excel_extension = self.config.get('File', 'excel_extension')
- self.max_file_size_mb = self.config.getfloat('File', 'max_file_size_mb')
-
- # 性能配置
- self.max_workers = self.config.getint('Performance', 'max_workers')
- self.batch_size = self.config.getint('Performance', 'batch_size')
- self.skip_existing = self.config.getboolean('Performance', 'skip_existing')
-
- # 初始化其他组件
- self.token_manager = TokenManager(
- self.config.get('API', 'api_key'),
- self.config.get('API', 'secret_key'),
- self.max_retries,
- self.retry_delay
- )
- self.record_manager = ProcessedRecordManager(self.processed_record_file)
-
- # 确保文件夹存在
- for folder in [self.input_folder, self.output_folder, self.temp_folder]:
- os.makedirs(folder, exist_ok=True)
- logger.info(f"已确保文件夹存在: {folder}")
-
- def get_unprocessed_images(self) -> List[str]:
- """获取待处理的图像文件列表"""
- all_files = []
- for ext in self.allowed_extensions:
- all_files.extend(Path(self.input_folder).glob(f"*{ext}"))
-
- # 转换为字符串路径
- file_paths = [str(file_path) for file_path in all_files]
-
- if self.skip_existing:
- # 过滤掉已处理的文件
- return [
- file_path for file_path in file_paths
- if not self.record_manager.is_processed(os.path.basename(file_path))
- ]
-
- return file_paths
-
- def validate_image(self, image_path: str) -> bool:
- """验证图像文件是否有效且符合大小限制"""
- # 检查文件是否存在
- if not os.path.exists(image_path):
- logger.error(f"文件不存在: {image_path}")
- return False
-
- # 检查是否是文件
- if not os.path.isfile(image_path):
- logger.error(f"路径不是文件: {image_path}")
- return False
-
- # 检查文件大小
- file_size_mb = os.path.getsize(image_path) / (1024 * 1024)
- if file_size_mb > self.max_file_size_mb:
- logger.error(f"文件过大 ({file_size_mb:.2f}MB > {self.max_file_size_mb}MB): {image_path}")
- return False
-
- # 检查文件扩展名
- _, ext = os.path.splitext(image_path)
- if ext.lower() not in self.allowed_extensions:
- logger.error(f"不支持的文件格式 {ext}: {image_path}")
- return False
-
- return True
-
- def rename_image_to_timestamp(self, image_path: str) -> str:
- """将图像文件重命名为时间戳格式"""
- try:
- # 获取目录和文件扩展名
- dir_name = os.path.dirname(image_path)
- file_ext = os.path.splitext(image_path)[1]
-
- # 生成时间戳文件名
- timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
- new_filename = f"{timestamp}{file_ext}"
-
- # 构建新路径
- new_path = os.path.join(dir_name, new_filename)
-
- # 如果目标文件已存在,添加毫秒级别的后缀
- if os.path.exists(new_path):
- timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S%f")
- new_filename = f"{timestamp}{file_ext}"
- new_path = os.path.join(dir_name, new_filename)
-
- # 重命名文件
- os.rename(image_path, new_path)
- logger.info(f"文件已重命名: {os.path.basename(image_path)} -> {new_filename}")
- return new_path
-
- except Exception as e:
- logger.error(f"重命名文件时出错: {e}")
- return image_path
-
- def recognize_table(self, image_path: str) -> Optional[Dict]:
- """使用百度表格OCR API识别图像中的表格"""
- # 获取访问令牌
- access_token = self.token_manager.get_token()
- if not access_token:
- logger.error("无法获取访问令牌")
- return None
-
- url = f"{self.api_url}?access_token={access_token}"
-
- for attempt in range(self.max_retries):
- try:
- # 读取图像文件并进行base64编码
- with open(image_path, 'rb') as f:
- image_data = f.read()
-
- image_base64 = base64.b64encode(image_data).decode('utf-8')
-
- # 设置请求头和请求参数
- headers = {'Content-Type': 'application/x-www-form-urlencoded'}
- params = {
- 'image': image_base64,
- 'return_excel': 'true' # 返回Excel文件编码
- }
-
- # 发送请求
- response = requests.post(
- url,
- data=params,
- headers=headers,
- timeout=self.timeout
- )
-
- # 检查响应状态
- if response.status_code == 200:
- result = response.json()
- if 'error_code' in result:
- error_msg = result.get('error_msg', '未知错误')
- logger.error(f"表格识别失败: {error_msg}")
-
- # 如果是授权错误,尝试刷新令牌
- if result.get('error_code') in [110, 111]: # 授权相关错误码
- self.token_manager.refresh_token()
- else:
- return result
- else:
- logger.error(f"表格识别失败: {response.status_code} - {response.text}")
-
- except Exception as e:
- logger.error(f"表格识别过程中发生错误 (尝试 {attempt+1}/{self.max_retries}): {e}")
-
- # 如果不是最后一次尝试,则等待后重试
- if attempt < self.max_retries - 1:
- wait_time = self.retry_delay * (2 ** attempt) # 指数退避
- logger.info(f"将在 {wait_time} 秒后重试...")
- time.sleep(wait_time)
-
- return None
-
- def save_to_excel(self, ocr_result: Dict, output_path: str) -> bool:
- """将表格识别结果保存为Excel文件"""
- try:
- # 检查结果中是否包含表格数据和Excel文件
- if not ocr_result:
- logger.error("无法保存结果: 识别结果为空")
- return False
-
- # 直接从excel_file字段获取Excel文件的base64编码
- excel_base64 = None
-
- if 'excel_file' in ocr_result:
- excel_base64 = ocr_result['excel_file']
- elif 'tables_result' in ocr_result and ocr_result['tables_result']:
- for table in ocr_result['tables_result']:
- if 'excel_file' in table:
- excel_base64 = table['excel_file']
- break
-
- if not excel_base64:
- logger.error("无法获取Excel文件编码")
- logger.debug(f"API返回结果: {json.dumps(ocr_result, ensure_ascii=False, indent=2)}")
- return False
-
- # 解码base64并保存Excel文件
- try:
- excel_data = base64.b64decode(excel_base64)
-
- # 确保输出目录存在
- os.makedirs(os.path.dirname(output_path), exist_ok=True)
-
- with open(output_path, 'wb') as f:
- f.write(excel_data)
-
- logger.info(f"成功保存表格数据到: {output_path}")
- return True
-
- except Exception as e:
- logger.error(f"解码Excel数据时出错: {e}")
- return False
-
- except Exception as e:
- logger.error(f"保存Excel文件时发生错误: {e}")
- return False
-
- def process_image(self, image_path: str) -> Optional[str]:
- """处理单个图像文件:验证、重命名、识别和保存"""
- try:
- # 获取原始图片文件名(不含扩展名)
- image_basename = os.path.basename(image_path)
- image_name_without_ext = os.path.splitext(image_basename)[0]
-
- # 检查是否已存在对应的Excel文件
- excel_filename = f"{image_name_without_ext}{self.excel_extension}"
- excel_path = os.path.join(self.output_folder, excel_filename)
-
- if os.path.exists(excel_path):
- logger.info(f"已存在对应的Excel文件,跳过处理: {image_basename} -> {excel_filename}")
- # 记录处理结果(虽然跳过了处理,但仍标记为已处理)
- self.record_manager.mark_as_processed(image_basename, excel_path)
- return excel_path
-
- # 检查文件是否已经处理过
- if self.skip_existing and self.record_manager.is_processed(image_basename):
- output_file = self.record_manager.get_output_file(image_basename)
- logger.info(f"文件已处理过,跳过: {image_basename} -> {output_file}")
- return output_file
-
- # 验证图像文件
- if not self.validate_image(image_path):
- logger.warning(f"图像验证失败: {image_path}")
- return None
-
- # 识别表格(不再重命名图片)
- logger.info(f"正在识别表格: {image_basename}")
- ocr_result = self.recognize_table(image_path)
-
- if not ocr_result:
- logger.error(f"表格识别失败: {image_basename}")
- return None
-
- # 保存结果到Excel,使用原始图片名
- if self.save_to_excel(ocr_result, excel_path):
- # 记录处理结果
- self.record_manager.mark_as_processed(image_basename, excel_path)
- return excel_path
-
- return None
-
- except Exception as e:
- logger.error(f"处理图像时发生错误: {e}")
- return None
-
- def process_images_batch(self, batch_size: int = None, max_workers: int = None) -> Tuple[int, int]:
- """批量处理图像文件"""
- if batch_size is None:
- batch_size = self.batch_size
-
- if max_workers is None:
- max_workers = self.max_workers
-
- # 获取待处理的图像文件
- image_files = self.get_unprocessed_images()
- total_files = len(image_files)
-
- if total_files == 0:
- logger.info("没有需要处理的图像文件")
- return 0, 0
-
- logger.info(f"找到 {total_files} 个待处理图像文件")
-
- # 处理所有文件
- processed_count = 0
- success_count = 0
-
- # 如果文件数量很少,直接顺序处理
- if total_files <= 2 or max_workers <= 1:
- for image_path in image_files:
- processed_count += 1
-
- logger.info(f"处理文件 ({processed_count}/{total_files}): {os.path.basename(image_path)}")
- output_path = self.process_image(image_path)
-
- if output_path:
- success_count += 1
- logger.info(f"处理成功 ({success_count}/{processed_count}): {os.path.basename(output_path)}")
- else:
- logger.warning(f"处理失败: {os.path.basename(image_path)}")
- else:
- # 使用线程池并行处理
- with ThreadPoolExecutor(max_workers=max_workers) as executor:
- for i in range(0, total_files, batch_size):
- batch = image_files[i:i+batch_size]
- batch_results = list(executor.map(self.process_image, batch))
-
- for j, result in enumerate(batch_results):
- processed_count += 1
- if result:
- success_count += 1
- logger.info(f"处理成功 ({success_count}/{processed_count}): {os.path.basename(result)}")
- else:
- logger.warning(f"处理失败: {os.path.basename(batch[j])}")
-
- logger.info(f"已处理 {processed_count}/{total_files} 个文件,成功率: {success_count/processed_count*100:.1f}%")
-
- logger.info(f"处理完成。总共处理 {processed_count} 个文件,成功 {success_count} 个,成功率: {success_count/max(processed_count,1)*100:.1f}%")
- return processed_count, success_count
-
- def check_processed_status(self) -> Dict[str, List[str]]:
- """检查处理状态,返回已处理和未处理的文件列表"""
- # 获取输入文件夹中的所有支持格式的图像文件
- all_images = []
- for ext in self.allowed_extensions:
- all_images.extend([str(file) for file in Path(self.input_folder).glob(f"*{ext}")])
-
- # 获取已处理的文件列表
- processed_files = list(self.record_manager.processed_files.keys())
-
- # 对路径进行规范化以便比较
- all_image_basenames = [os.path.basename(img) for img in all_images]
-
- # 找出未处理的文件
- unprocessed_files = [
- img for img, basename in zip(all_images, all_image_basenames)
- if basename not in processed_files
- ]
-
- # 找出已处理的文件及其对应的输出文件
- processed_with_output = {
- img: self.record_manager.get_output_file(basename)
- for img, basename in zip(all_images, all_image_basenames)
- if basename in processed_files
- }
-
- return {
- 'all': all_images,
- 'unprocessed': unprocessed_files,
- 'processed': processed_with_output
- }
-
-def main():
- """主函数: 解析命令行参数并执行相应操作"""
- import argparse
-
- parser = argparse.ArgumentParser(description='百度表格OCR识别工具')
- parser.add_argument('--config', type=str, default='config.ini', help='配置文件路径')
- parser.add_argument('--batch-size', type=int, help='批处理大小')
- parser.add_argument('--max-workers', type=int, help='最大工作线程数')
- parser.add_argument('--force', action='store_true', help='强制处理所有文件,包括已处理的文件')
- parser.add_argument('--check', action='store_true', help='检查处理状态而不执行处理')
-
- args = parser.parse_args()
-
- # 加载配置
- config_manager = ConfigManager(args.config)
-
- # 创建处理器
- processor = OCRProcessor(config_manager)
-
- # 根据命令行参数调整配置
- if args.force:
- processor.skip_existing = False
-
- if args.check:
- # 检查处理状态
- status = processor.check_processed_status()
-
- print("\n=== 处理状态 ===")
- print(f"总共 {len(status['all'])} 个图像文件")
- print(f"已处理: {len(status['processed'])} 个")
- print(f"未处理: {len(status['unprocessed'])} 个")
-
- if status['processed']:
- print("\n已处理文件:")
- for img, output in status['processed'].items():
- print(f" {os.path.basename(img)} -> {os.path.basename(output)}")
-
- if status['unprocessed']:
- print("\n未处理文件:")
- for img in status['unprocessed']:
- print(f" {os.path.basename(img)}")
-
- return
-
- # 处理图像
- batch_size = args.batch_size if args.batch_size is not None else processor.batch_size
- max_workers = args.max_workers if args.max_workers is not None else processor.max_workers
-
- processor.process_images_batch(batch_size, max_workers)
-
-if __name__ == "__main__":
- try:
- start_time = time.time()
- logger.info("开始百度表格OCR识别程序")
- main()
- elapsed_time = time.time() - start_time
- logger.info(f"百度表格OCR识别程序已完成,耗时: {elapsed_time:.2f}秒")
- except Exception as e:
- logger.error(f"程序执行过程中发生错误: {e}", exc_info=True)
- sys.exit(1)
\ No newline at end of file
diff --git a/backup/v1_backup_20250502190248/clean_files.py b/backup/v1_backup_20250502190248/clean_files.py
deleted file mode 100644
index bfd7b34..0000000
--- a/backup/v1_backup_20250502190248/clean_files.py
+++ /dev/null
@@ -1,587 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-文件清理工具
------------
-用于清理输入/输出目录中的旧文件,支持按天数和文件名模式进行清理。
-默认情况下会清理input目录下的所有图片文件和output目录下的Excel文件。
-"""
-
-import os
-import re
-import sys
-import logging
-import argparse
-from datetime import datetime, timedelta
-from pathlib import Path
-import time
-import glob
-
-# 配置日志
-log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'logs')
-os.makedirs(log_dir, exist_ok=True)
-log_file = os.path.join(log_dir, 'clean_files.log')
-
-logging.basicConfig(
- level=logging.INFO,
- format='%(asctime)s - %(levelname)s - %(message)s',
- handlers=[
- logging.FileHandler(log_file, encoding='utf-8'),
- logging.StreamHandler(sys.stdout)
- ]
-)
-logger = logging.getLogger(__name__)
-
-class FileCleaner:
- """文件清理工具类"""
-
- def __init__(self, input_dir="input", output_dir="output"):
- """初始化清理工具"""
- self.input_dir = input_dir
- self.output_dir = output_dir
- self.logs_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'logs')
-
- # 确保目录存在
- for directory in [self.input_dir, self.output_dir, self.logs_dir]:
- os.makedirs(directory, exist_ok=True)
- logger.info(f"确保目录存在: {directory}")
-
- def get_file_stats(self, directory):
- """获取目录的文件统计信息"""
- if not os.path.exists(directory):
- logger.warning(f"目录不存在: {directory}")
- return {}
-
- stats = {
- 'total_files': 0,
- 'total_size': 0,
- 'oldest_file': None,
- 'newest_file': None,
- 'file_types': {},
- 'files_by_age': {
- '1_day': 0,
- '7_days': 0,
- '30_days': 0,
- 'older': 0
- }
- }
-
- now = datetime.now()
- one_day_ago = now - timedelta(days=1)
- seven_days_ago = now - timedelta(days=7)
- thirty_days_ago = now - timedelta(days=30)
-
- for root, _, files in os.walk(directory):
- for file in files:
- file_path = os.path.join(root, file)
-
- # 跳过临时文件
- if file.startswith('~$') or file.startswith('.'):
- continue
-
- # 文件信息
- try:
- file_stats = os.stat(file_path)
- file_size = file_stats.st_size
- mod_time = datetime.fromtimestamp(file_stats.st_mtime)
-
- # 更新统计信息
- stats['total_files'] += 1
- stats['total_size'] += file_size
-
- # 更新最旧和最新文件
- if stats['oldest_file'] is None or mod_time < stats['oldest_file'][1]:
- stats['oldest_file'] = (file_path, mod_time)
-
- if stats['newest_file'] is None or mod_time > stats['newest_file'][1]:
- stats['newest_file'] = (file_path, mod_time)
-
- # 按文件类型统计
- ext = os.path.splitext(file)[1].lower()
- if ext in stats['file_types']:
- stats['file_types'][ext]['count'] += 1
- stats['file_types'][ext]['size'] += file_size
- else:
- stats['file_types'][ext] = {'count': 1, 'size': file_size}
-
- # 按年龄统计
- if mod_time > one_day_ago:
- stats['files_by_age']['1_day'] += 1
- elif mod_time > seven_days_ago:
- stats['files_by_age']['7_days'] += 1
- elif mod_time > thirty_days_ago:
- stats['files_by_age']['30_days'] += 1
- else:
- stats['files_by_age']['older'] += 1
-
- except Exception as e:
- logger.error(f"处理文件时出错 {file_path}: {e}")
-
- return stats
-
- def print_stats(self):
- """打印文件统计信息"""
- # 输入目录统计
- input_stats = self.get_file_stats(self.input_dir)
- output_stats = self.get_file_stats(self.output_dir)
-
- print("\n===== 文件统计信息 =====")
-
- # 打印输入目录统计
- if input_stats:
- print(f"\n输入目录 ({self.input_dir}):")
- print(f" 总文件数: {input_stats['total_files']}")
- print(f" 总大小: {self._format_size(input_stats['total_size'])}")
-
- if input_stats['oldest_file']:
- oldest = input_stats['oldest_file']
- print(f" 最旧文件: {os.path.basename(oldest[0])} ({oldest[1].strftime('%Y-%m-%d %H:%M:%S')})")
-
- if input_stats['newest_file']:
- newest = input_stats['newest_file']
- print(f" 最新文件: {os.path.basename(newest[0])} ({newest[1].strftime('%Y-%m-%d %H:%M:%S')})")
-
- print(" 文件年龄分布:")
- print(f" 1天内: {input_stats['files_by_age']['1_day']}个文件")
- print(f" 7天内(不含1天内): {input_stats['files_by_age']['7_days']}个文件")
- print(f" 30天内(不含7天内): {input_stats['files_by_age']['30_days']}个文件")
- print(f" 更旧: {input_stats['files_by_age']['older']}个文件")
-
- print(" 文件类型分布:")
- for ext, data in sorted(input_stats['file_types'].items(), key=lambda x: x[1]['count'], reverse=True):
- print(f" {ext or '无扩展名'}: {data['count']}个文件, {self._format_size(data['size'])}")
-
- # 打印输出目录统计
- if output_stats:
- print(f"\n输出目录 ({self.output_dir}):")
- print(f" 总文件数: {output_stats['total_files']}")
- print(f" 总大小: {self._format_size(output_stats['total_size'])}")
-
- if output_stats['oldest_file']:
- oldest = output_stats['oldest_file']
- print(f" 最旧文件: {os.path.basename(oldest[0])} ({oldest[1].strftime('%Y-%m-%d %H:%M:%S')})")
-
- if output_stats['newest_file']:
- newest = output_stats['newest_file']
- print(f" 最新文件: {os.path.basename(newest[0])} ({newest[1].strftime('%Y-%m-%d %H:%M:%S')})")
-
- print(" 文件年龄分布:")
- print(f" 1天内: {output_stats['files_by_age']['1_day']}个文件")
- print(f" 7天内(不含1天内): {output_stats['files_by_age']['7_days']}个文件")
- print(f" 30天内(不含7天内): {output_stats['files_by_age']['30_days']}个文件")
- print(f" 更旧: {output_stats['files_by_age']['older']}个文件")
-
- def _format_size(self, size_bytes):
- """格式化文件大小"""
- if size_bytes < 1024:
- return f"{size_bytes} 字节"
- elif size_bytes < 1024 * 1024:
- return f"{size_bytes/1024:.2f} KB"
- elif size_bytes < 1024 * 1024 * 1024:
- return f"{size_bytes/(1024*1024):.2f} MB"
- else:
- return f"{size_bytes/(1024*1024*1024):.2f} GB"
-
- def clean_files(self, directory, days=None, pattern=None, extensions=None, exclude_patterns=None, force=False, test_mode=False):
- """
- 清理指定目录中的文件
-
- 参数:
- directory (str): 要清理的目录
- days (int): 保留的天数,超过这个天数的文件将被清理,None表示不考虑时间
- pattern (str): 文件名匹配模式(正则表达式)
- extensions (list): 要删除的文件扩展名列表,如['.jpg', '.xlsx']
- exclude_patterns (list): 要排除的文件名模式列表
- force (bool): 是否强制清理,不显示确认提示
- test_mode (bool): 测试模式,只显示要删除的文件而不实际删除
-
- 返回:
- tuple: (cleaned_count, cleaned_size) 清理的文件数量和总大小
- """
- if not os.path.exists(directory):
- logger.warning(f"目录不存在: {directory}")
- return 0, 0
-
- cutoff_date = None
- if days is not None:
- cutoff_date = datetime.now() - timedelta(days=days)
-
- pattern_regex = re.compile(pattern) if pattern else None
-
- files_to_clean = []
-
- logger.info(f"扫描目录: {directory}")
-
- # 查找需要清理的文件
- for root, _, files in os.walk(directory):
- for file in files:
- file_path = os.path.join(root, file)
-
- # 跳过临时文件
- if file.startswith('~$') or file.startswith('.'):
- continue
-
- # 检查是否在排除列表中
- if exclude_patterns and any(pattern in file for pattern in exclude_patterns):
- logger.info(f"跳过文件: {file}")
- continue
-
- # 检查文件扩展名
- if extensions and not any(file.lower().endswith(ext.lower()) for ext in extensions):
- continue
-
- # 检查修改时间
- if cutoff_date:
- try:
- mod_time = datetime.fromtimestamp(os.path.getmtime(file_path))
- if mod_time >= cutoff_date:
- logger.debug(f"文件未超过保留天数: {file} - {mod_time.strftime('%Y-%m-%d %H:%M:%S')}")
- continue
- except Exception as e:
- logger.error(f"检查文件时间时出错 {file_path}: {e}")
- continue
-
- # 检查是否匹配模式
- if pattern_regex and not pattern_regex.search(file):
- continue
-
- try:
- file_size = os.path.getsize(file_path)
- files_to_clean.append((file_path, file_size))
- logger.info(f"找到要清理的文件: {file_path}")
- except Exception as e:
- logger.error(f"获取文件大小时出错 {file_path}: {e}")
-
- if not files_to_clean:
- logger.info(f"没有找到需要清理的文件: {directory}")
- return 0, 0
-
- # 显示要清理的文件
- total_size = sum(f[1] for f in files_to_clean)
- print(f"\n找到 {len(files_to_clean)} 个文件要清理,总大小: {self._format_size(total_size)}")
-
- if len(files_to_clean) > 10:
- print("前10个文件:")
- for file_path, size in files_to_clean[:10]:
- print(f" {os.path.basename(file_path)} ({self._format_size(size)})")
- print(f" ...以及其他 {len(files_to_clean) - 10} 个文件")
- else:
- for file_path, size in files_to_clean:
- print(f" {os.path.basename(file_path)} ({self._format_size(size)})")
-
- # 如果是测试模式,就不实际删除
- if test_mode:
- print("\n测试模式:不会实际删除文件。")
- return len(files_to_clean), total_size
-
- # 确认清理
- if not force:
- confirm = input(f"\n确定要清理这些文件吗?[y/N] ")
- if confirm.lower() != 'y':
- print("清理操作已取消。")
- return 0, 0
-
- # 执行清理
- cleaned_count = 0
- cleaned_size = 0
-
- for file_path, size in files_to_clean:
- try:
- # 删除文件
- try:
- # 尝试检查文件是否被其他进程占用
- if os.path.exists(file_path):
- # 在Windows系统上,可能需要先关闭可能打开的文件句柄
- if sys.platform == 'win32':
- try:
- # 尝试重命名文件,如果被占用通常会失败
- temp_path = file_path + '.temp'
- os.rename(file_path, temp_path)
- os.rename(temp_path, file_path)
- except Exception as e:
- logger.warning(f"文件可能被占用: {file_path}, 错误: {e}")
- # 尝试关闭文件句柄(仅Windows)
- try:
- import ctypes
- kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
- handle = kernel32.CreateFileW(file_path, 0x80000000, 0, None, 3, 0x80, None)
- if handle != -1:
- kernel32.CloseHandle(handle)
- except Exception:
- pass
-
- # 使用Path对象删除文件
- try:
- Path(file_path).unlink(missing_ok=True)
- logger.info(f"已删除文件: {file_path}")
-
- cleaned_count += 1
- cleaned_size += size
- except Exception as e1:
- # 如果Path.unlink失败,尝试使用os.remove
- try:
- os.remove(file_path)
- logger.info(f"使用os.remove删除文件: {file_path}")
-
- cleaned_count += 1
- cleaned_size += size
- except Exception as e2:
- logger.error(f"删除文件失败 {file_path}: {e1}, 再次尝试: {e2}")
- else:
- logger.warning(f"文件不存在或已被删除: {file_path}")
- except Exception as e:
- logger.error(f"删除文件时出错 {file_path}: {e}")
- except Exception as e:
- logger.error(f"处理文件时出错 {file_path}: {e}")
-
- print(f"\n已清理 {cleaned_count} 个文件,总大小: {self._format_size(cleaned_size)}")
-
- return cleaned_count, cleaned_size
-
- def clean_image_files(self, force=False, test_mode=False):
- """清理输入目录中的图片文件"""
- print(f"\n===== 清理输入目录图片文件 ({self.input_dir}) =====")
- image_extensions = ['.jpg', '.jpeg', '.png', '.bmp', '.gif']
- return self.clean_files(
- self.input_dir,
- days=None, # 不考虑天数,清理所有图片
- extensions=image_extensions,
- force=force,
- test_mode=test_mode
- )
-
- def clean_excel_files(self, force=False, test_mode=False):
- """清理输出目录中的Excel文件"""
- print(f"\n===== 清理输出目录Excel文件 ({self.output_dir}) =====")
- excel_extensions = ['.xlsx', '.xls']
- exclude_patterns = ['processed_files.json'] # 保留处理记录文件
- return self.clean_files(
- self.output_dir,
- days=None, # 不考虑天数,清理所有Excel
- extensions=excel_extensions,
- exclude_patterns=exclude_patterns,
- force=force,
- test_mode=test_mode
- )
-
- def clean_log_files(self, days=None, force=False, test_mode=False):
- """清理日志目录中的旧日志文件
-
- 参数:
- days (int): 保留的天数,超过这个天数的日志将被清理,None表示清理所有日志
- force (bool): 是否强制清理,不显示确认提示
- test_mode (bool): 测试模式,只显示要删除的文件而不实际删除
- """
- print(f"\n===== 清理日志文件 ({self.logs_dir}) =====")
- log_extensions = ['.log']
- # 排除当前正在使用的日志文件
- current_log = os.path.basename(log_file)
- logger.info(f"当前使用的日志文件: {current_log}")
-
- result = self.clean_files(
- self.logs_dir,
- days=days, # 如果days=None,清理所有日志文件
- extensions=log_extensions,
- exclude_patterns=[current_log], # 排除当前使用的日志文件
- force=force,
- test_mode=test_mode
- )
-
- return result
-
- def clean_logs(self, days=7, force=False, test=False):
- """清理日志目录中的日志文件"""
- try:
- logs_dir = self.logs_dir
- if not os.path.exists(logs_dir):
- logger.warning(f"日志目录不存在: {logs_dir}")
- return
-
- cutoff_date = datetime.now() - timedelta(days=days)
- files_to_delete = []
-
- # 检查是否有活跃标记文件
- active_files = set()
- for marker_file in glob.glob(os.path.join(logs_dir, '*.active')):
- active_log_name = os.path.basename(marker_file).replace('.active', '.log')
- active_files.add(active_log_name)
- logger.info(f"检测到活跃日志文件: {active_log_name}")
-
- for file_path in glob.glob(os.path.join(logs_dir, '*.log*')):
- file_name = os.path.basename(file_path)
-
- # 跳过活跃的日志文件
- if file_name in active_files:
- logger.info(f"跳过活跃日志文件: {file_name}")
- continue
-
- mtime = os.path.getmtime(file_path)
- if datetime.fromtimestamp(mtime) < cutoff_date:
- files_to_delete.append(file_path)
-
- if not files_to_delete:
- logger.info("没有找到需要清理的日志文件")
- return
-
- logger.info(f"找到 {len(files_to_delete)} 个过期的日志文件")
- for file_path in files_to_delete:
- if test:
- logger.info(f"测试模式 - 将删除: {os.path.basename(file_path)}")
- else:
- if not force:
- response = input(f"是否删除日志文件 {os.path.basename(file_path)}? (y/n): ")
- if response.lower() != 'y':
- logger.info(f"已跳过 {os.path.basename(file_path)}")
- continue
-
- try:
- os.remove(file_path)
- logger.info(f"已删除日志文件: {os.path.basename(file_path)}")
- except Exception as e:
- logger.error(f"删除文件失败: {file_path}, 错误: {e}")
-
- except Exception as e:
- logger.error(f"清理日志文件时出错: {e}")
-
- def clean_all_logs(self, force=False, test=False, except_current=True):
- """清理所有日志文件"""
- try:
- logs_dir = self.logs_dir
- if not os.path.exists(logs_dir):
- logger.warning(f"日志目录不存在: {logs_dir}")
- return
-
- # 检查是否有活跃标记文件
- active_files = set()
- for marker_file in glob.glob(os.path.join(logs_dir, '*.active')):
- active_log_name = os.path.basename(marker_file).replace('.active', '.log')
- active_files.add(active_log_name)
- logger.info(f"检测到活跃日志文件: {active_log_name}")
-
- files_to_delete = []
- for file_path in glob.glob(os.path.join(logs_dir, '*.log*')):
- file_name = os.path.basename(file_path)
-
- # 跳过当前正在使用的日志文件
- if except_current and file_name in active_files:
- logger.info(f"保留活跃日志文件: {file_name}")
- continue
-
- files_to_delete.append(file_path)
-
- if not files_to_delete:
- logger.info("没有找到需要清理的日志文件")
- return
-
- logger.info(f"找到 {len(files_to_delete)} 个日志文件需要清理")
- for file_path in files_to_delete:
- if test:
- logger.info(f"测试模式 - 将删除: {os.path.basename(file_path)}")
- else:
- if not force:
- response = input(f"是否删除日志文件 {os.path.basename(file_path)}? (y/n): ")
- if response.lower() != 'y':
- logger.info(f"已跳过 {os.path.basename(file_path)}")
- continue
-
- try:
- os.remove(file_path)
- logger.info(f"已删除日志文件: {os.path.basename(file_path)}")
- except Exception as e:
- logger.error(f"删除文件失败: {file_path}, 错误: {e}")
-
- except Exception as e:
- logger.error(f"清理所有日志文件时出错: {e}")
-
-def main():
- """主程序"""
- parser = argparse.ArgumentParser(description='文件清理工具')
- parser.add_argument('--stats', action='store_true', help='显示文件统计信息')
- parser.add_argument('--clean-input', action='store_true', help='清理输入目录中超过指定天数的文件')
- parser.add_argument('--clean-output', action='store_true', help='清理输出目录中超过指定天数的文件')
- parser.add_argument('--clean-images', action='store_true', help='清理输入目录中的所有图片文件')
- parser.add_argument('--clean-excel', action='store_true', help='清理输出目录中的所有Excel文件')
- parser.add_argument('--clean-logs', action='store_true', help='清理日志目录中超过指定天数的日志文件')
- parser.add_argument('--clean-all-logs', action='store_true', help='清理所有日志文件(除当前使用的)')
- parser.add_argument('--days', type=int, default=30, help='保留的天数,默认30天')
- parser.add_argument('--log-days', type=int, default=7, help='保留的日志天数,默认7天')
- parser.add_argument('--pattern', type=str, help='文件名匹配模式(正则表达式)')
- parser.add_argument('--force', action='store_true', help='强制清理,不显示确认提示')
- parser.add_argument('--test', action='store_true', help='测试模式,只显示要删除的文件而不实际删除')
- parser.add_argument('--input-dir', type=str, default='input', help='指定输入目录')
- parser.add_argument('--output-dir', type=str, default='output', help='指定输出目录')
- parser.add_argument('--help-only', action='store_true', help='只显示帮助信息,不执行任何操作')
- parser.add_argument('--all', action='store_true', help='清理所有类型的文件(输入、输出和日志)')
-
- args = parser.parse_args()
-
- cleaner = FileCleaner(args.input_dir, args.output_dir)
-
- # 显示统计信息
- if args.stats:
- cleaner.print_stats()
-
- # 如果指定了--help-only,只显示帮助信息
- if args.help_only:
- parser.print_help()
- return
-
- # 如果指定了--all,清理所有类型的文件
- if args.all:
- cleaner.clean_image_files(args.force, args.test)
- cleaner.clean_excel_files(args.force, args.test)
- cleaner.clean_log_files(args.log_days, args.force, args.test)
- cleaner.clean_all_logs(args.force, args.test)
- return
-
- # 清理输入目录中的图片文件
- if args.clean_images or not any([args.stats, args.clean_input, args.clean_output,
- args.clean_excel, args.clean_logs, args.clean_all_logs, args.help_only]):
- cleaner.clean_image_files(args.force, args.test)
-
- # 清理输出目录中的Excel文件
- if args.clean_excel or not any([args.stats, args.clean_input, args.clean_output,
- args.clean_images, args.clean_logs, args.clean_all_logs, args.help_only]):
- cleaner.clean_excel_files(args.force, args.test)
-
- # 清理日志文件(按天数)
- if args.clean_logs:
- cleaner.clean_log_files(args.log_days, args.force, args.test)
-
- # 清理所有日志文件
- if args.clean_all_logs:
- cleaner.clean_all_logs(args.force, args.test)
-
- # 清理输入目录(按天数)
- if args.clean_input:
- print(f"\n===== 清理输入目录 ({args.input_dir}) =====")
- cleaner.clean_files(
- args.input_dir,
- days=args.days,
- pattern=args.pattern,
- force=args.force,
- test_mode=args.test
- )
-
- # 清理输出目录(按天数)
- if args.clean_output:
- print(f"\n===== 清理输出目录 ({args.output_dir}) =====")
- cleaner.clean_files(
- args.output_dir,
- days=args.days,
- pattern=args.pattern,
- force=args.force,
- test_mode=args.test
- )
-
-if __name__ == "__main__":
- try:
- main()
- except KeyboardInterrupt:
- print("\n程序已被用户中断")
- except Exception as e:
- logger.error(f"程序运行出错: {e}", exc_info=True)
- print(f"程序运行出错: {e}")
- print("请查看日志文件了解详细信息")
- sys.exit(0)
\ No newline at end of file
diff --git a/backup/v1_backup_20250502190248/clean_logs.py b/backup/v1_backup_20250502190248/clean_logs.py
deleted file mode 100644
index 0d9a9b9..0000000
--- a/backup/v1_backup_20250502190248/clean_logs.py
+++ /dev/null
@@ -1,223 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-日志清理脚本
------------
-用于清理和管理日志文件,包括:
-1. 清理指定天数之前的日志文件
-2. 保留最新的N个日志文件
-3. 清理过大的日志文件
-4. 支持压缩旧日志文件
-"""
-
-import os
-import sys
-import time
-import shutil
-import logging
-import argparse
-from datetime import datetime, timedelta
-import gzip
-from pathlib import Path
-import glob
-import re
-
-# 配置日志
-logger = logging.getLogger(__name__)
-if not logger.handlers:
- log_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'logs', 'clean_logs.log')
- os.makedirs(os.path.dirname(log_file), exist_ok=True)
-
- logging.basicConfig(
- level=logging.INFO,
- format='%(asctime)s - %(levelname)s - %(message)s',
- handlers=[
- logging.FileHandler(log_file, encoding='utf-8'),
- logging.StreamHandler(sys.stdout)
- ]
- )
- logger = logging.getLogger(__name__)
-
- # 标记该日志文件为活跃
- active_marker = os.path.join(os.path.dirname(log_file), 'clean_logs.active')
- with open(active_marker, 'w') as f:
- f.write(f"Active since: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
-
-def is_log_active(log_file):
- """检查日志文件是否处于活跃状态(正在被使用)"""
- # 检查对应的活跃标记文件是否存在
- log_name = os.path.basename(log_file)
- base_name = os.path.splitext(log_name)[0]
- active_marker = os.path.join(os.path.dirname(log_file), f"{base_name}.active")
-
- # 如果活跃标记文件存在,说明日志文件正在被使用
- if os.path.exists(active_marker):
- logger.info(f"日志文件 {log_name} 正在使用中,不会被删除")
- return True
-
- # 检查是否是当前脚本正在使用的日志文件
- if log_name == os.path.basename(log_file):
- logger.info(f"当前脚本正在使用 {log_name},不会被删除")
- return True
-
- return False
-
-def clean_logs(log_dir="logs", max_days=7, max_files=10, max_size=100, force=False):
- """
- 清理日志文件
-
- 参数:
- log_dir: 日志目录
- max_days: 保留的最大天数
- max_files: 保留的最大文件数
- max_size: 日志文件大小上限(MB)
- force: 是否强制清理
- """
- logger.info(f"开始清理日志目录: {log_dir}")
-
- # 确保日志目录存在
- if not os.path.exists(log_dir):
- logger.warning(f"日志目录不存在: {log_dir}")
- return
-
- # 获取所有日志文件
- log_files = []
- for ext in ['*.log', '*.log.*']:
- log_files.extend(glob.glob(os.path.join(log_dir, ext)))
-
- if not log_files:
- logger.info(f"没有找到日志文件")
- return
-
- logger.info(f"找到 {len(log_files)} 个日志文件")
-
- # 按修改时间排序
- log_files.sort(key=lambda x: os.path.getmtime(x), reverse=True)
-
- # 处理大文件
- for log_file in log_files:
- # 跳过活跃的日志文件
- if is_log_active(log_file):
- continue
-
- # 检查文件大小
- file_size_mb = os.path.getsize(log_file) / (1024 * 1024)
- if file_size_mb > max_size:
- logger.info(f"日志文件 {os.path.basename(log_file)} 大小为 {file_size_mb:.2f}MB,超过限制 {max_size}MB")
-
- # 压缩并重命名大文件
- compressed_file = f"{log_file}.{datetime.now().strftime('%Y%m%d%H%M%S')}.zip"
- try:
- shutil.make_archive(os.path.splitext(compressed_file)[0], 'zip', log_dir, os.path.basename(log_file))
- logger.info(f"已压缩日志文件: {compressed_file}")
-
- # 清空原文件内容
- if not force:
- confirm = input(f"是否清空日志文件 {os.path.basename(log_file)}? (y/n): ")
- if confirm.lower() != 'y':
- logger.info("已取消清空操作")
- continue
-
- with open(log_file, 'w') as f:
- f.write(f"日志已于 {datetime.now()} 清空并压缩\n")
- logger.info(f"已清空日志文件: {os.path.basename(log_file)}")
- except Exception as e:
- logger.error(f"压缩日志文件时出错: {e}")
-
- # 清理过期的文件
- cutoff_date = datetime.now() - timedelta(days=max_days)
- files_to_delete = []
-
- for log_file in log_files[max_files:]:
- # 跳过活跃的日志文件
- if is_log_active(log_file):
- continue
-
- mtime = datetime.fromtimestamp(os.path.getmtime(log_file))
- if mtime < cutoff_date:
- files_to_delete.append(log_file)
-
- if not files_to_delete:
- logger.info("没有需要删除的过期日志文件")
- return
-
- logger.info(f"找到 {len(files_to_delete)} 个过期日志文件")
-
- # 确认删除
- if not force:
- print(f"以下 {len(files_to_delete)} 个文件将被删除:")
- for file in files_to_delete:
- print(f" - {os.path.basename(file)}")
- confirm = input("确认删除? (y/n): ")
- if confirm.lower() != 'y':
- logger.info("已取消删除操作")
- return
-
- # 删除文件
- deleted_count = 0
- for file in files_to_delete:
- try:
- os.remove(file)
- logger.info(f"已删除日志文件: {os.path.basename(file)}")
- deleted_count += 1
- except Exception as e:
- logger.error(f"删除日志文件时出错: {e}")
-
- logger.info(f"成功删除 {deleted_count} 个日志文件")
-
-def show_stats(log_dir="logs"):
- """显示日志文件统计信息"""
- if not os.path.exists(log_dir):
- print(f"日志目录不存在: {log_dir}")
- return
-
- log_files = []
- for ext in ['*.log', '*.log.*']:
- log_files.extend(glob.glob(os.path.join(log_dir, ext)))
-
- if not log_files:
- print("没有找到日志文件")
- return
-
- print(f"\n找到 {len(log_files)} 个日志文件:")
- print("=" * 80)
- print(f"{'文件名':<30} {'大小':<10} {'最后修改时间':<20} {'状态':<10}")
- print("-" * 80)
-
- total_size = 0
- for file in sorted(log_files, key=lambda x: os.path.getmtime(x), reverse=True):
- size = os.path.getsize(file)
- total_size += size
-
- mtime = datetime.fromtimestamp(os.path.getmtime(file))
- size_str = f"{size / 1024:.1f} KB" if size < 1024*1024 else f"{size / (1024*1024):.1f} MB"
-
- # 检查是否是活跃日志
- status = "活跃" if is_log_active(file) else ""
-
- print(f"{os.path.basename(file):<30} {size_str:<10} {mtime.strftime('%Y-%m-%d %H:%M:%S'):<20} {status:<10}")
-
- print("-" * 80)
- total_size_str = f"{total_size / 1024:.1f} KB" if total_size < 1024*1024 else f"{total_size / (1024*1024):.1f} MB"
- print(f"总大小: {total_size_str}")
- print("=" * 80)
-
-def main():
- parser = argparse.ArgumentParser(description="日志文件清理工具")
- parser.add_argument("--max-days", type=int, default=7, help="日志保留的最大天数")
- parser.add_argument("--max-files", type=int, default=10, help="保留的最大文件数")
- parser.add_argument("--max-size", type=float, default=100, help="日志文件大小上限(MB)")
- parser.add_argument("--force", action="store_true", help="强制清理,不提示确认")
- parser.add_argument("--stats", action="store_true", help="显示日志统计信息")
- parser.add_argument("--log-dir", type=str, default="logs", help="日志目录")
-
- args = parser.parse_args()
-
- if args.stats:
- show_stats(args.log_dir)
- else:
- clean_logs(args.log_dir, args.max_days, args.max_files, args.max_size, args.force)
-
-if __name__ == "__main__":
- main()
\ No newline at end of file
diff --git a/backup/v1_backup_20250502190248/excel_processor_step2.py b/backup/v1_backup_20250502190248/excel_processor_step2.py
deleted file mode 100644
index 6282490..0000000
--- a/backup/v1_backup_20250502190248/excel_processor_step2.py
+++ /dev/null
@@ -1,1364 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Excel处理程序 - 第二步
--------------------
-读取OCR识别后的Excel文件,提取条码、单价和数量,
-并创建采购单Excel文件。
-"""
-
-import os
-import sys
-import re
-import logging
-import pandas as pd
-import numpy as np
-import xlrd
-import xlwt
-from pathlib import Path
-from typing import Dict, List, Optional, Tuple, Union, Any
-from datetime import datetime
-import random
-from xlutils.copy import copy as xlcopy
-import time
-import json
-
-# 配置日志
-logger = logging.getLogger(__name__)
-if not logger.handlers:
- log_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'logs', 'excel_processor.log')
- os.makedirs(os.path.dirname(log_file), exist_ok=True)
-
-logging.basicConfig(
- level=logging.INFO,
- format='%(asctime)s - %(levelname)s - %(message)s',
- handlers=[
- logging.FileHandler(log_file, encoding='utf-8'),
- logging.StreamHandler(sys.stdout)
- ]
-)
-logger = logging.getLogger(__name__)
-logger.info("初始化日志系统")
-
-class ExcelProcessorStep2:
- """
- Excel处理器第二步:处理OCR识别后的Excel文件,
- 提取条码、单价和数量,并按照银豹采购单模板的格式填充
- """
-
- def __init__(self, output_dir="output"):
- """
- 初始化Excel处理器,并设置输出目录
- """
- logger.info("初始化ExcelProcessorStep2")
- self.output_dir = output_dir
-
- # 确保输出目录存在
- if not os.path.exists(output_dir):
- os.makedirs(output_dir)
- logger.info(f"创建输出目录: {output_dir}")
-
- # 设置路径
- self.template_path = os.path.join("templets", "银豹-采购单模板.xls")
-
- # 检查模板文件是否存在
- if not os.path.exists(self.template_path):
- logger.error(f"模板文件不存在: {self.template_path}")
- raise FileNotFoundError(f"模板文件不存在: {self.template_path}")
-
- # 用于记录已处理的文件
- self.cache_file = os.path.join(output_dir, "processed_files.json")
- self.processed_files = {} # 清空已处理文件记录
-
- # 特殊条码配置
- self.special_barcodes = {
- '6925019900087': {
- 'multiplier': 10, # 数量乘以10
- 'target_unit': '瓶', # 目标单位
- 'description': '特殊处理:数量*10,单位转换为瓶'
- }
- # 可以在这里添加更多特殊条码的配置
- }
-
- logger.info(f"初始化完成,模板文件: {self.template_path}")
-
- def _load_processed_files(self):
- """加载已处理文件的缓存"""
- if os.path.exists(self.cache_file):
- try:
- with open(self.cache_file, 'r', encoding='utf-8') as f:
- cache = json.load(f)
- logger.info(f"加载已处理文件缓存,共{len(cache)} 条记录")
- return cache
- except Exception as e:
- logger.warning(f"读取缓存文件失败: {e}")
- return {}
-
- def _save_processed_files(self):
- """保存已处理文件的缓存"""
- try:
- with open(self.cache_file, 'w', encoding='utf-8') as f:
- json.dump(self.processed_files, f, ensure_ascii=False, indent=2)
- logger.info(f"已更新处理文件缓存,共{len(self.processed_files)} 条记录")
- except Exception as e:
- logger.warning(f"保存缓存文件失败: {e}")
-
- def get_latest_excel(self):
- """
- 获取output目录下最新的Excel文件
- """
- logger.info(f"搜索目录 {self.output_dir} 中的Excel文件")
- excel_files = []
-
- for file in os.listdir(self.output_dir):
- # 忽略临时文件(以~$开头的文件)和已处理的文件(以"采购单_"开头的文件)
- if file.lower().endswith('.xlsx') and not file.startswith('~$') and not file.startswith('采购单_'):
- file_path = os.path.join(self.output_dir, file)
- excel_files.append((file_path, os.path.getmtime(file_path)))
-
- if not excel_files:
- logger.warning(f"未在 {self.output_dir} 目录下找到未处理的Excel文件")
- return None
-
- # 按修改时间排序,获取最新的文件
- latest_file = sorted(excel_files, key=lambda x: x[1], reverse=True)[0][0]
- logger.info(f"找到最新的Excel文件: {latest_file}")
- return latest_file
-
- def validate_barcode(self, barcode):
- """
- 验证条码是否有效
- 新增功能:如果条码是"仓库",则返回False以避免误认为有效条码
- """
- # 处理"仓库"特殊情况
- if isinstance(barcode, str) and barcode.strip() in ["仓库", "仓库全名"]:
- logger.warning(f"条码为仓库标识: {barcode}")
- return False
-
- # 处理科学计数法
- if isinstance(barcode, (int, float)):
- barcode = f"{barcode:.0f}"
-
- # 清理条码格式,移除可能的非数字字符(包括小数点)
- barcode_clean = re.sub(r'\.0+$', '', str(barcode)) # 移除末尾0
- barcode_clean = re.sub(r'\D', '', barcode_clean) # 只保留数字
-
- # 对特定的错误条码进行修正(开头改6开头)
- if len(barcode_clean) > 8 and barcode_clean.startswith('5') and not barcode_clean.startswith('53'):
- barcode_clean = '6' + barcode_clean[1:]
- logger.info(f"修正条码前缀 5->6: {barcode} -> {barcode_clean}")
-
- # 验证条码长度
- if len(barcode_clean) < 8 or len(barcode_clean) > 13:
- logger.warning(f"条码长度异常: {barcode_clean}, 长度={len(barcode_clean)}")
- return False
-
- # 验证条码是否全为数字
- if not barcode_clean.isdigit():
- logger.warning(f"条码包含非数字字符: {barcode_clean}")
- return False
-
- # 对于序号9的特殊情况,允许其条码格式
- if barcode_clean == "5321545613":
- logger.info(f"特殊条码验证通过: {barcode_clean}")
- return True
-
- logger.info(f"条码验证通过: {barcode_clean}")
- return True
-
- def parse_specification(self, spec_str):
- """
- 解析规格字符串,提取包装数量
- 支持格式1*15 1x15 格式
-
- 新增支持*5*10 格式,其中最后的数字表示包装数量(例如:1袋)
- """
- if not spec_str or not isinstance(spec_str, str):
- logger.warning(f"无效的规格字符串: {spec_str}")
- return None
-
- try:
- # 清理规格字符串
- spec_str = spec_str.strip()
-
- # 新增:匹配1*5*10 格式的三级规格
- match = re.search(r'(\d+)[\*xX×](\d+)[\*xX×](\d+)', spec_str)
- if match:
- # 取最后一个数字作为袋数量
- return int(match.group(3))
-
- # 1. 匹配 1*15 1x15 格式
- match = re.search(r'(\d+)[\*xX×](\d+)', spec_str)
- if match:
- # 取第二个数字作为包装数量
- return int(match.group(2))
-
- # 2. 匹配 24瓶个支袋格式
- match = re.search(r'(\d+)[瓶个支袋][//](件|箱)', spec_str)
- if match:
- return int(match.group(1))
-
- # 3. 匹配 500ml*15 格式
- match = re.search(r'\d+(?:ml|ML|毫升)[\*xX×](\d+)', spec_str)
- if match:
- return int(match.group(1))
-
- # 4. 提取最后一个数字作为包装数量(兜底方案)
- numbers = re.findall(r'\d+', spec_str)
- if numbers:
- # 对于类似 "330ml*24" 的规格,最后一个数字通常是包装数量
- return int(numbers[-1])
-
- except (ValueError, IndexError) as e:
- logger.warning(f"解析规格'{spec_str}'时出错: {e}")
-
- return None
-
- def infer_specification_from_name(self, product_name):
- """
- 从商品名称推断规格
- 根据特定的命名规则匹配规格信息
-
- 示例
- - 445水溶C血5入纸-> 1*15
- - 500-东方树叶-绿茶1*15-纸箱开盖活动装 -> 1*15
- - 12.9L桶装-> 12.9L*1
- - 900树叶茉莉花茶12入纸-> 1*12
- - 500茶π蜜桃乌龙15纸箱 -> 1*15
- """
- if not product_name or not isinstance(product_name, str):
- logger.warning(f"无效的商品名: {product_name}")
- return None, None
-
- product_name = product_name.strip()
- logger.info(f"从商品名称推断规格: {product_name}")
-
- # 特定商品规则匹配
- spec_rules = [
- # 445水溶C系列
- (r'445水溶C.*?(\d+)[入个]纸箱', lambda m: f"1*{m.group(1)}"),
-
- # 东方树叶系列
- (r'东方树叶.*?(\d+\*\d+).*纸箱', lambda m: m.group(1)),
- (r'东方树叶.*?纸箱.*?(\d+\*\d+)', lambda m: m.group(1)),
-
- # 桶装
- (r'(\d+\.?\d*L)桶装', lambda m: f"{m.group(1)}*1"),
-
- # 树叶茶系
- (r'树叶.*?(\d+)[入个]纸箱', lambda m: f"1*{m.group(1)}"),
- (r'(\d+)树叶.*?(\d+)[入个]纸箱', lambda m: f"1*{m.group(2)}"),
-
- # 茶m系列
- (r'茶m.*?(\d+)纸箱', lambda m: f"1*{m.group(1)}"),
- (r'(\d+)茶m.*?(\d+)纸箱', lambda m: f"1*{m.group(2)}"),
-
- # 茶π系列
- (r'茶[πΠπ].*?(\d+)纸箱', lambda m: f"1*{m.group(1)}"),
- (r'(\d+)茶[πΠπ].*?(\d+)纸箱', lambda m: f"1*{m.group(2)}"),
-
- # 通用入数匹配
- (r'.*?(\d+)[入个](?:纸箱|箱装)', lambda m: f"1*{m.group(1)}"),
- (r'.*?箱装.*?(\d+)[入个]', lambda m: f"1*{m.group(1)}"),
-
- # 通用数字+纸箱格式,如"500茶π蜜桃乌龙15纸箱"
- (r'.*?(\d+)纸箱', lambda m: f"1*{m.group(1)}")
- ]
-
- # 尝试所有规则
- for pattern, formatter in spec_rules:
- match = re.search(pattern, product_name)
- if match:
- spec = formatter(match)
- logger.info(f"根据名称 '{product_name}' 推断规格: {spec}")
-
- # 提取包装数量
- package_quantity = self.parse_specification(spec)
- if package_quantity:
- return spec, package_quantity
-
- # 尝试直接从名称中提取数字*数字格式
- match = re.search(r'(\d+\*\d+)', product_name)
- if match:
- spec = match.group(1)
- package_quantity = self.parse_specification(spec)
- if package_quantity:
- logger.info(f"从名称中直接提取规格: {spec}, 包装数量={package_quantity}")
- return spec, package_quantity
-
- # 尝试从名称中提取末尾数字
- match = re.search(r'(\d+)[入个]$', product_name)
- if match:
- qty = match.group(1)
- spec = f"1*{qty}"
- logger.info(f"从名称末尾提取入数: {spec}")
- return spec, int(qty)
-
- # 最后尝试提取任何位置的数字,默认如果有数字15,很可能5件装
- numbers = re.findall(r'\d+', product_name)
- if numbers:
- for num in numbers:
- # 检查是否为典型的件装数(12/15/24/30)
- if num in ['12', '15', '24', '30']:
- spec = f"1*{num}"
- logger.info(f"从名称中提取可能的件装数: {spec}")
- return spec, int(num)
-
- logger.warning(f"无法从商品名'{product_name}' 推断规格")
- return None, None
-
- def extract_unit_from_quantity(self, quantity_str):
- """
- 从数量字符串中提取单位
- 例如
- - '2' -> (2, '')
- - '5' -> (5, '')
- - '3' -> (3, '')
- - '10' -> (10, '')
- """
- if not quantity_str:
- return None, None
-
- # 如果是数字,直接返回数字和None
- if isinstance(quantity_str, (int, float)):
- return float(quantity_str), None
-
- # 转为字符串并清理
- quantity_str = str(quantity_str).strip()
- logger.info(f"从数量字符串提取单位: {quantity_str}")
-
- # 匹配数字+单位格式
- match = re.match(r'^([\d\.]+)\s*([^\d\s\.]+)$', quantity_str)
- if match:
- try:
- value = float(match.group(1))
- unit = match.group(2)
- logger.info(f"提取到数字: {value}, 单位: {unit}")
- return value, unit
- except ValueError:
- logger.warning(f"无法解析数量: {match.group(1)}")
-
- # 如果只有数字,直接返回数字
- if re.match(r'^[\d\.]+$', quantity_str):
- try:
- value = float(quantity_str)
- logger.info(f"提取到数字: {value}, 无单位")
- return value, None
- except ValueError:
- logger.warning(f"无法解析纯数字数字: {quantity_str}")
-
- # 如果只有单位,尝试查找其他可能包含数字的部分
- match = re.match(r'^([^\d\s\.]+)$', quantity_str)
- if match:
- unit = match.group(1)
- logger.info(f"仅提取到单位: {unit}, 无数值")
- return None, unit
-
- logger.warning(f"无法提取数量和单位: {quantity_str}")
- return None, None
-
- def extract_barcode(self, df: pd.DataFrame) -> List[str]:
- """从数据框中提取条码"""
- barcodes = []
-
- # 遍历数据框查找条码
- for _, row in df.iterrows():
- for col_name, value in row.items():
- # 转换为字符串并处理
- if value is not None and not pd.isna(value):
- value_str = str(value).strip()
- # 特殊处理特定条码
- if value_str == "5321545613":
- barcodes.append(value_str)
- logger.info(f"特殊条码提取: {value_str}")
- continue
-
- if self.validate_barcode(value_str):
- # 提取数字部分
- barcode = re.sub(r'\D', '', value_str)
- barcodes.append(barcode)
-
- logger.info(f"提取到{len(barcodes)} 个条码")
- return barcodes
-
- def extract_product_info(self, df: pd.DataFrame) -> List[Dict]:
- """
- 提取产品信息,包括条码、单价、数量、金额等
- 增加识别赠品功能:金额为0或为空的产品视为赠品
- 修改后的功能:当没有有效条码时,使用行号作为临时条码
- """
- logger.info(f"正在从数据帧中提取产品信息")
- product_info = []
-
- try:
- # 打印列名,用于调试
- logger.info(f"Excel文件的列名: {df.columns.tolist()}")
-
- # 检查是否有特殊表头结构(如"武侯环球乐百惠便利店3333.xlsx")
- # 判断依据:检查第3行是否包含常见的商品表头信息
- special_header = False
- if len(df) > 3: # 确保有足够的行
- row3 = df.iloc[3].astype(str)
- header_keywords = ['行号', '条形码', '条码', '商品名称', '规格', '单价', '数量', '金额', '单位']
- # 计算匹配的关键词数量
- matches = sum(1 for keyword in header_keywords if any(keyword in str(val) for val in row3.values))
- # 如果匹配了至少3个关键词,认为第3行是表头
- if matches >= 3:
- logger.info(f"检测到特殊表头结构,使用第3行作为列名: {row3.values.tolist()}")
- # 创建新的数据帧,使用第3行作为列名,数据从第4行开始
- header_row = df.iloc[3]
- data_rows = df.iloc[4:].reset_index(drop=True)
- # 为每一列分配一个名称(避免重复的列名)
- new_columns = []
- for i, col in enumerate(header_row):
- col_str = str(col)
- if col_str == 'nan' or col_str == 'None' or pd.isna(col):
- new_columns.append(f"Col_{i}")
- else:
- new_columns.append(col_str)
- # 使用新列名创建新的DataFrame
- data_rows.columns = new_columns
- df = data_rows
- special_header = True
- logger.info(f"重新构建的数据帧列名: {df.columns.tolist()}")
-
- # 检查是否有商品条码
- if '商品条码' in df.columns:
- # 遍历数据框的每一行
- for index, row in df.iterrows():
- # 打印当前行的所有值,用于调试
- logger.info(f"处理行{index+1}: {row.to_dict()}")
-
- # 跳过空行
- if row.isna().all():
- logger.info(f"跳过空行: {index+1}")
- continue
-
- # 跳过小计行
- if any('小计' in str(val) for val in row.values if isinstance(val, str)):
- logger.info(f"跳过小计行: {index+1}")
- continue
-
- # 获取条码(直接从商品条码列获取)
- barcode_value = row['商品条码']
- if pd.isna(barcode_value):
- logger.info(f"跳过无条码行: {index+1}")
- continue
-
- # 处理条码
- barcode = str(int(barcode_value)) if isinstance(barcode_value, (int, float)) else str(barcode_value)
- if not self.validate_barcode(barcode):
- logger.warning(f"无效条码: {barcode}")
- continue
-
- # 提取其他信息
- product = {
- 'barcode': barcode,
- 'name': row.get('商品全名', ''),
- 'specification': row.get('规格', ''),
- 'unit': row.get('单位', ''),
- 'quantity': 0,
- 'unit_price': 0,
- 'amount': 0,
- 'is_gift': False,
- 'package_quantity': 1 # 默认包装数量
- }
-
- # 提取规格并解析包装数量
- if '规格' in df.columns and not pd.isna(row['规格']):
- product['specification'] = str(row['规格'])
- package_quantity = self.parse_specification(product['specification'])
- if package_quantity:
- product['package_quantity'] = package_quantity
- logger.info(f"解析规格: {product['specification']} -> 包装数量={package_quantity}")
- else:
- # 逻辑1: 如果规格为空,尝试从商品名称推断规格
- if product['name']:
- inferred_spec, inferred_qty = self.infer_specification_from_name(product['name'])
- if inferred_spec:
- product['specification'] = inferred_spec
- product['package_quantity'] = inferred_qty
- logger.info(f"从商品名称推断规格: {product['name']} -> {inferred_spec}, 包装数量={inferred_qty}")
-
- # 提取数量和可能的单位
- if '数量' in df.columns and not pd.isna(row['数量']):
- try:
- # 尝试从数量中提取单位和数量
- extracted_qty, extracted_unit = self.extract_unit_from_quantity(row['数量'])
-
- # 处理提取到的数量
- if extracted_qty is not None:
- product['quantity'] = extracted_qty
- logger.info(f"提取数量: {product['quantity']}")
-
- # 处理提取到的单位
- if extracted_unit and (not product['unit'] or product['unit'] == ''):
- product['unit'] = extracted_unit
- logger.info(f"从数量中提取单位: {extracted_unit}")
- else:
- # 如果没有提取到数量,使用原始方法
- product['quantity'] = float(row['数量'])
- logger.info(f"使用原始数量: {product['quantity']}")
- except (ValueError, TypeError) as e:
- logger.warning(f"无效的数量: {row['数量']}, 错误: {str(e)}")
-
- # 提取单位(如果还没有单位)
- if (not product['unit'] or product['unit'] == '') and '单位' in df.columns and not pd.isna(row['单位']):
- product['unit'] = str(row['单位'])
- logger.info(f"从单位列提取单位: {product['unit']}")
-
- # 提取单价
- if '单价' in df.columns:
- if pd.isna(row['单价']):
- # 单价为空,视为赠品
- is_gift = True
- logger.info(f"单价为空,视为赠品")
- else:
- try:
- # 如果单价是字符串且不是数字,视为赠品
- if isinstance(row['单价'], str) and not row['单价'].replace('.', '').isdigit():
- is_gift = True
- logger.info(f"单价不是有效数字({row['单价']}),视为赠品")
- else:
- product['unit_price'] = float(row['单价'])
- logger.info(f"提取单价: {product['unit_price']}")
- except (ValueError, TypeError):
- is_gift = True
- logger.warning(f"无效的单价: {row['单价']}")
-
- # 提取金额
- if '金额' in df.columns:
- if amount_col and not pd.isna(row[amount_col]):
- try:
- # 清理金额字符串,处理可能的范围值(如"40-44")
- amount_str = str(row[amount_col])
- if '-' in amount_str:
- # 如果是范围,取第一个值
- amount_str = amount_str.split('-')[0]
- logger.info(f"金额为范围值({row[amount_col]}),取第一个值: {amount_str}")
-
- # 尝试转换为浮点数
- product['amount'] = float(amount_str)
- logger.info(f"提取金额: {product['amount']}")
- except (ValueError, TypeError) as e:
- logger.warning(f"无效的金额: {row[amount_col]}, 错误: {e}")
- # 金额无效时,设为0
- product['amount'] = 0
- logger.warning(f"设置金额为0")
- else:
- # 如果没有金额,计算金额
- product['amount'] = product['quantity'] * product['unit_price']
-
- # 判断是否为赠品
- is_gift = False
-
- # 赠品识别规则,根据README要求
- # 1. 商品单价为0或为空
- if product['unit_price'] == 0:
- is_gift = True
- logger.info(f"单价为空,视为赠品")
-
- # 2. 商品金额为0或为空
- if not is_gift and amount_col:
- try:
- if pd.isna(row[amount_col]):
- is_gift = True
- logger.info(f"金额为空,视为赠品")
- else:
- # 清理金额字符串,处理可能的范围值(如"40-44")
- amount_str = str(row[amount_col])
- if '-' in amount_str:
- # 如果是范围,取第一个值
- amount_str = amount_str.split('-')[0]
- logger.info(f"金额为范围值({row[amount_col]}),取第一个值: {amount_str}")
-
- # 转换为浮点数并检查是否为0
- amount_val = float(amount_str)
- if amount_val == 0:
- is_gift = True
- logger.info(f"金额为0,视为赠品")
- except (ValueError, TypeError) as e:
- logger.warning(f"无法解析金额: {row[amount_col]}, 错误: {e}")
- # 金额无效时,不视为赠品,继续处理
-
- # 从赠送量列提取赠品数量
- gift_quantity = 0
- if '赠送量' in df.columns and not pd.isna(row['赠送量']):
- try:
- gift_quantity = float(row['赠送量'])
- if gift_quantity > 0:
- # 如果有明确的赠送量,总是创建赠品记录
- logger.info(f"提取赠送量: {gift_quantity}")
- except (ValueError, TypeError):
- logger.warning(f"无效的赠送量: {row['赠送量']}")
-
- # 处理单位转换
- self.process_unit_conversion(product)
-
- # 如果单价为0但有金额和数量,计算单价(非赠品情况)
- if not is_gift and product['unit_price'] == 0 and product['amount'] > 0 and product['quantity'] > 0:
- product['unit_price'] = product['amount'] / product['quantity']
- logger.info(f"计算单价: {product['amount']} / {product['quantity']} = {product['unit_price']}")
-
- # 处理产品添加逻辑
- product['is_gift'] = is_gift
-
- if is_gift:
- # 如果是赠品且数量>0,使用商品本身的数量
- if product['quantity'] > 0:
- logger.info(f"添加赠品商品: 条码={barcode}, 数量={product['quantity']}")
- product_info.append(product)
- else:
- # 正常商品
- if product['quantity'] > 0:
- logger.info(f"添加正常商品: 条码={barcode}, 数量={product['quantity']}, 单价={product['unit_price']}")
- product_info.append(product)
-
- # 如果有额外的赠送量,添加专门的赠品记录
- if gift_quantity > 0 and not is_gift:
- gift_product = product.copy()
- gift_product['is_gift'] = True
- gift_product['quantity'] = gift_quantity
- gift_product['unit_price'] = 0
- gift_product['amount'] = 0
- product_info.append(gift_product)
- logger.info(f"添加额外赠品: 条码={barcode}, 数量={gift_quantity}")
-
- logger.info(f"提取到{len(product_info)} 个产品信息")
- return product_info
-
- # 如果没有直接匹配的列名,尝试使用更复杂的匹配逻辑
- logger.info("未找到直接匹配的列名或未提取到产品,尝试使用更复杂的匹配逻辑")
- # 定义可能的列名
- expected_columns = {
- '序号': ['序号', '行号', 'NO', 'NO.', '行号', '行号', '行号'],
- '条码': ['条码', '条形码', '商品条码', 'barcode', '商品条形码', '条形码', '商品条码', '商品编码', '商品编号', '条形码', '基本条码'],
- '名称': ['名称', '品名', '产品名称', '商品名称', '货物名称'],
- '规格': ['规格', '包装规格', '包装', '商品规格', '规格型号'],
- '采购单价': ['单价', '价格', '采购单价', '销售价'],
- '单位': ['单位', '采购单位'],
- '数量': ['数量', '采购数量', '购买数量', '采购数量', '订单数量', '采购数量'],
- '金额': ['金额', '订单金额', '总金额', '总价金额', '小计(元)'],
- '赠送量': ['赠送量', '赠品数量', '赠送数量', '赠品'],
- }
-
- # 如果是特殊表头处理后的数据,尝试直接从列名匹配
- if special_header:
- logger.info("使用特殊表头处理后的列名进行匹配")
- direct_map = {
- '行号': '序号',
- '条形码': '条码',
- '商品名称': '名称',
- '规格': '规格',
- '单价': '采购单价',
- '单位': '单位',
- '数量': '数量',
- '金额': '金额',
- '箱数': '箱数', # 可能包含单位信息
- }
-
- column_mapping = {}
- for target_key, source_key in direct_map.items():
- if target_key in df.columns:
- column_mapping[source_key] = target_key
- logger.info(f"特殊表头匹配: {source_key} -> {target_key}")
-
- # 如果特殊表头处理没有找到足够的列,或者不是特殊表头,使用原有的映射逻辑
- if not special_header or len(column_mapping) < 3:
- # 检查第一行的内容,尝试判断是否是特殊格式的Excel
- if len(df) > 0: # 确保DataFrame不为空
- first_row = df.iloc[0].astype(str)
- # 检查是否包含"商品全名"、"基本条码"、"仓库全名"等特定字段
- if any("商品全名" in str(val) for val in first_row.values) and any("基本条码" in str(val) for val in first_row.values):
- logger.info("检测到特殊格式Excel,使用特定的列映射")
-
- # 找出各列的索引
- name_idx = None
- barcode_idx = None
- spec_idx = None
- unit_idx = None
- qty_idx = None
- price_idx = None
- amount_idx = None
-
- for idx, val in enumerate(first_row):
- val_str = str(val).strip()
- if val_str == "商品全名":
- name_idx = df.columns[idx]
- elif val_str == "基本条码":
- barcode_idx = df.columns[idx]
- elif val_str == "规格":
- spec_idx = df.columns[idx]
- elif val_str == "数量":
- qty_idx = df.columns[idx]
- elif val_str == "单位":
- unit_idx = df.columns[idx]
- elif val_str == "单价":
- price_idx = df.columns[idx]
- elif val_str == "金额":
- amount_idx = df.columns[idx]
-
- # 使用找到的索引创建列映射
- if name_idx and barcode_idx:
- column_mapping = {
- '名称': name_idx,
- '条码': barcode_idx
- }
-
- if spec_idx:
- column_mapping['规格'] = spec_idx
- if unit_idx:
- column_mapping['单位'] = unit_idx
- if qty_idx:
- column_mapping['数量'] = qty_idx
- if price_idx:
- column_mapping['采购单价'] = price_idx
- if amount_idx:
- column_mapping['金额'] = amount_idx
-
- logger.info(f"特殊格式Excel的列映射: {column_mapping}")
-
- # 跳过第一行(表头)
- df = df.iloc[1:].reset_index(drop=True)
- logger.info("已跳过第一行(表头)")
- else:
- logger.warning("无法在特殊格式Excel中找到必要的列")
- else:
- # 映射实际的列名
- column_mapping = {}
-
- # 检查是否有表头
- has_header = False
- for col in df.columns:
- if not str(col).startswith('Unnamed:'):
- has_header = True
- break
-
- if has_header:
- # 有表头的情况,使用原有的映射逻辑
- for key, patterns in expected_columns.items():
- for col in df.columns:
- # 移除列名中的空白字符以进行比较
- clean_col = re.sub(r'\s+', '', str(col))
- for pattern in patterns:
- clean_pattern = re.sub(r'\s+', '', pattern)
- if clean_col == clean_pattern:
- column_mapping[key] = col
- break
- if key in column_mapping:
- break
- else:
- # 无表头的情况,根据列的位置进行映射
- # 假设列的顺序是:空列、序号、条码、名称、规格、单价、单位、数量、金额
- if len(df.columns) >= 9:
- column_mapping = {
- '序号': df.columns[1], # Unnamed: 1
- '条码': df.columns[2], # Unnamed: 2
- '名称': df.columns[3], # Unnamed: 3
- '规格': df.columns[4], # Unnamed: 4
- '采购单价': df.columns[7], # Unnamed: 7
- '单位': df.columns[5], # Unnamed: 5
- '数量': df.columns[6], # Unnamed: 6
- '金额': df.columns[8] # Unnamed: 8
- }
- else:
- logger.warning(f"列数不足,无法进行映射。当前列数: {len(df.columns)}")
- return []
-
- logger.info(f"列映射结果: {column_mapping}")
-
- # 如果找到了必要的列,直接从DataFrame提取数据
- if '条码' in column_mapping:
- barcode_col = column_mapping['条码']
- quantity_col = column_mapping.get('数量')
- price_col = column_mapping.get('采购单价')
- amount_col = column_mapping.get('金额')
- unit_col = column_mapping.get('单位')
- spec_col = column_mapping.get('规格')
- gift_col = column_mapping.get('赠送量')
-
- # 详细打印各行的关键数据
- logger.info("逐行显示数据内容:")
- for idx, row in df.iterrows():
- # 获取关键字段数据
- barcode_val = row[barcode_col] if barcode_col and not pd.isna(row[barcode_col]) else ""
- quantity_val = row[quantity_col] if quantity_col and not pd.isna(row[quantity_col]) else ""
- unit_val = row[unit_col] if unit_col and not pd.isna(row[unit_col]) else ""
- price_val = row[price_col] if price_col and not pd.isna(row[price_col]) else ""
- spec_val = row[spec_col] if spec_col and not pd.isna(row[spec_col]) else ""
- gift_val = row[gift_col] if gift_col and not pd.isna(row[gift_col]) else ""
-
- logger.info(f"行{idx}, 条码:{barcode_val}, 数量:{quantity_val}, 单位:{unit_val}, " +
- f"单价:{price_val}, 规格:{spec_val}, 赠送量:{gift_val}")
-
- # 逐行处理数据
- for idx, row in df.iterrows():
- try:
- # 跳过表头和汇总行
- skip_row = False
- for col in row.index:
- if pd.notna(row[col]) and isinstance(row[col], str):
- # 检查是否为表头、页脚或汇总行
- if any(keyword in str(row[col]).lower() for keyword in ['序号', '小计', '合计', '总计', '页码', '行号', '页小计']):
- skip_row = True
- logger.info(f"跳过非商品行: {row[col]}")
- break
-
- if skip_row:
- continue
-
- # 检查是否有有效的数量和单价
- has_valid_data = False
- if quantity_col and not pd.isna(row[quantity_col]):
- try:
- qty = float(row[quantity_col])
- if qty > 0:
- has_valid_data = True
- except (ValueError, TypeError):
- pass
-
- if not has_valid_data:
- logger.info(f"行{idx}没有有效数量,跳过")
- continue
-
- # 提取或生成条码
- barcode_value = row[barcode_col] if not pd.isna(row[barcode_col]) else None
-
- # 检查条码是否有效,如果是"仓库"或无效条码,跳过该行
- barcode = None
- if barcode_value is not None:
- barcode_str = str(int(barcode_value)) if isinstance(barcode_value, (int, float)) else str(barcode_value)
- if barcode_str not in ["仓库", "仓库全名"] and self.validate_barcode(barcode_str):
- barcode = barcode_str
-
- # 如果没有有效条码,跳过该行
- if barcode is None:
- logger.info(f"行{idx}无有效条码,跳过该行")
- continue
-
- # 创建产品信息
- product = {
- 'barcode': barcode,
- 'name': row[column_mapping['名称']] if '名称' in column_mapping and not pd.isna(row[column_mapping['名称']]) else '',
- 'specification': row[spec_col] if spec_col and not pd.isna(row[spec_col]) else '',
- 'unit': row[unit_col] if unit_col and not pd.isna(row[unit_col]) else '',
- 'quantity': 0,
- 'unit_price': 0,
- 'amount': 0,
- 'is_gift': False,
- 'package_quantity': 1 # 默认包装数量
- }
-
- # 提取规格并解析包装数量
- if spec_col and not pd.isna(row[spec_col]):
- product['specification'] = str(row[spec_col])
- package_quantity = self.parse_specification(product['specification'])
- if package_quantity:
- product['package_quantity'] = package_quantity
- logger.info(f"解析规格: {product['specification']} -> 包装数量={package_quantity}")
- else:
- # 逻辑1: 如果规格为空,尝试从商品名称推断规格
- if '名称' in column_mapping and not pd.isna(row[column_mapping['名称']]):
- product_name = str(row[column_mapping['名称']])
- inferred_spec, inferred_qty = self.infer_specification_from_name(product_name)
- if inferred_spec:
- product['specification'] = inferred_spec
- product['package_quantity'] = inferred_qty
- logger.info(f"从商品名称推断规格: {product_name} -> {inferred_spec}, 包装数量={inferred_qty}")
-
- # 提取数量和可能的单位
- if quantity_col and not pd.isna(row[quantity_col]):
- try:
- # 尝试从数量中提取单位和数量
- extracted_qty, extracted_unit = self.extract_unit_from_quantity(row[quantity_col])
-
- # 处理提取到的数量
- if extracted_qty is not None:
- product['quantity'] = extracted_qty
- logger.info(f"提取数量: {product['quantity']}")
-
- # 处理提取到的单位
- if extracted_unit and (not product['unit'] or product['unit'] == ''):
- product['unit'] = extracted_unit
- logger.info(f"从数量中提取单位: {extracted_unit}")
- else:
- # 如果没有提取到数量,使用原始方法
- product['quantity'] = float(row[quantity_col])
- logger.info(f"使用原始数量: {product['quantity']}")
- except (ValueError, TypeError) as e:
- logger.warning(f"无效的数量: {row[quantity_col]}, 错误: {str(e)}")
- continue # 如果数量无效,跳过此行
- else:
- # 如果没有数量,跳过此行
- logger.warning(f"行{idx}缺少数量,跳过")
- continue
-
- # 提取单价
- if price_col and not pd.isna(row[price_col]):
- try:
- product['unit_price'] = float(row[price_col])
- logger.info(f"提取单价: {product['unit_price']}")
- except (ValueError, TypeError) as e:
- logger.warning(f"无效的单价: {row[price_col]}, 错误: {e}")
- # 单价无效时,可能是赠品
- is_gift = True
-
- # 初始化赠品标志
- is_gift = False
-
- # 提取金额
- # 忽略金额栏中可能存在的备注信息
- if amount_col and not pd.isna(row[amount_col]):
- amount_value = row[amount_col]
- if isinstance(amount_value, (int, float)):
- # 如果是数字类型,直接使用
- product['amount'] = float(amount_value)
- logger.info(f"提取金额: {product['amount']}")
- if product['amount'] == 0:
- is_gift = True
- logger.info(f"金额为0,视为赠品")
- else:
- # 如果不是数字类型,尝试从字符串中提取数字
- try:
- # 尝试转换为浮点数
- amount_str = str(amount_value)
- if amount_str.replace('.', '', 1).isdigit():
- product['amount'] = float(amount_str)
- logger.info(f"从字符串提取金额: {product['amount']}")
- if product['amount'] == 0:
- is_gift = True
- logger.info(f"金额为0,视为赠品")
- else:
- # 金额栏含有非数字内容,可能是备注,此时使用单价*数量计算金额
- logger.warning(f"金额栏包含非数字内容: {amount_value},将被视为备注,金额计算为单价*数量")
- product['amount'] = product['unit_price'] * product['quantity']
- logger.info(f"计算金额: {product['unit_price']} * {product['quantity']} = {product['amount']}")
- except (ValueError, TypeError) as e:
- logger.warning(f"无法解析金额: {amount_value}, 错误: {e}")
- # 计算金额
- product['amount'] = product['unit_price'] * product['quantity']
- logger.info(f"计算金额: {product['unit_price']} * {product['quantity']} = {product['amount']}")
- else:
- # 如果金额为空,可能是赠品,或需要计算金额
- if product['unit_price'] > 0:
- product['amount'] = product['unit_price'] * product['quantity']
- logger.info(f"计算金额: {product['unit_price']} * {product['quantity']} = {product['amount']}")
- else:
- is_gift = True
- logger.info(f"单价或金额为空,视为赠品")
-
- # 处理单位转换
- self.process_unit_conversion(product)
-
- # 处理产品添加逻辑
- product['is_gift'] = is_gift
-
- if is_gift:
- # 如果是赠品且数量>0,使用商品本身的数量
- if product['quantity'] > 0:
- logger.info(f"添加赠品商品: 条码={barcode}, 数量={product['quantity']}")
- product_info.append(product)
- else:
- # 正常商品
- if product['quantity'] > 0:
- logger.info(f"添加正常商品: 条码={barcode}, 数量={product['quantity']}, 单价={product['unit_price']}")
- product_info.append(product)
-
- # 如果有额外的赠送量,添加专门的赠品记录
- if gift_col and not pd.isna(row[gift_col]):
- try:
- gift_quantity = float(row[gift_col])
- if gift_quantity > 0:
- gift_product = product.copy()
- gift_product['is_gift'] = True
- gift_product['quantity'] = gift_quantity
- gift_product['unit_price'] = 0
- gift_product['amount'] = 0
- product_info.append(gift_product)
- logger.info(f"添加额外赠品: 条码={barcode}, 数量={gift_quantity}")
- except (ValueError, TypeError) as e:
- logger.warning(f"无效的赠送量: {row[gift_col]}, 错误: {e}")
-
- except Exception as e:
- logger.warning(f"处理行{idx}时出错: {e}")
- continue # 跳过有错误的行,继续处理下一行
-
- logger.info(f"提取到{len(product_info)} 个产品信息")
- return product_info
-
- except Exception as e:
- logger.error(f"提取产品信息时出错: {e}", exc_info=True)
- return []
-
- def fill_template(self, template_file_path, products, output_file_path):
- """
- 填充采购单模板并保存为新文件
- 按照模板格式填充(银豹采购单模板)
- - 列B(1): 条码(必填)
- - 列C(2): 采购量(必填) 对于只有赠品的商品,此列为空
- - 列D(3): 赠送量 - 同一条码的赠品数量
- - 列E(4): 采购单价(必填)- 保留4位小数
-
- 特殊处理
- - 同一条码既有正常商品又有赠品时,保持正常商品的采购量不变,将赠品数量填写到赠送量栏位
- - 只有赠品没有正常商品的情况,采购量列填写0,赠送量填写赠品数量
- - 赠品的判断依据:is_gift标记为True
- """
- logger.info(f"开始填充模板: {template_file_path}")
-
- try:
- # 打开模板文件
- workbook = xlrd.open_workbook(template_file_path)
- workbook = xlcopy(workbook)
- worksheet = workbook.get_sheet(0) # 默认第一个工作表
-
- # 从第2行开始填充数据(索引从0开始,对应Excel中的行号)
- row_index = 1 # Excel的行号从0开始,对应Excel中的行号
-
- # 先对产品按条码进行分组,识别赠品和普通商品
- barcode_groups = {}
-
- # 遍历所有产品,按条码分组
- logger.info(f"开始处理{len(products)} 个产品信息")
- for product in products:
- barcode = product.get('barcode', '')
- if not barcode:
- logger.warning(f"跳过无条码商品")
- continue
-
- # 使用产品中的is_gift标记来判断是否为赠品
- is_gift = product.get('is_gift', False)
-
- # 获取数量和单位
- quantity = product.get('quantity', 0)
- unit_price = product.get('unit_price', 0)
-
- logger.info(f"处理商品: 条码={barcode}, 数量={quantity}, 单价={unit_price}, 是否赠品={is_gift}")
-
- if barcode not in barcode_groups:
- barcode_groups[barcode] = {
- 'normal': None, # 正常商品信息
- 'gift_quantity': 0 # 赠品数量
- }
-
- if is_gift:
- # 是赠品,累加赠品数量
- barcode_groups[barcode]['gift_quantity'] += quantity
- logger.info(f"发现赠品:条码{barcode}, 数量={quantity}")
- else:
- # 是正常商品
- if barcode_groups[barcode]['normal'] is None:
- barcode_groups[barcode]['normal'] = {
- 'product': product,
- 'quantity': quantity,
- 'price': unit_price
- }
- logger.info(f"发现正常商品:条码{barcode}, 数量={quantity}, 单价={unit_price}")
- else:
- # 如果有多个正常商品记录,累加数量
- barcode_groups[barcode]['normal']['quantity'] += quantity
- logger.info(f"累加正常商品数量:条码{barcode}, 新增={quantity}, 累计={barcode_groups[barcode]['normal']['quantity']}")
-
- # 如果单价不同,取平均值
- if unit_price != barcode_groups[barcode]['normal']['price']:
- avg_price = (barcode_groups[barcode]['normal']['price'] + unit_price) / 2
- barcode_groups[barcode]['normal']['price'] = avg_price
- logger.info(f"调整单价(取平均值):条码{barcode}, 原价={barcode_groups[barcode]['normal']['price']}, 新价={unit_price}, 平均={avg_price}")
-
- # 输出调试信息
- logger.info(f"分组后共{len(barcode_groups)} 个不同条码的商品")
- for barcode, group in barcode_groups.items():
- if group['normal'] is not None:
- logger.info(f"条码 {barcode} 处理结果:正常商品数量{group['normal']['quantity']},单价{group['normal']['price']},赠品数量{group['gift_quantity']}")
- else:
- logger.info(f"条码 {barcode} 处理结果:只有赠品,数量={group['gift_quantity']}")
-
- # 准备填充数据
- for barcode, group in barcode_groups.items():
- # 1. 列B(1): 条码(必填)
- worksheet.write(row_index, 1, barcode)
-
- if group['normal'] is not None:
- # 有正常商品
- product = group['normal']['product']
-
- # 2. 列C(2): 采购量(必填) 使用正常商品的采购量
- normal_quantity = group['normal']['quantity']
- worksheet.write(row_index, 2, normal_quantity)
-
- # 3. 列D(3): 赠送量 - 添加赠品数量
- if group['gift_quantity'] > 0:
- worksheet.write(row_index, 3, group['gift_quantity'])
- logger.info(f"条码 {barcode} 填充:采购量={normal_quantity},赠品数量{group['gift_quantity']}")
-
- # 4. 列E(4): 采购单价(必填)
- purchase_price = group['normal']['price']
- style = xlwt.XFStyle()
- style.num_format_str = '0.0000'
- worksheet.write(row_index, 4, round(purchase_price, 4), style)
-
- elif group['gift_quantity'] > 0:
- # 只有赠品,没有正常商品
- logger.info(f"条码 {barcode} 只有赠品,数量{group['gift_quantity']},采购量=0,赠送量={group['gift_quantity']}")
-
- # 2. 列C(2): 采购量(必填) 对于只有赠品的条目,采购量填写为0
- worksheet.write(row_index, 2, 0)
-
- # 3. 列D(3): 赠送量 - 填写赠品数量
- worksheet.write(row_index, 3, group['gift_quantity'])
-
- # 4. 列E(4): 采购单价(必填) - 对于只有赠品的条目,采购单价为0
- style = xlwt.XFStyle()
- style.num_format_str = '0.0000'
- worksheet.write(row_index, 4, 0, style)
-
- row_index += 1
-
- # 保存文件
- workbook.save(output_file_path)
- logger.info(f"采购单已保存: {output_file_path}")
- return True
-
- except Exception as e:
- logger.error(f"填充模板时出错: {str(e)}", exc_info=True)
- return False
-
- def create_new_xls(self, input_file_path, products):
- """
- 根据输入的Excel文件创建新的采购单
- """
- try:
- # 获取输入文件的文件名(不带扩展名)
- input_filename = os.path.basename(input_file_path)
- name_without_ext = os.path.splitext(input_filename)[0]
-
- # 创建基本输出文件路径
- base_output_path = os.path.join("output", f"采购单_{name_without_ext}.xls")
-
- # 如果文件已存在,自动添加时间戳避免覆盖
- output_file_path = base_output_path
- if os.path.exists(base_output_path):
- timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
- name_parts = os.path.splitext(base_output_path)
- output_file_path = f"{name_parts[0]}_{timestamp}{name_parts[1]}"
- logger.info(f"文件 {base_output_path} 已存在,重命名为 {output_file_path}")
-
- # 填充模板
- result = self.fill_template(self.template_path, products, output_file_path)
-
- if result:
- logger.info(f"成功创建采购单: {output_file_path}")
- return output_file_path
- else:
- logger.error("创建采购单失败")
- return None
-
- except Exception as e:
- logger.error(f"创建采购单时出错: {str(e)}")
- return None
-
- def process_specific_file(self, file_path):
- """
- 处理指定的Excel文件
- """
- if not os.path.exists(file_path):
- logger.error(f"文件不存在: {file_path}")
- return False
-
- # 检查文件是否已处理
- file_stat = os.stat(file_path)
- file_key = f"{os.path.basename(file_path)}_{file_stat.st_size}_{file_stat.st_mtime}"
-
- if file_key in self.processed_files:
- output_file = self.processed_files[file_key]
- if os.path.exists(output_file):
- logger.info(f"文件已处理过,采购单文件: {output_file}")
- return True
-
- logger.info(f"开始处理Excel文件: {file_path}")
- try:
- # 读取Excel文件
- df = pd.read_excel(file_path)
-
- # 删除行号列(如果存在)
- if '行号' in df.columns:
- df = df.drop('行号', axis=1)
- logger.info("已删除行号列")
-
- # 提取商品信息
- products = self.extract_product_info(df)
-
- if not products:
- logger.warning("未从Excel文件中提取到有效的商品信息")
- return False
-
- # 获取文件名(不含扩展名)
- file_name = os.path.splitext(os.path.basename(file_path))[0]
-
- # 基本输出文件路径
- base_output_file = os.path.join(self.output_dir, f"采购单_{file_name}.xls")
-
- # 如果文件已存在,自动添加时间戳避免覆盖
- output_file = base_output_file
- if os.path.exists(base_output_file):
- timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
- name_parts = os.path.splitext(base_output_file)
- output_file = f"{name_parts[0]}_{timestamp}{name_parts[1]}"
- logger.info(f"文件 {base_output_file} 已存在,重命名为 {output_file}")
-
- # 填充模板
- result = self.fill_template(self.template_path, products, output_file)
-
- if result:
- # 记录已处理文件
- self.processed_files[file_key] = output_file
- self._save_processed_files()
-
- logger.info(f"Excel处理成功,采购单已保存至: {output_file}")
- return True
- else:
- logger.error("填充模板失败")
- return False
-
- except Exception as e:
- logger.error(f"处理Excel文件时出错: {str(e)}")
- return False
-
- def process_latest_file(self):
- """
- 处理最新的Excel文件
- """
- latest_file = self.get_latest_excel()
- if not latest_file:
- logger.error("未找到可处理的Excel文件")
- return False
-
- return self.process_specific_file(latest_file)
-
- def process(self):
- """
- 处理最新的Excel文件
- """
- return self.process_latest_file()
-
- def process_unit_conversion(self, product):
- """
- 处理单位转换
- """
- if product['unit'] in ['提', '盒']:
- # 检查是否是特殊条码
- if product['barcode'] in self.special_barcodes:
- special_config = self.special_barcodes[product['barcode']]
- # 特殊条码处理
- actual_quantity = product['quantity'] * special_config['multiplier']
- logger.info(f"特殊条码处理: {product['quantity']}{product['unit']} -> {actual_quantity}{special_config['target_unit']}")
-
- # 更新产品信息
- product['original_quantity'] = product['quantity']
- product['quantity'] = actual_quantity
- product['original_unit'] = product['unit']
- product['unit'] = special_config['target_unit']
-
- # 如果有单价,计算转换后的单价
- if product['unit_price'] > 0:
- product['original_unit_price'] = product['unit_price']
- product['unit_price'] = product['unit_price'] / special_config['multiplier']
- logger.info(f"单价转换: {product['original_unit_price']}/{product['original_unit']} -> {product['unit_price']}/{special_config['target_unit']}")
- else:
- # 提取规格中的数字
- spec_parts = re.findall(r'\d+', product['specification'])
-
- # 检查是否是1*5*12这样的三级格式
- if len(spec_parts) >= 3:
- # 三级规格:按件处理
- actual_quantity = product['quantity'] * product['package_quantity']
- logger.info(f"{product['unit']}单位三级规格转换: {product['quantity']}{product['unit']} -> {actual_quantity}瓶")
-
- # 更新产品信息
- product['original_quantity'] = product['quantity']
- product['quantity'] = actual_quantity
- product['original_unit'] = product['unit']
- product['unit'] = '瓶'
-
- # 如果有单价,计算转换后的单价
- if product['unit_price'] > 0:
- product['original_unit_price'] = product['unit_price']
- product['unit_price'] = product['unit_price'] / product['package_quantity']
- logger.info(f"单价转换: {product['original_unit_price']}/{product['original_unit']} -> {product['unit_price']}/瓶")
- else:
- # 二级规格:保持原数量不变
- logger.info(f"{product['unit']}单位二级规格保持原数量: {product['quantity']}{product['unit']}")
- # 对于"件"单位或其他特殊条码的处理
- elif product['barcode'] in self.special_barcodes:
- special_config = self.special_barcodes[product['barcode']]
- # 特殊条码处理
- actual_quantity = product['quantity'] * special_config['multiplier']
- logger.info(f"特殊条码处理: {product['quantity']}{product['unit']} -> {actual_quantity}{special_config['target_unit']}")
-
- # 更新产品信息
- product['original_quantity'] = product['quantity']
- product['quantity'] = actual_quantity
- product['original_unit'] = product['unit']
- product['unit'] = special_config['target_unit']
-
- # 如果有单价,计算转换后的单价
- if product['unit_price'] > 0:
- product['original_unit_price'] = product['unit_price']
- product['unit_price'] = product['unit_price'] / special_config['multiplier']
- logger.info(f"单价转换: {product['original_unit_price']}/{product['original_unit']} -> {product['unit_price']}/{special_config['target_unit']}")
- elif product['unit'] == '件':
- # 标准件处理:数量×包装数量
- if product['package_quantity'] and product['package_quantity'] > 1:
- actual_quantity = product['quantity'] * product['package_quantity']
- logger.info(f"件单位转换: {product['quantity']}件 -> {actual_quantity}瓶")
-
- # 更新产品信息
- product['original_quantity'] = product['quantity']
- product['quantity'] = actual_quantity
- product['original_unit'] = product['unit']
- product['unit'] = '瓶'
-
- # 如果有单价,计算转换后的单价
- if product['unit_price'] > 0:
- product['original_unit_price'] = product['unit_price']
- product['unit_price'] = product['unit_price'] / product['package_quantity']
- logger.info(f"单价转换: {product['original_unit_price']}/件 -> {product['unit_price']}/瓶")
-
-def main():
- """主程序"""
- import argparse
-
- # 解析命令行参数
- parser = argparse.ArgumentParser(description='Excel处理程序 - 第二步')
- parser.add_argument('--input', type=str, help='指定输入Excel文件路径,默认使用output目录中最新的Excel文件')
- parser.add_argument('--output', type=str, help='指定输出文件路径,默认使用模板文件路径加时间')
- args = parser.parse_args()
-
- processor = ExcelProcessorStep2()
-
- # 处理Excel文件
- try:
- # 根据是否指定输入文件选择处理方式
- if args.input:
- # 使用指定文件处理
- result = processor.process_specific_file(args.input)
- else:
- # 使用默认处理流程(查找最新文件)
- result = processor.process()
-
- if result:
- print("处理成功!已将数据填充并保存")
- else:
- print("处理失败!请查看日志了解详细信息")
- except Exception as e:
- logger.error(f"处理过程中发生错误: {e}", exc_info=True)
- print(f"处理过程中发生错误: {e}")
- print("请查看日志文件了解详细信息")
-
-if __name__ == "__main__":
- try:
- main()
- except Exception as e:
- logger.error(f"程序执行过程中发生错误: {e}", exc_info=True)
- sys.exit(1)
diff --git a/backup/v1_backup_20250502190248/input/微信图片_20250227193150(1).xlsx b/backup/v1_backup_20250502190248/input/微信图片_20250227193150(1).xlsx
deleted file mode 100644
index ba11083..0000000
Binary files a/backup/v1_backup_20250502190248/input/微信图片_20250227193150(1).xlsx and /dev/null differ
diff --git a/backup/v1_backup_20250502190248/input/微信图片_20250227193150.jpg b/backup/v1_backup_20250502190248/input/微信图片_20250227193150.jpg
deleted file mode 100644
index 5b2702a..0000000
Binary files a/backup/v1_backup_20250502190248/input/微信图片_20250227193150.jpg and /dev/null differ
diff --git a/backup/v1_backup_20250502190248/logs/.keep b/backup/v1_backup_20250502190248/logs/.keep
deleted file mode 100644
index 49cc8ef..0000000
Binary files a/backup/v1_backup_20250502190248/logs/.keep and /dev/null differ
diff --git a/backup/v1_backup_20250502190248/logs/clean_logs.active b/backup/v1_backup_20250502190248/logs/clean_logs.active
deleted file mode 100644
index c1180a1..0000000
--- a/backup/v1_backup_20250502190248/logs/clean_logs.active
+++ /dev/null
@@ -1 +0,0 @@
-Active since: 2025-05-01 19:46:44
\ No newline at end of file
diff --git a/backup/v1_backup_20250502190248/logs/ocr_processor.active b/backup/v1_backup_20250502190248/logs/ocr_processor.active
deleted file mode 100644
index 1335c3c..0000000
--- a/backup/v1_backup_20250502190248/logs/ocr_processor.active
+++ /dev/null
@@ -1 +0,0 @@
-Active since: 2025-05-01 19:49:19
\ No newline at end of file
diff --git a/backup/v1_backup_20250502190248/merge_purchase_orders.py b/backup/v1_backup_20250502190248/merge_purchase_orders.py
deleted file mode 100644
index eb7f79a..0000000
--- a/backup/v1_backup_20250502190248/merge_purchase_orders.py
+++ /dev/null
@@ -1,420 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-合并采购单程序
--------------------
-将多个采购单Excel文件合并成一个文件。
-"""
-
-import os
-import sys
-import logging
-import pandas as pd
-import xlrd
-import xlwt
-from pathlib import Path
-from typing import Dict, List, Optional, Tuple, Union, Any
-from datetime import datetime
-import random
-from xlutils.copy import copy as xlcopy
-import time
-import json
-import re
-
-# 配置日志
-logger = logging.getLogger(__name__)
-if not logger.handlers:
- log_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'logs', 'merge_purchase_orders.log')
- os.makedirs(os.path.dirname(log_file), exist_ok=True)
-
- logging.basicConfig(
- level=logging.INFO,
- format='%(asctime)s - %(levelname)s - %(message)s',
- handlers=[
- logging.FileHandler(log_file, encoding='utf-8'),
- logging.StreamHandler(sys.stdout)
- ]
- )
-logger = logging.getLogger(__name__)
-logger.info("初始化日志系统")
-
-class PurchaseOrderMerger:
- """
- 采购单合并器:将多个采购单Excel文件合并成一个文件
- """
-
- def __init__(self, output_dir="output"):
- """
- 初始化采购单合并器,并设置输出目录
- """
- logger.info("初始化PurchaseOrderMerger")
- self.output_dir = output_dir
-
- # 确保输出目录存在
- if not os.path.exists(output_dir):
- os.makedirs(output_dir)
- logger.info(f"创建输出目录: {output_dir}")
-
- # 设置路径
- self.template_path = os.path.join("templets", "银豹-采购单模板.xls")
-
- # 检查模板文件是否存在
- if not os.path.exists(self.template_path):
- logger.error(f"模板文件不存在: {self.template_path}")
- raise FileNotFoundError(f"模板文件不存在: {self.template_path}")
-
- # 用于记录已处理的文件
- self.cache_file = os.path.join(output_dir, "merged_files.json")
- self.merged_files = self._load_merged_files()
-
- logger.info(f"初始化完成,模板文件: {self.template_path}")
-
- def _load_merged_files(self):
- """加载已合并文件的缓存"""
- if os.path.exists(self.cache_file):
- try:
- with open(self.cache_file, 'r', encoding='utf-8') as f:
- cache = json.load(f)
- logger.info(f"加载已合并文件缓存,共{len(cache)} 条记录")
- return cache
- except Exception as e:
- logger.warning(f"读取缓存文件失败: {e}")
- return {}
-
- def _save_merged_files(self):
- """保存已合并文件的缓存"""
- try:
- with open(self.cache_file, 'w', encoding='utf-8') as f:
- json.dump(self.merged_files, f, ensure_ascii=False, indent=2)
- logger.info(f"已更新合并文件缓存,共{len(self.merged_files)} 条记录")
- except Exception as e:
- logger.warning(f"保存缓存文件失败: {e}")
-
- def get_latest_purchase_orders(self):
- """
- 获取output目录下最新的采购单Excel文件
- """
- logger.info(f"搜索目录 {self.output_dir} 中的采购单Excel文件")
- excel_files = []
-
- for file in os.listdir(self.output_dir):
- # 只处理以"采购单_"开头的Excel文件
- if file.lower().endswith('.xls') and file.startswith('采购单_'):
- file_path = os.path.join(self.output_dir, file)
- excel_files.append((file_path, os.path.getmtime(file_path)))
-
- if not excel_files:
- logger.warning(f"未在 {self.output_dir} 目录下找到采购单Excel文件")
- return []
-
- # 按修改时间排序,获取最新的文件
- sorted_files = sorted(excel_files, key=lambda x: x[1], reverse=True)
- logger.info(f"找到{len(sorted_files)} 个采购单Excel文件")
- return [file[0] for file in sorted_files]
-
- def read_purchase_order(self, file_path):
- """
- 读取采购单Excel文件
- """
- try:
- # 读取Excel文件
- df = pd.read_excel(file_path)
- logger.info(f"成功读取采购单文件: {file_path}")
-
- # 打印列名,用于调试
- logger.info(f"Excel文件的列名: {df.columns.tolist()}")
-
- # 检查是否有特殊表头结构(如"武侯环球乐百惠便利店3333.xlsx")
- # 判断依据:检查第3行是否包含常见的商品表头信息
- special_header = False
- if len(df) > 3: # 确保有足够的行
- row3 = df.iloc[3].astype(str)
- header_keywords = ['行号', '条形码', '条码', '商品名称', '规格', '单价', '数量', '金额', '单位']
- # 计算匹配的关键词数量
- matches = sum(1 for keyword in header_keywords if any(keyword in str(val) for val in row3.values))
- # 如果匹配了至少3个关键词,认为第3行是表头
- if matches >= 3:
- logger.info(f"检测到特殊表头结构,使用第3行作为列名: {row3.values.tolist()}")
- # 创建新的数据帧,使用第3行作为列名,数据从第4行开始
- header_row = df.iloc[3]
- data_rows = df.iloc[4:].reset_index(drop=True)
- # 为每一列分配一个名称(避免重复的列名)
- new_columns = []
- for i, col in enumerate(header_row):
- col_str = str(col)
- if col_str == 'nan' or col_str == 'None' or pd.isna(col):
- new_columns.append(f"Col_{i}")
- else:
- new_columns.append(col_str)
- # 使用新列名创建新的DataFrame
- data_rows.columns = new_columns
- df = data_rows
- special_header = True
- logger.info(f"重新构建的数据帧列名: {df.columns.tolist()}")
-
- # 定义可能的列名映射
- column_mapping = {
- '条码': ['条码', '条形码', '商品条码', 'barcode', '商品条形码', '条形码', '商品条码', '商品编码', '商品编号', '条形码', '条码(必填)'],
- '采购量': ['数量', '采购数量', '购买数量', '采购数量', '订单数量', '采购数量', '采购量(必填)'],
- '采购单价': ['单价', '价格', '采购单价', '销售价', '采购单价(必填)'],
- '赠送量': ['赠送量', '赠品数量', '赠送数量', '赠品']
- }
-
- # 映射实际的列名
- mapped_columns = {}
- for target_col, possible_names in column_mapping.items():
- for col in df.columns:
- # 移除列名中的空白字符和括号内容以进行比较
- clean_col = re.sub(r'\s+', '', str(col))
- clean_col = re.sub(r'(.*?)', '', clean_col) # 移除括号内容
- for name in possible_names:
- clean_name = re.sub(r'\s+', '', name)
- clean_name = re.sub(r'(.*?)', '', clean_name) # 移除括号内容
- if clean_col == clean_name:
- mapped_columns[target_col] = col
- break
- if target_col in mapped_columns:
- break
-
- # 如果找到了必要的列,重命名列
- if mapped_columns:
- df = df.rename(columns=mapped_columns)
- logger.info(f"列名映射结果: {mapped_columns}")
-
- return df
- except Exception as e:
- logger.error(f"读取采购单文件失败: {file_path}, 错误: {str(e)}")
- return None
-
- def merge_purchase_orders(self, file_paths):
- """
- 合并多个采购单文件
- """
- if not file_paths:
- logger.warning("没有需要合并的采购单文件")
- return None
-
- # 读取所有采购单文件
- dfs = []
- for file_path in file_paths:
- df = self.read_purchase_order(file_path)
- if df is not None:
- # 确保条码列是字符串类型
- df['条码(必填)'] = df['条码(必填)'].astype(str)
- # 去除可能的小数点和.0
- df['条码(必填)'] = df['条码(必填)'].apply(lambda x: x.split('.')[0] if '.' in x else x)
-
- # 处理NaN值,将其转换为空字符串
- for col in df.columns:
- df[col] = df[col].apply(lambda x: '' if pd.isna(x) else x)
-
- dfs.append(df)
-
- if not dfs:
- logger.error("没有成功读取任何采购单文件")
- return None
-
- # 合并所有数据框
- merged_df = pd.concat(dfs, ignore_index=True)
- logger.info(f"合并了{len(dfs)} 个采购单文件,共{len(merged_df)} 条记录")
-
- # 检查并合并相同条码和单价的数据
- merged_data = {}
- for _, row in merged_df.iterrows():
- # 使用映射后的列名访问数据
- barcode = str(row['条码(必填)']) # 保持字符串格式
- # 移除条码中可能的小数点
- barcode = barcode.split('.')[0] if '.' in barcode else barcode
-
- unit_price = float(row['采购单价(必填)'])
- quantity = float(row['采购量(必填)'])
-
- # 检查赠送量是否为空
- has_gift = '赠送量' in row and row['赠送量'] != '' and not pd.isna(row['赠送量'])
- gift_quantity = float(row['赠送量']) if has_gift else ''
-
- # 商品名称处理,确保不会出现"nan"
- product_name = row['商品名称']
- if pd.isna(product_name) or product_name == 'nan' or product_name == 'None':
- product_name = ''
-
- # 创建唯一键:条码+单价
- key = f"{barcode}_{unit_price}"
-
- if key in merged_data:
- # 如果已存在相同条码和单价的数据,累加数量
- merged_data[key]['采购量(必填)'] += quantity
-
- # 如果当前记录有赠送量且之前的记录也有赠送量,则累加赠送量
- if has_gift and merged_data[key]['赠送量'] != '':
- merged_data[key]['赠送量'] += gift_quantity
- # 如果当前记录有赠送量但之前的记录没有,则设置赠送量
- elif has_gift:
- merged_data[key]['赠送量'] = gift_quantity
- # 其他情况保持原样(为空)
-
- logger.info(f"合并相同条码和单价的数据: 条码={barcode}, 单价={unit_price}, 数量={quantity}, 赠送量={gift_quantity}")
-
- # 如果当前商品名称不为空,且原来的为空,则更新商品名称
- if product_name and not merged_data[key]['商品名称']:
- merged_data[key]['商品名称'] = product_name
- else:
- # 如果是新数据,直接添加
- merged_data[key] = {
- '商品名称': product_name,
- '条码(必填)': barcode, # 使用处理后的条码
- '采购量(必填)': quantity,
- '赠送量': gift_quantity,
- '采购单价(必填)': unit_price
- }
-
- # 将合并后的数据转换回DataFrame
- final_df = pd.DataFrame(list(merged_data.values()))
- logger.info(f"合并后剩余{len(final_df)} 条唯一记录")
-
- return final_df
-
- def create_merged_purchase_order(self, df):
- """
- 创建合并后的采购单Excel文件
- """
- try:
- # 获取当前时间戳
- timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
-
- # 创建输出文件路径
- output_file = os.path.join(self.output_dir, f"合并采购单_{timestamp}.xls")
-
- # 打开模板文件
- workbook = xlrd.open_workbook(self.template_path)
- workbook = xlcopy(workbook)
- worksheet = workbook.get_sheet(0)
-
- # 从第2行开始填充数据
- row_index = 1
-
- # 按条码排序
- df = df.sort_values('条码(必填)')
-
- # 填充数据
- for _, row in df.iterrows():
- # 1. 列A(0): 商品名称
- product_name = str(row['商品名称'])
- # 检查并处理nan值
- if product_name == 'nan' or product_name == 'None':
- product_name = ''
- worksheet.write(row_index, 0, product_name)
-
- # 2. 列B(1): 条码
- worksheet.write(row_index, 1, str(row['条码(必填)']))
-
- # 3. 列C(2): 采购量
- worksheet.write(row_index, 2, float(row['采购量(必填)']))
-
- # 4. 列D(3): 赠送量
- # 只有当赠送量不为空且不为0时才写入
- if '赠送量' in row and row['赠送量'] != '' and not pd.isna(row['赠送量']):
- # 将赠送量转换为数字
- try:
- gift_quantity = float(row['赠送量'])
- # 只有当赠送量大于0时才写入
- if gift_quantity > 0:
- worksheet.write(row_index, 3, gift_quantity)
- except (ValueError, TypeError):
- # 如果转换失败,忽略赠送量
- pass
-
- # 5. 列E(4): 采购单价
- style = xlwt.XFStyle()
- style.num_format_str = '0.0000'
- worksheet.write(row_index, 4, float(row['采购单价(必填)']), style)
-
- row_index += 1
-
- # 保存文件
- workbook.save(output_file)
- logger.info(f"合并采购单已保存: {output_file}")
-
- # 记录已合并文件
- for file_path in self.get_latest_purchase_orders():
- file_stat = os.stat(file_path)
- file_key = f"{os.path.basename(file_path)}_{file_stat.st_size}_{file_stat.st_mtime}"
- self.merged_files[file_key] = output_file
-
- self._save_merged_files()
-
- return output_file
-
- except Exception as e:
- logger.error(f"创建合并采购单失败: {str(e)}")
- return None
-
- def process(self):
- """
- 处理最新的采购单文件
- """
- # 获取最新的采购单文件
- file_paths = self.get_latest_purchase_orders()
- if not file_paths:
- logger.error("未找到可处理的采购单文件")
- return False
-
- # 合并采购单
- merged_df = self.merge_purchase_orders(file_paths)
- if merged_df is None:
- logger.error("合并采购单失败")
- return False
-
- # 创建合并后的采购单
- output_file = self.create_merged_purchase_order(merged_df)
- if output_file is None:
- logger.error("创建合并采购单失败")
- return False
-
- logger.info(f"处理完成,合并采购单已保存至: {output_file}")
- return True
-
-def main():
- """主程序"""
- import argparse
-
- # 解析命令行参数
- parser = argparse.ArgumentParser(description='合并采购单程序')
- parser.add_argument('--input', type=str, help='指定输入采购单文件路径,多个文件用逗号分隔')
- args = parser.parse_args()
-
- merger = PurchaseOrderMerger()
-
- # 处理采购单文件
- try:
- if args.input:
- # 使用指定文件处理
- file_paths = [path.strip() for path in args.input.split(',')]
- merged_df = merger.merge_purchase_orders(file_paths)
- if merged_df is not None:
- output_file = merger.create_merged_purchase_order(merged_df)
- if output_file:
- print(f"处理成功!合并采购单已保存至: {output_file}")
- else:
- print("处理失败!请查看日志了解详细信息")
- else:
- print("处理失败!请查看日志了解详细信息")
- else:
- # 使用默认处理流程(查找最新文件)
- result = merger.process()
- if result:
- print("处理成功!已将数据合并并保存")
- else:
- print("处理失败!请查看日志了解详细信息")
- except Exception as e:
- logger.error(f"处理过程中发生错误: {e}", exc_info=True)
- print(f"处理过程中发生错误: {e}")
- print("请查看日志文件了解详细信息")
-
-if __name__ == "__main__":
- try:
- main()
- except Exception as e:
- logger.error(f"程序执行过程中发生错误: {e}", exc_info=True)
- sys.exit(1)
\ No newline at end of file
diff --git a/backup/v1_backup_20250502190248/output/merged_files.json b/backup/v1_backup_20250502190248/output/merged_files.json
deleted file mode 100644
index f44be11..0000000
--- a/backup/v1_backup_20250502190248/output/merged_files.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "采购单_武侯环球乐百惠便利店849.xls_5632_1746098172.9159887": "output\\合并采购单_20250501193931.xls",
- "采购单_武侯环球乐百惠便利店3333.xls_9728_1746097892.1829922": "output\\合并采购单_20250501193931.xls"
-}
\ No newline at end of file
diff --git a/backup/v1_backup_20250502190248/requirements.txt b/backup/v1_backup_20250502190248/requirements.txt
deleted file mode 100644
index 6b54bae..0000000
--- a/backup/v1_backup_20250502190248/requirements.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-configparser>=5.0.0
-numpy>=1.19.0
-openpyxl>=3.0.0
-pandas>=1.3.0
-pathlib>=1.0.1
-requests>=2.25.0
-xlrd>=2.0.0,<2.1.0
-xlutils>=2.0.0
-xlwt>=1.3.0
\ No newline at end of file
diff --git a/backup/v1_backup_20250502190248/run_ocr.py b/backup/v1_backup_20250502190248/run_ocr.py
deleted file mode 100644
index 7c9f43a..0000000
--- a/backup/v1_backup_20250502190248/run_ocr.py
+++ /dev/null
@@ -1,235 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-OCR流程运行脚本
--------------
-整合百度OCR和Excel处理功能的便捷脚本
-"""
-
-import os
-import sys
-import argparse
-import logging
-import configparser
-from pathlib import Path
-from datetime import datetime
-
-# 确保logs目录存在
-log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'logs')
-os.makedirs(log_dir, exist_ok=True)
-
-# 设置日志文件路径
-log_file = os.path.join(log_dir, 'ocr_processor.log')
-
-# 配置日志
-logger = logging.getLogger('ocr_processor')
-if not logger.handlers:
- # 创建文件处理器
- file_handler = logging.FileHandler(log_file, encoding='utf-8')
- file_handler.setLevel(logging.INFO)
-
- # 创建控制台处理器
- console_handler = logging.StreamHandler(sys.stdout)
- console_handler.setLevel(logging.INFO)
-
- # 设置格式
- formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
- file_handler.setFormatter(formatter)
- console_handler.setFormatter(formatter)
-
- # 添加处理器到日志器
- logger.addHandler(file_handler)
- logger.addHandler(console_handler)
-
- # 设置日志级别
- logger.setLevel(logging.INFO)
-
-logger.info("OCR处理器初始化")
-
-# 标记该日志文件为活跃,避免被清理工具删除
-try:
- # 创建一个标记文件,表示该日志文件正在使用中
- active_marker = os.path.join(log_dir, 'ocr_processor.active')
- with open(active_marker, 'w') as f:
- f.write(f"Active since: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
-except Exception as e:
- logger.warning(f"无法创建日志活跃标记: {e}")
-
-def parse_args():
- """解析命令行参数"""
- parser = argparse.ArgumentParser(description='OCR流程运行脚本')
- parser.add_argument('--step', type=int, default=0, help='运行步骤: 1-OCR识别, 2-Excel处理, 0-全部运行 (默认)')
- parser.add_argument('--config', type=str, default='config.ini', help='配置文件路径')
- parser.add_argument('--force', action='store_true', help='强制处理所有文件,包括已处理的文件')
- parser.add_argument('--input', type=str, help='指定输入文件(仅用于单文件处理)')
- parser.add_argument('--output', type=str, help='指定输出文件(仅用于单文件处理)')
- return parser.parse_args()
-
-def check_env():
- """检查配置是否有效"""
- try:
- # 尝试读取配置文件
- config = configparser.ConfigParser()
- if not config.read('config.ini', encoding='utf-8'):
- logger.warning("未找到配置文件config.ini或文件为空")
- return
-
- # 检查API密钥是否已配置
- if not config.has_section('API'):
- logger.warning("配置文件中缺少[API]部分")
- return
-
- api_key = config.get('API', 'api_key', fallback='')
- secret_key = config.get('API', 'secret_key', fallback='')
-
- if not api_key or not secret_key:
- logger.warning("API密钥未设置或为空,请在config.ini中配置API密钥")
-
- except Exception as e:
- logger.error(f"检查配置时出错: {e}")
-
-def run_ocr(args):
- """运行OCR识别过程"""
- logger.info("开始OCR识别过程...")
-
- # 导入模块
- try:
- from baidu_table_ocr import OCRProcessor, ConfigManager
-
- # 创建配置管理器
- config_manager = ConfigManager(args.config)
-
- # 创建处理器
- processor = OCRProcessor(config_manager)
-
- # 检查输入目录中是否有图片
- input_files = processor.get_unprocessed_images()
- if not input_files and not args.input:
- logger.warning(f"在{processor.input_folder}目录中没有找到未处理的图片文件")
- return False
-
- # 单文件处理或批量处理
- if args.input:
- if not os.path.exists(args.input):
- logger.error(f"输入文件不存在: {args.input}")
- return False
-
- logger.info(f"处理单个文件: {args.input}")
- output_file = processor.process_image(args.input)
- if output_file:
- logger.info(f"OCR识别成功,输出文件: {output_file}")
- return True
- else:
- logger.error("OCR识别失败")
- return False
- else:
- # 批量处理
- batch_size = processor.batch_size
- max_workers = processor.max_workers
-
- # 如果需要强制处理,先设置skip_existing为False
- if args.force:
- processor.skip_existing = False
-
- logger.info(f"批量处理文件,批量大小: {batch_size}, 最大线程数: {max_workers}")
- total, success = processor.process_images_batch(
- batch_size=batch_size,
- max_workers=max_workers
- )
-
- logger.info(f"OCR识别完成,总计处理: {total},成功: {success}")
- return success > 0
-
- except ImportError as e:
- logger.error(f"导入OCR模块失败: {e}")
- return False
- except Exception as e:
- logger.error(f"OCR识别过程出错: {e}")
- return False
-
-def run_excel_processing(args):
- """运行Excel处理过程"""
- logger.info("开始Excel处理过程...")
-
- # 导入模块
- try:
- from excel_processor_step2 import ExcelProcessorStep2
-
- # 创建处理器
- processor = ExcelProcessorStep2()
-
- # 单文件处理或批量处理
- if args.input:
- if not os.path.exists(args.input):
- logger.error(f"输入文件不存在: {args.input}")
- return False
-
- logger.info(f"处理单个Excel文件: {args.input}")
- result = processor.process_specific_file(args.input)
- if result:
- logger.info(f"Excel处理成功")
- return True
- else:
- logger.error("Excel处理失败,请查看日志了解详细信息")
- return False
- else:
- # 检查output目录中最新的Excel文件
- latest_file = processor.get_latest_excel()
- if not latest_file:
- logger.error("未找到可处理的Excel文件,无法进行处理")
- return False
-
- # 处理最新的Excel文件
- logger.info(f"处理最新的Excel文件: {latest_file}")
- result = processor.process_latest_file()
-
- if result:
- logger.info("Excel处理成功")
- return True
- else:
- logger.error("Excel处理失败,请查看日志了解详细信息")
- return False
-
- except ImportError as e:
- logger.error(f"导入Excel处理模块失败: {e}")
- return False
- except Exception as e:
- logger.error(f"Excel处理过程出错: {e}")
- return False
-
-def main():
- """主函数"""
- # 解析命令行参数
- args = parse_args()
-
- # 检查环境变量
- check_env()
-
- # 根据步骤运行相应的处理
- ocr_success = False
-
- if args.step == 0 or args.step == 1:
- ocr_success = run_ocr(args)
- if not ocr_success:
- if args.step == 1:
- logger.error("OCR识别失败,请检查input目录是否有图片或检查API配置")
- sys.exit(1)
- else:
- logger.warning("OCR识别未处理任何文件,跳过Excel处理步骤")
- return
- else:
- # 如果只运行第二步,假设OCR已成功完成
- ocr_success = True
-
- # 只有当OCR成功或只运行第二步时才执行Excel处理
- if ocr_success and (args.step == 0 or args.step == 2):
- excel_result = run_excel_processing(args)
- if not excel_result and args.step == 2:
- logger.error("Excel处理失败")
- sys.exit(1)
-
- logger.info("处理完成")
-
-if __name__ == "__main__":
- main()
\ No newline at end of file
diff --git a/backup/v1_backup_20250502190248/templets/银豹-采购单模板.xls b/backup/v1_backup_20250502190248/templets/银豹-采购单模板.xls
deleted file mode 100644
index a8fb1bc..0000000
Binary files a/backup/v1_backup_20250502190248/templets/银豹-采购单模板.xls and /dev/null differ
diff --git a/backup/v1_backup_20250502190248/test_ocr_log.py b/backup/v1_backup_20250502190248/test_ocr_log.py
deleted file mode 100644
index b9ec6ee..0000000
--- a/backup/v1_backup_20250502190248/test_ocr_log.py
+++ /dev/null
@@ -1,66 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-测试OCR处理器日志文件创建
-"""
-
-import os
-import sys
-import logging
-from datetime import datetime
-
-# 确保logs目录存在
-log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'logs')
-os.makedirs(log_dir, exist_ok=True)
-print(f"日志目录: {log_dir}")
-
-# 设置日志文件路径
-log_file = os.path.join(log_dir, 'ocr_processor.log')
-print(f"日志文件路径: {log_file}")
-
-# 配置日志
-logger = logging.getLogger('ocr_processor')
-if not logger.handlers:
- # 创建文件处理器
- file_handler = logging.FileHandler(log_file, encoding='utf-8')
- file_handler.setLevel(logging.INFO)
-
- # 创建控制台处理器
- console_handler = logging.StreamHandler(sys.stdout)
- console_handler.setLevel(logging.INFO)
-
- # 设置格式
- formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
- file_handler.setFormatter(formatter)
- console_handler.setFormatter(formatter)
-
- # 添加处理器到日志器
- logger.addHandler(file_handler)
- logger.addHandler(console_handler)
-
- # 设置日志级别
- logger.setLevel(logging.INFO)
-
-# 写入测试日志
-logger.info("这是一条测试日志消息")
-logger.info(f"测试时间: {datetime.now()}")
-
-# 标记该日志文件为活跃,避免被清理工具删除
-try:
- # 创建一个标记文件,表示该日志文件正在使用中
- active_marker = os.path.join(log_dir, 'ocr_processor.active')
- with open(active_marker, 'w') as f:
- f.write(f"Active since: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
- print(f"活跃标记文件: {active_marker}")
-except Exception as e:
- print(f"无法创建日志活跃标记: {e}")
-
-# 检查文件是否已创建
-if os.path.exists(log_file):
- print(f"日志文件已成功创建: {log_file}")
- print(f"文件大小: {os.path.getsize(log_file)} 字节")
-else:
- print(f"错误: 日志文件创建失败: {log_file}")
-
-print("测试完成")
\ No newline at end of file
diff --git a/backup/v1_backup_20250502190248/test_unit_conversion.py b/backup/v1_backup_20250502190248/test_unit_conversion.py
deleted file mode 100644
index e0db03c..0000000
--- a/backup/v1_backup_20250502190248/test_unit_conversion.py
+++ /dev/null
@@ -1,141 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-单位转换处理规则测试
--------------------
-这个脚本用于演示excel_processor_step2.py中的单位转换处理规则,
-包括件、提、盒单位的处理,以及特殊条码的处理。
-"""
-
-import os
-import sys
-import logging
-
-# 配置日志
-logging.basicConfig(
- level=logging.INFO,
- format='%(asctime)s - %(levelname)s - %(message)s',
- handlers=[
- logging.StreamHandler(sys.stdout)
- ]
-)
-logger = logging.getLogger(__name__)
-
-def test_unit_conversion(barcode, unit, quantity, specification, unit_price):
- """
- 测试单位转换处理逻辑
- """
- logger.info(f"测试条码: {barcode}, 单位: {unit}, 数量: {quantity}, 规格: {specification}, 单价: {unit_price}")
-
- # 特殊条码处理
- special_barcodes = {
- '6925019900087': {
- 'multiplier': 10, # 数量乘以10
- 'target_unit': '瓶', # 目标单位
- 'description': '特殊处理:数量*10,单位转换为瓶'
- }
- }
-
- # 解析规格
- package_quantity = None
- is_tertiary_spec = False
-
- if specification:
- import re
- # 三级规格,如1*5*12
- match = re.search(r'(\d+)[\*xX×](\d+)[\*xX×](\d+)', specification)
- if match:
- package_quantity = int(match.group(3))
- is_tertiary_spec = True
- else:
- # 二级规格,如1*15
- match = re.search(r'(\d+)[\*xX×](\d+)', specification)
- if match:
- package_quantity = int(match.group(2))
-
- # 初始化结果
- result_quantity = quantity
- result_unit = unit
- result_unit_price = unit_price
-
- # 处理单位转换
- if barcode in special_barcodes:
- # 特殊条码处理
- special_config = special_barcodes[barcode]
- result_quantity = quantity * special_config['multiplier']
- result_unit = special_config['target_unit']
-
- if unit_price:
- result_unit_price = unit_price / special_config['multiplier']
-
- logger.info(f"特殊条码处理: {quantity}{unit} -> {result_quantity}{result_unit}")
- if unit_price:
- logger.info(f"单价转换: {unit_price}/{unit} -> {result_unit_price}/{result_unit}")
-
- elif unit in ['提', '盒']:
- # 提和盒单位特殊处理
- if is_tertiary_spec and package_quantity:
- # 三级规格:按照件的计算方式处理
- result_quantity = quantity * package_quantity
- result_unit = '瓶'
-
- if unit_price:
- result_unit_price = unit_price / package_quantity
-
- logger.info(f"{unit}单位三级规格转换: {quantity}{unit} -> {result_quantity}瓶")
- if unit_price:
- logger.info(f"单价转换: {unit_price}/{unit} -> {result_unit_price}/瓶")
- else:
- # 二级规格或无规格:保持原数量不变
- logger.info(f"{unit}单位二级规格保持原数量: {quantity}{unit}")
-
- elif unit == '件' and package_quantity:
- # 件单位处理:数量×包装数量
- result_quantity = quantity * package_quantity
- result_unit = '瓶'
-
- if unit_price:
- result_unit_price = unit_price / package_quantity
-
- logger.info(f"件单位转换: {quantity}件 -> {result_quantity}瓶")
- if unit_price:
- logger.info(f"单价转换: {unit_price}/件 -> {result_unit_price}/瓶")
-
- else:
- # 其他单位保持不变
- logger.info(f"保持原单位不变: {quantity}{unit}")
-
- # 输出处理结果
- logger.info(f"处理结果 => 数量: {result_quantity}, 单位: {result_unit}, 单价: {result_unit_price}")
- logger.info("-" * 50)
-
- return result_quantity, result_unit, result_unit_price
-
-def run_tests():
- """运行一系列测试用例"""
-
- # 标准件单位测试
- test_unit_conversion("1234567890123", "件", 1, "1*12", 108)
- test_unit_conversion("1234567890124", "件", 2, "1*24", 120)
-
- # 提和盒单位测试 - 二级规格
- test_unit_conversion("1234567890125", "提", 3, "1*16", 50)
- test_unit_conversion("1234567890126", "盒", 5, "1*20", 60)
-
- # 提和盒单位测试 - 三级规格
- test_unit_conversion("1234567890127", "提", 2, "1*5*12", 100)
- test_unit_conversion("1234567890128", "盒", 3, "1*6*8", 120)
-
- # 特殊条码测试
- test_unit_conversion("6925019900087", "副", 2, "1*10", 50)
- test_unit_conversion("6925019900087", "提", 1, "1*16", 30)
-
- # 其他单位测试
- test_unit_conversion("1234567890129", "包", 4, "1*24", 12)
- test_unit_conversion("1234567890130", "瓶", 10, "", 5)
-
-if __name__ == "__main__":
- logger.info("开始测试单位转换处理规则")
- run_tests()
- logger.info("单位转换处理规则测试完成")
\ No newline at end of file
diff --git a/data/output/processed_files.json b/data/output/processed_files.json
index 02cc1a7..9d08736 100644
--- a/data/output/processed_files.json
+++ b/data/output/processed_files.json
@@ -1,4 +1,3 @@
{
- "data/input\\微信图片_20250509142624.jpg": "data/output\\微信图片_20250509142624.xlsx",
"D:/My Documents/python/orc-order-v2/data/output/微信图片_20250509142624.xlsx": "data/output\\采购单_微信图片_20250509142624.xls"
}
\ No newline at end of file
diff --git a/data/output/采购单_微信图片_20250509142624.xls b/data/output/采购单_微信图片_20250509142624.xls
index e210a8a..0764cbe 100644
Binary files a/data/output/采购单_微信图片_20250509142624.xls and b/data/output/采购单_微信图片_20250509142624.xls differ
diff --git a/diff.txt b/diff.txt
deleted file mode 100644
index 5c99a20..0000000
Binary files a/diff.txt and /dev/null differ
diff --git a/logs/__main__.active b/logs/__main__.active
index 7313447..2912e4d 100644
--- a/logs/__main__.active
+++ b/logs/__main__.active
@@ -1 +1 @@
-Active since: 2025-05-10 11:55:27
\ No newline at end of file
+Active since: 2025-05-10 12:29:42
\ No newline at end of file
diff --git a/logs/app.core.excel.converter.active b/logs/app.core.excel.converter.active
index e883d50..9dd4f3a 100644
--- a/logs/app.core.excel.converter.active
+++ b/logs/app.core.excel.converter.active
@@ -1 +1 @@
-Active since: 2025-05-10 12:00:40
\ No newline at end of file
+Active since: 2025-05-10 12:29:41
\ No newline at end of file
diff --git a/logs/app.core.excel.converter.log b/logs/app.core.excel.converter.log
index b1c2ac6..e1fab18 100644
--- a/logs/app.core.excel.converter.log
+++ b/logs/app.core.excel.converter.log
@@ -1999,3 +1999,18 @@
2025-05-10 12:04:27,177 - app.core.excel.converter - INFO - 条码映射配置保存成功,共9项
2025-05-10 12:04:28,985 - app.core.excel.converter - INFO - 成功加载条码映射配置,共9项
2025-05-10 12:10:44,392 - app.core.excel.converter - INFO - 条码映射配置保存成功,共19项
+2025-05-10 12:29:42,166 - app.core.excel.converter - INFO - 成功加载条码映射配置,共19项
+2025-05-10 12:29:42,276 - app.core.excel.converter - INFO - 解析容量(ml)规格: 250ML*12 -> 1*12
+2025-05-10 12:29:42,277 - app.core.excel.converter - INFO - 解析容量(ml)规格: 260ML*24 -> 1*24
+2025-05-10 12:29:42,279 - app.core.excel.converter - INFO - 解析容量(ml)规格: 250ML*12 -> 1*12
+2025-05-10 12:29:42,280 - app.core.excel.converter - INFO - 解析容量(ml)规格: 250ML*12 -> 1*12
+2025-05-10 12:29:42,288 - app.core.excel.converter - INFO - 解析容量(ml)规格: 250ML*12 -> 1*12
+2025-05-10 12:29:42,342 - app.core.excel.converter - INFO - 解析容量(ml)规格: 250ML*12 -> 1*12
+2025-05-10 12:29:42,343 - app.core.excel.converter - INFO - 解析容量(ml)规格: 250ML*12 -> 1*12
+2025-05-10 12:29:42,344 - app.core.excel.converter - INFO - 解析容量(ml)规格: 245ML*12 -> 1*12
+2025-05-10 12:29:42,344 - app.core.excel.converter - INFO - 解析容量(ml)规格: 125ML*36 -> 1*36
+2025-05-10 12:29:42,345 - app.core.excel.converter - INFO - 解析容量(ml)规格: 250ML*12 -> 1*12
+2025-05-10 12:29:42,345 - app.core.excel.converter - INFO - 解析容量(ml)规格: 250ML*24 -> 1*24
+2025-05-10 12:29:42,346 - app.core.excel.converter - INFO - 解析容量(ml)规格: 250ML*24 -> 1*24
+2025-05-10 12:29:42,346 - app.core.excel.converter - INFO - 解析容量(ml)规格: 250ML*24 -> 1*24
+2025-05-10 12:29:46,511 - app.core.excel.converter - INFO - 解析容量(ml)规格: 250ML*12 -> 1*12
diff --git a/logs/app.core.excel.handlers.barcode_mapper.active b/logs/app.core.excel.handlers.barcode_mapper.active
index e883d50..9dd4f3a 100644
--- a/logs/app.core.excel.handlers.barcode_mapper.active
+++ b/logs/app.core.excel.handlers.barcode_mapper.active
@@ -1 +1 @@
-Active since: 2025-05-10 12:00:40
\ No newline at end of file
+Active since: 2025-05-10 12:29:41
\ No newline at end of file
diff --git a/logs/app.core.excel.handlers.barcode_mapper.log b/logs/app.core.excel.handlers.barcode_mapper.log
index 751c488..b5570f6 100644
--- a/logs/app.core.excel.handlers.barcode_mapper.log
+++ b/logs/app.core.excel.handlers.barcode_mapper.log
@@ -10,3 +10,17 @@
2025-05-10 11:55:28,217 - app.core.excel.handlers.barcode_mapper - INFO - 条码映射: 6920584471055 -> 6920584471017
2025-05-10 11:55:28,220 - app.core.excel.handlers.barcode_mapper - INFO - 条码映射: 6925861571159 -> 69021824
2025-05-10 11:55:28,222 - app.core.excel.handlers.barcode_mapper - INFO - 条码映射: 6925861571466 -> 6925861571459
+2025-05-10 12:29:42,275 - app.core.excel.handlers.barcode_mapper - INFO - 条码映射: 6907992508344 -> 6907992508191
+2025-05-10 12:29:42,277 - app.core.excel.handlers.barcode_mapper - INFO - 条码映射: 6903979000979 -> 6903979000962
+2025-05-10 12:29:42,278 - app.core.excel.handlers.barcode_mapper - INFO - 条码映射: 6923644283582 -> 6923644283575
+2025-05-10 12:29:42,279 - app.core.excel.handlers.barcode_mapper - INFO - 条码映射: 6923644268909 -> 6923644268510
+2025-05-10 12:29:42,288 - app.core.excel.handlers.barcode_mapper - INFO - 条码映射: 6923644268930 -> 6923644268497
+2025-05-10 12:29:42,342 - app.core.excel.handlers.barcode_mapper - INFO - 条码映射: 6923644268916 -> 6923644268503
+2025-05-10 12:29:42,343 - app.core.excel.handlers.barcode_mapper - INFO - 条码映射: 6923644266318 -> 6923644266066
+2025-05-10 12:29:42,343 - app.core.excel.handlers.barcode_mapper - INFO - 条码映射: 6920584471055 -> 6920584471017
+2025-05-10 12:29:42,344 - app.core.excel.handlers.barcode_mapper - INFO - 条码映射: 6925861571159 -> 69021824
+2025-05-10 12:29:42,345 - app.core.excel.handlers.barcode_mapper - INFO - 条码映射: 6925861571466 -> 6925861571459
+2025-05-10 12:29:42,345 - app.core.excel.handlers.barcode_mapper - INFO - 条码映射: 6923644210151 -> 6923644223458
+2025-05-10 12:29:42,346 - app.core.excel.handlers.barcode_mapper - INFO - 条码映射: 6907992501819 -> 6907992500133
+2025-05-10 12:29:42,346 - app.core.excel.handlers.barcode_mapper - INFO - 条码映射: 6907992502052 -> 6907992100272
+2025-05-10 12:29:46,511 - app.core.excel.handlers.barcode_mapper - INFO - 条码映射: 6907992507385 -> 6907992507095
diff --git a/logs/app.core.excel.handlers.unit_converter_handlers.active b/logs/app.core.excel.handlers.unit_converter_handlers.active
index e883d50..9dd4f3a 100644
--- a/logs/app.core.excel.handlers.unit_converter_handlers.active
+++ b/logs/app.core.excel.handlers.unit_converter_handlers.active
@@ -1 +1 @@
-Active since: 2025-05-10 12:00:40
\ No newline at end of file
+Active since: 2025-05-10 12:29:41
\ No newline at end of file
diff --git a/logs/app.core.excel.handlers.unit_converter_handlers.log b/logs/app.core.excel.handlers.unit_converter_handlers.log
index 0e3682d..df022af 100644
--- a/logs/app.core.excel.handlers.unit_converter_handlers.log
+++ b/logs/app.core.excel.handlers.unit_converter_handlers.log
@@ -86,3 +86,17 @@
2025-05-10 11:55:28,225 - app.core.excel.handlers.unit_converter_handlers - INFO - 件单位处理: 数量: 3.0 -> 72.0, 单价: 63.0 -> 2.625, 单位: 件 -> 瓶
2025-05-10 11:55:28,225 - app.core.excel.handlers.unit_converter_handlers - INFO - 件单位处理: 数量: 3.0 -> 72.0, 单价: 54.0 -> 2.25, 单位: 件 -> 瓶
2025-05-10 11:55:28,226 - app.core.excel.handlers.unit_converter_handlers - INFO - 件单位处理: 数量: 2.0 -> 24.0, 单价: 42.0 -> 3.5, 单位: 件 -> 瓶
+2025-05-10 12:29:42,276 - app.core.excel.handlers.unit_converter_handlers - INFO - 件单位处理: 数量: 1.0 -> 12.0, 单价: 52.0 -> 4.333333333333333, 单位: 件 -> 瓶
+2025-05-10 12:29:42,278 - app.core.excel.handlers.unit_converter_handlers - INFO - 件单位处理: 数量: 3.0 -> 72.0, 单价: 50.0 -> 2.0833333333333335, 单位: 件 -> 瓶
+2025-05-10 12:29:42,279 - app.core.excel.handlers.unit_converter_handlers - INFO - 件单位处理: 数量: 1.0 -> 12.0, 单价: 30.0 -> 2.5, 单位: 件 -> 瓶
+2025-05-10 12:29:42,280 - app.core.excel.handlers.unit_converter_handlers - INFO - 件单位处理: 数量: 2.0 -> 24.0, 单价: 30.0 -> 2.5, 单位: 件 -> 瓶
+2025-05-10 12:29:42,288 - app.core.excel.handlers.unit_converter_handlers - INFO - 件单位处理: 数量: 2.0 -> 24.0, 单价: 30.0 -> 2.5, 单位: 件 -> 瓶
+2025-05-10 12:29:42,342 - app.core.excel.handlers.unit_converter_handlers - INFO - 件单位处理: 数量: 2.0 -> 24.0, 单价: 30.0 -> 2.5, 单位: 件 -> 瓶
+2025-05-10 12:29:42,343 - app.core.excel.handlers.unit_converter_handlers - INFO - 件单位处理: 数量: 4.0 -> 48.0, 单价: 45.0 -> 3.75, 单位: 件 -> 瓶
+2025-05-10 12:29:42,344 - app.core.excel.handlers.unit_converter_handlers - INFO - 件单位处理: 数量: 2.0 -> 24.0, 单价: 50.0 -> 4.166666666666667, 单位: 件 -> 瓶
+2025-05-10 12:29:42,344 - app.core.excel.handlers.unit_converter_handlers - INFO - 件单位处理: 数量: 2.0 -> 72.0, 单价: 65.0 -> 1.8055555555555556, 单位: 件 -> 瓶
+2025-05-10 12:29:42,345 - app.core.excel.handlers.unit_converter_handlers - INFO - 件单位处理: 数量: 1.0 -> 12.0, 单价: 45.0 -> 3.75, 单位: 件 -> 瓶
+2025-05-10 12:29:42,345 - app.core.excel.handlers.unit_converter_handlers - INFO - 件单位处理: 数量: 3.0 -> 72.0, 单价: 55.0 -> 2.2916666666666665, 单位: 件 -> 瓶
+2025-05-10 12:29:42,346 - app.core.excel.handlers.unit_converter_handlers - INFO - 件单位处理: 数量: 3.0 -> 72.0, 单价: 63.0 -> 2.625, 单位: 件 -> 瓶
+2025-05-10 12:29:42,346 - app.core.excel.handlers.unit_converter_handlers - INFO - 件单位处理: 数量: 3.0 -> 72.0, 单价: 54.0 -> 2.25, 单位: 件 -> 瓶
+2025-05-10 12:29:46,511 - app.core.excel.handlers.unit_converter_handlers - INFO - 件单位处理: 数量: 2.0 -> 24.0, 单价: 42.0 -> 3.5, 单位: 件 -> 瓶
diff --git a/logs/app.core.excel.merger.active b/logs/app.core.excel.merger.active
index 7313447..9dd4f3a 100644
--- a/logs/app.core.excel.merger.active
+++ b/logs/app.core.excel.merger.active
@@ -1 +1 @@
-Active since: 2025-05-10 11:55:27
\ No newline at end of file
+Active since: 2025-05-10 12:29:41
\ No newline at end of file
diff --git a/logs/app.core.excel.merger.log b/logs/app.core.excel.merger.log
index 076323f..fe574b7 100644
--- a/logs/app.core.excel.merger.log
+++ b/logs/app.core.excel.merger.log
@@ -514,3 +514,5 @@
2025-05-09 16:01:39,294 - app.core.excel.merger - INFO - 初始化PurchaseOrderMerger完成,模板文件: templates\银豹-采购单模板.xls
2025-05-10 11:55:28,007 - app.core.excel.merger - INFO - 使用输出目录: D:\My Documents\python\orc-order-v2\data\output
2025-05-10 11:55:28,008 - app.core.excel.merger - INFO - 初始化PurchaseOrderMerger完成,模板文件: templates\银豹-采购单模板.xls
+2025-05-10 12:29:42,168 - app.core.excel.merger - INFO - 使用输出目录: D:\My Documents\python\orc-order-v2\data\output
+2025-05-10 12:29:42,169 - app.core.excel.merger - INFO - 初始化PurchaseOrderMerger完成,模板文件: templates\银豹-采购单模板.xls
diff --git a/logs/app.core.excel.processor.active b/logs/app.core.excel.processor.active
index 7313447..9dd4f3a 100644
--- a/logs/app.core.excel.processor.active
+++ b/logs/app.core.excel.processor.active
@@ -1 +1 @@
-Active since: 2025-05-10 11:55:27
\ No newline at end of file
+Active since: 2025-05-10 12:29:41
\ No newline at end of file
diff --git a/logs/app.core.excel.processor.log b/logs/app.core.excel.processor.log
index 0d51c51..04686a6 100644
--- a/logs/app.core.excel.processor.log
+++ b/logs/app.core.excel.processor.log
@@ -6276,3 +6276,81 @@ ValueError: could not convert string to float: '2\n96'
2025-05-10 11:55:36,925 - app.core.excel.processor - INFO - 条码 6907992507385 处理结果:正常商品数量24.0,单价3.5,赠品数量0
2025-05-10 11:55:36,934 - app.core.excel.processor - INFO - 采购单已保存到: data/output\采购单_微信图片_20250509142624.xls
2025-05-10 11:55:36,939 - app.core.excel.processor - INFO - 采购单已保存到: data/output\采购单_微信图片_20250509142624.xls
+2025-05-10 12:29:42,165 - app.core.excel.processor - INFO - 使用输出目录: D:\My Documents\python\orc-order-v2\data\output
+2025-05-10 12:29:42,165 - app.core.excel.processor - INFO - 使用临时目录: D:\My Documents\python\orc-order-v2\data\temp
+2025-05-10 12:29:42,167 - app.core.excel.processor - INFO - 初始化ExcelProcessor完成,模板文件: templates/银豹-采购单模板.xls
+2025-05-10 12:29:42,169 - app.core.excel.processor - INFO - 开始处理Excel文件: D:/My Documents/python/orc-order-v2/data/output/微信图片_20250509142624.xlsx
+2025-05-10 12:29:42,211 - app.core.excel.processor - INFO - 成功读取Excel文件: D:/My Documents/python/orc-order-v2/data/output/微信图片_20250509142624.xlsx, 共 16 行
+2025-05-10 12:29:42,216 - app.core.excel.processor - INFO - 找到可能的表头行: 第1行,评分: 45
+2025-05-10 12:29:42,220 - app.core.excel.processor - INFO - 识别到表头在第 1 行
+2025-05-10 12:29:42,261 - app.core.excel.processor - INFO - 使用表头行重新读取数据,共 15 行有效数据
+2025-05-10 12:29:42,261 - app.core.excel.processor - INFO - 找到精确匹配的条码列: 商品条码
+2025-05-10 12:29:42,262 - app.core.excel.processor - INFO - 使用条码列: 商品条码
+2025-05-10 12:29:42,262 - app.core.excel.processor - INFO - 找到name列(部分匹配): 商品条码
+2025-05-10 12:29:42,262 - app.core.excel.processor - INFO - 找到specification列: 规格
+2025-05-10 12:29:42,262 - app.core.excel.processor - INFO - 找到quantity列: 数量
+2025-05-10 12:29:42,262 - app.core.excel.processor - INFO - 找到unit列: 单位
+2025-05-10 12:29:42,262 - app.core.excel.processor - INFO - 找到price列: 单价
+2025-05-10 12:29:42,263 - app.core.excel.processor - INFO - 列名映射结果: {'barcode': '商品条码', 'name': '商品条码', 'specification': '规格', 'quantity': '数量', 'unit': '单位', 'price': '单价'}
+2025-05-10 12:29:42,270 - app.core.excel.processor - INFO - 是否存在规格列: True
+2025-05-10 12:29:42,271 - app.core.excel.processor - INFO - 第1行: 提取商品信息 条码=6907992508344, 名称=6907992508344.0, 规格=, 数量=1.0, 单位=件, 单价=52.0
+2025-05-10 12:29:42,277 - app.core.excel.processor - INFO - 第2行: 提取商品信息 条码=6903979000979, 名称=6903979000979.0, 规格=, 数量=3.0, 单位=件, 单价=50.0
+2025-05-10 12:29:42,278 - app.core.excel.processor - INFO - 第3行: 提取商品信息 条码=6923644283582, 名称=6923644283582.0, 规格=, 数量=1.0, 单位=件, 单价=30.0
+2025-05-10 12:29:42,279 - app.core.excel.processor - INFO - 第4行: 提取商品信息 条码=6923644268909, 名称=6923644268909.0, 规格=, 数量=2.0, 单位=件, 单价=30.0
+2025-05-10 12:29:42,287 - app.core.excel.processor - INFO - 第5行: 提取商品信息 条码=6923644268930, 名称=6923644268930.0, 规格=, 数量=2.0, 单位=件, 单价=30.0
+2025-05-10 12:29:42,342 - app.core.excel.processor - INFO - 第6行: 提取商品信息 条码=6923644268916, 名称=6923644268916.0, 规格=, 数量=2.0, 单位=件, 单价=30.0
+2025-05-10 12:29:42,343 - app.core.excel.processor - INFO - 第7行: 提取商品信息 条码=6923644266318, 名称=6923644266318.0, 规格=, 数量=4.0, 单位=件, 单价=45.0
+2025-05-10 12:29:42,343 - app.core.excel.processor - INFO - 第8行: 提取商品信息 条码=6920584471055, 名称=6920584471055.0, 规格=, 数量=2.0, 单位=件, 单价=50.0
+2025-05-10 12:29:42,344 - app.core.excel.processor - INFO - 第9行: 提取商品信息 条码=6925861571159, 名称=6925861571159.0, 规格=, 数量=2.0, 单位=件, 单价=65.0
+2025-05-10 12:29:42,344 - app.core.excel.processor - INFO - 第10行: 提取商品信息 条码=6925861571466, 名称=6925861571466.0, 规格=, 数量=1.0, 单位=件, 单价=45.0
+2025-05-10 12:29:42,345 - app.core.excel.processor - INFO - 第11行: 提取商品信息 条码=6923644210151, 名称=6923644210151.0, 规格=, 数量=3.0, 单位=件, 单价=55.0
+2025-05-10 12:29:42,345 - app.core.excel.processor - INFO - 第12行: 提取商品信息 条码=6907992501819, 名称=6907992501819.0, 规格=, 数量=3.0, 单位=件, 单价=63.0
+2025-05-10 12:29:42,346 - app.core.excel.processor - INFO - 第13行: 提取商品信息 条码=6907992502052, 名称=6907992502052.0, 规格=, 数量=3.0, 单位=件, 单价=54.0
+2025-05-10 12:29:46,510 - app.core.excel.processor - INFO - 第14行: 提取商品信息 条码=6907992507385, 名称=6907992507385.0, 规格=, 数量=2.0, 单位=件, 单价=42.0
+2025-05-10 12:29:46,511 - app.core.excel.processor - INFO - 提取到 14 个商品信息
+2025-05-10 12:29:46,521 - app.core.excel.processor - INFO - 开始处理14 个产品信息
+2025-05-10 12:29:46,522 - app.core.excel.processor - INFO - 处理商品: 条码=6907992508191, 数量=12.0, 单价=4.333333333333333, 是否赠品=False
+2025-05-10 12:29:46,522 - app.core.excel.processor - INFO - 发现正常商品:条码6907992508191, 数量=12.0, 单价=4.333333333333333
+2025-05-10 12:29:46,522 - app.core.excel.processor - INFO - 处理商品: 条码=6903979000962, 数量=72.0, 单价=2.0833333333333335, 是否赠品=False
+2025-05-10 12:29:46,522 - app.core.excel.processor - INFO - 发现正常商品:条码6903979000962, 数量=72.0, 单价=2.0833333333333335
+2025-05-10 12:29:46,522 - app.core.excel.processor - INFO - 处理商品: 条码=6923644283575, 数量=12.0, 单价=2.5, 是否赠品=False
+2025-05-10 12:29:46,522 - app.core.excel.processor - INFO - 发现正常商品:条码6923644283575, 数量=12.0, 单价=2.5
+2025-05-10 12:29:46,522 - app.core.excel.processor - INFO - 处理商品: 条码=6923644268510, 数量=24.0, 单价=2.5, 是否赠品=False
+2025-05-10 12:29:46,522 - app.core.excel.processor - INFO - 发现正常商品:条码6923644268510, 数量=24.0, 单价=2.5
+2025-05-10 12:29:46,522 - app.core.excel.processor - INFO - 处理商品: 条码=6923644268497, 数量=24.0, 单价=2.5, 是否赠品=False
+2025-05-10 12:29:46,522 - app.core.excel.processor - INFO - 发现正常商品:条码6923644268497, 数量=24.0, 单价=2.5
+2025-05-10 12:29:46,522 - app.core.excel.processor - INFO - 处理商品: 条码=6923644268503, 数量=24.0, 单价=2.5, 是否赠品=False
+2025-05-10 12:29:46,523 - app.core.excel.processor - INFO - 发现正常商品:条码6923644268503, 数量=24.0, 单价=2.5
+2025-05-10 12:29:46,523 - app.core.excel.processor - INFO - 处理商品: 条码=6923644266066, 数量=48.0, 单价=3.75, 是否赠品=False
+2025-05-10 12:29:46,523 - app.core.excel.processor - INFO - 发现正常商品:条码6923644266066, 数量=48.0, 单价=3.75
+2025-05-10 12:29:46,523 - app.core.excel.processor - INFO - 处理商品: 条码=6920584471017, 数量=24.0, 单价=4.166666666666667, 是否赠品=False
+2025-05-10 12:29:46,523 - app.core.excel.processor - INFO - 发现正常商品:条码6920584471017, 数量=24.0, 单价=4.166666666666667
+2025-05-10 12:29:46,523 - app.core.excel.processor - INFO - 处理商品: 条码=69021824, 数量=72.0, 单价=1.8055555555555556, 是否赠品=False
+2025-05-10 12:29:46,523 - app.core.excel.processor - INFO - 发现正常商品:条码69021824, 数量=72.0, 单价=1.8055555555555556
+2025-05-10 12:29:46,523 - app.core.excel.processor - INFO - 处理商品: 条码=6925861571459, 数量=12.0, 单价=3.75, 是否赠品=False
+2025-05-10 12:29:46,523 - app.core.excel.processor - INFO - 发现正常商品:条码6925861571459, 数量=12.0, 单价=3.75
+2025-05-10 12:29:46,523 - app.core.excel.processor - INFO - 处理商品: 条码=6923644223458, 数量=72.0, 单价=2.2916666666666665, 是否赠品=False
+2025-05-10 12:29:46,523 - app.core.excel.processor - INFO - 发现正常商品:条码6923644223458, 数量=72.0, 单价=2.2916666666666665
+2025-05-10 12:29:46,523 - app.core.excel.processor - INFO - 处理商品: 条码=6907992500133, 数量=72.0, 单价=2.625, 是否赠品=False
+2025-05-10 12:29:46,523 - app.core.excel.processor - INFO - 发现正常商品:条码6907992500133, 数量=72.0, 单价=2.625
+2025-05-10 12:29:46,523 - app.core.excel.processor - INFO - 处理商品: 条码=6907992100272, 数量=72.0, 单价=2.25, 是否赠品=False
+2025-05-10 12:29:46,523 - app.core.excel.processor - INFO - 发现正常商品:条码6907992100272, 数量=72.0, 单价=2.25
+2025-05-10 12:29:46,523 - app.core.excel.processor - INFO - 处理商品: 条码=6907992507095, 数量=24.0, 单价=3.5, 是否赠品=False
+2025-05-10 12:29:46,524 - app.core.excel.processor - INFO - 发现正常商品:条码6907992507095, 数量=24.0, 单价=3.5
+2025-05-10 12:29:52,708 - app.core.excel.processor - INFO - 分组后共14 个不同条码的商品
+2025-05-10 12:29:52,708 - app.core.excel.processor - INFO - 条码 6907992508191 处理结果:正常商品数量12.0,单价4.333333333333333,赠品数量0
+2025-05-10 12:29:52,708 - app.core.excel.processor - INFO - 条码 6903979000962 处理结果:正常商品数量72.0,单价2.0833333333333335,赠品数量0
+2025-05-10 12:29:52,708 - app.core.excel.processor - INFO - 条码 6923644283575 处理结果:正常商品数量12.0,单价2.5,赠品数量0
+2025-05-10 12:29:52,708 - app.core.excel.processor - INFO - 条码 6923644268510 处理结果:正常商品数量24.0,单价2.5,赠品数量0
+2025-05-10 12:29:52,708 - app.core.excel.processor - INFO - 条码 6923644268497 处理结果:正常商品数量24.0,单价2.5,赠品数量0
+2025-05-10 12:29:52,709 - app.core.excel.processor - INFO - 条码 6923644268503 处理结果:正常商品数量24.0,单价2.5,赠品数量0
+2025-05-10 12:29:52,709 - app.core.excel.processor - INFO - 条码 6923644266066 处理结果:正常商品数量48.0,单价3.75,赠品数量0
+2025-05-10 12:29:52,709 - app.core.excel.processor - INFO - 条码 6920584471017 处理结果:正常商品数量24.0,单价4.166666666666667,赠品数量0
+2025-05-10 12:29:52,709 - app.core.excel.processor - INFO - 条码 69021824 处理结果:正常商品数量72.0,单价1.8055555555555556,赠品数量0
+2025-05-10 12:29:52,709 - app.core.excel.processor - INFO - 条码 6925861571459 处理结果:正常商品数量12.0,单价3.75,赠品数量0
+2025-05-10 12:29:52,709 - app.core.excel.processor - INFO - 条码 6923644223458 处理结果:正常商品数量72.0,单价2.2916666666666665,赠品数量0
+2025-05-10 12:29:52,709 - app.core.excel.processor - INFO - 条码 6907992500133 处理结果:正常商品数量72.0,单价2.625,赠品数量0
+2025-05-10 12:29:52,709 - app.core.excel.processor - INFO - 条码 6907992100272 处理结果:正常商品数量72.0,单价2.25,赠品数量0
+2025-05-10 12:29:52,709 - app.core.excel.processor - INFO - 条码 6907992507095 处理结果:正常商品数量24.0,单价3.5,赠品数量0
+2025-05-10 12:29:52,712 - app.core.excel.processor - INFO - 采购单已保存到: data/output\采购单_微信图片_20250509142624.xls
+2025-05-10 12:29:52,714 - app.core.excel.processor - INFO - 采购单已保存到: data/output\采购单_微信图片_20250509142624.xls
diff --git a/logs/app.core.excel.validators.active b/logs/app.core.excel.validators.active
index e883d50..9dd4f3a 100644
--- a/logs/app.core.excel.validators.active
+++ b/logs/app.core.excel.validators.active
@@ -1 +1 @@
-Active since: 2025-05-10 12:00:40
\ No newline at end of file
+Active since: 2025-05-10 12:29:41
\ No newline at end of file
diff --git a/logs/app.core.ocr.baidu_ocr.active b/logs/app.core.ocr.baidu_ocr.active
index 60fd47e..28d2b0e 100644
--- a/logs/app.core.ocr.baidu_ocr.active
+++ b/logs/app.core.ocr.baidu_ocr.active
@@ -1 +1 @@
-Active since: 2025-05-10 11:55:26
\ No newline at end of file
+Active since: 2025-05-10 12:29:40
\ No newline at end of file
diff --git a/logs/app.core.ocr.table_ocr.active b/logs/app.core.ocr.table_ocr.active
index 60fd47e..28d2b0e 100644
--- a/logs/app.core.ocr.table_ocr.active
+++ b/logs/app.core.ocr.table_ocr.active
@@ -1 +1 @@
-Active since: 2025-05-10 11:55:26
\ No newline at end of file
+Active since: 2025-05-10 12:29:40
\ No newline at end of file
diff --git a/logs/app.core.utils.file_utils.active b/logs/app.core.utils.file_utils.active
index ea21b97..122d54b 100644
--- a/logs/app.core.utils.file_utils.active
+++ b/logs/app.core.utils.file_utils.active
@@ -1 +1 @@
-Active since: 2025-05-10 11:55:25
\ No newline at end of file
+Active since: 2025-05-10 12:29:39
\ No newline at end of file
diff --git a/logs/app.services.ocr_service.active b/logs/app.services.ocr_service.active
index 60fd47e..28d2b0e 100644
--- a/logs/app.services.ocr_service.active
+++ b/logs/app.services.ocr_service.active
@@ -1 +1 @@
-Active since: 2025-05-10 11:55:26
\ No newline at end of file
+Active since: 2025-05-10 12:29:40
\ No newline at end of file
diff --git a/logs/app.services.order_service.active b/logs/app.services.order_service.active
index 7313447..9dd4f3a 100644
--- a/logs/app.services.order_service.active
+++ b/logs/app.services.order_service.active
@@ -1 +1 @@
-Active since: 2025-05-10 11:55:27
\ No newline at end of file
+Active since: 2025-05-10 12:29:41
\ No newline at end of file
diff --git a/logs/app.services.order_service.log b/logs/app.services.order_service.log
index d556dd0..fe5a1bd 100644
--- a/logs/app.services.order_service.log
+++ b/logs/app.services.order_service.log
@@ -344,3 +344,6 @@
2025-05-10 11:55:28,003 - app.services.order_service - INFO - 初始化OrderService
2025-05-10 11:55:28,008 - app.services.order_service - INFO - OrderService初始化完成
2025-05-10 11:55:28,008 - app.services.order_service - INFO - OrderService开始处理指定Excel文件: D:/My Documents/python/orc-order-v2/data/output/微信图片_20250509142624.xlsx
+2025-05-10 12:29:42,163 - app.services.order_service - INFO - 初始化OrderService
+2025-05-10 12:29:42,169 - app.services.order_service - INFO - OrderService初始化完成
+2025-05-10 12:29:42,169 - app.services.order_service - INFO - OrderService开始处理指定Excel文件: D:/My Documents/python/orc-order-v2/data/output/微信图片_20250509142624.xlsx
diff --git a/logs/app.services.tobacco_service.active b/logs/app.services.tobacco_service.active
index 7313447..2912e4d 100644
--- a/logs/app.services.tobacco_service.active
+++ b/logs/app.services.tobacco_service.active
@@ -1 +1 @@
-Active since: 2025-05-10 11:55:27
\ No newline at end of file
+Active since: 2025-05-10 12:29:42
\ No newline at end of file
diff --git a/v2-优化总结.md b/v2-优化总结.md
deleted file mode 100644
index 2f0f175..0000000
--- a/v2-优化总结.md
+++ /dev/null
@@ -1,329 +0,0 @@
-# OCR订单处理系统 v2 版本优化总结
-
-## 主要优化点
-
-### 1. 项目结构优化
-
-- **模块化重构**:将代码按功能分为配置、核心功能、服务和CLI等模块
-- **目录结构规范化**:创建统一的data目录管理所有输入和输出文件
-- **配置集中管理**:使用ConfigManager统一管理配置,支持默认值和配置文件读取
-
-### 2. OCR功能优化
-
-- **修复百度API调用问题**:解决"无法获取请求ID"的错误
-- **改进表格识别**:优化表格结构识别,提高识别准确率
-- **增加重试机制**:添加API调用失败重试机制,提高成功率
-
-### 3. 文件处理优化
-
-- **统一文件路径**:规范化文件路径处理,使用data/input和data/output目录
-- **简化处理流程**:直接从data/input读取,处理后输出到data/output,无需中间转移
-- **文件名处理**:优化输出文件命名方式,移除时间戳,采用"采购单_原名称.xls"格式
-
-### 4. 单位转换优化
-
-- **完整的单位处理规则**:实现v1版本中所有的单位转换规则,包括:
- - "件"和"箱"单位转换为"瓶"
- - "提"和"盒"单位的特殊处理(区分二级和三级规格)
- - 特殊条码的处理
-- **规格推断**:从商品名称自动推断规格,提高数据完整性
-- **单位提取**:从数量字段自动提取单位
-
-### 5. 用户界面优化
-
-- **双栏布局**:从单栏设计改为左右双栏布局,增加实时日志显示区域
-- **多线程处理**:使用多线程避免UI阻塞,提升用户体验
-- **状态反馈**:添加更详细的处理状态反馈,清晰显示处理进度
-- **文件清理功能**:增加文件清理功能,可选择性清理输入输出文件,支持文件备份
-
-### 6. 采购单处理优化
-
-- **商品合并处理**:对相同条码商品进行合并处理,累计数量
-- **赠品处理**:正确区分正常商品和赠品,分别处理
-- **条码修正**:自动修正特定错误格式的条码(如5开头改为6开头)
-- **模板填充精确定位**:确保按照银豹采购单模板的要求正确填充数据
-
-## 代码质量改进
-
-1. **代码结构清晰**:遵循单一职责原则,每个模块专注于特定功能
-2. **错误处理完善**:增加完整的异常处理和错误日志记录
-3. **代码注释充分**:添加详细的函数和类注释,便于理解和维护
-4. **类型提示**:添加Python类型提示,提高代码可读性和IDE支持
-5. **日志系统改进**:实现分级日志系统,便于调试和问题追踪
-
-## 文件管理改进
-
-1. **统一目录结构**:规范化目录结构,避免多个相似功能的目录
-2. **备份机制**:实现文件备份功能,避免意外数据丢失
-3. **清理工具**:添加文件清理工具,可选择性地清理输入和输出文件
-4. **处理记录**:保存文件处理记录,避免重复处理
-
-## 性能优化
-
-1. **减少文件操作**:优化文件读写次数,减少不必要的文件复制操作
-2. **批量处理**:支持批量模式,提高处理效率
-3. **资源释放**:及时释放文件句柄和内存资源,避免资源泄漏
-
-## 可维护性改进
-
-1. **配置外部化**:将配置参数提取到config.ini文件,便于调整
-2. **模块间低耦合**:模块之间通过明确的接口交互,降低耦合度
-3. **可扩展设计**:系统设计考虑未来扩展,如添加新的特殊条码处理规则
-4. **完整文档**:提供详细的README文档,说明系统功能和使用方法
-
-# OCR订单处理系统 v2 优化建议
-
-经过全面审查系统代码和架构,以下是对 OCR 订单处理系统的优化建议,旨在提高系统的性能、可维护性和用户体验。
-
-## 1. 架构与结构优化
-
-### 1.1 依赖注入与组件化
-
-**当前情况**:系统主要组件在代码中直接实例化,造成模块间高耦合。
-
-**优化建议**:
-- 实现简单的依赖注入系统,降低模块间耦合度
-- 使用工厂模式创建核心组件,便于测试和替换
-- 示例代码:
- ```python
- class AppContainer:
- def __init__(self, config):
- self.config = config
- self._services = {}
-
- def get_ocr_service(self):
- if 'ocr_service' not in self._services:
- self._services['ocr_service'] = OCRService(self.config)
- return self._services['ocr_service']
- ```
-
-### 1.2 配置系统增强
-
-**当前情况**:配置存储在 `config.ini`,但部分硬编码的配置分散在代码中。
-
-**优化建议**:
-- 将所有配置项集中到配置文件,消除硬编码的配置
-- 添加环境变量支持,便于部署和CI/CD集成
-- 增加配置验证机制,防止错误配置
-- 支持不同环境(开发、测试、生产)的配置切换
-
-### 1.3 模块化 UI 与核心逻辑分离
-
-**当前情况**:`启动器.py` 文件过大 (1050行),同时包含 UI 和业务逻辑。
-
-**优化建议**:
-- 将 UI 逻辑与业务逻辑完全分离
-- 采用 MVC 或 MVVM 模式重构 UI 代码
-- 将 UI 组件模块化,每个页面/功能对应单独的类
-
-## 2. 性能优化
-
-### 2.1 数据处理性能
-
-**当前情况**:处理大量数据时效率较低,特别是 Excel 数据处理部分。
-
-**优化建议**:
-- 使用 DataFrame 矢量化操作替代循环,提高数据处理速度
-- 对于大文件,实现分块读取和处理机制
-- 优化正则表达式,减少重复编译
-- 示例改进:
- ```python
- # 优化前
- for idx, row in df.iterrows():
- # 处理每一行...
-
- # 优化后
- # 使用 apply 或向量化操作
- df['barcode'] = df['barcode'].apply(format_barcode)
- ```
-
-### 2.2 并发处理增强
-
-**当前情况**:已有初步的多线程支持,但未充分利用。
-
-**优化建议**:
-- 扩展并行处理能力,特别是在 OCR 识别部分
-- 实现任务队列系统,支持后台处理
-- 添加进度报告机制,提高用户体验
-- 考虑使用 asyncio 进行 I/O 密集型任务处理
-
-### 2.3 缓存机制
-
-**当前情况**:每次处理都重新加载和解析数据。
-
-**优化建议**:
-- 实现内存缓存机制,缓存常用数据和配置
-- 添加条码和商品信息的本地数据库,减少重复处理
-- 对规格解析结果进行缓存,提高处理速度
-
-## 3. 代码质量改进
-
-### 3.1 单元测试与代码覆盖率
-
-**当前情况**:缺乏系统性的单元测试。
-
-**优化建议**:
-- 为核心功能编写单元测试,特别是单位转换和条码处理逻辑
-- 实现测试数据生成器,支持边界情况测试
-- 使用测试覆盖率工具,确保关键代码被测试覆盖
-- 集成持续测试到开发流程中
-
-### 3.2 代码重构
-
-**当前情况**:部分函数过长,职责不够单一。
-
-**优化建议**:
-- 对长函数进行拆分,特别是 `extract_product_info`(300+ 行)
-- 使用 Strategy 模式重构条码处理和单位转换逻辑
-- 简化复杂的嵌套条件语句,提高代码可读性
-- 提取通用功能到辅助函数,减少代码重复
-
-### 3.3 错误处理增强
-
-**当前情况**:错误处理主要依靠日志记录。
-
-**优化建议**:
-- 设计更细粒度的异常类型,便于精确处理不同错误
-- 实现全局异常处理,防止程序崩溃
-- 添加用户友好的错误提示,而不只是记录日志
-- 增加错误恢复机制,允许在出错后继续处理其他项目
-
-## 4. 功能增强
-
-### 4.1 数据验证与清洗增强
-
-**当前情况**:基本的数据验证和清洗逻辑。
-
-**优化建议**:
-- 增强数据验证规则,特别是对条码和数量的验证
-- 实现更智能的数据修复功能,处理常见错误格式
-- 添加数据异常检测算法,自动标记异常数据
-- 提供手动数据修正界面,允许用户修正识别错误
-
-### 4.2 批量处理功能增强
-
-**当前情况**:支持基本的批量处理。
-
-**优化建议**:
-- 支持拖放多个文件进行处理
-- 添加文件队列管理,显示待处理/已处理状态
-- 实现处理中断和恢复功能
-- 支持处理结果预览和批量修改
-
-### 4.3 数据导出与集成
-
-**当前情况**:生成固定格式的 Excel 文件。
-
-**优化建议**:
-- 支持多种导出格式(CSV、JSON、XML 等)
-- 提供数据库存储选项,便于数据管理和查询
-- 添加 API 接口,支持与其他系统集成
-- 实现定制化报表生成功能
-
-## 5. 用户体验改进
-
-### 5.1 界面优化
-
-**当前情况**:基本的功能界面。
-
-**优化建议**:
-- 重新设计 UI,采用现代化界面框架(如 PyQt6 或 wx.Python)
-- 添加暗色主题支持
-- 实现响应式布局,适应不同屏幕尺寸
-- 增加操作引导和工具提示
-
-### 5.2 用户反馈与报告
-
-**当前情况**:主要通过日志记录处理结果。
-
-**优化建议**:
-- 设计直观的处理结果报告页面
-- 添加数据可视化功能,展示处理统计信息
-- 实现处理报告导出功能
-- 设计更友好的错误提示和建议
-
-### 5.3 配置与偏好设置
-
-**当前情况**:配置主要在 config.ini 中修改。
-
-**优化建议**:
-- 设计图形化配置界面,无需直接编辑配置文件
-- 支持用户偏好设置保存
-- 添加配置导入/导出功能
-- 实现配置模板,快速切换不同配置
-
-## 6. 安全性改进
-
-### 6.1 API 密钥管理
-
-**当前情况**:API 密钥直接存储在配置文件中。
-
-**优化建议**:
-- 实现 API 密钥加密存储
-- 支持从环境变量或安全存储获取敏感信息
-- 添加 API 密钥轮换机制
-- 实现访问审计日志
-
-### 6.2 数据安全
-
-**当前情况**:数据以明文形式存储和处理。
-
-**优化建议**:
-- 添加敏感数据(如价格信息)的加密选项
-- 实现自动数据备份机制
-- 添加访问控制,限制对敏感数据的访问
-- 支持数据匿名化处理,用于测试和分析
-
-## 7. 部署与维护改进
-
-### 7.1 打包与分发
-
-**当前情况**:依赖 Python 环境和手动安装依赖。
-
-**优化建议**:
-- 使用 PyInstaller 或 cx_Freeze 创建独立可执行文件
-- 提供自动安装脚本,简化部署过程
-- 支持自动更新机制
-- 创建详细的安装和部署文档
-
-### 7.2 监控与日志
-
-**当前情况**:基本的日志记录功能。
-
-**优化建议**:
-- 实现结构化日志系统,支持日志搜索和分析
-- 添加系统性能监控功能
-- 设计操作审计日志,记录关键操作
-- 支持日志远程存储和集中管理
-
-### 7.3 文档完善
-
-**当前情况**:有基本的 README 文档。
-
-**优化建议**:
-- 创建详细的开发者文档,包括架构说明和 API 参考
-- 编写用户手册和操作指南
-- 添加代码内文档字符串,支持自动文档生成
-- 提供常见问题解答和故障排除指南
-
-## 8. 当前优化重点
-
-基于系统现状,建议首先关注以下优化点:
-
-1. **重构单位转换逻辑**:将复杂的单位转换和条码映射逻辑模块化,提高可维护性
-2. **增强数据验证**:改进条码和规格提取逻辑,减少处理错误
-3. **UI 改进**:将大型启动器文件拆分为多个组件,采用 MVC 模式
-4. **添加单元测试**:为核心业务逻辑添加测试用例,确保功能正确性
-5. **实现缓存机制**:提高重复数据处理效率
-
-## 9. 长期优化计划
-
-长期来看,建议考虑以下方向:
-
-1. **迁移到 Web 应用**:考虑将系统转换为 Web 应用,提供更好的跨平台支持
-2. **数据智能分析**:增加智能分析功能,如采购趋势分析、异常检测等
-3. **与 ERP 系统集成**:提供与主流 ERP 系统的集成接口
-4. **移动端支持**:开发移动应用或响应式 Web 界面,支持手机操作
-5. **OCR 引擎替换选项**:支持多种 OCR 引擎,降低对单一 API 的依赖
-
-通过以上优化,OCR 订单处理系统将更加健壮、高效、易用,能够更好地满足业务需求,并为未来功能扩展提供良好的基础。
\ No newline at end of file
diff --git a/启动器.py b/启动器.py
index 4951e41..51eefb6 100644
--- a/启动器.py
+++ b/启动器.py
@@ -888,13 +888,13 @@ def main():
row7 = tk.Frame(button_area)
row7.pack(fill=tk.X, pady=button_pady)
- # 演示自定义弹窗按钮
+ # 统计报告按钮
tk.Button(
row7,
- text="自定义弹窗演示",
+ text="统计报告",
width=button_width,
height=button_height,
- command=lambda: show_demo_dialog(log_text)
+ command=lambda: generate_stats_report(log_text)
).pack(side=tk.LEFT, padx=button_padx)
# 条码映射编辑按钮
@@ -936,6 +936,9 @@ def main():
process_single_image = process_single_image_with_status
process_excel_file = process_excel_file_with_status
+ # 绑定键盘快捷键
+ bind_keyboard_shortcuts(root, log_text, status_bar)
+
# 启动主循环
root.mainloop()
@@ -1208,38 +1211,98 @@ def show_tobacco_result_preview(returncode, output):
f"显示预览时发生错误: {e}\n请检查日志了解详细信息。"
)
-def show_demo_dialog(log_widget):
- """演示自定义弹窗功能"""
+def generate_stats_report(log_widget):
+ """生成处理统计报告"""
try:
- add_to_log(log_widget, "显示自定义弹窗演示...\n", "info")
+ add_to_log(log_widget, "正在生成统计报告...\n", "info")
- # 创建一个示例结果文件路径
- sample_file = os.path.join(os.path.abspath("data/output"), "样例文件.xlsx")
-
- # 获取当前时间
- current_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
-
- # 创建其他信息
- additional_info = {
- "客户名称": "示例客户",
- "订单编号": "ORD-20250509-001",
- "处理类型": "自定义处理"
+ # 分析处理记录
+ stats = {
+ "ocr_processed": 0,
+ "ocr_success": 0,
+ "orders_processed": 0,
+ "total_amount": 0,
+ "success_rate": 0
}
- # 显示自定义弹窗
- show_custom_dialog(
- title="自定义弹窗演示",
- message="这是一个自定义弹窗示例",
- result_file=sample_file, # 文件可能不存在,会展示文件未找到的情况
- time_info=current_time,
- count_info="50个商品",
- amount_info="¥1,234.56",
- additional_info=additional_info
- )
+ # 读取历史记录文件
+ processed_files = os.path.join("data/output", "processed_files.json")
+ merged_files = os.path.join("data/output", "merged_files.json")
- add_to_log(log_widget, "自定义弹窗已显示\n", "success")
+ if os.path.exists(processed_files):
+ try:
+ with open(processed_files, 'r', encoding='utf-8') as f:
+ data = json.load(f)
+ stats["ocr_processed"] = len(data)
+ stats["ocr_success"] = sum(1 for item in data.values() if item.get("success", False))
+ except Exception as e:
+ add_to_log(log_widget, f"读取OCR处理记录时出错: {str(e)}\n", "error")
+
+ if os.path.exists(merged_files):
+ try:
+ with open(merged_files, 'r', encoding='utf-8') as f:
+ data = json.load(f)
+ stats["orders_processed"] = len(data)
+ except Exception as e:
+ add_to_log(log_widget, f"读取订单处理记录时出错: {str(e)}\n", "error")
+
+ # 计算成功率
+ if stats["ocr_processed"] > 0:
+ stats["success_rate"] = round((stats["ocr_success"] / stats["ocr_processed"]) * 100, 2)
+
+ # 创建报告对话框
+ report_dialog = tk.Toplevel()
+ report_dialog.title("处理统计报告")
+ report_dialog.geometry("500x400")
+ center_window(report_dialog)
+
+ tk.Label(report_dialog, text="OCR订单处理统计报告", font=("Arial", 16, "bold")).pack(pady=10)
+
+ # 显示统计数据
+ stats_frame = tk.Frame(report_dialog)
+ stats_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=10)
+
+ tk.Label(stats_frame, text=f"处理的图片总数: {stats['ocr_processed']}个", font=("Arial", 12)).pack(anchor=tk.W, pady=5)
+ tk.Label(stats_frame, text=f"成功处理的图片数: {stats['ocr_success']}个", font=("Arial", 12)).pack(anchor=tk.W, pady=5)
+ tk.Label(stats_frame, text=f"成功率: {stats['success_rate']}%", font=("Arial", 12)).pack(anchor=tk.W, pady=5)
+ tk.Label(stats_frame, text=f"处理的订单数: {stats['orders_processed']}个", font=("Arial", 12)).pack(anchor=tk.W, pady=5)
+
+ # 分析文件目录情况
+ input_dir = "data/input"
+ output_dir = "data/output"
+
+ input_files_count = len([f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f))]) if os.path.exists(input_dir) else 0
+ output_files_count = len([f for f in os.listdir(output_dir) if os.path.isfile(os.path.join(output_dir, f))]) if os.path.exists(output_dir) else 0
+
+ tk.Label(stats_frame, text=f"输入目录文件数: {input_files_count}个", font=("Arial", 12)).pack(anchor=tk.W, pady=5)
+ tk.Label(stats_frame, text=f"输出目录文件数: {output_files_count}个", font=("Arial", 12)).pack(anchor=tk.W, pady=5)
+
+ # 分析日志文件
+ logs_dir = "logs"
+ log_files_count = len([f for f in os.listdir(logs_dir) if os.path.isfile(os.path.join(logs_dir, f))]) if os.path.exists(logs_dir) else 0
+
+ tk.Label(stats_frame, text=f"日志文件数: {log_files_count}个", font=("Arial", 12)).pack(anchor=tk.W, pady=5)
+
+ # 附加信息
+ recent_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+
+ add_info_frame = tk.Frame(stats_frame, relief=tk.GROOVE, borderwidth=1)
+ add_info_frame.pack(fill=tk.X, pady=10)
+
+ tk.Label(add_info_frame, text="系统信息", font=("Arial", 10, "bold")).pack(anchor=tk.W, padx=10, pady=5)
+ tk.Label(add_info_frame, text=f"报告生成时间: {recent_time}", font=("Arial", 10)).pack(anchor=tk.W, padx=10, pady=2)
+ tk.Label(add_info_frame, text=f"系统版本: v1.5", font=("Arial", 10)).pack(anchor=tk.W, padx=10, pady=2)
+
+ # 按钮
+ button_frame = tk.Frame(report_dialog)
+ button_frame.pack(pady=10)
+
+ tk.Button(button_frame, text="确定", command=report_dialog.destroy).pack()
+
+ add_to_log(log_widget, "统计报告已生成\n", "success")
except Exception as e:
- add_to_log(log_widget, f"显示自定义弹窗时出错: {str(e)}\n", "error")
+ add_to_log(log_widget, f"生成统计报告时出错: {str(e)}\n", "error")
+ messagebox.showerror("错误", f"生成统计报告时出错: {str(e)}")
def edit_barcode_mappings(log_widget):
"""编辑条码映射配置"""
@@ -1267,5 +1330,73 @@ def edit_barcode_mappings(log_widget):
add_to_log(log_widget, f"编辑条码映射时出错: {str(e)}\n", "error")
messagebox.showerror("错误", f"编辑条码映射时出错: {str(e)}")
+def bind_keyboard_shortcuts(root, log_widget, status_bar):
+ """绑定键盘快捷键"""
+ # Ctrl+O - 处理单个图片
+ root.bind('', lambda e: process_single_image_with_status(log_widget, status_bar))
+
+ # Ctrl+E - 处理Excel文件
+ root.bind('', lambda e: process_excel_file_with_status(log_widget, status_bar))
+
+ # Ctrl+B - 批量处理
+ root.bind('', lambda e: run_command_with_logging(["python", "run.py", "ocr", "--batch"], log_widget, status_bar))
+
+ # Ctrl+P - 完整流程
+ root.bind('', lambda e: run_command_with_logging(["python", "run.py", "pipeline"], log_widget, status_bar))
+
+ # Ctrl+M - 合并采购单
+ root.bind('', lambda e: run_command_with_logging(["python", "run.py", "merge"], log_widget, status_bar))
+
+ # Ctrl+T - 处理烟草订单
+ root.bind('', lambda e: run_command_with_logging(["python", "run.py", "tobacco"], log_widget, status_bar, on_complete=show_tobacco_result_preview))
+
+ # Ctrl+S - 统计报告
+ root.bind('', lambda e: generate_stats_report(log_widget))
+
+ # F5 - 刷新/清除缓存
+ root.bind('', lambda e: clean_cache(log_widget))
+
+ # Escape - 退出
+ root.bind('', lambda e: root.quit() if messagebox.askyesno("确认退出", "确定要退出程序吗?") else None)
+
+ # F1 - 显示快捷键帮助
+ root.bind('', lambda e: show_shortcuts_help())
+
+def show_shortcuts_help():
+ """显示快捷键帮助对话框"""
+ help_dialog = tk.Toplevel()
+ help_dialog.title("快捷键帮助")
+ help_dialog.geometry("400x450")
+ center_window(help_dialog)
+
+ tk.Label(help_dialog, text="键盘快捷键", font=("Arial", 16, "bold")).pack(pady=10)
+
+ help_text = tk.Text(help_dialog, wrap=tk.WORD, width=50, height=20)
+ help_text.pack(padx=20, pady=10, fill=tk.BOTH, expand=True)
+
+ shortcuts = """
+ Ctrl+O: 处理单个图片
+ Ctrl+E: 处理Excel文件
+ Ctrl+B: OCR批量识别
+ Ctrl+P: 完整处理流程
+ Ctrl+M: 合并采购单
+ Ctrl+T: 处理烟草订单
+ Ctrl+S: 显示统计报告
+ F5: 清除处理缓存
+ F1: 显示此帮助
+ Esc: 退出程序
+ """
+
+ help_text.insert(tk.END, shortcuts)
+ help_text.configure(state=tk.DISABLED)
+
+ # 按钮
+ tk.Button(help_dialog, text="确定", command=help_dialog.destroy).pack(pady=10)
+
+ # 确保窗口显示在最前
+ help_dialog.lift()
+ help_dialog.attributes('-topmost', True)
+ help_dialog.after_idle(lambda: help_dialog.attributes('-topmost', False))
+
if __name__ == "__main__":
main()
\ No newline at end of file
diff --git a/更新日志.md b/更新日志.md
new file mode 100644
index 0000000..141b2d8
--- /dev/null
+++ b/更新日志.md
@@ -0,0 +1,99 @@
+# OCR订单处理系统 - 更新日志
+
+## v1.5 (2025-05-09)
+
+### 功能改进
+- 烟草订单处理结果展示:改进烟草订单处理完成后的结果展示界面
+ - 美化结果展示界面,显示订单时间、总金额和处理条目数
+ - 添加文件信息展示,包括文件大小和创建时间
+ - 提供打开文件、打开所在文件夹等便捷操作按钮
+ - 统一与Excel处理结果展示风格,提升用户体验
+ - 增强结果文件路径解析能力,确保正确找到并显示结果文件
+- 条码映射编辑功能:
+ - 添加图形化条码映射编辑工具,方便管理条码映射和特殊处理规则
+ - 支持添加、修改和删除条码映射关系
+ - 支持配置特殊处理规则,如乘数、目标单位、固定单价等
+ - 自动保存到配置文件,便于后续使用
+
+### 问题修复
+- 修复烟草订单处理时出现双重弹窗问题
+- 修复烟草订单处理完成后结果展示弹窗无法正常显示的问题
+- 修复ConfigParser兼容性问题,支持标准ConfigParser对象
+- 修复百度OCR客户端中getint方法调用不兼容问题
+- 修复OCRService中缺少batch_process方法的问题,确保OCR功能正常工作
+- 改进日志管理,确保所有日志正确关闭
+- 优化UI界面,统一按钮样式
+- 修复启动器中处理烟草订单按钮的显示样式
+- 修复run.py中close_logger调用缺少参数的问题
+
+### 代码改进
+- 改进TobaccoService类对配置的处理方式,使用标准get方法
+- 添加fallback机制以增强配置健壮性
+- 优化启动器中结果预览逻辑,避免重复弹窗
+- 统一UI组件风格,提升用户体验
+- 增强错误处理,提供更清晰的错误信息
+
+## v1.4 (2025-05-09)
+
+### 新功能
+- 烟草订单处理:新增烟草公司特定格式订单明细文件处理功能
+ - 支持自动处理标准烟草订单明细格式
+ - 根据烟草公司"盒码"作为条码生成银豹采购单
+ - 自动将"订单量"转换为"采购量"并计算采购单价
+ - 处理结果以银豹采购单格式保存,方便直接导入
+
+### 功能优化
+- 配置兼容性:优化配置处理逻辑,兼容标准ConfigParser对象
+- 启动器优化:启动器界面增加"处理烟草订单"功能按钮
+- 代码结构优化:将烟草订单处理功能模块化,集成到整体服务架构
+
+## v1.3 (2025-07-20)
+
+### 功能优化
+- 采购单赠品处理逻辑优化:修改了银豹采购单中赠品的处理方式
+ - 之前:赠品数量单独填写在"赠送量"列,与正常采购量分开处理
+ - 现在:将赠品数量合并到采购量中,赠送量列留空
+ - 有正常商品且有赠品的情况:采购量 = 正常商品数量 + 赠品数量,单价 = 原单价 × 正常商品数量 ÷ 总数量
+ - 只有赠品的情况:采购量填写赠品数量,单价为0
+- 更新说明:经用户反馈,赠品处理逻辑已还原为原始方式,正常商品数量和赠品数量分开填写
+
+## v1.2 (2025-07-15)
+
+### 功能优化
+- 规格提取优化:改进了从商品名称中提取规格的逻辑,优先识别"容量*数量"格式
+ - 例如从"美汁源果粒橙1.8L*8瓶"能准确提取"1.8L*8"而非错误的"1.8L*1"
+- 规格解析增强:优化`parse_specification`方法,能正确解析"1.8L*8"格式规格,确保准确提取包装数量
+- 单位推断增强:在`extract_product_info`方法中增加新逻辑,当单位为空且有条码、规格、数量、单价时,根据规格格式(如容量*数量格式或简单数量*数量格式)自动推断单位为"件"
+- 件单位处理优化:确保当设置单位为"件"时,正确触发UnitConverter单位处理逻辑,将数量乘以包装数量,单价除以包装数量,单位转为"瓶"
+- 整体改进:提高了系统处理复杂格式商品名称和规格的能力,使单位转换更加准确可靠
+- 规格提取逻辑修正:修复了在Excel中已有规格信息时仍会从商品名称推断规格的问题,现在系统会优先使用Excel中的数据,只有在规格为空时才尝试从商品名称推断
+
+## v1.1 (2025-05-07)
+
+### 功能更新
+- 单位自动推断:当单位为空但有商品编码、规格、数量、单价等信息,且规格符合容量*数量格式时,自动将单位设置为"件"并按照件的处理规则进行转换
+- 规格解析优化:改进对容量*数量格式规格的解析,如"1.8L*8"能正确识别包装数量为8
+- 规格提取增强:从商品名称中提取"容量*数量"格式的规格时,能正确识别如"美汁源果粒橙1.8L*8瓶"中的"1.8L*8"部分
+- 条码映射功能:增加特定条码的自动映射功能,支持将特定条码自动转换为指定的目标条码
+ - 6920584471055 → 6920584471017
+ - 6925861571159 → 69021824
+ - 6923644268923 → 6923644268480
+ - 条码映射后会继续按照件/箱等单位的标准处理规则进行数量和单价的转换
+
+## v1.0 (2025-05-02)
+
+### 主要功能
+- 图像OCR识别:支持对采购单图片进行OCR识别并生成Excel文件
+- Excel数据处理:智能处理Excel文件,提取和转换商品信息
+- 采购单生成:按照模板格式生成标准采购单Excel文件
+- 采购单合并:支持多个采购单合并为一个总单
+- 图形界面:提供简洁直观的操作界面
+- 命令行支持:支持命令行调用,方便自动化处理
+
+### 技术改进
+- 模块化架构:重构代码为配置、核心功能、服务和CLI等模块
+- 单位智能处理:完善的单位转换规则,支持多种计量单位
+- 规格智能推断:从商品名称自动推断规格信息
+- 日志管理:完善的日志记录系统,支持终端和GUI同步显示
+- 表头智能识别:自动识别Excel中的表头位置,兼容多种格式
+- 改进用户体验:界面优化,批量处理支持,实时状态反馈
\ No newline at end of file
diff --git a/项目结构优化方案.md b/项目结构优化方案.md
deleted file mode 100644
index dce208a..0000000
--- a/项目结构优化方案.md
+++ /dev/null
@@ -1,209 +0,0 @@
-# OCR订单处理系统 - 项目结构优化方案
-
-根据对v1目录项目的分析,提出以下项目结构优化方案。本方案旨在提高代码的可维护性、可扩展性和可读性。
-
-## 主要优化目标
-
-1. **模块化设计**:将功能拆分为独立模块,降低耦合度
-2. **统一配置管理**:简化配置处理,避免重复代码
-3. **标准化日志系统**:统一日志管理,便于调试和问题追踪
-4. **清晰的项目结构**:采用现代Python项目结构
-5. **规范化开发流程**:添加单元测试,代码质量检查
-
-## 项目新结构
-
-```
-orc-order-v2/ # 项目根目录
-│
-├── app/ # 应用主目录
-│ ├── __init__.py # 包初始化
-│ ├── config/ # 配置目录
-│ │ ├── __init__.py
-│ │ ├── settings.py # 基础配置
-│ │ └── defaults.py # 默认配置值
-│ │
-│ ├── core/ # 核心功能
-│ │ ├── __init__.py
-│ │ ├── ocr/ # OCR相关功能
-│ │ │ ├── __init__.py
-│ │ │ ├── baidu_ocr.py # 百度OCR基本功能
-│ │ │ └── table_ocr.py # 表格OCR处理
-│ │ │
-│ │ ├── excel/ # Excel处理相关功能
-│ │ │ ├── __init__.py
-│ │ │ ├── processor.py # Excel处理核心
-│ │ │ ├── merger.py # 订单合并功能
-│ │ │ └── converter.py # 单位转换与规格处理
-│ │ │
-│ │ └── utils/ # 工具函数
-│ │ ├── __init__.py
-│ │ ├── file_utils.py # 文件操作工具
-│ │ ├── log_utils.py # 日志工具
-│ │ └── string_utils.py # 字符串处理工具
-│ │
-│ ├── services/ # 业务服务
-│ │ ├── __init__.py
-│ │ ├── ocr_service.py # OCR服务
-│ │ └── order_service.py # 订单处理服务
-│ │
-│ └── cli/ # 命令行接口
-│ ├── __init__.py
-│ ├── ocr_cli.py # OCR命令行工具
-│ ├── excel_cli.py # Excel处理命令行工具
-│ └── merge_cli.py # 订单合并命令行工具
-│
-├── templates/ # 模板文件
-│ └── 银豹-采购单模板.xls # 订单模板
-│
-├── data/ # 数据目录
-│ ├── input/ # 输入文件
-│ ├── output/ # 输出文件
-│ └── temp/ # 临时文件
-│
-├── logs/ # 日志目录
-│
-├── tests/ # 测试目录
-│ ├── __init__.py
-│ ├── test_ocr.py
-│ ├── test_excel.py
-│ └── test_merger.py
-│
-├── pyproject.toml # 项目配置
-├── setup.py # 安装配置
-├── requirements.txt # 依赖管理
-├── config.ini.example # 配置示例
-├── .gitignore # Git忽略文件
-├── README.md # 项目说明
-└── run.py # 主入口脚本
-```
-
-## 功能优化
-
-### 1. 配置管理优化
-
-创建统一的配置管理系统,避免多个模块各自实现配置处理:
-
-```python
-# app/config/settings.py
-import os
-import configparser
-from typing import Dict, List, Any
-
-from .defaults import DEFAULT_CONFIG
-
-class ConfigManager:
- """统一配置管理"""
- _instance = None
-
- def __new__(cls, config_file=None):
- if cls._instance is None:
- cls._instance = super(ConfigManager, cls).__new__(cls)
- cls._instance._init(config_file)
- return cls._instance
-
- def _init(self, config_file):
- self.config_file = config_file or 'config.ini'
- self.config = configparser.ConfigParser()
- self.load_config()
-
- def load_config(self):
- # 配置加载实现...
-```
-
-### 2. 日志系统优化
-
-创建统一的日志管理系统:
-
-```python
-# app/core/utils/log_utils.py
-import os
-import sys
-import logging
-from datetime import datetime
-from typing import Optional
-
-def setup_logger(name: str, log_file: Optional[str] = None, level=logging.INFO):
- """配置并返回日志记录器"""
- # 日志配置实现...
-```
-
-### 3. 核心业务逻辑优化
-
-#### OCR处理优化
-
-将百度OCR API调用与业务逻辑分离:
-
-```python
-# app/core/ocr/baidu_ocr.py
-class BaiduOCRClient:
- """百度OCR API客户端"""
- # API调用实现...
-
-# app/services/ocr_service.py
-class OCRService:
- """OCR处理服务"""
- # 业务逻辑实现...
-```
-
-#### Excel处理优化
-
-将Excel处理逻辑模块化:
-
-```python
-# app/core/excel/processor.py
-class ExcelProcessor:
- """Excel处理核心"""
- # Excel处理实现...
-
-# app/core/excel/converter.py
-class UnitConverter:
- """单位转换处理"""
- # 单位转换实现...
-```
-
-### 4. 命令行接口优化
-
-使用标准的命令行接口设计:
-
-```python
-# app/cli/ocr_cli.py
-import argparse
-import sys
-from app.services.ocr_service import OCRService
-
-def create_parser():
- """创建命令行参数解析器"""
- # 参数配置实现...
-
-def main():
- """OCR处理命令行入口"""
- # 命令实现...
-
-if __name__ == "__main__":
- main()
-```
-
-## 代码优化方向
-
-1. **类型提示**:使用Python类型注解,提高代码可读性
-2. **异常处理**:优化异常处理流程,便于调试
-3. **代码复用**:减少重复代码,提取公共功能
-4. **单元测试**:为核心功能编写测试用例
-
-## 迁移路径
-
-1. 创建新的项目结构
-2. 迁移配置管理模块
-3. 迁移日志系统
-4. 迁移OCR核心功能
-5. 迁移Excel处理功能
-6. 迁移命令行接口
-7. 编写单元测试
-8. 完善文档
-
-## 后续优化建议
-
-1. **Web界面**:考虑添加简单的Web界面便于操作
-2. **多OCR引擎支持**:增加更多OCR引擎选择
-3. **进度报告**:添加处理进度报告功能
-4. **并行处理优化**:改进并行处理机制,提高性能
\ No newline at end of file