新增快捷键,新增日志统计
This commit is contained in:
parent
f5eda6cbd8
commit
201aac35e6
100
README.md
100
README.md
@ -293,106 +293,6 @@ python run.py <命令> [选项]
|
|||||||
|
|
||||||
MIT License
|
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或联系开发者。
|
如有问题,请提交Issue或联系开发者。
|
||||||
52
backup/v1_backup_20250502190248/.gitignore
vendored
52
backup/v1_backup_20250502190248/.gitignore
vendored
@ -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
|
|
||||||
@ -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
|
|
||||||
@ -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())
|
|
||||||
@ -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)
|
|
||||||
@ -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)
|
|
||||||
@ -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.
Binary file not shown.
|
Before Width: | Height: | Size: 5.4 MiB |
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
Active since: 2025-05-01 19:46:44
|
|
||||||
@ -1 +0,0 @@
|
|||||||
Active since: 2025-05-01 19:49:19
|
|
||||||
@ -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)
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"采购单_武侯环球乐百惠便利店849.xls_5632_1746098172.9159887": "output\\合并采购单_20250501193931.xls",
|
|
||||||
"采购单_武侯环球乐百惠便利店3333.xls_9728_1746097892.1829922": "output\\合并采购单_20250501193931.xls"
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
@ -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()
|
|
||||||
Binary file not shown.
@ -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("测试完成")
|
|
||||||
@ -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("单位转换处理规则测试完成")
|
|
||||||
@ -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"
|
"D:/My Documents/python/orc-order-v2/data/output/微信图片_20250509142624.xlsx": "data/output\\采购单_微信图片_20250509142624.xls"
|
||||||
}
|
}
|
||||||
Binary file not shown.
@ -1 +1 @@
|
|||||||
Active since: 2025-05-10 11:55:27
|
Active since: 2025-05-10 12:29:42
|
||||||
@ -1 +1 @@
|
|||||||
Active since: 2025-05-10 12:00:40
|
Active since: 2025-05-10 12:29:41
|
||||||
@ -1999,3 +1999,18 @@
|
|||||||
2025-05-10 12:04:27,177 - app.core.excel.converter - INFO - 条码映射配置保存成功,共9项
|
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: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: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
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
Active since: 2025-05-10 12:00:40
|
Active since: 2025-05-10 12:29:41
|
||||||
@ -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,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,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 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
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
Active since: 2025-05-10 12:00:40
|
Active since: 2025-05-10 12:29:41
|
||||||
@ -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, 单价: 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,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 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, 单位: 件 -> 瓶
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
Active since: 2025-05-10 11:55:27
|
Active since: 2025-05-10 12:29:41
|
||||||
@ -514,3 +514,5 @@
|
|||||||
2025-05-09 16:01:39,294 - app.core.excel.merger - INFO - 初始化PurchaseOrderMerger完成,模板文件: templates\银豹-采购单模板.xls
|
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,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 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
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
Active since: 2025-05-10 11:55:27
|
Active since: 2025-05-10 12:29:41
|
||||||
@ -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,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,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 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
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
Active since: 2025-05-10 12:00:40
|
Active since: 2025-05-10 12:29:41
|
||||||
@ -1 +1 @@
|
|||||||
Active since: 2025-05-10 11:55:26
|
Active since: 2025-05-10 12:29:40
|
||||||
@ -1 +1 @@
|
|||||||
Active since: 2025-05-10 11:55:26
|
Active since: 2025-05-10 12:29:40
|
||||||
@ -1 +1 @@
|
|||||||
Active since: 2025-05-10 11:55:25
|
Active since: 2025-05-10 12:29:39
|
||||||
@ -1 +1 @@
|
|||||||
Active since: 2025-05-10 11:55:26
|
Active since: 2025-05-10 12:29:40
|
||||||
@ -1 +1 @@
|
|||||||
Active since: 2025-05-10 11:55:27
|
Active since: 2025-05-10 12:29:41
|
||||||
@ -344,3 +344,6 @@
|
|||||||
2025-05-10 11:55:28,003 - app.services.order_service - INFO - 初始化OrderService
|
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初始化完成
|
||||||
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 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
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
Active since: 2025-05-10 11:55:27
|
Active since: 2025-05-10 12:29:42
|
||||||
329
v2-优化总结.md
329
v2-优化总结.md
@ -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 订单处理系统将更加健壮、高效、易用,能够更好地满足业务需求,并为未来功能扩展提供良好的基础。
|
|
||||||
189
启动器.py
189
启动器.py
@ -888,13 +888,13 @@ def main():
|
|||||||
row7 = tk.Frame(button_area)
|
row7 = tk.Frame(button_area)
|
||||||
row7.pack(fill=tk.X, pady=button_pady)
|
row7.pack(fill=tk.X, pady=button_pady)
|
||||||
|
|
||||||
# 演示自定义弹窗按钮
|
# 统计报告按钮
|
||||||
tk.Button(
|
tk.Button(
|
||||||
row7,
|
row7,
|
||||||
text="自定义弹窗演示",
|
text="统计报告",
|
||||||
width=button_width,
|
width=button_width,
|
||||||
height=button_height,
|
height=button_height,
|
||||||
command=lambda: show_demo_dialog(log_text)
|
command=lambda: generate_stats_report(log_text)
|
||||||
).pack(side=tk.LEFT, padx=button_padx)
|
).pack(side=tk.LEFT, padx=button_padx)
|
||||||
|
|
||||||
# 条码映射编辑按钮
|
# 条码映射编辑按钮
|
||||||
@ -936,6 +936,9 @@ def main():
|
|||||||
process_single_image = process_single_image_with_status
|
process_single_image = process_single_image_with_status
|
||||||
process_excel_file = process_excel_file_with_status
|
process_excel_file = process_excel_file_with_status
|
||||||
|
|
||||||
|
# 绑定键盘快捷键
|
||||||
|
bind_keyboard_shortcuts(root, log_text, status_bar)
|
||||||
|
|
||||||
# 启动主循环
|
# 启动主循环
|
||||||
root.mainloop()
|
root.mainloop()
|
||||||
|
|
||||||
@ -1208,38 +1211,98 @@ def show_tobacco_result_preview(returncode, output):
|
|||||||
f"显示预览时发生错误: {e}\n请检查日志了解详细信息。"
|
f"显示预览时发生错误: {e}\n请检查日志了解详细信息。"
|
||||||
)
|
)
|
||||||
|
|
||||||
def show_demo_dialog(log_widget):
|
def generate_stats_report(log_widget):
|
||||||
"""演示自定义弹窗功能"""
|
"""生成处理统计报告"""
|
||||||
try:
|
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")
|
stats = {
|
||||||
|
"ocr_processed": 0,
|
||||||
# 获取当前时间
|
"ocr_success": 0,
|
||||||
current_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
"orders_processed": 0,
|
||||||
|
"total_amount": 0,
|
||||||
# 创建其他信息
|
"success_rate": 0
|
||||||
additional_info = {
|
|
||||||
"客户名称": "示例客户",
|
|
||||||
"订单编号": "ORD-20250509-001",
|
|
||||||
"处理类型": "自定义处理"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# 显示自定义弹窗
|
# 读取历史记录文件
|
||||||
show_custom_dialog(
|
processed_files = os.path.join("data/output", "processed_files.json")
|
||||||
title="自定义弹窗演示",
|
merged_files = os.path.join("data/output", "merged_files.json")
|
||||||
message="这是一个自定义弹窗示例",
|
|
||||||
result_file=sample_file, # 文件可能不存在,会展示文件未找到的情况
|
|
||||||
time_info=current_time,
|
|
||||||
count_info="50个商品",
|
|
||||||
amount_info="¥1,234.56",
|
|
||||||
additional_info=additional_info
|
|
||||||
)
|
|
||||||
|
|
||||||
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:
|
except Exception as e:
|
||||||
add_to_log(log_widget, f"显示自定义弹窗时出错: {str(e)}\n", "error")
|
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")
|
||||||
|
messagebox.showerror("错误", f"生成统计报告时出错: {str(e)}")
|
||||||
|
|
||||||
def edit_barcode_mappings(log_widget):
|
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")
|
add_to_log(log_widget, f"编辑条码映射时出错: {str(e)}\n", "error")
|
||||||
messagebox.showerror("错误", f"编辑条码映射时出错: {str(e)}")
|
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__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
99
更新日志.md
Normal file
99
更新日志.md
Normal 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中的表头位置,兼容多种格式
|
||||||
|
- 改进用户体验:界面优化,批量处理支持,实时状态反馈
|
||||||
209
项目结构优化方案.md
209
项目结构优化方案.md
@ -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. **并行处理优化**:改进并行处理机制,提高性能
|
|
||||||
Loading…
Reference in New Issue
Block a user