新增快捷键,新增日志统计

This commit is contained in:
侯欢 2025-05-10 12:32:10 +08:00
parent f5eda6cbd8
commit 201aac35e6
46 changed files with 398 additions and 5224 deletions

100
README.md
View File

@ -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或联系开发者。

View File

@ -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

View File

@ -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 标准单位处理
| 单位 | 处理规则 | 示例 |
|------|----------|------|
| 件 | 数量×包装数量<br>单价÷包装数量<br>单位转换为"瓶" | 1件(规格1*12) → 12瓶<br>单价108元/件 → 9元/瓶 |
| 箱 | 数量×包装数量<br>单价÷包装数量<br>单位转换为"瓶" | 2箱(规格1*24) → 48瓶<br>单价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

View File

@ -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())

View File

@ -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)

View File

@ -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)

View File

@ -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()

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 MiB

View File

@ -1 +0,0 @@
Active since: 2025-05-01 19:46:44

View File

@ -1 +0,0 @@
Active since: 2025-05-01 19:49:19

View File

@ -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)

View File

@ -1,4 +0,0 @@
{
"采购单_武侯环球乐百惠便利店849.xls_5632_1746098172.9159887": "output\\合并采购单_20250501193931.xls",
"采购单_武侯环球乐百惠便利店3333.xls_9728_1746097892.1829922": "output\\合并采购单_20250501193931.xls"
}

View File

@ -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

View File

@ -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()

View File

@ -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("测试完成")

View File

@ -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("单位转换处理规则测试完成")

View File

@ -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"
}

BIN
diff.txt

Binary file not shown.

View File

@ -1 +1 @@
Active since: 2025-05-10 11:55:27
Active since: 2025-05-10 12:29:42

View File

@ -1 +1 @@
Active since: 2025-05-10 12:00:40
Active since: 2025-05-10 12:29:41

View File

@ -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

View File

@ -1 +1 @@
Active since: 2025-05-10 12:00:40
Active since: 2025-05-10 12:29:41

View File

@ -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

View File

@ -1 +1 @@
Active since: 2025-05-10 12:00:40
Active since: 2025-05-10 12:29:41

View File

@ -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, 单位: 件 -> 瓶

View File

@ -1 +1 @@
Active since: 2025-05-10 11:55:27
Active since: 2025-05-10 12:29:41

View File

@ -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

View File

@ -1 +1 @@
Active since: 2025-05-10 11:55:27
Active since: 2025-05-10 12:29:41

View File

@ -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

View File

@ -1 +1 @@
Active since: 2025-05-10 12:00:40
Active since: 2025-05-10 12:29:41

View File

@ -1 +1 @@
Active since: 2025-05-10 11:55:26
Active since: 2025-05-10 12:29:40

View File

@ -1 +1 @@
Active since: 2025-05-10 11:55:26
Active since: 2025-05-10 12:29:40

View File

@ -1 +1 @@
Active since: 2025-05-10 11:55:25
Active since: 2025-05-10 12:29:39

View File

@ -1 +1 @@
Active since: 2025-05-10 11:55:26
Active since: 2025-05-10 12:29:40

View File

@ -1 +1 @@
Active since: 2025-05-10 11:55:27
Active since: 2025-05-10 12:29:41

View File

@ -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

View File

@ -1 +1 @@
Active since: 2025-05-10 11:55:27
Active since: 2025-05-10 12:29:42

View File

@ -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 订单处理系统将更加健壮、高效、易用,能够更好地满足业务需求,并为未来功能扩展提供良好的基础。

View File

@ -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('<Control-o>', lambda e: process_single_image_with_status(log_widget, status_bar))
# Ctrl+E - 处理Excel文件
root.bind('<Control-e>', lambda e: process_excel_file_with_status(log_widget, status_bar))
# Ctrl+B - 批量处理
root.bind('<Control-b>', lambda e: run_command_with_logging(["python", "run.py", "ocr", "--batch"], log_widget, status_bar))
# Ctrl+P - 完整流程
root.bind('<Control-p>', lambda e: run_command_with_logging(["python", "run.py", "pipeline"], log_widget, status_bar))
# Ctrl+M - 合并采购单
root.bind('<Control-m>', lambda e: run_command_with_logging(["python", "run.py", "merge"], log_widget, status_bar))
# Ctrl+T - 处理烟草订单
root.bind('<Control-t>', lambda e: run_command_with_logging(["python", "run.py", "tobacco"], log_widget, status_bar, on_complete=show_tobacco_result_preview))
# Ctrl+S - 统计报告
root.bind('<Control-s>', lambda e: generate_stats_report(log_widget))
# F5 - 刷新/清除缓存
root.bind('<F5>', lambda e: clean_cache(log_widget))
# Escape - 退出
root.bind('<Escape>', lambda e: root.quit() if messagebox.askyesno("确认退出", "确定要退出程序吗?") else None)
# F1 - 显示快捷键帮助
root.bind('<F1>', 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()

99
更新日志.md Normal file
View File

@ -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中的表头位置兼容多种格式
- 改进用户体验:界面优化,批量处理支持,实时状态反馈

View File

@ -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. **并行处理优化**:改进并行处理机制,提高性能