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