实用程序:基于Python+Tkinter开发表格比对&整理工具

本文涉及的产品
实时计算 Flink 版,1000CU*H 3个月
实时数仓Hologres,5000CU*H 100GB 3个月
智能开放搜索 OpenSearch行业算法版,1GB 20LCU 1个月
简介: 一款基于Python+Tkinter开发的免费开源Excel处理工具,支持表格差异比对与错乱行整理,完整保留图片,兼容.xlsx和.csv格式。操作简单,支持自定义比对列、多线程处理,解决日常办公中数据比对、行合并及图片丢失等痛点,适用于各类Excel数据清理场景。(239字)

前言

日常办公和数据处理中,Excel表格是高频使用的工具,但我们经常会遇到一些「棘手问题」:比如两个表格需要比对差异(还可能带图片、比对列名不同)、表格数据行错乱(一行数据拆成了多行)、导出数据时图片丢失……市面上要么是Excel自带的比对功能太弱,要么是第三方工具收费且不支持图片保留。

基于此,我用Python+Tkinter开发了一款轻量、免费、开源的表格比对&整理工具,核心解决「表格比对」「错乱行整理」两大痛点,本文会详细介绍工具功能、使用方法和核心代码逻辑。

代码已经开源在github,地址如下:
https://github.com/ChenAI-TGF/Table_Comparison_Program

1.png

一、工具核心优势

  1. 双核心功能:支持表格差异比对 + 错乱行整理,一站式解决Excel处理痛点;
  2. 图片保留:读取/导出Excel时完整保留图片,解决常规工具丢失图片的问题;
  3. 灵活配置:支持自定义比对列(不同表格列名不同也能比对)、自定义整理行的判断列;
  4. 友好交互:带进度条、多线程处理(避免UI卡死)、中文适配,新手也能快速上手;
  5. 格式兼容:支持.xlsx(带图片)和.csv(无图片)格式,覆盖主流表格格式。

二、功能演示

2.1 环境准备

工具基于Python开发,需先配置环境:

# 安装核心依赖(tkinter一般Python自带,无需额外安装)
pip install pandas openpyxl
  • 系统兼容:Windows/macOS/Linux(Windows体验最佳);
  • 格式说明:仅支持.xlsx(可保留图片)和.csv(无图片),不支持旧版.xls(openpyxl不兼容)。

2.2 工具启动

将本文末尾的完整代码保存为table_tool.py,运行命令:

python table_tool.py

启动后界面如下,主要分为「表格比对区」「表格整理区」「进度条区」三大模块,布局清晰:
2.png

2.3 功能1:表格比对(支持图片保留)

适用场景:比如有「表格A」和「表格B」,需要找出「A有B无」「B有A无」「两者共有」的数据,且表格中包含图片(如产品图、二维码)。

操作步骤:

  1. 上传文件:点击「文件A/文件B」的「选择文件」按钮,上传需要比对的两个表格;
    3.png

  2. 配置比对规则:

    • 选择「文件A/B子表格」(支持多sheet表格);
    • 选择「A/B表格比对列」(比如A表用「申请注册号」,B表用「注册号」比对);
      4.png
  3. 开始比对:点击「开始比对」,进度条会实时显示处理进度(多线程不卡UI);
    5.png
    6.png

  4. 导出结果:

    • 点击「导出A有B无的数据」/「导出B有A无的数据」:导出差异数据,保留对应图片;
    • 点击「导出共有数据」:可选择按A表/ B表格式导出,自动映射图片到对应行。
      7.png

效果示例

  • 原表格A有100行,表格B有80行
  • 比对后导出「A有B无」数据20行

2.4 功能2:表格整理(解决一行数据占多行问题)

适用场景:比如表格中一行数据被拆成了多行(如商品信息行,后续行是补充的属性,无核心编号),需要合并为一行,且保留原表格中的图片。

操作步骤:

  1. 上传待整理文件:点击「待整理文件」的「选择文件」按钮;
  2. 配置整理规则:
    • 选择「待整理子表格」;
    • 选择「新行判断列」(核心列,比如「商品编号」——该列非空则为新行,空则为补充行);
  3. 开始整理:点击「开始整理表格」,进度条显示处理进度;
    8.png

  4. 导出结果:点击「导出整理后表格」,合并后的行数据+图片会被完整导出。
    9.png
    效果示例

  • 原表格有200行(实际只有80条有效数据,每行占2-3行);
  • 整理后变为80行,图片自动映射到对应合并后的行。

三、核心代码讲解

工具的核心类是TableCompareApp,整体架构分为「UI搭建」「文件读取(含图片)」「比对逻辑」「整理逻辑」「导出功能」五部分,下面重点讲解核心逻辑(UI部分简讲)。

3.1 UI部分

UI采用Tkinter的LabelFrame「Frame「Combobox「Progressbar`等组件,按功能分区域布局:

  • 「文件上传区」:负责A/B文件、待整理文件的选择;
  • 「信息展示区」:显示文件格式、行列数、图片数;
  • 「配置区」:子表格、比对列、整理判断列的选择;
  • 「操作区」:比对/整理/导出按钮;
  • 「进度条区」:实时显示处理进度。

核心细节:

  • 中文适配:通过root.option_add("*Font", "SimHei 9")解决Windows下Tkinter中文乱码;
  • 按钮状态控制:根据文件上传/配置完成状态,动态启用/禁用按钮(如check_compare_btn_state方法)。

3.2 核心1:文件读取(保留图片)

文件读取是工具的基础,核心是用openpyxl读取.xlsx文件(支持图片提取),代码在load_file/load_clean_file方法中:

# 关键代码片段:读取Excel并提取图片
if file_ext == ".xlsx":
    # 用openpyxl读取工作簿(保留图片)
    wb = load_workbook(file_path, data_only=True)
    sheets = wb.sheetnames

    for sheet in sheets:
        ws = wb[sheet]
        # 1. 读取表格数据到DataFrame(处理表头)
        df = pd.DataFrame(ws.values)
        if len(df) > 0:
            df.columns = df.iloc[0]  # 第一行设为列名
            df = df.drop(0).reset_index(drop=True)
        data_dict[sheet] = df

        # 2. 提取图片及位置(核心:保留图片+行列映射)
        sheet_images = []
        for img in ws._images:
            # 转换为1-based行列号(Excel原生格式)
            row = img.anchor._from.row + 1  
            col = img.anchor._from.col + 1
            sheet_images.append((img, row, col))
        images_dict[sheet] = sheet_images

关键逻辑

  • openpyxlws._images能获取工作表中所有图片对象;
  • 图片的anchor属性包含位置信息,需转换为1-based(Excel行/列从1开始,openpyxl内部是0-based);
  • 数据与图片分开存储,用字典关联「sheet名-数据/图片」,保证一一对应。

3.3 核心2:表格比对逻辑(含图片映射)

比对逻辑在compare_task方法中,采用多线程执行(避免UI卡死),核心分为「数据比对」和「图片映射」两部分:

3.3.1 数据比对核心

# 数据预处理:统一转字符串、填充空值(避免类型不一致导致比对错误)
df_a[col_a] = df_a[col_a].astype(str).fillna("")
df_b[col_b] = df_b[col_b].astype(str).fillna("")

# 集合运算:快速找差异/共有值(效率远高于循环)
a_vals = set(df_a[col_a].unique())
b_vals = set(df_b[col_b].unique())
a_not_b_vals = a_vals - b_vals  # A有B无
b_not_a_vals = b_vals - a_vals  # B有A无
common_vals = a_vals & b_vals   # 共有值

# 筛选对应行
a_not_b_df = df_a[df_a[col_a].isin(a_not_b_vals)].reset_index(drop=True)

优势:用Python集合运算替代逐行循环,比对效率提升10倍以上,适合大数据量。

3.3.2 图片映射核心

比对后数据行号会变化,需将原图片的位置映射到新行:

# A格式共有数据图片映射示例
a_common_original_rows = df_a[df_a[col_a].isin(common_vals)].index.tolist()
a_common_images = []
for img, orig_row, col in img_a_list:
    # 原Excel行 → 原DataFrame行(Excel行1=表头,行2=df第0行)
    orig_df_row = orig_row - 2  
    if orig_df_row in a_common_original_rows:
        # 新Excel行 = 新DataFrame行 + 2(表头占1行)
        new_row = a_common_original_rows.index(orig_df_row) + 2
        a_common_images.append((img, new_row, col))

关键逻辑

  • 建立「原DataFrame行号 → 新DataFrame行号」的映射;
  • 转换为Excel原生行号(+2),保证图片位置准确。

3.4 核心3:表格整理逻辑(含图片映射)

整理逻辑在clean_table_task方法中,核心是「合并错乱行」+「行号映射」:

3.4.1 行合并核心

cleaned_rows = []
current_row = None
orig_to_new_row = {
   }  # 原行→新行映射字典
new_row_idx = 0

for idx, row in df.iterrows():
    # 关键列非空 → 新行开始
    if str(row[key_col]).strip() != "":
        if current_row is not None:
            cleaned_rows.append(current_row)
            new_row_idx += 1
        current_row = row.to_dict()
        orig_to_new_row[idx] = new_row_idx
    else:
        # 关键列空 → 补充到当前行(仅填充空值)
        if current_row is not None:
            for col in df.columns:
                if str(row[col]).strip() != "" and str(current_row[col]).strip() == "":
                    current_row[col] = row[col]
            orig_to_new_row[idx] = new_row_idx

# 保存最后一行
if current_row is not None:
    cleaned_rows.append(current_row)
cleaned_table_result = pd.DataFrame(cleaned_rows)

核心规则:仅当「新行判断列」非空时,才新建一行;否则将当前行的非空数据补充到上一行的空值中,保证一行数据完整。

3.4.2 图片映射核心

# 整理后图片映射
cleaned_table_images = []
for img, orig_row, col in orig_images:
    orig_df_row = orig_row - 2  # Excel行→原DataFrame行
    if orig_df_row in orig_to_new_row:
        # 新Excel行 = 新DataFrame行 + 2
        new_excel_row = orig_to_new_row[orig_df_row] + 2
        cleaned_table_images.append((img, new_excel_row, col))

逻辑:通过orig_to_new_row字典,将原图片的行号映射到合并后的新行号,保证图片跟随对应数据行。

3.5 核心4:导出功能

导出功能封装在export_with_images方法中,核心是用openpyxl写入数据+图片:

if file_ext == ".xlsx":
    # 创建新工作簿
    wb = Workbook()
    ws = wb.active
    ws.title = "数据"

    # 1. 写入DataFrame数据(保留表头)
    for r in dataframe_to_rows(df, index=False, header=True):
        ws.append(r)

    # 2. 插入图片到对应位置
    for img, row, col in images:
        new_img = copy.deepcopy(img)  # 复制图片避免引用冲突
        new_img.anchor = f"{chr(64+col)}{row}"  # 设置锚点(如A2、B5)
        ws.add_image(new_img)

    wb.save(file_path)
    wb.close()

关键细节

  • dataframe_to_rows:将DataFrame转换为Excel行格式,保留表头;
  • 图片锚点:用chr(64+col)将列号(数字)转换为Excel列字母(如1→A,2→B);
  • 深拷贝图片:避免多个图片引用同一对象导致导出失败。

3.6 辅助:进度条+多线程

  • 多线程:比对/整理任务放在独立线程中执行(threading.Thread),设置daemon=True保证线程随主程序退出;
  • 进度条更新:通过update_progress方法实时刷新进度条,调用root.update_idletasks()强制刷新UI,避免进度条卡住。

四、使用注意事项

  1. 格式限制:仅支持.xlsx(带图片)和.csv(无图片),.xls格式需先转换为.xlsx;
  2. 图片位置:Excel中图片需「嵌入单元格」(而非浮窗),否则行列映射可能出错;
  3. 关键列选择:整理功能的「新行判断列」需选有唯一标识的列(如编号、名称),否则合并逻辑会出错;
  4. 大数据量:超过10万行的表格建议拆分处理,避免内存占用过高;
  5. 编码问题:CSV文件建议用UTF-8编码,否则可能出现中文乱码。

五、总结

这款工具以「解决实际痛点」为核心,通过Python+Tkinter实现了轻量化、可定制的表格处理能力,尤其是「图片保留」功能填补了常规工具的空白。代码完全开源,大家可以根据自己的需求二次开发(比如增加批量处理、格式转换、更多比对规则等)。

无论是办公人员快速处理表格,还是开发者学习Tkinter+Excel操作,这款工具都有一定的参考价值。如果有其他需求(比如支持更多格式、自动识别关键列),也可以基于核心逻辑扩展。

六、完整代码

import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import pandas as pd
import os
import threading
import time
import copy
from openpyxl import load_workbook, Workbook
from openpyxl.drawing.image import Image as OpenpyxlImage
from openpyxl.utils.dataframe import dataframe_to_rows

# 全局变量存储文件数据(新增图片存储字段)
file_a_data = {
   "sheets": [], "data": {
   }, "path": "", "images": {
   }}  # images: {
   sheet: [(img, row, col), ...]}
file_b_data = {
   "sheets": [], "data": {
   }, "path": "", "images": {
   }}
# 新增:存储共有数据及对应图片
compare_result = {
   
    "a_not_b": None, "b_not_a": None, "a_common": None, "b_common": None,
    "a_common_images": [], "b_common_images": []
}
# 整理表格相关全局变量(新增图片存储)
clean_file_data = {
   "sheets": [], "data": {
   }, "path": "", "images": {
   }}
cleaned_table_result = None
cleaned_table_images = []  # 整理后的图片:[(img, new_row, col), ...]

class TableCompareApp:
    def __init__(self, root):
        self.root = root
        self.root.title("表格比对&整理工具")
        self.root.geometry("950x850")

        # 进度条变量
        self.progress_var = tk.DoubleVar()

        # 构建UI
        self._create_widgets()

    def _create_widgets(self):
        # 1. 文件上传区域(原有比对功能)
        upload_frame = ttk.LabelFrame(self.root, text="文件上传(比对功能)")
        upload_frame.pack(padx=10, pady=10, fill="x")

        # 文件A上传
        ttk.Label(upload_frame, text="文件A:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
        self.file_a_label = ttk.Label(upload_frame, text="未选择文件", foreground="gray")
        self.file_a_label.grid(row=0, column=1, padx=5, pady=5, sticky="w")
        ttk.Button(upload_frame, text="选择文件", command=lambda: self.load_file("A")).grid(row=0, column=2, padx=5, pady=5)

        # 文件B上传
        ttk.Label(upload_frame, text="文件B:").grid(row=1, column=0, padx=5, pady=5, sticky="w")
        self.file_b_label = ttk.Label(upload_frame, text="未选择文件", foreground="gray")
        self.file_b_label.grid(row=1, column=1, padx=5, pady=5, sticky="w")
        ttk.Button(upload_frame, text="选择文件", command=lambda: self.load_file("B")).grid(row=1, column=2, padx=5, pady=5)

        # 2. 文件基本信息区域(原有)
        info_frame = ttk.LabelFrame(self.root, text="文件基本信息")
        info_frame.pack(padx=10, pady=5, fill="x")

        self.file_a_info = ttk.Label(info_frame, text="文件A信息:无")
        self.file_a_info.pack(padx=5, pady=2, anchor="w")

        self.file_b_info = ttk.Label(info_frame, text="文件B信息:无")
        self.file_b_info.pack(padx=10, pady=2, anchor="w")

        # 3. 比对配置区域(原有)
        config_frame = ttk.LabelFrame(self.root, text="比对配置(支持不同列比对)")
        config_frame.pack(padx=10, pady=5, fill="x")

        # 子表格选择
        ttk.Label(config_frame, text="文件A子表格:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
        self.sheet_a_combobox = ttk.Combobox(config_frame, state="disabled")
        self.sheet_a_combobox.grid(row=0, column=1, padx=5, pady=5, sticky="w")

        ttk.Label(config_frame, text="文件B子表格:").grid(row=1, column=0, padx=5, pady=5, sticky="w")
        self.sheet_b_combobox = ttk.Combobox(config_frame, state="disabled")
        self.sheet_b_combobox.grid(row=1, column=1, padx=5, pady=5, sticky="w")

        # 比对列选择
        ttk.Label(config_frame, text="A表格比对列:").grid(row=2, column=0, padx=5, pady=5, sticky="w")
        self.compare_col_a_combobox = ttk.Combobox(config_frame, state="disabled")
        self.compare_col_a_combobox.grid(row=2, column=1, padx=5, pady=5, sticky="w")

        ttk.Label(config_frame, text="B表格比对列:").grid(row=3, column=0, padx=5, pady=5, sticky="w")
        self.compare_col_b_combobox = ttk.Combobox(config_frame, state="disabled")
        self.compare_col_b_combobox.grid(row=3, column=1, padx=5, pady=5, sticky="w")

        # 4. 操作按钮区域(新增:导出共有数据按钮)
        compare_btn_frame = ttk.Frame(self.root)
        compare_btn_frame.pack(padx=10, pady=10)

        self.compare_btn = ttk.Button(compare_btn_frame, text="开始比对", command=self.start_compare, state="disabled")
        self.compare_btn.pack(side="left", padx=5)

        self.export_a_btn = ttk.Button(compare_btn_frame, text="导出A有B无的数据", command=lambda: self.export_result("a_not_b"), state="disabled")
        self.export_a_btn.pack(side="left", padx=5)

        self.export_b_btn = ttk.Button(compare_btn_frame, text="导出B有A无的数据", command=lambda: self.export_result("b_not_a"), state="disabled")
        self.export_b_btn.pack(side="left", padx=5)

        # 新增:导出共有数据按钮
        self.export_common_btn = ttk.Button(compare_btn_frame, text="导出共有数据", command=self.export_common_result, state="disabled")
        self.export_common_btn.pack(side="left", padx=5)

        # ========== 表格整理功能区域 ==========
        clean_frame = ttk.LabelFrame(self.root, text="表格整理功能(解决一行数据占多行问题)")
        clean_frame.pack(padx=10, pady=15, fill="x")

        # 选择要整理的文件
        ttk.Label(clean_frame, text="待整理文件:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
        self.clean_file_label = ttk.Label(clean_frame, text="未选择文件", foreground="gray")
        self.clean_file_label.grid(row=0, column=1, padx=5, pady=5, sticky="w")
        ttk.Button(clean_frame, text="选择文件", command=self.load_clean_file).grid(row=0, column=2, padx=5, pady=5)

        # 选择待整理的子表格
        ttk.Label(clean_frame, text="待整理子表格:").grid(row=1, column=0, padx=5, pady=5, sticky="w")
        self.clean_sheet_combobox = ttk.Combobox(clean_frame, state="disabled")
        self.clean_sheet_combobox.grid(row=1, column=1, padx=5, pady=5, sticky="w")

        # 选择关键列(判断新行的依据:关键列非空=新行)
        ttk.Label(clean_frame, text="新行判断列:").grid(row=2, column=0, padx=5, pady=5, sticky="w")
        self.clean_key_col_combobox = ttk.Combobox(clean_frame, state="disabled")
        self.clean_key_col_combobox.grid(row=2, column=1, padx=5, pady=5, sticky="w")

        # 整理/导出按钮
        clean_btn_frame = ttk.Frame(clean_frame)
        clean_btn_frame.grid(row=3, column=0, columnspan=3, pady=10)

        self.clean_btn = ttk.Button(clean_btn_frame, text="开始整理表格", command=self.start_clean_table, state="disabled")
        self.clean_btn.pack(side="left", padx=5)

        self.export_clean_btn = ttk.Button(clean_btn_frame, text="导出整理后表格", command=self.export_cleaned_table, state="disabled")
        self.export_clean_btn.pack(side="left", padx=5)

        # ========== 进度条区域(共用) ==========
        progress_frame = ttk.Frame(self.root)
        progress_frame.pack(padx=10, pady=5, fill="x")

        self.progress_bar = ttk.Progressbar(progress_frame, variable=self.progress_var, maximum=100)
        self.progress_bar.pack(fill="x", padx=5, pady=5)

        self.progress_label = ttk.Label(progress_frame, text="进度:0%")
        self.progress_label.pack(anchor="center")

    # ========== 核心修复:支持读取图片 ==========
    def load_file(self, file_type):
        """加载比对用的A/B文件(支持读取图片)"""
        file_path = filedialog.askopenfilename(
            title=f"选择{file_type}文件",
            filetypes=[("Excel文件", "*.xlsx"), ("CSV文件", "*.csv"), ("所有文件", "*.*")]
        )
        if not file_path:
            return

        # 初始化数据
        if file_type == "A":
            file_a_data.update({
   "sheets": [], "data": {
   }, "path": file_path, "images": {
   }})
            self.file_a_label.config(text=os.path.basename(file_path))
            target_data = file_a_data
        else:
            file_b_data.update({
   "sheets": [], "data": {
   }, "path": file_path, "images": {
   }})
            self.file_b_label.config(text=os.path.basename(file_path))
            target_data = file_b_data

        try:
            file_ext = os.path.splitext(file_path)[1].lower()
            sheets = []
            data_dict = {
   }
            images_dict = {
   }  # 存储每个sheet的图片 (img, row, col)

            if file_ext == ".xlsx":
                # 用openpyxl读取工作簿(保留图片)
                wb = load_workbook(file_path, data_only=True)
                sheets = wb.sheetnames

                for sheet in sheets:
                    ws = wb[sheet]
                    # 读取数据到DataFrame
                    df = pd.DataFrame(ws.values)
                    # 设置列名(第一行)
                    if len(df) > 0:
                        df.columns = df.iloc[0]
                        df = df.drop(0).reset_index(drop=True)
                    else:
                        df.columns = []
                    data_dict[sheet] = df

                    # 提取图片及位置(openpyxl行/列从0开始,转换为1-based)
                    sheet_images = []
                    for img in ws._images:
                        row = img.anchor._from.row + 1  # 转换为1-based行号
                        col = img.anchor._from.col + 1  # 转换为1-based列号
                        sheet_images.append((img, row, col))
                    images_dict[sheet] = sheet_images

                wb.close()

            elif file_ext == ".csv":
                sheets = ["默认表格"]
                df = pd.read_csv(file_path)
                data_dict["默认表格"] = df
                images_dict["默认表格"] = []  # CSV无图片
            else:
                messagebox.showerror("错误", "仅支持xlsx和csv格式文件!")
                return

            # 更新数据和图片
            target_data["sheets"] = sheets
            target_data["data"] = data_dict
            target_data["images"] = images_dict

            # 更新信息显示(包含图片数)
            if file_type == "A":
                first_sheet = sheets[0]
                first_df = data_dict[first_sheet]
                img_count = len(images_dict[first_sheet])
                info_text = f"文件A信息:格式={file_ext[1:].upper()} | 子表格数={len(sheets)} | "
                info_text += f"首个表格行数={len(first_df)} | 列数={len(first_df.columns)} | 图片数={img_count}"
                self.file_a_info.config(text=info_text)
                self.sheet_a_combobox.config(state="normal", values=sheets)
                self.sheet_a_combobox.current(0)
                self.update_compare_columns()
            else:
                first_sheet = sheets[0]
                first_df = data_dict[first_sheet]
                img_count = len(images_dict[first_sheet])
                info_text = f"文件B信息:格式={file_ext[1:].upper()} | 子表格数={len(sheets)} | "
                info_text += f"首个表格行数={len(first_df)} | 列数={len(first_df.columns)} | 图片数={img_count}"
                self.file_b_info.config(text=info_text)
                self.sheet_b_combobox.config(state="normal", values=sheets)
                self.sheet_b_combobox.current(0)
                self.update_compare_columns()

            self.check_compare_btn_state()

        except Exception as e:
            messagebox.showerror("读取失败", f"文件{file_type}读取错误:{str(e)}")

    def load_clean_file(self):
        """加载待整理的表格文件(支持读取图片)"""
        file_path = filedialog.askopenfilename(
            title="选择待整理的表格文件",
            filetypes=[("Excel文件", "*.xlsx"), ("CSV文件", "*.csv"), ("所有文件", "*.*")]
        )
        if not file_path:
            return

        # 清空原有整理文件数据
        clean_file_data.update({
   "sheets": [], "data": {
   }, "path": file_path, "images": {
   }})
        self.clean_file_label.config(text=os.path.basename(file_path))

        try:
            file_ext = os.path.splitext(file_path)[1].lower()
            sheets = []
            data_dict = {
   }
            images_dict = {
   }

            if file_ext == ".xlsx":
                # 用openpyxl读取工作簿(保留图片)
                wb = load_workbook(file_path, data_only=True)
                sheets = wb.sheetnames

                for sheet in sheets:
                    ws = wb[sheet]
                    # 读取数据到DataFrame
                    df = pd.DataFrame(ws.values)
                    # 设置列名(第一行)
                    if len(df) > 0:
                        df.columns = df.iloc[0]
                        df = df.drop(0).reset_index(drop=True)
                    else:
                        df.columns = []
                    data_dict[sheet] = df

                    # 提取图片及位置
                    sheet_images = []
                    for img in ws._images:
                        row = img.anchor._from.row + 1
                        col = img.anchor._from.col + 1
                        sheet_images.append((img, row, col))
                    images_dict[sheet] = sheet_images

                wb.close()

            elif file_ext == ".csv":
                sheets = ["默认表格"]
                df = pd.read_csv(file_path)
                data_dict["默认表格"] = df
                images_dict["默认表格"] = []
            else:
                messagebox.showerror("错误", "仅支持xlsx和csv格式文件!")
                return

            # 存储数据和图片
            clean_file_data["sheets"] = sheets
            clean_file_data["data"] = data_dict
            clean_file_data["images"] = images_dict

            # 更新子表格下拉框
            self.clean_sheet_combobox.config(state="normal", values=sheets)
            self.clean_sheet_combobox.current(0)

            # 更新关键列下拉框(新行判断列)
            self.update_clean_key_columns()

            # 启用整理按钮
            self.clean_btn.config(state="normal")

            # 显示文件信息(包含图片数)
            first_sheet = sheets[0]
            first_df = data_dict[first_sheet]
            img_count = len(images_dict[first_sheet])
            info_text = f"待整理文件信息:格式={file_ext[1:].upper()} | 子表格数={len(sheets)} | "
            info_text += f"首个表格行数={len(first_df)} | 列数={len(first_df.columns)} | 图片数={img_count}"
            messagebox.showinfo("文件加载成功", info_text)

        except Exception as e:
            messagebox.showerror("读取失败", f"待整理文件读取错误:{str(e)}")

    # ========== 通用导出函数(核心修复:支持导出图片) ==========
    def export_with_images(self, df, images, title):
        """通用导出函数(支持图片保留)"""
        # 选择导出路径
        file_path = filedialog.asksaveasfilename(
            title=title,
            filetypes=[("Excel文件", "*.xlsx"), ("CSV文件", "*.csv")],
            defaultextension=".xlsx"
        )
        if not file_path:
            return

        file_ext = os.path.splitext(file_path)[1].lower()

        try:
            if file_ext == ".xlsx":
                # 创建新工作簿
                wb = Workbook()
                ws = wb.active
                ws.title = "数据"

                # 写入DataFrame数据(保留表头)
                for r in dataframe_to_rows(df, index=False, header=True):
                    ws.append(r)

                # 插入图片到对应位置
                for img, row, col in images:
                    # 复制图片对象避免引用冲突
                    new_img = copy.deepcopy(img)
                    # 设置图片锚点(列字母+行号)
                    new_img.anchor = f"{chr(64+col)}{row}"
                    ws.add_image(new_img)

                # 保存工作簿
                wb.save(file_path)
                wb.close()
                messagebox.showinfo("导出成功", f"数据已导出至:{file_path}\n包含图片:{len(images)}张")

            elif file_ext == ".csv":
                # CSV不支持图片,提示用户
                if images:
                    messagebox.showwarning("格式提示", "CSV格式不支持存储图片,图片将丢失!")
                df.to_csv(file_path, index=False, encoding="utf-8-sig")
                messagebox.showinfo("导出成功", f"数据已导出至:{file_path}\n注意:CSV格式不包含图片")

        except Exception as e:
            messagebox.showerror("导出失败", f"导出错误:{str(e)}")

    # ========== 比对功能(修复:处理图片) ==========
    def update_compare_columns(self):
        """更新比对列下拉框"""
        if file_a_data["sheets"] and self.sheet_a_combobox.get():
            sheet_a = self.sheet_a_combobox.get()
            cols_a = file_a_data["data"][sheet_a].columns.tolist()
            self.compare_col_a_combobox.config(state="normal", values=cols_a)
            self.compare_col_a_combobox.current(0)

        if file_b_data["sheets"] and self.sheet_b_combobox.get():
            sheet_b = self.sheet_b_combobox.get()
            cols_b = file_b_data["data"][sheet_b].columns.tolist()
            self.compare_col_b_combobox.config(state="normal", values=cols_b)
            self.compare_col_b_combobox.current(0)

    def check_compare_btn_state(self):
        """检查比对按钮状态"""
        if (file_a_data["path"] and file_b_data["path"] and 
            self.compare_col_a_combobox.get() and self.compare_col_b_combobox.get()):
            self.compare_btn.config(state="normal")
        else:
            self.compare_btn.config(state="disabled")

    def update_progress(self, value):
        """更新进度条"""
        self.progress_var.set(value)
        self.progress_label.config(text=f"进度:{int(value)}%")
        self.root.update_idletasks()

    def compare_task(self):
        """比对任务(修复:处理共有数据图片)"""
        try:
            # 1. 获取配置信息
            sheet_a = self.sheet_a_combobox.get()
            sheet_b = self.sheet_b_combobox.get()
            col_a = self.compare_col_a_combobox.get()
            col_b = self.compare_col_b_combobox.get()

            # 2. 获取数据和图片
            df_a = file_a_data["data"][sheet_a].copy()
            df_b = file_b_data["data"][sheet_b].copy()
            img_a_list = file_a_data["images"].get(sheet_a, [])
            img_b_list = file_b_data["images"].get(sheet_b, [])

            # 3. 进度条初始化
            self.update_progress(0)
            time.sleep(0.1)

            # 4. 数据预处理
            self.update_progress(20)
            df_a[col_a] = df_a[col_a].astype(str).fillna("")
            df_b[col_b] = df_b[col_b].astype(str).fillna("")

            # 5. 核心比对逻辑
            self.update_progress(50)
            a_vals = set(df_a[col_a].unique())
            b_vals = set(df_b[col_b].unique())

            # A有B无的行
            a_not_b_vals = a_vals - b_vals
            a_not_b_df = df_a[df_a[col_a].isin(a_not_b_vals)].reset_index(drop=True)

            # B有A无的行
            b_not_a_vals = b_vals - a_vals
            b_not_a_df = df_b[df_b[col_b].isin(b_not_a_vals)].reset_index(drop=True)

            # 共有数据
            common_vals = a_vals & b_vals
            a_common_df = df_a[df_a[col_a].isin(common_vals)].reset_index(drop=True)
            b_common_df = df_b[df_b[col_b].isin(common_vals)].reset_index(drop=True)

            # 处理共有数据的图片(映射行号)
            # A格式共有数据图片
            a_common_original_rows = df_a[df_a[col_a].isin(common_vals)].index.tolist()
            a_common_images = []
            for img, orig_row, col in img_a_list:
                orig_df_row = orig_row - 2  # Excel行1=表头,行2=df第0if orig_df_row in a_common_original_rows:
                    new_row = a_common_original_rows.index(orig_df_row) + 2  # 新表行2开始是数据
                    a_common_images.append((img, new_row, col))

            # B格式共有数据图片
            b_common_original_rows = df_b[df_b[col_b].isin(common_vals)].index.tolist()
            b_common_images = []
            for img, orig_row, col in img_b_list:
                orig_df_row = orig_row - 2
                if orig_df_row in b_common_original_rows:
                    new_row = b_common_original_rows.index(orig_df_row) + 2
                    b_common_images.append((img, new_row, col))

            # 6. 存储结果
            compare_result["a_not_b"] = a_not_b_df
            compare_result["b_not_a"] = b_not_a_df
            compare_result["a_common"] = a_common_df
            compare_result["b_common"] = b_common_df
            compare_result["a_common_images"] = a_common_images
            compare_result["b_common_images"] = b_common_images

            # 7. 完成进度
            self.update_progress(100)

            # 8. 启用导出按钮
            self.export_a_btn.config(state="normal")
            self.export_b_btn.config(state="normal")
            self.export_common_btn.config(state="normal")

            # 显示结果(包含图片数)
            messagebox.showinfo("比对完成", 
                                f"比对结果:\nA有B无的数据行数:{len(a_not_b_df)}\n"
                                f"B有A无的数据行数:{len(b_not_a_df)}\n"
                                f"共有数据行数(A格式):{len(a_common_df)} | 图片数:{len(a_common_images)}\n"
                                f"共有数据行数(B格式):{len(b_common_df)} | 图片数:{len(b_common_images)}")

        except Exception as e:
            messagebox.showerror("比对失败", f"比对过程出错:{str(e)}")
            self.update_progress(0)

    def start_compare(self):
        """启动比对"""
        self.compare_btn.config(state="disabled")
        self.export_a_btn.config(state="disabled")
        self.export_b_btn.config(state="disabled")
        self.export_common_btn.config(state="disabled")

        compare_thread = threading.Thread(target=self.compare_task)
        compare_thread.daemon = True
        compare_thread.start()

    def export_result(self, result_type):
        """导出比对结果(修复:支持图片)"""
        if compare_result[result_type] is None:
            messagebox.showwarning("提示", "暂无可导出的数据!")
            return

        # 筛选对应图片
        if result_type == "a_not_b":
            df = compare_result["a_not_b"]
            sheet_a = self.sheet_a_combobox.get()
            img_a_list = file_a_data["images"].get(sheet_a, [])
            col_a = self.compare_col_a_combobox.get()
            a_not_b_vals = set(df[col_a].unique())
            a_not_b_original_rows = file_a_data["data"][sheet_a][file_a_data["data"][sheet_a][col_a].isin(a_not_b_vals)].index.tolist()
            images = []
            for img, orig_row, col in img_a_list:
                orig_df_row = orig_row - 2
                if orig_df_row in a_not_b_original_rows:
                    new_row = a_not_b_original_rows.index(orig_df_row) + 2
                    images.append((img, new_row, col))
            title = "保存A有B无的数据"
        elif result_type == "b_not_a":
            df = compare_result["b_not_a"]
            sheet_b = self.sheet_b_combobox.get()
            img_b_list = file_b_data["images"].get(sheet_b, [])
            col_b = self.compare_col_b_combobox.get()
            b_not_a_vals = set(df[col_b].unique())
            b_not_a_original_rows = file_b_data["data"][sheet_b][file_b_data["data"][sheet_b][col_b].isin(b_not_a_vals)].index.tolist()
            images = []
            for img, orig_row, col in img_b_list:
                orig_df_row = orig_row - 2
                if orig_df_row in b_not_a_original_rows:
                    new_row = b_not_a_original_rows.index(orig_df_row) + 2
                    images.append((img, new_row, col))
            title = "保存B有A无的数据"
        else:
            return

        self.export_with_images(df, images, title)

    def export_common_result(self):
        """导出共有数据(修复:支持图片)"""
        if compare_result["a_common"] is None or compare_result["b_common"] is None:
            messagebox.showwarning("提示", "暂无共有数据可导出!")
            return

        # 选择导出格式
        choice = messagebox.askquestion(
            "选择导出格式",
            "请选择共有数据的导出格式:\n【是】= 按A表格格式导出(保留A表格列结构)\n【否】= 按B表格格式导出(保留B表格列结构)",
            icon="question"
        )

        if choice == "yes":
            df = compare_result["a_common"]
            images = compare_result["a_common_images"]
            title = "保存按A表格格式的共有数据"
        elif choice == "no":
            df = compare_result["b_common"]
            images = compare_result["b_common_images"]
            title = "保存按B表格格式的共有数据"
        else:
            return

        self.export_with_images(df, images, title)

    # ========== 表格整理功能(修复:处理图片) ==========
    def update_clean_key_columns(self):
        """更新整理功能的关键列下拉框"""
        if clean_file_data["sheets"] and self.clean_sheet_combobox.get():
            sheet = self.clean_sheet_combobox.get()
            cols = clean_file_data["data"][sheet].columns.tolist()
            self.clean_key_col_combobox.config(state="normal", values=cols)
            self.clean_key_col_combobox.current(0)

    def clean_table_task(self):
        """表格整理任务(修复:合并数据+保留图片)"""
        global cleaned_table_result, cleaned_table_images
        try:
            # 获取配置
            sheet = self.clean_sheet_combobox.get()
            key_col = self.clean_key_col_combobox.get()
            df = clean_file_data["data"][sheet].copy()
            orig_images = clean_file_data["images"].get(sheet, [])

            # 进度条初始化
            self.update_progress(0)
            time.sleep(0.1)

            # 预处理
            self.update_progress(10)
            df = df.fillna("")

            # 核心整理逻辑
            self.update_progress(20)
            cleaned_rows = []
            current_row = None
            orig_to_new_row = {
   }  # 原df行号 → 新df行号映射
            new_row_idx = 0

            total_rows = len(df)
            for idx, row in df.iterrows():
                # 更新进度
                progress = 20 + (idx / total_rows) * 70
                self.update_progress(progress)

                # 关键列非空 = 新行开始
                if str(row[key_col]).strip() != "":
                    if current_row is not None:
                        cleaned_rows.append(current_row)
                        new_row_idx += 1
                    current_row = row.to_dict()
                    orig_to_new_row[idx] = new_row_idx
                else:
                    # 补充到当前行
                    if current_row is not None:
                        for col in df.columns:
                            if str(row[col]).strip() != "" and str(current_row[col]).strip() == "":
                                current_row[col] = row[col]
                        orig_to_new_row[idx] = new_row_idx

            # 保存最后一行
            if current_row is not None:
                cleaned_rows.append(current_row)
                new_row_idx += 1

            # 转换为DataFrame
            self.update_progress(90)
            cleaned_table_result = pd.DataFrame(cleaned_rows)

            # 处理图片(映射到新行号)
            cleaned_table_images = []
            for img, orig_row, col in orig_images:
                orig_df_row = orig_row - 2  # Excel行 → df行号
                if orig_df_row in orig_to_new_row:
                    # 新Excel行号 = 新df行号 + 2(行1是表头)
                    new_excel_row = orig_to_new_row[orig_df_row] + 2
                    cleaned_table_images.append((img, new_excel_row, col))

            # 完成进度
            self.update_progress(100)

            # 启用导出按钮
            self.export_clean_btn.config(state="normal")

            messagebox.showinfo("整理完成", 
                                f"表格整理成功!\n原行数:{len(df)} | 整理后行数:{len(cleaned_table_result)}\n"
                                f"原图片数:{len(orig_images)} | 整理后图片数:{len(cleaned_table_images)}")

        except Exception as e:
            messagebox.showerror("整理失败", f"表格整理出错:{str(e)}")
            self.update_progress(0)

    def start_clean_table(self):
        """启动表格整理"""
        if not self.clean_key_col_combobox.get():
            messagebox.showwarning("提示", "请选择新行判断列!")
            return

        self.clean_btn.config(state="disabled")
        self.export_clean_btn.config(state="disabled")

        clean_thread = threading.Thread(target=self.clean_table_task)
        clean_thread.daemon = True
        clean_thread.start()

    def export_cleaned_table(self):
        """导出整理后的表格(修复:保留图片)"""
        global cleaned_table_result, cleaned_table_images
        if cleaned_table_result is None:
            messagebox.showwarning("提示", "暂无整理后的数据!")
            return

        self.export_with_images(cleaned_table_result, cleaned_table_images, "保存整理后的表格")

if __name__ == "__main__":
    root = tk.Tk()
    # 解决tkinter中文乱码(Windows)
    try:
        root.option_add("*Font", "SimHei 9")
    except:
        pass

    app = TableCompareApp(root)
    root.mainloop()
相关文章
|
2天前
|
云安全 监控 安全
|
7天前
|
机器学习/深度学习 人工智能 自然语言处理
Z-Image:冲击体验上限的下一代图像生成模型
通义实验室推出全新文生图模型Z-Image,以6B参数实现“快、稳、轻、准”突破。Turbo版本仅需8步亚秒级生成,支持16GB显存设备,中英双语理解与文字渲染尤为出色,真实感和美学表现媲美国际顶尖模型,被誉为“最值得关注的开源生图模型之一”。
966 5
|
13天前
|
人工智能 Java API
Java 正式进入 Agentic AI 时代:Spring AI Alibaba 1.1 发布背后的技术演进
Spring AI Alibaba 1.1 正式发布,提供极简方式构建企业级AI智能体。基于ReactAgent核心,支持多智能体协作、上下文工程与生产级管控,助力开发者快速打造可靠、可扩展的智能应用。
1101 41
|
9天前
|
机器学习/深度学习 人工智能 数据可视化
1秒生图!6B参数如何“以小博大”生成超真实图像?
Z-Image是6B参数开源图像生成模型,仅需16GB显存即可生成媲美百亿级模型的超真实图像,支持中英双语文本渲染与智能编辑,登顶Hugging Face趋势榜,首日下载破50万。
673 39
|
13天前
|
人工智能 前端开发 算法
大厂CIO独家分享:AI如何重塑开发者未来十年
在 AI 时代,若你还在紧盯代码量、执着于全栈工程师的招聘,或者仅凭技术贡献率来评判价值,执着于业务提效的比例而忽略产研价值,你很可能已经被所谓的“常识”困住了脚步。
776 69
大厂CIO独家分享:AI如何重塑开发者未来十年
|
9天前
|
存储 自然语言处理 测试技术
一行代码,让 Elasticsearch 集群瞬间雪崩——5000W 数据压测下的性能避坑全攻略
本文深入剖析 Elasticsearch 中模糊查询的三大陷阱及性能优化方案。通过5000 万级数据量下做了高压测试,用真实数据复刻事故现场,助力开发者规避“查询雪崩”,为您的业务保驾护航。
479 30
|
16天前
|
数据采集 人工智能 自然语言处理
Meta SAM3开源:让图像分割,听懂你的话
Meta发布并开源SAM 3,首个支持文本或视觉提示的统一图像视频分割模型,可精准分割“红色条纹伞”等开放词汇概念,覆盖400万独特概念,性能达人类水平75%–80%,推动视觉分割新突破。
945 59
Meta SAM3开源:让图像分割,听懂你的话
|
6天前
|
弹性计算 网络协议 Linux
阿里云ECS云服务器详细新手购买流程步骤(图文详解)
新手怎么购买阿里云服务器ECS?今天出一期阿里云服务器ECS自定义购买流程:图文全解析,阿里云服务器ECS购买流程图解,自定义购买ECS的设置选项是最复杂的,以自定义购买云服务器ECS为例,包括付费类型、地域、网络及可用区、实例、镜像、系统盘、数据盘、公网IP、安全组及登录凭证详细设置教程:
205 114