import tkinter as tk
from tkinter import filedialog, messagebox, ttk, scrolledtext
import csv
from datetime import datetime
import logging
import os
from collections import defaultdict
class CSVProcessorApp:
def __init__(self, root):
self.root = root
self.root.title("CSV_ProcessPro")
self.root.geometry("800x600")
self.root.resizable(False, False)
# 初始化变量
self.file_path = tk.StringVar()
self.csv_data = []
self.headers = []
self.raw_data = [] # 存储原始数据
self.header_row_index = tk.IntVar(value=0) # 表头行索引
self.setup_variables()
self.setup_logging()
self.create_widgets()
self.setup_styles()
def setup_styles(self):
"""设置全局样式"""
self.style = ttk.Style()
self.style.configure("TFrame", background="#f0f0f0")
self.style.configure("TLabel", background="#f0f0f0", font=('Arial', 9))
self.style.configure("TButton", font=('Arial', 9, 'bold'))
self.style.configure("Accent.TButton", foreground="black", font=('Arial', 9, 'bold'),
borderwidth=2, relief="raised")
self.style.map("Accent.TButton",
background=[("active", "#4a90e2"), ("!active", "#d4e6ff")],
bordercolor=[("active", "#4a90e2"), ("!active", "#ffcc00")])
self.style.configure("Remove.TButton", foreground="black", font=('Arial', 8),
background="#ffcccc", borderwidth=1, relief="solid")
self.style.map("Remove.TButton",
background=[("active", "#ff9999"), ("!active", "#ffcccc")])
self.style.configure("Header.TCombobox", font=('Arial', 9))
def setup_variables(self):
"""初始化所有动态变量"""
# 排序相关
self.sort_header = tk.StringVar()
self.sort_order = tk.StringVar(value="升序")
# 去重相关
self.dedupe_header = tk.StringVar()
# 删除行相关
self.delete_keyword = tk.StringVar()
self.delete_column = tk.StringVar()
self.delete_case_sensitive = tk.BooleanVar()
# 合并文件相关
self.merge_file_paths = []
self.merge_column = tk.StringVar()
# 状态变量
self.enable_sort = tk.BooleanVar()
self.enable_dedupe = tk.BooleanVar()
self.enable_custom_letter_sort = tk.BooleanVar()
self.letter_range_start = tk.StringVar(value="A")
self.letter_range_end = tk.StringVar(value="Z")
# 组合处理相关
self.enable_delete = tk.BooleanVar(value=True)
self.enable_combined_sort = tk.BooleanVar(value=True)
self.enable_combined_dedupe = tk.BooleanVar(value=True)
def setup_logging(self):
"""配置日志记录"""
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('csv_processor.log', encoding='utf-8'),
logging.StreamHandler()
]
)
self.logger = logging.getLogger(__name__)
self.logger.info("===== 程序启动 =====")
def create_widgets(self):
"""创建所有界面组件"""
# 主容器
main_container = ttk.Frame(self.root, padding=5)
main_container.pack(fill=tk.BOTH, expand=True)
# 使用notebook分页组织功能
self.notebook = ttk.Notebook(main_container)
self.notebook.pack(fill=tk.BOTH, expand=True)
# 创建各个标签页
self.create_file_tab()
self.create_process_tab()
self.create_delete_tab()
self.create_merge_tab()
self.create_combined_tab() # 新增组合处理标签页
self.create_log_tab()
def create_file_tab(self):
"""创建文件操作标签页"""
tab = ttk.Frame(self.notebook)
self.notebook.add(tab, text="文件操作")
# 文件选择部分
frame = ttk.LabelFrame(tab, text="CSV文件选择", padding=10)
frame.pack(fill=tk.X, padx=5, pady=5)
ttk.Label(frame, text="文件路径:").grid(row=0, column=0, sticky=tk.W)
ttk.Entry(frame, textvariable=self.file_path, width=40).grid(row=0, column=1, sticky=tk.EW)
ttk.Button(frame, text="浏览", command=self.select_file).grid(row=0, column=2, padx=5)
# 表头行选择
ttk.Label(frame, text="表头选择:").grid(row=1, column=0, sticky=tk.W)
self.header_row_combobox = ttk.Combobox(
frame, textvariable=self.header_row_index, state="readonly", width=5,
style="Header.TCombobox"
)
self.header_row_combobox.grid(row=1, column=1, sticky=tk.W)
ttk.Label(frame, text="(0表示第一行)").grid(row=1, column=2, sticky=tk.W)
# 重新解析按钮
ttk.Button(frame, text="重新解析", command=self.reparse_data,
style="Accent.TButton").grid(row=1, column=3, padx=5)
# 文件信息显示
self.file_info = scrolledtext.ScrolledText(tab, height=8, width=80)
self.file_info.pack(fill=tk.X, padx=5, pady=5)
def create_process_tab(self):
"""创建数据处理标签页"""
tab = ttk.Frame(self.notebook)
self.notebook.add(tab, text="排序/去重")
# 排序选项部分
frame = ttk.LabelFrame(tab, text="排序选项", padding=10)
frame.pack(fill=tk.X, padx=5, pady=5)
ttk.Checkbutton(frame, text="启用排序", variable=self.enable_sort,
command=self.toggle_sort).grid(row=0, column=0, sticky=tk.W)
ttk.Label(frame, text="排序表头:").grid(row=1, column=0, sticky=tk.W)
self.sort_header_combobox = ttk.Combobox(frame, textvariable=self.sort_header, state="readonly")
self.sort_header_combobox.grid(row=1, column=1, sticky=tk.EW)
ttk.Label(frame, text="排序方式:").grid(row=2, column=0, sticky=tk.W)
self.sort_order_combobox = ttk.Combobox(
frame, textvariable=self.sort_order,
values=["升序", "降序", "自定义字母排序"]
)
self.sort_order_combobox.grid(row=2, column=1, sticky=tk.W)
# 自定义字母排序范围
ttk.Checkbutton(frame, text="启用字母范围过滤", variable=self.enable_custom_letter_sort,
command=self.toggle_letter_sort).grid(row=3, column=0, sticky=tk.W)
ttk.Label(frame, text="字母范围:").grid(row=4, column=0, sticky=tk.W)
self.letter_range_start_entry = ttk.Entry(frame, textvariable=self.letter_range_start, width=5)
self.letter_range_start_entry.grid(row=4, column=1, sticky=tk.W)
ttk.Label(frame, text="到").grid(row=4, column=2)
self.letter_range_end_entry = ttk.Entry(frame, textvariable=self.letter_range_end, width=5)
self.letter_range_end_entry.grid(row=4, column=3, sticky=tk.W)
# 去重选项部分
frame = ttk.LabelFrame(tab, text="去重选项", padding=10)
frame.pack(fill=tk.X, padx=5, pady=5)
ttk.Checkbutton(frame, text="启用去重", variable=self.enable_dedupe,
command=self.toggle_dedupe).grid(row=0, column=0, sticky=tk.W)
ttk.Label(frame, text="去重表头:").grid(row=1, column=0, sticky=tk.W)
self.dedupe_header_combobox = ttk.Combobox(frame, textvariable=self.dedupe_header, state="readonly")
self.dedupe_header_combobox.grid(row=1, column=1, sticky=tk.EW)
# 处理按钮
btn_frame = ttk.Frame(tab)
btn_frame.pack(pady=10)
ttk.Button(btn_frame, text="处理并保存到桌面", command=self.process_csv,
style="Accent.TButton").pack()
def create_delete_tab(self):
"""创建删除行标签页"""
tab = ttk.Frame(self.notebook)
self.notebook.add(tab, text="删除行")
frame = ttk.LabelFrame(tab, text="删除包含指定字符的行", padding=10)
frame.pack(fill=tk.X, padx=5, pady=5)
# 删除条件设置
ttk.Label(frame, text="搜索列:").grid(row=0, column=0, sticky=tk.W)
self.delete_column_combobox = ttk.Combobox(frame, textvariable=self.delete_column, state="readonly")
self.delete_column_combobox.grid(row=0, column=1, sticky=tk.EW)
ttk.Label(frame, text="关键字:").grid(row=1, column=0, sticky=tk.W)
ttk.Entry(frame, textvariable=self.delete_keyword).grid(row=1, column=1, sticky=tk.EW)
ttk.Checkbutton(frame, text="区分大小写", variable=self.delete_case_sensitive).grid(row=2, column=0, sticky=tk.W)
# 执行按钮
btn_frame = ttk.Frame(tab)
btn_frame.pack(pady=10)
ttk.Button(btn_frame, text="执行删除并保存到桌面", command=self.delete_rows_with_keyword,
style="Accent.TButton").pack()
def create_merge_tab(self):
"""创建文件合并标签页"""
tab = ttk.Frame(self.notebook)
self.notebook.add(tab, text="文件合并")
# 合并文件部分
frame = ttk.LabelFrame(tab, text="合并CSV文件", padding=10)
frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
# 文件列表容器
list_frame = ttk.Frame(frame)
list_frame.pack(fill=tk.BOTH, expand=True)
ttk.Label(list_frame, text="已选择文件:").grid(row=0, column=0, sticky=tk.W)
# 文件列表和滚动条
self.merge_file_canvas = tk.Canvas(list_frame, height=150)
self.merge_file_canvas.grid(row=1, column=0, sticky=tk.EW)
scrollbar = ttk.Scrollbar(list_frame, orient="vertical", command=self.merge_file_canvas.yview)
scrollbar.grid(row=1, column=1, sticky=tk.NS)
self.merge_file_canvas.configure(yscrollcommand=scrollbar.set)
self.merge_file_frame = ttk.Frame(self.merge_file_canvas)
self.merge_file_canvas.create_window((0, 0), window=self.merge_file_frame, anchor="nw")
# 按钮区域
btn_frame = ttk.Frame(frame)
btn_frame.pack(fill=tk.X, pady=5)
ttk.Button(btn_frame, text="添加文件", command=self.add_merge_file).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="清空列表", command=self.clear_merge_list).pack(side=tk.LEFT, padx=5)
# 合并选项
opt_frame = ttk.Frame(frame)
opt_frame.pack(fill=tk.X, pady=5)
ttk.Label(opt_frame, text="合并依据列(可选):").grid(row=0, column=0, sticky=tk.W)
self.merge_column_combo = ttk.Combobox(opt_frame, textvariable=self.merge_column, state="readonly")
self.merge_column_combo.grid(row=0, column=1, sticky=tk.EW)
# 合并按钮
btn_frame = ttk.Frame(tab)
btn_frame.pack(pady=10)
ttk.Button(btn_frame, text="执行合并并保存到桌面", command=self.merge_csv_files,
style="Accent.TButton").pack()
def create_combined_tab(self):
"""创建组合处理标签页"""
tab = ttk.Frame(self.notebook)
self.notebook.add(tab, text="组合处理")
# 组合处理选项
frame = ttk.LabelFrame(tab, text="组合处理选项", padding=10)
frame.pack(fill=tk.X, padx=5, pady=5)
# 删除行选项
delete_frame = ttk.Frame(frame)
delete_frame.pack(fill=tk.X, pady=5)
ttk.Checkbutton(delete_frame, text="启用删除行", variable=self.enable_delete).pack(side=tk.LEFT, padx=5)
ttk.Label(delete_frame, text="搜索列:").pack(side=tk.LEFT, padx=5)
self.combined_delete_column_combobox = ttk.Combobox(
delete_frame, textvariable=self.delete_column, state="readonly", width=15
)
self.combined_delete_column_combobox.pack(side=tk.LEFT, padx=5)
ttk.Label(delete_frame, text="关键字:").pack(side=tk.LEFT, padx=5)
ttk.Entry(delete_frame, textvariable=self.delete_keyword, width=15).pack(side=tk.LEFT, padx=5)
ttk.Checkbutton(delete_frame, text="区分大小写", variable=self.delete_case_sensitive).pack(side=tk.LEFT, padx=5)
# 排序选项
sort_frame = ttk.Frame(frame)
sort_frame.pack(fill=tk.X, pady=5)
ttk.Checkbutton(sort_frame, text="启用排序", variable=self.enable_combined_sort).pack(side=tk.LEFT, padx=5)
ttk.Label(sort_frame, text="排序表头:").pack(side=tk.LEFT, padx=5)
self.combined_sort_header_combobox = ttk.Combobox(
sort_frame, textvariable=self.sort_header, state="readonly", width=15
)
self.combined_sort_header_combobox.pack(side=tk.LEFT, padx=5)
ttk.Label(sort_frame, text="排序方式:").pack(side=tk.LEFT, padx=5)
self.combined_sort_order_combobox = ttk.Combobox(
sort_frame, textvariable=self.sort_order,
values=["升序", "降序", "自定义字母排序"], width=15
)
self.combined_sort_order_combobox.pack(side=tk.LEFT, padx=5)
# 自定义字母排序范围(新增)
ttk.Checkbutton(
sort_frame, text="启用字母范围", variable=self.enable_custom_letter_sort
).pack(side=tk.LEFT, padx=5)
ttk.Label(sort_frame, text="从").pack(side=tk.LEFT, padx=5)
ttk.Entry(sort_frame, textvariable=self.letter_range_start, width=3).pack(side=tk.LEFT)
ttk.Label(sort_frame, text="到").pack(side=tk.LEFT, padx=5)
ttk.Entry(sort_frame, textvariable=self.letter_range_end, width=3).pack(side=tk.LEFT)
# 去重选项
dedupe_frame = ttk.Frame(frame)
dedupe_frame.pack(fill=tk.X, pady=5)
ttk.Checkbutton(dedupe_frame, text="启用去重", variable=self.enable_combined_dedupe).pack(side=tk.LEFT, padx=5)
ttk.Label(dedupe_frame, text="去重表头:").pack(side=tk.LEFT, padx=5)
self.combined_dedupe_header_combobox = ttk.Combobox(
dedupe_frame, textvariable=self.dedupe_header, state="readonly", width=15
)
self.combined_dedupe_header_combobox.pack(side=tk.LEFT, padx=5)
# 处理按钮
btn_frame = ttk.Frame(tab)
btn_frame.pack(pady=10)
ttk.Button(btn_frame, text="执行组合处理并保存到桌面", command=self.combined_process,
style="Accent.TButton").pack()
def create_log_tab(self):
"""创建日志标签页"""
tab = ttk.Frame(self.notebook)
self.notebook.add(tab, text="运行日志")
self.log_text = scrolledtext.ScrolledText(tab, height=15, width=80)
self.log_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
def log_message(self, message, level="info"):
"""记录日志并显示在GUI中"""
log_methods = {
"info": self.logger.info,
"error": self.logger.error,
"warning": self.logger.warning
}
# 记录到日志文件
log_methods.get(level, self.logger.info)(message)
# 显示在GUI日志标签页
timestamp = datetime.now().strftime("%H:%M:%S")
tagged_msg = f"[{timestamp}] {message}"
self.log_text.insert(tk.END, tagged_msg + "\n")
self.log_text.see(tk.END)
# 同时在文件信息标签页显示重要信息
if level in ["error", "warning"]:
self.file_info.config(state=tk.NORMAL)
self.file_info.insert(tk.END, tagged_msg + "\n")
self.file_info.config(state=tk.DISABLED)
self.file_info.see(tk.END)
def select_file(self):
"""选择CSV文件"""
file_path = filedialog.askopenfilename(
title="选择CSV文件",
filetypes=[("CSV文件", "*.csv"), ("文本文件", "*.txt"), ("所有文件", "*.*")]
)
if file_path:
self.file_path.set(file_path)
self.log_message(f"已选择文件: {file_path}")
self.load_csv(file_path)
def reparse_data(self):
"""重新解析数据(使用新的表头行)"""
if not self.file_path.get():
messagebox.showwarning("警告", "请先选择CSV文件")
return
self.log_message(f"重新解析数据,使用表头行: {self.header_row_index.get()}")
self.parse_data(self.raw_data)
def load_csv(self, file_path):
"""加载CSV文件内容"""
try:
with open(file_path, 'r', encoding='utf-8-sig') as file:
reader = csv.reader(file)
self.raw_data = list(reader) # 保存原始数据
self.parse_data(self.raw_data)
self.log_message(f"文件加载成功,共 {len(self.csv_data)} 行")
except Exception as e:
error_msg = f"读取CSV文件失败: {str(e)}"
self.log_message(error_msg, "error")
messagebox.showerror("错误", error_msg)
def parse_data(self, raw_data):
"""解析原始数据,根据选择的表头行"""
if not raw_data:
return
# 更新表头行选择框
row_options = list(range(len(raw_data)))
self.header_row_combobox['values'] = row_options
# 使用用户选择的表头行
header_index = self.header_row_index.get()
if header_index < 0 or header_index >= len(raw_data):
header_index = 0
self.header_row_index.set(0)
# 设置表头和数据
# 表头行之前的数据保留为数据行
self.headers = raw_data[header_index]
self.csv_data = raw_data[:header_index] + raw_data[header_index+1:]
# 更新UI
self.update_ui_with_headers()
self.show_file_info(self.file_path.get())
def show_file_info(self, file_path):
"""显示文件信息"""
self.file_info.config(state=tk.NORMAL)
self.file_info.delete(1.0, tk.END)
info = [
f"文件路径: {file_path}",
f"总行数: {len(self.csv_data)}",
f"列数: {len(self.headers)}",
f"表头: {', '.join(self.headers)}",
f"表头行: {self.header_row_index.get()}",
"="*40,
"前5行数据预览:"
]
self.file_info.insert(tk.END, "\n".join(info) + "\n")
# 显示前5行数据
for i, row in enumerate(self.csv_data[:5], 1):
self.file_info.insert(tk.END, f"{i}. {', '.join(row)}\n")
self.file_info.config(state=tk.DISABLED)
def update_ui_with_headers(self):
"""根据加载的CSV更新UI元素"""
# 更新所有下拉框
for combo in [
self.sort_header_combobox,
self.dedupe_header_combobox,
self.delete_column_combobox,
self.merge_column_combo,
self.combined_delete_column_combobox,
self.combined_sort_header_combobox,
self.combined_dedupe_header_combobox
]:
combo['values'] = self.headers
# 设置默认值
if self.headers:
self.sort_header.set(self.headers[0])
self.dedupe_header.set(self.headers[0])
self.delete_column.set(self.headers[0])
self.merge_column.set("")
def toggle_sort(self):
"""切换排序功能的启用状态"""
state = "normal" if self.enable_sort.get() else "disabled"
self.sort_header_combobox['state'] = state
self.sort_order_combobox['state'] = state
self.toggle_letter_sort()
self.log_message(f"排序功能 {'启用' if self.enable_sort.get() else '禁用'}")
def toggle_dedupe(self):
"""切换去重功能的启用状态"""
state = "normal" if self.enable_dedupe.get() else "disabled"
self.dedupe_header_combobox['state'] = state
self.log_message(f"去重功能 {'启用' if self.enable_dedupe.get() else '禁用'}")
def toggle_letter_sort(self):
"""控制字母范围输入框的启用状态"""
if not self.enable_sort.get():
return
state = "normal" if self.enable_custom_letter_sort.get() else "disabled"
self.letter_range_start_entry['state'] = state
self.letter_range_end_entry['state'] = state
self.log_message(f"字母范围过滤 {'启用' if self.enable_custom_letter_sort.get() else '禁用'}")
def add_merge_file(self):
"""添加要合并的文件"""
file_paths = filedialog.askopenfilenames(
title="选择要合并的CSV文件",
filetypes=[("CSV文件", "*.csv"), ("文本文件", "*.txt"), ("所有文件", "*.*")]
)
if file_paths:
for path in file_paths:
if path not in self.merge_file_paths:
self.merge_file_paths.append(path)
self.update_merge_file_list()
def clear_merge_list(self):
"""清空合并文件列表"""
if self.merge_file_paths:
self.merge_file_paths = []
self.update_merge_file_list()
self.log_message("已清空合并文件列表")
def update_merge_file_list(self):
"""更新合并文件列表显示"""
# 清除现有内容
for widget in self.merge_file_frame.winfo_children():
widget.destroy()
if not self.merge_file_paths:
ttk.Label(self.merge_file_frame, text="尚未选择任何文件").pack()
self.merge_file_canvas.configure(scrollregion=self.merge_file_canvas.bbox("all"))
return
# 添加文件列表
for i, path in enumerate(self.merge_file_paths):
row_frame = ttk.Frame(self.merge_file_frame)
row_frame.pack(fill=tk.X, pady=2)
ttk.Label(row_frame, text=f"{i+1}. {os.path.basename(path)}", width=40, anchor="w").pack(side=tk.LEFT)
ttk.Button(row_frame, text="移除", command=lambda p=path: self.remove_merge_file(p),
style="Remove.TButton").pack(side=tk.LEFT, padx=2)
# 更新滚动区域
self.merge_file_frame.update_idletasks()
self.merge_file_canvas.configure(scrollregion=self.merge_file_canvas.bbox("all"))
def remove_merge_file(self, file_path):
"""移除指定的合并文件"""
if file_path in self.merge_file_paths:
self.merge_file_paths.remove(file_path)
self.update_merge_file_list()
self.log_message(f"已移除文件: {file_path}")
def delete_rows(self, data, column, keyword, case_sensitive):
"""删除包含关键字的行(通用方法)"""
if not column or not keyword or not data:
return data
try:
col_index = self.headers.index(column)
if not case_sensitive:
keyword = keyword.lower()
new_data = [data[0]] # 保留表头
deleted_count = 0
for row in data[1:]:
if len(row) > col_index:
value = row[col_index]
compare_value = value if case_sensitive else value.lower()
if keyword not in compare_value:
new_data.append(row)
else:
deleted_count += 1
self.log_message(f"删除行: 移除了 {deleted_count} 行包含 '{keyword}' 的数据")
return new_data
except Exception as e:
error_msg = f"删除行时出错: {str(e)}"
self.log_message(error_msg, "error")
messagebox.showerror("错误", error_msg)
return data
def sort_data(self, data, header, order, enable_letter_sort, letter_start, letter_end):
"""对数据进行排序(通用方法)"""
if not header or not data:
return data
try:
sort_index = self.headers.index(header)
reverse = (order == "降序")
# 字母范围过滤
if enable_letter_sort:
try:
letter_start = letter_start.upper()
letter_end = letter_end.upper()
if not (len(letter_start) == 1 and len(letter_end) == 1 and
letter_start.isalpha() and letter_end.isalpha()):
raise ValueError("字母范围必须是单个字母(如A-Z)")
filtered_rows = []
for row in data[1:]: # 跳过表头
if len(row) > sort_index:
value = str(row[sort_index]).strip().upper()
if value and letter_start <= value[0] <= letter_end:
filtered_rows.append(row)
data = [data[0]] + filtered_rows
self.log_message(f"字母范围过滤完成:{letter_start} 到 {letter_end}")
except Exception as e:
self.log_message(f"字母范围过滤失败: {str(e)}", "error")
messagebox.showerror("错误", f"字母范围过滤失败: {str(e)}")
return data
# 排序逻辑
def sort_key(row):
if len(row) > sort_index:
value = row[sort_index]
# 尝试解析为日期
for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d", "%m/%d/%Y", "%Y.%m.%d"):
try:
return datetime.strptime(value, fmt)
except ValueError:
continue
# 尝试解析为数字
try:
return float(value)
except ValueError:
pass
return value.lower() # 默认按字符串排序
return ""
# 执行排序
if order == "自定义字母排序":
data[1:] = sorted(
data[1:],
key=lambda x: str(sort_key(x)).lower() if len(x) > sort_index else "",
reverse=False
)
else:
data[1:] = sorted(data[1:], key=sort_key, reverse=reverse)
self.log_message(f"排序完成,表头 '{header}',顺序: {order}")
return data
except Exception as e:
self.log_message(f"排序时出错: {str(e)}", "error")
messagebox.showerror("错误", f"排序时出错: {str(e)}")
return data
def dedupe_data(self, data, header):
"""对数据进行去重(通用方法)"""
if not header or not data:
return data
try:
dedupe_index = self.headers.index(header)
seen = set()
unique_rows = [data[0]] # 保留表头
for row in data[1:]:
if len(row) > dedupe_index:
key = row[dedupe_index]
if key not in seen:
seen.add(key)
unique_rows.append(row)
self.log_message(
f"去重完成,根据表头 '{header}' 删除重复项,"
f"原始行数: {len(data)},去重后行数: {len(unique_rows)}"
)
return unique_rows
except Exception as e:
self.log_message(f"去重时出错: {str(e)}", "error")
messagebox.showerror("错误", f"去重时出错: {str(e)}")
return data
def delete_rows_with_keyword(self):
"""删除包含关键字的行并保存到桌面"""
if not self.file_path.get():
messagebox.showwarning("警告", "请先选择CSV文件")
return
column = self.delete_column.get()
keyword = self.delete_keyword.get()
if not column:
messagebox.showwarning("警告", "请选择要搜索的列")
return
if not keyword:
messagebox.showwarning("警告", "请输入要搜索的关键字")
return
try:
# 执行删除
processed_data = self.delete_rows(
self.csv_data,
column,
keyword,
self.delete_case_sensitive.get()
)
# 生成保存路径
operation = f"deleted_{keyword}"
save_path = self.generate_filename(self.file_path.get(), operation)
# 保存文件
if self.save_csv_file(processed_data, save_path):
# 更新当前数据
self.csv_data = processed_data
messagebox.showinfo("成功", f"结果已保存到桌面:\n{os.path.basename(save_path)}")
except Exception as e:
error_msg = f"删除行时出错: {str(e)}"
self.log_message(error_msg, "error")
messagebox.showerror("错误", error_msg)
def get_desktop_path(self):
"""获取桌面路径"""
try:
desktop = os.path.join(os.path.join(os.environ['USERPROFILE']), 'Desktop')
if os.path.exists(desktop):
return desktop
except KeyError:
pass
# 如果上面的方法失败,尝试其他方法
desktop = os.path.join(os.path.expanduser('~'), 'Desktop')
if os.path.exists(desktop):
return desktop
# 如果还是失败,返回当前目录
return os.getcwd()
def generate_filename(self, original_name, operation):
"""生成新的文件名"""
if not original_name:
original_name = "processed"
base = os.path.basename(original_name)
name, ext = os.path.splitext(base)
# 清理操作名称中的特殊字符
clean_op = "".join(c if c.isalnum() else "_" for c in operation)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
new_name = f"{name}_{clean_op}_{timestamp}{ext}"
return os.path.join(self.get_desktop_path(), new_name)
def save_csv_file(self, data, save_path):
"""保存CSV文件到指定路径"""
try:
with open(save_path, 'w', encoding='utf-8-sig', newline='') as file:
writer = csv.writer(file)
writer.writerows(data)
# 更新文件信息显示
self.show_file_info(save_path)
self.log_message(f"文件已保存到: {save_path}")
return True
except Exception as e:
error_msg = f"保存文件时出错: {str(e)}"
self.log_message(error_msg, "error")
messagebox.showerror("错误", error_msg)
return False
def process_csv(self):
"""处理CSV文件(排序、去重等)并保存到桌面"""
if not self.file_path.get():
messagebox.showwarning("警告", "请先选择CSV文件")
return
if not self.csv_data:
messagebox.showwarning("警告", "CSV文件没有数据")
return
self.log_message("开始处理CSV文件...")
processed_data = self.csv_data.copy()
# 去重处理
if self.enable_dedupe.get():
processed_data = self.dedupe_data(
processed_data,
self.dedupe_header.get()
)
# 排序处理
if self.enable_sort.get():
processed_data = self.sort_data(
processed_data,
self.sort_header.get(),
self.sort_order.get(),
self.enable_custom_letter_sort.get(),
self.letter_range_start.get(),
self.letter_range_end.get()
)
# 生成操作描述
operations = []
if self.enable_sort.get():
operations.append(f"sorted_{self.sort_header.get()}_{self.sort_order.get()}")
if self.enable_dedupe.get():
operations.append(f"deduped_{self.dedupe_header.get()}")
operation = "_".join(operations) if operations else "processed"
# 生成保存路径
save_path = self.generate_filename(self.file_path.get(), operation)
# 保存文件
if self.save_csv_file(processed_data, save_path):
# 更新当前数据
self.csv_data = processed_data
messagebox.showinfo("成功", f"文件处理完成,已保存到桌面:\n{os.path.basename(save_path)}")
def combined_process(self):
"""组合处理:删除行 -> 排序 -> 去重"""
if not self.file_path.get():
messagebox.showwarning("警告", "请先选择CSV文件")
return
if not self.csv_data:
messagebox.showwarning("警告", "CSV文件没有数据")
return
self.log_message("开始组合处理CSV文件...")
processed_data = self.csv_data.copy()
operations = []
# 1. 删除行
if self.enable_delete.get():
column = self.delete_column.get()
keyword = self.delete_keyword.get()
if column and keyword:
processed_data = self.delete_rows(
processed_data,
column,
keyword,
self.delete_case_sensitive.get()
)
operations.append(f"deleted_{keyword}")
# 2. 排序
if self.enable_combined_sort.get():
header = self.sort_header.get()
order = self.sort_order.get()
if header:
processed_data = self.sort_data(
processed_data,
header,
order,
self.enable_custom_letter_sort.get(),
self.letter_range_start.get(),
self.letter_range_end.get()
)
operations.append(f"sorted_{header}_{order}")
# 3. 去重
if self.enable_combined_dedupe.get():
header = self.dedupe_header.get()
if header:
processed_data = self.dedupe_data(
processed_data,
header
)
operations.append(f"deduped_{header}")
# 生成操作描述
operation = "combined_" + "_".join(operations) if operations else "combined_processed"
# 生成保存路径
save_path = self.generate_filename(self.file_path.get(), operation)
# 保存文件
if self.save_csv_file(processed_data, save_path):
# 更新当前数据
self.csv_data = processed_data
messagebox.showinfo("成功", f"组合处理完成,已保存到桌面:\n{os.path.basename(save_path)}")
def merge_csv_files(self):
"""合并多个CSV文件并保存到桌面"""
if not self.merge_file_paths:
messagebox.showwarning("警告", "请先添加要合并的文件")
return
try:
# 检查所有文件是否存在
missing_files = [f for f in self.merge_file_paths if not os.path.exists(f)]
if missing_files:
raise FileNotFoundError(f"以下文件不存在: {', '.join(missing_files)}")
merge_column = self.merge_column.get()
common_headers = None
all_data = []
# 收集所有文件的表头和数据
header_sets = []
for file_path in self.merge_file_paths:
with open(file_path, 'r', encoding='utf-8-sig') as file:
reader = csv.reader(file)
data = list(reader)
if data:
header_sets.append(set(data[0]))
all_data.append(data)
# 找出共同表头
if header_sets:
common_headers = set(header_sets[0])
for headers in header_sets[1:]:
common_headers.intersection_update(headers)
common_headers = sorted(common_headers)
if not common_headers:
raise ValueError("选中的文件没有共同的列,无法合并")
# 如果没有指定合并依据列,使用所有共同列
merge_indices = None
if merge_column:
if merge_column not in common_headers:
raise ValueError(f"合并依据列 '{merge_column}' 不在共同列中")
merge_indices = [i for i, h in enumerate(common_headers) if h == merge_column]
# 合并数据
merged_data = [common_headers.copy()]
key_counter = defaultdict(int)
for data in all_data:
if not data:
continue
headers = data[0]
header_map = {h: i for i, h in enumerate(headers)}
for row in data[1:]:
# 如果指定了合并列,检查是否已存在相同键
if merge_indices:
merge_values = [row[header_map[h]] for h in common_headers if h == merge_column]
if merge_values:
key = tuple(merge_values)
key_counter[key] += 1
if key_counter[key] > 1:
continue # 跳过重复键的行
# 构建新行,只保留共同列
new_row = []
for col in common_headers:
if col in header_map and len(row) > header_map[col]:
new_row.append(row[header_map[col]])
else:
new_row.append("")
merged_data.append(new_row)
# 生成操作描述
operation = "merged"
if merge_column:
operation += f"_by_{merge_column}"
# 生成保存路径
first_file = os.path.basename(self.merge_file_paths[0])
save_path = self.generate_filename(first_file, operation)
# 保存文件
if self.save_csv_file(merged_data, save_path):
messagebox.showinfo("成功", f"文件合并完成,已保存到桌面:\n{os.path.basename(save_path)}")
except Exception as e:
error_msg = f"合并文件时出错: {str(e)}"
self.log_message(error_msg, "error")
messagebox.showerror("错误", error_msg)
if __name__ == "__main__":
root = tk.Tk()
app = CSVProcessorApp(root)
root.mainloop()
优化这个代码,
按钮:
正常状态:浅蓝色背景(#4a90e2) + 浅黄色边框(#ffcc00) + 白色加粗文字
悬停状态:稍深的蓝色背景(#3a80d2)
所有操作按钮(浏览、处理、保存等)都使用此样式
字体:
全局使用11号Arial字体(原始为9号)
按钮文字加粗
日志区域同样增大字体
布局:
增加所有组件之间的间距,使界面更宽松
标签页内边距增加,减少拥挤感
按钮宽度增加,更易点击
颜色方案:
主色调:浅蓝色按钮 + 浅黄色边框
辅助色:白色文字 + 浅灰色背景