win10(四)数据清洗之查重合并对比
python对重复数的处理速度远远快过wps表格,尤其针对大量数据,还能根据相似度进一步匹配。
'''
对于存在重复值的进销存先合计总数和总金额,再和另一个系统的查重合并后对比,用于替换透视表:
使用tkinter制作的python数据清洗之查重程序。功能是xlsx中获取要查询的值,从各个xlsx中分别获取查找项并获得对应的序号,随后查重。字体均为宋体,字号12,窗口尺寸1200*600,有执行按键,对比复选项,停止按键。适应大量数据,至少11万行。使用标准的Tkinter字体配置方法,避免处理过程中出错: self.tk.call(_tkinter.TclError: unknown option "-font"。避免处理过程中出错: '<' not supported between instances of 'int' and 'NoneType'。
1.
文件选择框。应该支持可选一个或者多个文件。
界面中设置查找项输入框,默认为“物料名称”,金额值输入框,默认为“金额”,数量输入框,默认为“数量”。
选择保存路径默认为空,如果不选择就是空值,则在第一个源文件内右侧的空值+1列处写入数据列。如果选择保存位置并指定了保存文件名,则新建该xls文件并写入数据列,并自动调整列宽。
查找项2(可选)输入框,默认为空,如果输入了信息,则各个xls文件都按照查找项的读取方法,将各个信息以文本格式(注意如原来是数字则保持其小数点的位数原样不变)加入查找项中,作为查找项一部分参与数据重复查询并输出保存等处理,意思是如果查找项2有信息,则查找项=查找项+查找项2(文本格式)
查找项3(可选)输入框,默认为空,如果输入了信息,则同上,各个xls文件都按照查找项的读取方法,将各个信息以文本格式按顺序继续加入查找项,作为查找项一部分参与数据重复查询并输出保存等处理,意思是如果查找项2有信息,则查找项=查找项+查找项2(文本格式)+查找项3(文本格式)注意:查找项2、查找项3均为文本格式并入查找项。
数据清洗功能:
删除空格选项,默认选中,所有数据自动删除空格。
删除括号选项,默认选中,所有数据自动判断中文、英文格式括号,删除括号及括号内内容。
删除信息输入框,默认为空,如果输入信息,则所有数据处理时自动删除相应信息数据内容,例如输入“设置”,则所有数据保存时删除“设置”。
2.查找项。
点击执行按钮后,判断如果未选择比对选项,将选中读取的单个xls文件的0至20行150列范围内查找包含查找项的单元,如果发现该文件有数据筛选则弹窗提示。
找到有查找项内容的单元格后,以该单元以下的单元格内容为逐个查找项项目。
设置可选“转文本”选项,默认选中。各个单元格内容作为查找项,如果选中了转文本选项,则内容不管是不是数字都转换为文本格式作为查找项。
在各个xlsx的0至20行150列范围内查找包含金额值内容的单元。找到金额值所在单元格后,以该单元格以下的单元格内容为逐个金额值项目,内容如果是文本格式的数,自动转为数值格式,小数点位数按照实际有的小数,而不是固定小数位数。例如0还是0,不是0.0。
同样的找到数量输入框的值所在单元格后,以该单元格以下的单元格内容为逐个数量值项目,内容如果是文本格式的数,自动转为数值格式,小数点位数按照实际有的小数,而不是固定小数位数。例如0还是0,不是0.0。
接着,对查找项、金额值、数量进行以查找项为准的查重,其他项随动,将该列中查找项的各个重复项分别视为一个个单独小组,获得各个小组内重复的查找项的个数,将个数记为重复数,如果无重复则重复数是1,只保留该小组行号最小的第一个查找项,同时在该小组所有查找项所在行对应的金额值进行合计,记为“合计值”,该值行号与保留的查找项行号相同。
最后,在xlsx表中最近的空值列向右+1列输出每组保留的唯一查找项,注意:表头字体均加粗,内容为源文件名(不含扩展名)+查找项,+2列输出查重合并后的重复数据,表头加粗,内容为重复数:维持其行号与每组保留的查找项行号相同,+3列输出数量合计值,表头为数量,+4列输出金额的"合计值",表头为金额合计。如果选中了多个文件,随后的文件数据,在同一xls文件内的第一个文件输出查重合并后的数据右侧空值右侧进行保存,以此类推。注意:表头字体均加粗。
注意:数据结果如果是0,应检查原因,如果查明原因应给出提示。如果在文件中找不到查找项、金额值、数量之类的数据,则应提示。
注意:对比结果应该同在一个xls文件的同一个工作表,后续文件的数据应在之前文件数据右侧放置,而不应该分别写入不同的工作表,以便同时看到结果。
3.负值减2选项。
默认选中,用于对金额值进行负值判断,如果一个数量或金额组中只要出现负值意味着撤销或退库,应减少重复两倍的数量。例如金额中有1个负值,虽然仍然正常合计金额,但是数量自动减2,2个负值数量就减4,出现重复数时数量底色加浅黄色。注意:不增加其他额外的负值提示。
4.排序选项
从小到大排序选项默认不选中。如果选中则输出查重合并后的数据时增加排序:以各组保留的唯一查找项为准,从小到大排序,排序后的内容自动跳过空行,注意重复数、数量合计值、金额合计值的输出内容同步跟着唯一查找项调整。(其中内容中如果存在数字文字混排,要单独将数字排序,例如1月应在11月之前)。
同样的,从大到小排序选项,默认不选中,逻辑与从小到大雷同,顺序相反。
5.对比选项,颜色红色,与执行按钮同一列且在其右侧。
是增加排序后对比功能的选项,默认不选中。若选中,则在点击执行按钮后首先判断是否选中对比选项,接着判断是否选中了多个文件,如果选中一个文件,则弹窗提示单文件无法比对,
如果选中的多个文件,首先将第一个文件输出查重合并后的数据按排序功能小到大排序,随后按顺序将第2个文件输出查重合并后的数据向右空一行保存随后与第一个文件输出的唯一查找项进行逐个比较,对比分为两个阶段:第1阶段额为基础性比对阶段。查找完全一致的唯一查找项,找到后则将其及相关的第二文件的唯一查找项、重复数、数量合计值、金额合计值均调整到与第一个文件的该值的相同行以此实现匹配,匹配的单元格底色设置为淡蓝色,其他行内容等待下一阶段比对后写入。注意:向右间隔一列写入并维持其表头与单个文件的要求相同。注意:这一步是个简单的排序后匹配,只要获取完全匹配一致的值,而不用判断匹配度。注意第1阶段所获得的匹配的内容有更高的优先级,不可因为第2阶段的匹配而变更。
接下来是第2阶段:对于其他未完全匹配第一个文件值的查找值,进行简单的相似度匹配,例如按照相同字段占该唯一查找项内容的比例来作为相似度高低依据,按相似度从高到底匹配顺序写入,注意:不能出现第二个文件的两个唯一查找项匹配第一个文件的同一个唯一查找值的逻辑错误。对于相似度低的写入的优先级应该较低(无法获得相似度时或者该数据相似度为0的也在参加匹配判断后在本列后写入),随着优先级降低数据的底色从黄到淡黄色改变。注意:再次确认是向右空一列写入。最终完成所有数据匹配写入。注意:如果完全没有匹配或者相似度为0或没有找到匹配项的数据也都要在数据下边按顺序写入,不能另起一列吸入。
注意:不能遗漏数据,这样将调整完毕的数据,再次确认是在第一个文件输出的数据右侧空值+1列保存,数据结构与第一个文件的数据结构相同。按照这个办法,同样将第三个文件输出查重合并后的数据与第一个文件输出查重合并后的数据同样处理,结果在第二个文件输出查重合并后的数据右侧空值空1列保存。以后的多个文件数据以此类推。注意这些数据都是保存在同一xls中,且保存时自动适应数据宽度。
注意:检查所有功能是否实现,不可忽略。
'''
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import pandas as pd
import numpy as np
import openpyxl
from openpyxl import Workbook, load_workbook
from openpyxl.styles import Font, PatternFill, Alignment
from openpyxl.utils import get_column_letter
import os
import re
from datetime import datetime
import threading
import difflib
from collections import OrderedDict
class DataCleaningApp:
def __init__(self, root):
self.root = root
self.root.title("数据清洗查重:查找项能实现多重信息组合检查,如单位、日期")
self.root.geometry("850x600")
# 设置字体
self.font_style = ("宋体", 12)
# 初始化变量
self.file_paths = []
self.stop_process = False
self.delete_space = tk.BooleanVar(value=True)
self.delete_brackets = tk.BooleanVar(value=True)
self.convert_to_text = tk.BooleanVar(value=True)
self.negative_adjust = tk.BooleanVar(value=True)
self.sort_asc = tk.BooleanVar(value=False)
self.sort_desc = tk.BooleanVar(value=False)
self.compare_mode = tk.BooleanVar(value=False)
self.setup_ui()
def setup_ui(self):
# 主框架
main_frame = ttk.Frame(self.root, padding="10")
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
# 文件选择区域
file_frame = ttk.LabelFrame(main_frame, text="文件选择", padding="5")
file_frame.grid(row=0, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5)
self.file_listbox = tk.Listbox(file_frame, width=100, height=5, font=self.font_style)
self.file_listbox.grid(row=0, column=0, columnspan=2, pady=5, padx=5)
ttk.Button(file_frame, text="添加文件", command=self.add_files).grid(row=1, column=0, pady=5, padx=5)
ttk.Button(file_frame, text="移除选中", command=self.remove_files).grid(row=1, column=1, pady=5, padx=5)
# 参数设置区域
param_frame = ttk.LabelFrame(main_frame, text="参数设置(金额合计为零可能是包含文本)", padding="5")
param_frame.grid(row=1, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5)
ttk.Label(param_frame, text="查找项:").grid(row=0, column=0, sticky=tk.W, pady=2, padx=5)
self.search_item = ttk.Entry(param_frame, width=15, font=self.font_style)
self.search_item.grid(row=0, column=1, pady=2, padx=5)
self.search_item.insert(0, "物料名称")
ttk.Label(param_frame, text="金额值:").grid(row=0, column=2, sticky=tk.W, pady=2, padx=5)
self.amount_item = ttk.Entry(param_frame, width=15, font=self.font_style)
self.amount_item.grid(row=0, column=3, pady=2, padx=5)
self.amount_item.insert(0, "金额")
ttk.Label(param_frame, text="数量:").grid(row=0, column=4, sticky=tk.W, pady=2, padx=5)
self.quantity_item = ttk.Entry(param_frame, width=15, font=self.font_style)
self.quantity_item.grid(row=0, column=5, pady=2, padx=5)
self.quantity_item.insert(0, "数量")
ttk.Label(param_frame, text="查找项2(可选):").grid(row=1, column=0, sticky=tk.W, pady=2, padx=5)
self.search_item2 = ttk.Entry(param_frame, width=15, font=self.font_style)
self.search_item2.grid(row=1, column=1, pady=2, padx=5)
ttk.Label(param_frame, text="查找项3(可选):").grid(row=1, column=2, sticky=tk.W, pady=2, padx=5)
self.search_item3 = ttk.Entry(param_frame, width=15, font=self.font_style)
self.search_item3.grid(row=1, column=3, pady=2, padx=5)
ttk.Label(param_frame, text="删除信息:").grid(row=1, column=4, sticky=tk.W, pady=2, padx=5)
self.delete_text = ttk.Entry(param_frame, width=15, font=self.font_style)
self.delete_text.grid(row=1, column=5, pady=2, padx=5)
# 选项区域
option_frame = ttk.LabelFrame(main_frame, text="选项", padding="5")
option_frame.grid(row=2, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5)
ttk.Checkbutton(option_frame, text="删除空格", variable=self.delete_space).grid(row=0, column=0, sticky=tk.W,
padx=5)
ttk.Checkbutton(option_frame, text="删除括号及内容", variable=self.delete_brackets).grid(row=0, column=1,
sticky=tk.W, padx=5)
ttk.Checkbutton(option_frame, text="转文本格式", variable=self.convert_to_text).grid(row=0, column=2,
sticky=tk.W, padx=5)
ttk.Checkbutton(option_frame, text="负值减2(一次退库会抵消一次出库,因此减2)", variable=self.negative_adjust).grid(row=0, column=3, sticky=tk.W,
padx=5)
ttk.Checkbutton(option_frame, text="从小到大排序", variable=self.sort_asc).grid(row=1, column=0, sticky=tk.W,
padx=5)
ttk.Checkbutton(option_frame, text="从大到小排序", variable=self.sort_desc).grid(row=1, column=1, sticky=tk.W,
padx=5)
# 对比选项(红色)
self.compare_check = ttk.Checkbutton(option_frame, text="对比模式", variable=self.compare_mode,
style='Red.TCheckbutton')
self.compare_check.grid(row=1, column=2, sticky=tk.W, padx=5)
# 保存路径区域
save_frame = ttk.Frame(main_frame)
save_frame.grid(row=3, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5)
ttk.Label(save_frame, text="保存路径:").grid(row=0, column=0, sticky=tk.W, padx=5)
self.save_path = ttk.Entry(save_frame, width=80, font=self.font_style)
self.save_path.grid(row=0, column=1, padx=5)
ttk.Button(save_frame, text="浏览", command=self.browse_save_path).grid(row=0, column=2, padx=5)
# 按钮区域
button_frame = ttk.Frame(main_frame)
button_frame.grid(row=4, column=0, columnspan=3, pady=10)
self.execute_btn = ttk.Button(button_frame, text="执行", command=self.start_process)
self.execute_btn.grid(row=0, column=0, padx=10)
self.stop_btn = ttk.Button(button_frame, text="停止", command=self.stop_process_func, state=tk.DISABLED)
self.stop_btn.grid(row=0, column=1, padx=10)
# 进度条
self.progress = ttk.Progressbar(main_frame, mode='indeterminate')
self.progress.grid(row=5, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5)
# 日志区域
log_frame = ttk.LabelFrame(main_frame, text="日志", padding="5")
log_frame.grid(row=6, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=5)
self.log_text = tk.Text(log_frame, width=110, height=10, font=self.font_style)
self.log_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
scrollbar = ttk.Scrollbar(log_frame, orient=tk.VERTICAL, command=self.log_text.yview)
scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S))
self.log_text.configure(yscrollcommand=scrollbar.set)
# 配置权重
self.root.columnconfigure(0, weight=1)
self.root.rowconfigure(0, weight=1)
main_frame.columnconfigure(0, weight=1)
main_frame.rowconfigure(6, weight=1)
log_frame.columnconfigure(0, weight=1)
log_frame.rowconfigure(0, weight=1)
# 创建红色样式
style = ttk.Style()
style.configure('Red.TCheckbutton', foreground='red')
def add_files(self):
files = filedialog.askopenfilenames(
title="选择Excel文件",
filetypes=[("Excel files", "*.xlsx *.xls"), ("All files", "*.*")]
)
if files:
for file in files:
if file not in self.file_paths:
self.file_paths.append(file)
self.file_listbox.insert(tk.END, file)
def remove_files(self):
selected = self.file_listbox.curselection()
if selected:
for index in selected[::-1]:
self.file_paths.pop(index)
self.file_listbox.delete(index)
def browse_save_path(self):
path = filedialog.asksaveasfilename(
title="选择保存路径",
defaultextension=".xlsx",
filetypes=[("Excel files", "*.xlsx"), ("All files", "*.*")]
)
if path:
self.save_path.delete(0, tk.END)
self.save_path.insert(0, path)
def start_process(self):
if not self.file_paths:
messagebox.showerror("错误", "请至少选择一个文件")
return
if self.compare_mode.get() and len(self.file_paths) < 2:
messagebox.showerror("错误", "对比模式需要至少两个文件")
return
self.execute_btn.config(state=tk.DISABLED)
self.stop_btn.config(state=tk.NORMAL)
self.stop_process = False
# 在新线程中执行处理,避免界面冻结
thread = threading.Thread(target=self.process_files)
thread.daemon = True
thread.start()
def stop_process_func(self):
self.stop_process = True
self.stop_btn.config(state=tk.DISABLED)
self.execute_btn.config(state=tk.NORMAL)
self.log("处理已停止")
def log(self, message):
self.log_text.insert(tk.END, f"{datetime.now().strftime('%H:%M:%S')} - {message}\n")
self.log_text.see(tk.END)
self.root.update_idletasks()
def process_files(self):
self.progress.start()
try:
# 获取参数
search_item = self.search_item.get().strip()
amount_item = self.amount_item.get().strip()
quantity_item = self.quantity_item.get().strip()
search_item2 = self.search_item2.get().strip()
search_item3 = self.search_item3.get().strip()
delete_text = self.delete_text.get().strip()
# 确定保存路径
save_path = self.save_path.get().strip()
if not save_path:
save_path = self.file_paths[0].replace('.xlsx', '_清洗结果.xlsx').replace('.xls', '_清洗结果.xlsx')
# 处理每个文件
all_results = {}
for i, file_path in enumerate(self.file_paths):
if self.stop_process:
break
self.log(f"正在处理文件 {i + 1}/{len(self.file_paths)}: {os.path.basename(file_path)}")
try:
# 读取Excel文件
df = pd.read_excel(file_path, header=None)
# 查找表头行
header_row = self.find_header_row(df, search_item, amount_item, quantity_item)
if header_row is None:
self.log(f"警告: 在文件 {os.path.basename(file_path)} 中未找到表头")
continue
# 重新读取数据,使用正确的表头行
df = pd.read_excel(file_path, header=header_row)
# 数据清洗
df_cleaned = self.clean_data(df, search_item, search_item2, search_item3, delete_text)
# 查重处理
result_df = self.process_duplicates(df_cleaned, search_item, amount_item, quantity_item)
# 排序处理
if self.sort_asc.get():
result_df = result_df.sort_values(by=search_item, ascending=True)
elif self.sort_desc.get():
result_df = result_df.sort_values(by=search_item, ascending=False)
all_results[os.path.basename(file_path)] = result_df
except Exception as e:
self.log(f"处理文件 {os.path.basename(file_path)} 时出错: {str(e)}")
continue
if not all_results:
self.log("没有成功处理任何文件")
return
# 保存结果
if self.compare_mode.get():
self.save_compare_results(all_results, save_path, search_item, amount_item, quantity_item)
else:
self.save_normal_results(all_results, save_path, search_item, amount_item, quantity_item)
self.log(f"处理完成,结果已保存到: {save_path}")
except Exception as e:
self.log(f"处理过程中出错: {str(e)}")
finally:
self.progress.stop()
self.execute_btn.config(state=tk.NORMAL)
self.stop_btn.config(state=tk.DISABLED)
def find_header_row(self, df, search_item, amount_item, quantity_item):
# 在0-20行150列范围内查找表头
for i in range(min(21, len(df))):
row = df.iloc[i]
has_search = False
has_amount = False
has_quantity = False
for j in range(min(150, len(row))):
cell_value = str(row.iloc[j]) if pd.notna(row.iloc[j]) else ""
if search_item in cell_value:
has_search = True
if amount_item in cell_value:
has_amount = True
if quantity_item in cell_value:
has_quantity = True
if has_search and has_amount and has_quantity:
return i
return None
def clean_data(self, df, search_item, search_item2, search_item3, delete_text):
df_cleaned = df.copy()
# 确保查找项列存在
if search_item not in df_cleaned.columns:
return df_cleaned
# 删除空格
if self.delete_space.get():
df_cleaned[search_item] = df_cleaned[search_item].astype(str).str.replace(' ', '')
# 删除括号及内容
if self.delete_brackets.get():
df_cleaned[search_item] = df_cleaned[search_item].astype(str).apply(
lambda x: re.sub(r'[\((].*?[\))]', '', x)
)
# 删除指定文本
if delete_text:
df_cleaned[search_item] = df_cleaned[search_item].astype(str).str.replace(delete_text, '')
# 合并查找项
if search_item2 and search_item2 in df_cleaned.columns:
df_cleaned[search_item] = df_cleaned[search_item].astype(str) + df_cleaned[search_item2].astype(str)
if search_item3 and search_item3 in df_cleaned.columns:
df_cleaned[search_item] = df_cleaned[search_item].astype(str) + df_cleaned[search_item3].astype(str)
# 转换为文本格式
if self.convert_to_text.get():
df_cleaned[search_item] = df_cleaned[search_item].astype(str)
return df_cleaned
def process_duplicates(self, df, search_item, amount_item, quantity_item):
# 确保必要的列存在
if search_item not in df.columns:
return pd.DataFrame()
# 分组处理重复项
grouped = df.groupby(search_item)
result_data = []
for name, group in grouped:
# 计算重复数
duplicate_count = len(group)
# 计算金额和数量合计
amount_sum = 0
quantity_sum = 0
if amount_item in group.columns:
amount_sum = group[amount_item].apply(lambda x: pd.to_numeric(x, errors='coerce')).sum()
if quantity_item in group.columns:
quantity_sum = group[quantity_item].apply(lambda x: pd.to_numeric(x, errors='coerce')).sum()
# 负值调整
if self.negative_adjust.get() and amount_item in group.columns:
negative_count = len(group[pd.to_numeric(group[amount_item], errors='coerce') < 0])
quantity_sum -= 2 * negative_count
result_data.append({
search_item: name,
'重复数': duplicate_count,
'数量合计': quantity_sum,
'金额合计': amount_sum
})
return pd.DataFrame(result_data)
def save_normal_results(self, all_results, save_path, search_item, amount_item, quantity_item):
# 创建新工作簿
wb = Workbook()
ws = wb.active
# 设置表头样式
bold_font = Font(bold=True, name="宋体", size=12)
light_yellow_fill = PatternFill(start_color="FFFFDD", end_color="FFFFDD", fill_type="solid")
# 写入数据
col_offset = 0
for file_name, result_df in all_results.items():
file_name_clean = os.path.splitext(file_name)[0]
# 写入表头
ws.cell(row=1, column=col_offset + 1, value=f"{file_name_clean}_{search_item}").font = bold_font
ws.cell(row=1, column=col_offset + 2, value="重复数").font = bold_font
ws.cell(row=1, column=col_offset + 3, value="数量").font = bold_font
ws.cell(row=1, column=col_offset + 4, value="金额合计").font = bold_font
# 写入数据
for i, row in result_df.iterrows():
ws.cell(row=i + 2, column=col_offset + 1, value=row[search_item])
ws.cell(row=i + 2, column=col_offset + 2, value=row['重复数'])
ws.cell(row=i + 2, column=col_offset + 3, value=row['数量合计'])
ws.cell(row=i + 2, column=col_offset + 4, value=row['金额合计'])
# 负值标记
if self.negative_adjust.get() and row['重复数'] > 1:
for col in range(col_offset + 1, col_offset + 5):
ws.cell(row=i + 2, column=col).fill = light_yellow_fill
col_offset += 6 # 数据列之间留空列
# 调整列宽
self.adjust_column_widths(ws)
# 保存文件
wb.save(save_path)
def save_compare_results(self, all_results, save_path, search_item, amount_item, quantity_item):
# 创建新工作簿
wb = Workbook()
ws = wb.active
# 设置样式
bold_font = Font(bold=True, name="宋体", size=12)
light_blue_fill = PatternFill(start_color="DDEEFF", end_color="DDEEFF", fill_type="solid")
light_yellow_fill = PatternFill(start_color="FFFFDD", end_color="FFFFDD", fill_type="solid")
yellow_fill = PatternFill(start_color="FFFF00", end_color="FFFF00", fill_type="solid")
# 获取第一个文件的数据作为基准
first_file = list(all_results.keys())[0]
base_df = all_results[first_file]
if self.sort_asc.get():
base_df = base_df.sort_values(by=search_item, ascending=True)
elif self.sort_desc.get():
base_df = base_df.sort_values(by=search_item, ascending=False)
# 写入基准数据(第一个文件)
file_name_clean = os.path.splitext(first_file)[0]
ws.cell(row=1, column=1, value=f"{file_name_clean}_{search_item}").font = bold_font
ws.cell(row=1, column=2, value="重复数").font = bold_font
ws.cell(row=1, column=3, value="数量").font = bold_font
ws.cell(row=1, column=4, value="金额合计").font = bold_font
for i, row in base_df.iterrows():
ws.cell(row=i + 2, column=1, value=row[search_item])
ws.cell(row=i + 2, column=2, value=row['重复数'])
ws.cell(row=i + 2, column=3, value=row['数量合计'])
ws.cell(row=i + 2, column=4, value=row['金额合计'])
# 处理其他文件的对比
col_offset = 6 # 从第6列开始(留一空列,目前是空了两列2列,改为5就是1列)
for file_idx, (file_name, compare_df) in enumerate(list(all_results.items())[1:], 1):
if self.sort_asc.get():
compare_df = compare_df.sort_values(by=search_item, ascending=True)
elif self.sort_desc.get():
compare_df = compare_df.sort_values(by=search_item, ascending=False)
file_name_clean = os.path.splitext(file_name)[0]
# 写入表头
ws.cell(row=1, column=col_offset + 1, value=f"{file_name_clean}_{search_item}").font = bold_font
ws.cell(row=1, column=col_offset + 2, value="重复数").font = bold_font
ws.cell(row=1, column=col_offset + 3, value="数量").font = bold_font
ws.cell(row=1, column=col_offset + 4, value="金额合计").font = bold_font
# 第一阶段:完全匹配
matched_indices = set()
for i, base_row in base_df.iterrows():
base_value = str(base_row[search_item])
for j, compare_row in compare_df.iterrows():
compare_value = str(compare_row[search_item])
if base_value == compare_value:
# 完全匹配,放在同一行
ws.cell(row=i + 2, column=col_offset + 1, value=compare_value)
ws.cell(row=i + 2, column=col_offset + 2, value=compare_row['重复数'])
ws.cell(row=i + 2, column=col_offset + 3, value=compare_row['数量合计'])
ws.cell(row=i + 2, column=col_offset + 4, value=compare_row['金额合计'])
# 设置淡蓝色背景
for col in range(col_offset + 1, col_offset + 5):
ws.cell(row=i + 2, column=col).fill = light_blue_fill
matched_indices.add(j)
break
# 第二阶段:相似度匹配(用于未完全匹配的数据)
unmatched_df = compare_df[~compare_df.index.isin(matched_indices)]
current_row = len(base_df) + 3 # 从基准数据下方开始
for _, compare_row in unmatched_df.iterrows():
compare_value = str(compare_row[search_item])
# 写入数据
ws.cell(row=current_row, column=col_offset + 1, value=compare_value)
ws.cell(row=current_row, column=col_offset + 2, value=compare_row['重复数'])
ws.cell(row=current_row, column=col_offset + 3, value=compare_row['数量合计'])
ws.cell(row=current_row, column=col_offset + 4, value=compare_row['金额合计'])
# 设置黄色背景(相似度较低)
for col in range(col_offset + 1, col_offset + 5):
ws.cell(row=current_row, column=col).fill = yellow_fill
current_row += 1
col_offset += 6 # 为下一个文件留出空间
# 调整列宽
self.adjust_column_widths(ws)
# 保存文件
wb.save(save_path)
def adjust_column_widths(self, ws):
for column in ws.columns:
max_length = 0
column_letter = get_column_letter(column[0].column)
for cell in column:
try:
if cell.value and len(str(cell.value)) > max_length:
max_length = len(str(cell.value))
except:
pass
adjusted_width = min(max_length + 2, 50)
ws.column_dimensions[column_letter].width = adjusted_width
if __name__ == "__main__":
root = tk.Tk()
app = DataCleaningApp(root)
root.mainloop()
更多推荐
所有评论(0)