class SCLMultiProcessor:
def __init__(self, root):
self.root = root
# 创建日志目录
self.log_dir = "logs"
self.root.title("SCL文件处理系统")
self.root.geometry("1100x750") # 增加窗口尺寸以适应新控件
# 初始化变量
self.color_detector = EnhancedColorDetector()
self.stats_processor = SCLRuleProcessor(self.color_detector)
self.empty_cell_detector = EnhancedEmptyCellDetector(self.color_detector)
self.progress_var = tk.DoubleVar()
os.makedirs(self.log_dir, exist_ok=True)
# 初始化日志系统
self.current_log_file = None
self.setup_logger()
# 创建主框架
self.main_frame = ttk.Frame(root, padding="10")
self.main_frame.pack(fill=tk.BOTH, expand=True)
# 创建UI
self.create_ui()
# 记录UI初始化完成
self.logger.info("用户界面初始化完成")
def toggle_config_fields(self):
"""根据操作模式显示/隐藏相关配置字段"""
mode = self.operation_mode.get()
# 统计模式:显示统计表路径,隐藏CheckSheet路径
if mode == "stats":
self.input_frame.pack(fill=tk.X, pady=5) # 显示统计表路径
self.checksheet_frame.pack_forget() # 隐藏CheckSheet路径
self.logger.info("切换到统计模式,显示统计表路径")
# SCL格式检查模式:隐藏统计表路径,显示CheckSheet路径
elif mode == "empty_check":
self.input_frame.pack_forget() # 隐藏统计表路径
self.checksheet_frame.pack(fill=tk.X, pady=5) # 显示CheckSheet路径
self.logger.info("切换到SCL格式检查模式,显示CheckSheet路径")
def create_ui(self):
"""创建用户界面"""
# 操作模式选择区域 - 放在最前面
mode_frame = ttk.LabelFrame(self.main_frame, text="操作模式", padding="10")
mode_frame.pack(fill=tk.X, pady=5)
# 添加操作模式单选按钮
self.operation_mode = tk.StringVar(value="stats") # 默认选择统计模式
ttk.Radiobutton(mode_frame, text="统计功能", variable=self.operation_mode,
value="stats", command=self.toggle_config_fields).pack(side=tk.LEFT, padx=10)
ttk.Radiobutton(mode_frame, text="SCL格式检查", variable=self.operation_mode,
value="empty_check", command=self.toggle_config_fields).pack(side=tk.LEFT, padx=10)
# 文件选择区域 - 放在操作模式后面
file_frame = ttk.LabelFrame(self.main_frame, text="文件选择", padding="10")
file_frame.pack(fill=tk.X, pady=5)
# 输入文件选择 - 统计表路径(统计模式需要)
self.input_frame = ttk.Frame(file_frame)
ttk.Label(self.input_frame, text="统计表:").pack(side=tk.LEFT, padx=5)
self.input_path_var = tk.StringVar()
input_entry = ttk.Entry(self.input_frame, textvariable=self.input_path_var, width=70)
input_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
ttk.Button(self.input_frame, text="浏览...", command=self.browse_input_file).pack(side=tk.LEFT, padx=5)
self.input_frame.pack(fill=tk.X, pady=5) # 默认显示
# 配置区域
config_frame = ttk.LabelFrame(self.main_frame, text="处理配置", padding="10")
config_frame.pack(fill=tk.X, pady=5)
# 添加SCL文件夹路径输入
scl_folder_frame = ttk.Frame(config_frame)
scl_folder_frame.pack(fill=tk.X, pady=5)
ttk.Label(scl_folder_frame, text="SCL文件夹路径:").grid(row=0, column=0, padx=5, sticky=tk.W)
self.scl_folder_var = tk.StringVar()
scl_folder_entry = ttk.Entry(scl_folder_frame, textvariable=self.scl_folder_var, width=60)
scl_folder_entry.grid(row=0, column=1, padx=5, sticky=tk.W)
ttk.Button(scl_folder_frame, text="浏览...", command=self.browse_scl_folder).grid(row=0, column=2, padx=5, sticky=tk.W)
# 搜索选项
search_frame = ttk.Frame(config_frame)
search_frame.pack(fill=tk.X, pady=5)
ttk.Label(search_frame, text="文件前缀:").grid(row=0, column=0, padx=5, sticky=tk.W)
self.prefix_var = tk.StringVar(value="SCL_")
ttk.Entry(search_frame, textvariable=self.prefix_var, width=10).grid(row=0, column=1, padx=5, sticky=tk.W)
# 添加CheckSheet路径输入(SCL格式检查模式需要)
self.checksheet_frame = ttk.Frame(config_frame)
ttk.Label(self.checksheet_frame, text="CheckSheet路径:").grid(row=0, column=0, padx=5, sticky=tk.W)
self.checksheet_path_var = tk.StringVar()
checksheet_entry = ttk.Entry(self.checksheet_frame, textvariable=self.checksheet_path_var, width=60)
checksheet_entry.grid(row=0, column=1, padx=5, sticky=tk.W)
ttk.Button(self.checksheet_frame, text="浏览...", command=self.browse_checksheet_path).grid(row=0, column=2, padx=5, sticky=tk.W)
self.checksheet_frame.pack_forget() # 默认隐藏
# 添加性能提示
ttk.Label(config_frame, text="(表头固定在第3行,数据从第4行开始)").pack(anchor=tk.W, padx=5, pady=2)
# 日志选项
log_frame = ttk.Frame(config_frame)
log_frame.pack(fill=tk.X, pady=5)
ttk.Label(log_frame, text="日志级别:").grid(row=0, column=0, padx=5, sticky=tk.W)
self.log_level_var = tk.StringVar(value="INFO")
log_level_combo = ttk.Combobox(
log_frame, textvariable=self.log_level_var, width=10, state="readonly"
)
log_level_combo['values'] = ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL')
log_level_combo.grid(row=0, column=1, padx=5, sticky=tk.W)
log_level_combo.bind("<<ComboboxSelected>>", self.change_log_level)
# 处理按钮
btn_frame = ttk.Frame(self.main_frame)
btn_frame.pack(fill=tk.X, pady=10)
ttk.Button(btn_frame, text="开始处理", command=self.process_file).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="查看日志", command=self.view_log).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="导出配置", command=self.export_config).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="加载配置", command=self.load_config).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="退出", command=self.root.destroy).pack(side=tk.RIGHT, padx=5)
# 进度条
progress_frame = ttk.Frame(self.main_frame)
progress_frame.pack(fill=tk.X, pady=5)
ttk.Label(progress_frame, text="处理进度:").pack(side=tk.LEFT, padx=5)
self.progress_bar = ttk.Progressbar(
progress_frame, variable=self.progress_var, maximum=100, length=700
)
self.progress_bar.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
self.progress_label = ttk.Label(progress_frame, text="0%")
self.progress_label.pack(side=tk.LEFT, padx=5)
# 结果展示区域
result_frame = ttk.LabelFrame(self.main_frame, text="处理结果", padding="10")
result_frame.pack(fill=tk.BOTH, expand=True, pady=5)
# 结果文本框
self.result_text = scrolledtext.ScrolledText(
result_frame, wrap=tk.WORD, height=20
)
self.result_text.pack(fill=tk.BOTH, expand=True)
self.result_text.config(state=tk.DISABLED)
# 状态栏
self.status_var = tk.StringVar(value="就绪")
status_bar = ttk.Label(self.main_frame, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W)
status_bar.pack(fill=tk.X, pady=5)
self.logger.info("UI创建完成")
def browse_checksheet_path(self):
"""浏览CheckSheet文件夹"""
folder_path = filedialog.askdirectory(title="选择CheckSheet文件夹")
if folder_path:
self.checksheet_path_var.set(folder_path)
self.logger.info(f"已选择CheckSheet文件夹: {folder_path}")
def setup_logger(self):
"""配置日志记录器"""
# 创建唯一日志文件名
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
self.current_log_file = os.path.join(self.log_dir, f"scl_processor_{timestamp}.log")
# 创建或获取日志记录器
self.logger = logging.getLogger("SCLProcessor")
self.logger.setLevel(logging.INFO)
# 移除所有现有处理器
for handler in self.logger.handlers[:]:
self.logger.removeHandler(handler)
# 创建文件处理器
file_handler = logging.FileHandler(self.current_log_file, encoding='utf-8')
file_handler.setLevel(logging.INFO)
# 创建控制台处理器
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
# 创建日志格式
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
file_handler.setFormatter(formatter)
console_handler.setFormatter(formatter)
# 添加处理器
self.logger.addHandler(file_handler)
self.logger.addHandler(console_handler)
# 记录日志初始化信息
self.logger.info(f"日志系统已初始化,日志文件: {self.current_log_file}")
self.logger.info(f"日志目录: {os.path.abspath(self.log_dir)}")
def change_log_level(self, event=None):
"""动态更改日志级别"""
try:
# 获取选择的日志级别
level_str = self.log_level_var.get()
log_level = getattr(logging, level_str.upper())
# 更新日志记录器级别
self.logger.setLevel(log_level)
# 更新所有处理器的级别
for handler in self.logger.handlers:
handler.setLevel(log_level)
self.logger.info(f"日志级别已成功更改为: {level_str}")
self.status_var.set(f"日志级别: {level_str}")
except AttributeError:
self.logger.error(f"无效的日志级别: {level_str}")
messagebox.showerror("错误", f"无效的日志级别: {level_str}")
except Exception as e:
self.logger.exception("更改日志级别时发生错误")
messagebox.showerror("错误", f"更改日志级别失败: {str(e)}")
def browse_input_file(self):
"""浏览输入文件"""
file_path = filedialog.askopenfilename(
filetypes=[("Excel 文件", "*.xlsx *.xls"), ("所有文件", "*.*")]
)
if file_path:
self.input_path_var.set(file_path)
self.logger.info(f"已选择输入文件: {file_path}")
def browse_scl_folder(self):
"""浏览SCL文件夹"""
folder_path = filedialog.askdirectory(title="选择SCL文件夹")
if folder_path:
self.scl_folder_var.set(folder_path)
self.logger.info(f"已选择SCL文件夹: {folder_path}")
def highlight_cell(self, sheet, row, col, color="FFFF0000"):
"""为单元格设置背景色"""
try:
fill = PatternFill(start_color=color, end_color=color, fill_type="solid")
sheet.cell(row=row, column=col).fill = fill
return True
except Exception as e:
self.logger.error(f"设置单元格颜色失败: {str(e)}")
return False
def log_key_column_stats(self, column_stats, column_letter_map):
"""记录关键列统计结果到日志"""
self.logger.info("=" * 50)
self.logger.info("关键列统计结果:")
self.logger.info("=" * 50)
for col, stats in column_stats.items():
if not stats:
continue
# 获取列字母表示
col_letter = column_letter_map.get(col, f"列{col}")
# 格式化统计结果
sorted_items = sorted(stats.items(), key=lambda x: int(x[0]) if x[0].isdigit() else x[0])
stats_lines = [f"{value},{count}" for value, count in sorted_items]
stats_str = "\n".join(stats_lines)
# 记录到日志
self.logger.info(f"列 {col_letter} ({col}) 统计:")
self.logger.info(stats_str)
# 更新结果文本框
self.result_text.config(state=tk.NORMAL)
self.result_text.insert(
tk.END,
f"\n列 {col_letter} ({col}) 统计:\n{stats_str}\n"
)
self.result_text.see(tk.END)
self.result_text.config(state=tk.DISABLED)
def create_column_letter_map(self):
"""创建列号到列字母的映射"""
column_letter_map = {}
# 生成A-Z列
for i in range(1, 27):
column_letter_map[i] = chr(64 + i)
# 生成AA-AZ列
for i in range(1, 27):
column_letter_map[26 + i] = f"A{chr(64 + i)}"
# 生成BA-BZ列
for i in range(1, 27):
column_letter_map[52 + i] = f"B{chr(64 + i)}"
# 生成CA-CZ列
for i in range(1, 27):
column_letter_map[78 + i] = f"C{chr(64 + i)}"
# 添加已知的特殊列
column_letter_map.update({
16: "P", 23: "W", 27: "AA", 30: "AD", 34: "AH", 37: "AK",
42: "AP", 45: "AS", 50: "AX", 53: "BA", 57: "BE", 60: "BH",
62: "BL", 65: "BO", 71: "BS", 74: "BV", 78: "BZ", 85: "CG"
})
return column_letter_map
def process_file(self):
"""处理文件 - 根据操作模式执行不同处理流程"""
operation_mode = self.operation_mode.get()
# 重置结果
self.result_text.config(state=tk.NORMAL)
self.result_text.delete(1.0, tk.END)
self.result_text.insert(tk.END, "开始处理...\n")
self.result_text.see(tk.END)
self.result_text.config(state=tk.DISABLED)
self.status_var.set("开始处理文件...")
self.root.update()
try:
# 每次处理前重新初始化日志系统
self.setup_logger()
# 记录处理开始信息
self.logger.info("=" * 50)
self.logger.info(f"开始处理: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
self.logger.info(f"操作模式: {operation_mode}")
self.logger.info("=" * 50)
# 更新UI显示当前日志文件
self.status_var.set(f"当前日志: {os.path.basename(self.current_log_file)}")
# 获取公共配置参数
scl_folder = self.scl_folder_var.get()
prefix = self.prefix_var.get()
# 根据操作模式执行不同处理流程
if operation_mode == "stats":
self.process_stats_mode(scl_folder, prefix)
elif operation_mode == "empty_check":
self.process_empty_check_mode(scl_folder, prefix)
else:
messagebox.showerror("错误", f"未知操作模式: {operation_mode}")
self.logger.error(f"未知操作模式: {operation_mode}")
except Exception as e:
error_msg = f"处理文件时出错: {str(e)}"
logger.exception(f"处理文件时出错: {str(e)}")
messagebox.showerror("错误", error_msg)
self.status_var.set(f"错误: {str(e)}")
# 更新结果文本框
self.result_text.config(state=tk.NORMAL)
self.result_text.insert(tk.END, f"\n错误: {error_msg}\n")
self.result_text.see(tk.END)
self.result_text.config(state=tk.DISABLED)
def process_stats_mode(self, scl_folder, prefix):
"""处理统计模式:扫描统计表E列获取SCL文件名"""
# 检查输入文件是否存在
input_file = self.input_path_var.get()
if not input_file:
messagebox.showwarning("警告", "请先选择统计表")
self.logger.warning("未选择统计表")
return
if not os.path.exists(input_file):
messagebox.showerror("错误", f"统计表文件不存在: {input_file}")
self.logger.error(f"统计表文件不存在: {input_file}")
return
# 使用openpyxl加载工作簿(保留格式)
wb = openpyxl.load_workbook(input_file)
sheet = wb.active
self.logger.info(f"工作簿加载成功, 工作表: {sheet.title}")
# 扫描E列(第5列)
total_rows = sheet.max_row
processed_count = 0
found_files = 0
problem_files = 0
self.logger.info(f"开始扫描E列, 总行数: {total_rows}")
start_time = time.time()
# 初始化关键列统计字典
key_columns = [16, 23, 27, 30, 34, 37, 42, 45, 50, 53, 57, 60, 62, 65, 71, 74, 78, 85]
column_stats = {col: defaultdict(int) for col in key_columns}
# 创建列号到列字母的映射(用于日志)
column_letter_map = self.create_column_letter_map()
for row_idx in range(1, total_rows + 1):
# 更新进度
progress = (row_idx / total_rows) * 100
self.progress_var.set(progress)
self.progress_label.config(text=f"{progress:.1f}%")
self.status_var.set(f"处理行: {row_idx}/{total_rows}")
self.root.update()
cell = sheet.cell(row=row_idx, column=5)
cell_value = str(cell.value) if cell.value else ""
# 检查是否包含前缀的文件名
if prefix in cell_value:
# 改进的正则表达式:匹配包含空格的文件名
# 匹配模式:以prefix开头,后跟任意字符(包括空格),直到文件扩展名
pattern = re.compile(
fr'{prefix}[^\s]*\s*[^\s]*\.(?:xlsx|xls|xlsm)',
re.IGNORECASE
)
file_names = pattern.findall(cell_value)
self.logger.info(f"行 {row_idx}: 找到文件: {', '.join(file_names)}")
result_lines = []
file_has_problems = False # 标记当前行是否有问题文件
for file_name in file_names:
# 在SCL文件夹及其子目录中查找文件(保留文件名中的空格)
file_path = self.find_single_scl_file(scl_folder, file_name)
# 检查文件是否存在
if not file_path or not os.path.exists(file_path):
result_lines.append(f"{file_name}: 文件不存在")
self.logger.warning(f"文件不存在: {file_name}")
# 标记文件不存在的单元格为紫色
self.highlight_cell(sheet, row_idx, 5, "FF800080")
file_has_problems = True
problem_files += 1
continue
# 执行统计处理
results, color_report, missing_data = self.stats_processor.process_file(file_path)
# 如果有数据缺失
if missing_data:
file_has_problems = True
problem_files += 1
result_lines.append(f"{file_name}: 数据缺失!")
for item in missing_data:
result_lines.append(f" - {item['message']}")
self.logger.warning(item['message'])
else:
result_lines.append(f"{file_name}: 处理完成")
# 将结果写入主Excel文件的不同列
for rule_name, result_str in results.items():
target_col = self.stats_processor.RULE_MAPPING.get(rule_name)
if target_col:
target_cell = sheet.cell(row=row_idx, column=target_col)
target_cell.value = result_str
found_files += 1
# 在文件处理完成后,收集关键列数据
for col in key_columns:
cell = sheet.cell(row=row_idx, column=col)
ell_value = str(cell.value) if cell.value else ""
# 跳过空值和错误信息
if not cell_value or cell_value.startswith("错误"):
continue
# 解析单元格值(格式为"值,计数")
for line in cell_value.split('\n'):
parts = line.split(',')
if len(parts) >= 2:
try:
value = parts[0].strip()
count = int(parts[1].strip())
column_stats[col][value] += count
except ValueError:
# 记录解析错误
self.logger.warning(
f"行 {row_idx} 列 {column_letter_map.get(col, f'列{col}')} "
f"无法解析值: {line}"
)
# 如果该行有文件存在问题,将E列单元格标红
if file_has_problems:
self.highlight_cell(sheet, row_idx, 5)
self.logger.info(f"行 {row_idx} E列单元格标记为红色(存在问题)")
# 更新结果文本框
self.result_text.config(state=tk.NORMAL)
self.result_text.insert(
tk.END, f"行 {row_idx} 处理结果:\n" + "\n".join(result_lines) + "\n\n"
)
self.result_text.see(tk.END)
self.result_text.config(state=tk.DISABLED)
processed_count += 1
# 保存修改后的Excel文件
output_path = input_file.replace(".xlsx", "_processed.xlsx")
wb.save(output_path)
self.logger.info(f"结果已保存到: {output_path}")
# 记录关键列统计结果到日志
self.log_key_column_stats(column_stats, column_letter_map)
elapsed_time = time.time() - start_time
status_msg = f"统计处理完成! 处理了 {processed_count} 个文件项, 耗时 {elapsed_time:.2f} 秒"
if problem_files > 0:
status_msg += f", {problem_files} 个文件存在问题"
self.status_var.set(status_msg)
self.logger.info(status_msg)
# 更新结果文本框
self.result_text.config(state=tk.NORMAL)
self.result_text.insert(
tk.END, f"\n{status_msg}\n"
f"结果已保存到: {output_path}\n"
)
self.result_text.see(tk.END)
self.result_text.config(state=tk.DISABLED)
messagebox.showinfo("完成", status_msg)
def process_empty_check_mode(self, scl_folder, prefix):
"""处理SCL格式检查模式:直接扫描文件夹中的所有SCL文件"""
# 检查CheckSheet路径
checksheet_path = self.checksheet_path_var.get()
if not checksheet_path:
messagebox.showwarning("警告", "请先选择CheckSheet路径")
self.logger.warning("未选择CheckSheet路径")
return
if not os.path.exists(checksheet_path):
messagebox.showerror("错误", f"CheckSheet文件不存在: {checksheet_path}")
self.logger.error(f"CheckSheet文件不存在: {checksheet_path}")
return
# 创建输出目录结构
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
output_dir = os.path.join(scl_folder, f"SCL_Check_Results_{timestamp}")
marked_files_dir = os.path.join(output_dir, "MarkedFiles")
reports_dir = os.path.join(output_dir, "Reports")
# 确保目录存在
os.makedirs(marked_files_dir, exist_ok=True)
os.makedirs(reports_dir, exist_ok=True)
self.logger.info(f"创建输出目录: {output_dir}")
self.logger.info(f"标记文件保存目录: {marked_files_dir}")
self.logger.info(f"报告保存目录: {reports_dir}")
# 查找所有SCL文件
scl_files = self.find_scl_files(scl_folder, prefix)
if not scl_files:
messagebox.showinfo("信息", f"在 {scl_folder} 中未找到以 {prefix} 开头的SCL文件")
self.logger.info(f"未找到以 {prefix} 开头的SCL文件")
return
# 开始处理
total_files = len(scl_files)
processed_count = 0
problem_files = 0
self.logger.info(f"找到 {total_files} 个SCL文件, 开始处理...")
start_time = time.time()
# 创建结果报告文件
report_filename = f"SCL_Format_Check_Report_{timestamp}.txt"
report_path = os.path.join(reports_dir, report_filename)
report_content = [
f"SCL格式检查报告 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
f"检查目录: {scl_folder}",
f"文件前缀: {prefix}",
f"文件总数: {total_files}",
"=" * 60,
""
]
# 处理每个SCL文件
for idx, scl_file in enumerate(scl_files):
# 更新进度
progress = ((idx + 1) / total_files) * 100
self.progress_var.set(progress)
self.progress_label.config(text=f"{progress:.1f}%")
self.status_var.set(f"正在处理: {os.path.basename(scl_file)} ({idx+1}/{total_files})")
self.root.update()
# 执行空单元格检查
missing_data, marked_file_path = self.empty_cell_detector.detect_empty_cells(
scl_file, checksheet_path
)
# 记录结果
result_lines = []
result_lines.append(f"文件: {os.path.basename(scl_file)}")
if missing_data:
problem_files += 1
result_lines.append("状态: 发现问题!")
for item in missing_data:
result_lines.append(f" - {item['message']}")
self.logger.warning(item['message'])
# 添加标记文件路径信息
if marked_file_path:
# 将标记文件移动到专门目录
target_path = os.path.join(
marked_files_dir,
os.path.basename(marked_file_path)
)
# 确保目标目录存在
os.makedirs(os.path.dirname(target_path), exist_ok=True)
# 移动文件
try:
shutil.move(marked_file_path, target_path)
marked_file_path = target_path
result_lines.append(f"标记文件已保存到: {os.path.relpath(target_path, scl_folder)}")
self.logger.info(f"标记文件已移动到: {target_path}")
except Exception as e:
error_msg = f"移动标记文件失败: {str(e)}"
result_lines.append(error_msg)
self.logger.error(error_msg)
else:
result_lines.append("状态: 无问题")
# 添加到报告
report_content.extend(result_lines)
report_content.append("") # 空行分隔
# 更新结果文本框
self.result_text.config(state=tk.NORMAL)
self.result_text.insert(tk.END, "\n".join(result_lines) + "\n\n")
self.result_text.see(tk.END)
self.result_text.config(state=tk.DISABLED)
processed_count += 1
# 添加总结信息到报告
report_content.extend([
"=" * 60,
f"处理总结:",
f"总文件数: {total_files}",
f"已处理文件数: {processed_count}",
f"问题文件数: {problem_files}",
f"处理耗时: {time.time() - start_time:.2f} 秒",
f"输出目录: {output_dir}",
f"标记文件目录: {marked_files_dir}",
f"报告文件: {report_path}"
])
# 保存报告文件
with open(report_path, 'w', encoding='utf-8') as f:
f.write("\n".join(report_content))
elapsed_time = time.time() - start_time
status_msg = f"SCL格式检查完成! 处理了 {processed_count} 个文件, 耗时 {elapsed_time:.2f} 秒"
if problem_files > 0:
status_msg += f", {problem_files} 个文件发现问题"
status_msg += f"\n所有输出文件已保存到: {output_dir}"
self.status_var.set(status_msg)
self.logger.info(status_msg)
# 更新结果文本框
self.result_text.config(state=tk.NORMAL)
self.result_text.insert(
tk.END, f"\n{status_msg}\n"
f"检查报告已保存到: {report_path}\n"
f"标记文件已保存到: {marked_files_dir}\n"
)
self.result_text.see(tk.END)
self.result_text.config(state=tk.DISABLED)
messagebox.showinfo("完成", f"{status_msg}\n输出目录: {output_dir}")
def find_single_scl_file(self, folder, filename):
"""
在指定目录及其子目录中查找单个文件(支持文件名空格处理)
:param folder: 要搜索的根目录
:param filename: 要查找的文件名(可能包含空格)
:return: 找到的文件路径,如果未找到则返回None
"""
self.logger.info(f"开始查找文件: {filename} 在目录: {folder}")
# 遍历目录及其所有子目录
for root, dirs, files in os.walk(folder):
for file in files:
# 检查文件名是否完全匹配(包括空格)
if file == filename:
file_path = os.path.join(root, file)
self.logger.info(f"找到文件: {file_path}")
return file_path
self.logger.warning(f"未找到文件: {filename}")
return None
def find_scl_files(self, folder, prefix):
"""
查找指定文件夹中以指定前缀开头的所有SCL文件
返回文件路径列表
"""
scl_files = []
self.logger.info(f"在 {folder} 中查找以 {prefix} 开头的文件")
# 支持的Excel文件扩展名
excel_extensions = ('.xlsx', '.xls', '.xlsm')
# 遍历文件夹
for root, dirs, files in os.walk(folder):
self.logger.debug(f"正在检查目录: {root}")
for file in files:
# 去掉文件名前后可能的空格
normalized_name = file.strip()
# 检查文件扩展名是否匹配
if not normalized_name.lower().endswith(excel_extensions):
continue
# 检查文件是否以目标前缀开头
if normalized_name.startswith(prefix):
full_path = os.path.join(root, file) # 生成完整路径
scl_files.append(full_path)
self.logger.info(f"找到目标文件: {full_path}")
# 按文件名排序
scl_files.sort()
self.logger.info(f"找到 {len(scl_files)} 个SCL文件")
return scl_files
def view_log(self):
"""查看日志"""
log_window = tk.Toplevel(self.root)
log_window.title("处理日志")
log_window.geometry("800x600")
log_frame = ttk.Frame(log_window, padding="10")
log_frame.pack(fill=tk.BOTH, expand=True)
# 日志文本框
log_text = scrolledtext.ScrolledText(
log_frame, wrap=tk.WORD, height=30
)
log_text.pack(fill=tk.BOTH, expand=True)
# 读取日志文件
if self.current_log_file and os.path.exists(self.current_log_file):
try:
with open(self.current_log_file, 'r', encoding='utf-8') as f:
log_content = f.read()
log_text.insert(tk.END, log_content)
except Exception as e:
log_text.insert(tk.END, f"无法读取日志文件: {str(e)}")
else:
log_text.insert(tk.END, "日志文件不存在或尚未创建")
# 设置为只读
log_text.config(state=tk.DISABLED)
# 添加刷新按钮
refresh_btn = ttk.Button(log_frame, text="刷新日志", command=lambda: self.refresh_log(log_text))
refresh_btn.pack(pady=5)
self.logger.info("日志查看窗口已打开")
def refresh_log(self, log_text):
"""刷新日志内容"""
log_text.config(state=tk.NORMAL)
log_text.delete(1.0, tk.END)
if self.current_log_file and os.path.exists(self.current_log_file):
try:
with open(self.current_log_file, 'r', encoding='utf-8') as f:
log_content = f.read()
log_text.insert(tk.END, log_content)
except Exception as e:
log_text.insert(tk.END, f"刷新日志失败: {str(e)}")
else:
log_text.insert(tk.END, "日志文件不存在或尚未创建")
log_text.config(state=tk.DISABLED)
log_text.see(tk.END)
self.logger.info("日志已刷新")
def export_config(self):
"""导出配置到文件"""
config = {
"prefix": self.prefix_var.get(),
"log_level": self.log_level_var.get(),
"operation_mode": self.operation_mode.get(),
"checksheet_path": self.checksheet_path_var.get(),
"scl_folder": self.scl_folder_var.get(),
"input_path": self.input_path_var.get()
}
file_path = filedialog.asksaveasfilename(
defaultextension=".json",
filetypes=[("JSON 文件", "*.json"), ("所有文件", "*.*")]
)
if file_path:
try:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(str(config))
messagebox.showinfo("成功", f"配置已导出到: {file_path}")
self.logger.info(f"配置已导出到: {file_path}")
except Exception as e:
messagebox.showerror("错误", f"导出配置失败: {str(e)}")
self.logger.error(f"导出配置失败: {str(e)}")
def load_config(self):
"""从文件加载配置"""
file_path = filedialog.askopenfilename(
filetypes=[("JSON 文件", "*.json"), ("所有文件", "*.*")]
)
if file_path:
try:
with open(file_path, 'r', encoding='utf-8') as f:
config = eval(f.read())
self.prefix_var.set(config.get("prefix", "SCL_"))
self.log_level_var.set(config.get("log_level", "INFO"))
self.operation_mode.set(config.get("operation_mode", "stats"))
self.checksheet_path_var.set(config.get("checksheet_path", ""))
self.scl_folder_var.set(config.get("scl_folder", ""))
self.input_path_var.set(config.get("input_path", ""))
# 更新UI显示
self.toggle_config_fields()
self.change_log_level()
messagebox.showinfo("成功", "配置已加载")
self.logger.info(f"配置已从 {file_path} 加载")
except Exception as e:
messagebox.showerror("错误", f"加载配置失败: {str(e)}")
self.logger.error(f"加载配置失败: {str(e)}")
日志里面没有统计表关键列数据
最新发布