基于Python+Tkinter開發(fā)一個(gè)表格比對(duì)和&整理工具
前言
日常辦公和數(shù)據(jù)處理中,Excel表格是高頻使用的工具,但我們經(jīng)常會(huì)遇到一些「棘手問題」:比如兩個(gè)表格需要比對(duì)差異(還可能帶圖片、比對(duì)列名不同)、表格數(shù)據(jù)行錯(cuò)亂(一行數(shù)據(jù)拆成了多行)、導(dǎo)出數(shù)據(jù)時(shí)圖片丟失……市面上要么是Excel自帶的比對(duì)功能太弱,要么是第三方工具收費(fèi)且不支持圖片保留。
基于此,我用Python+Tkinter開發(fā)了一款輕量、免費(fèi)、開源的表格比對(duì)&整理工具,核心解決「表格比對(duì)」「錯(cuò)亂行整理」兩大痛點(diǎn),本文會(huì)詳細(xì)介紹工具功能、使用方法和核心代碼邏輯。
代碼已經(jīng)開源在github,地址如下:https://github.com/ChenAI-TGF/Table_Comparison_Program

一、工具核心優(yōu)勢(shì)
- 雙核心功能:支持表格差異比對(duì) + 錯(cuò)亂行整理,一站式解決Excel處理痛點(diǎn);
- 圖片保留:讀取/導(dǎo)出Excel時(shí)完整保留圖片,解決常規(guī)工具丟失圖片的問題;
- 靈活配置:支持自定義比對(duì)列(不同表格列名不同也能比對(duì))、自定義整理行的判斷列;
- 友好交互:帶進(jìn)度條、多線程處理(避免UI卡死)、中文適配,新手也能快速上手;
- 格式兼容:支持.xlsx(帶圖片)和.csv(無圖片)格式,覆蓋主流表格格式。
二、功能演示
2.1 環(huán)境準(zhǔn)備
工具基于Python開發(fā),需先配置環(huán)境:
# 安裝核心依賴(tkinter一般Python自帶,無需額外安裝) pip install pandas openpyxl
- 系統(tǒng)兼容:Windows/macOS/Linux(Windows體驗(yàn)最佳);
- 格式說明:僅支持.xlsx(可保留圖片)和.csv(無圖片),不支持舊版.xls(openpyxl不兼容)。
2.2 工具啟動(dòng)
將本文末尾的完整代碼保存為table_tool.py,運(yùn)行命令:
python table_tool.py
啟動(dòng)后界面如下,主要分為「表格比對(duì)區(qū)」「表格整理區(qū)」「進(jìn)度條區(qū)」三大模塊,布局清晰:

2.3 功能1:表格比對(duì)(支持圖片保留)
適用場(chǎng)景:比如有「表格A」和「表格B」,需要找出「A有B無」「B有A無」「兩者共有」的數(shù)據(jù),且表格中包含圖片(如產(chǎn)品圖、二維碼)。
操作步驟:
上傳文件:點(diǎn)擊「文件A/文件B」的「選擇文件」按鈕,上傳需要比對(duì)的兩個(gè)表格;

配置比對(duì)規(guī)則:
- 選擇「文件A/B子表格」(支持多sheet表格);
- 選擇「A/B表格比對(duì)列」(比如A表用「申請(qǐng)注冊(cè)號(hào)」,B表用「注冊(cè)號(hào)」比對(duì));

開始比對(duì):點(diǎn)擊「開始比對(duì)」,進(jìn)度條會(huì)實(shí)時(shí)顯示處理進(jìn)度(多線程不卡UI);


導(dǎo)出結(jié)果:
- 點(diǎn)擊「導(dǎo)出A有B無的數(shù)據(jù)」/「導(dǎo)出B有A無的數(shù)據(jù)」:導(dǎo)出差異數(shù)據(jù),保留對(duì)應(yīng)圖片;
- 點(diǎn)擊「導(dǎo)出共有數(shù)據(jù)」:可選擇按A表/ B表格式導(dǎo)出,自動(dòng)映射圖片到對(duì)應(yīng)行。

效果示例:
- 原表格A有100行,表格B有80行
- 比對(duì)后導(dǎo)出「A有B無」數(shù)據(jù)20行
2.4 功能2:表格整理(解決一行數(shù)據(jù)占多行問題)
適用場(chǎng)景:比如表格中一行數(shù)據(jù)被拆成了多行(如商品信息行,后續(xù)行是補(bǔ)充的屬性,無核心編號(hào)),需要合并為一行,且保留原表格中的圖片。
操作步驟:
1.上傳待整理文件:點(diǎn)擊「待整理文件」的「選擇文件」按鈕;
2.配置整理規(guī)則:
- 選擇「待整理子表格」;
- 選擇「新行判斷列」(核心列,比如「商品編號(hào)」——該列非空則為新行,空則為補(bǔ)充行);
3.開始整理:點(diǎn)擊「開始整理表格」,進(jìn)度條顯示處理進(jìn)度;

4. 導(dǎo)出結(jié)果:點(diǎn)擊「導(dǎo)出整理后表格」,合并后的行數(shù)據(jù)+圖片會(huì)被完整導(dǎo)出。

效果示例:
- 原表格有200行(實(shí)際只有80條有效數(shù)據(jù),每行占2-3行);
- 整理后變?yōu)?0行,圖片自動(dòng)映射到對(duì)應(yīng)合并后的行。
三、核心代碼講解
工具的核心類是TableCompareApp,整體架構(gòu)分為「UI搭建」「文件讀?。ê瑘D片)」「比對(duì)邏輯」「整理邏輯」「導(dǎo)出功能」五部分,下面重點(diǎn)講解核心邏輯(UI部分簡(jiǎn)講)。
3.1 UI部分
UI采用Tkinter的LabelFrame「Frame「Combobox「Progressbar`等組件,按功能分區(qū)域布局:
- 「文件上傳區(qū)」:負(fù)責(zé)A/B文件、待整理文件的選擇;
- 「信息展示區(qū)」:顯示文件格式、行列數(shù)、圖片數(shù);
- 「配置區(qū)」:子表格、比對(duì)列、整理判斷列的選擇;
- 「操作區(qū)」:比對(duì)/整理/導(dǎo)出按鈕;
- 「進(jìn)度條區(qū)」:實(shí)時(shí)顯示處理進(jìn)度。
核心細(xì)節(jié):
- 中文適配:通過
root.option_add("*Font", "SimHei 9")解決Windows下Tkinter中文亂碼; - 按鈕狀態(tài)控制:根據(jù)文件上傳/配置完成狀態(tài),動(dòng)態(tài)啟用/禁用按鈕(如
check_compare_btn_state方法)。
3.2 核心1:文件讀取(保留圖片)
文件讀取是工具的基礎(chǔ),核心是用openpyxl讀取.xlsx文件(支持圖片提?。a在load_file/load_clean_file方法中:
# 關(guān)鍵代碼片段:讀取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. 讀取表格數(shù)據(jù)到DataFrame(處理表頭)
df = pd.DataFrame(ws.values)
if len(df) > 0:
df.columns = df.iloc[0] # 第一行設(shè)為列名
df = df.drop(0).reset_index(drop=True)
data_dict[sheet] = df
# 2. 提取圖片及位置(核心:保留圖片+行列映射)
sheet_images = []
for img in ws._images:
# 轉(zhuǎn)換為1-based行列號(hào)(Excel原生格式)
row = img.anchor._from.row + 1
col = img.anchor._from.col + 1
sheet_images.append((img, row, col))
images_dict[sheet] = sheet_images
關(guān)鍵邏輯:
openpyxl的ws._images能獲取工作表中所有圖片對(duì)象;- 圖片的
anchor屬性包含位置信息,需轉(zhuǎn)換為1-based(Excel行/列從1開始,openpyxl內(nèi)部是0-based); - 數(shù)據(jù)與圖片分開存儲(chǔ),用字典關(guān)聯(lián)「sheet名-數(shù)據(jù)/圖片」,保證一一對(duì)應(yīng)。
3.3 核心2:表格比對(duì)邏輯(含圖片映射)
比對(duì)邏輯在compare_task方法中,采用多線程執(zhí)行(避免UI卡死),核心分為「數(shù)據(jù)比對(duì)」和「圖片映射」兩部分:
3.3.1 數(shù)據(jù)比對(duì)核心
# 數(shù)據(jù)預(yù)處理:統(tǒng)一轉(zhuǎn)字符串、填充空值(避免類型不一致導(dǎo)致比對(duì)錯(cuò)誤)
df_a[col_a] = df_a[col_a].astype(str).fillna("")
df_b[col_b] = df_b[col_b].astype(str).fillna("")
# 集合運(yùn)算:快速找差異/共有值(效率遠(yuǎn)高于循環(huán))
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 # 共有值
# 篩選對(duì)應(yīng)行
a_not_b_df = df_a[df_a[col_a].isin(a_not_b_vals)].reset_index(drop=True)
優(yōu)勢(shì):用Python集合運(yùn)算替代逐行循環(huán),比對(duì)效率提升10倍以上,適合大數(shù)據(jù)量。
3.3.2 圖片映射核心
比對(duì)后數(shù)據(jù)行號(hào)會(huì)變化,需將原圖片的位置映射到新行:
# A格式共有數(shù)據(jù)圖片映射示例
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))
關(guān)鍵邏輯:
- 建立「原DataFrame行號(hào) → 新DataFrame行號(hào)」的映射;
- 轉(zhuǎn)換為Excel原生行號(hào)(+2),保證圖片位置準(zhǔn)確。
3.4 核心3:表格整理邏輯(含圖片映射)
整理邏輯在clean_table_task方法中,核心是「合并錯(cuò)亂行」+「行號(hào)映射」:
3.4.1 行合并核心
cleaned_rows = []
current_row = None
orig_to_new_row = {} # 原行→新行映射字典
new_row_idx = 0
for idx, row in df.iterrows():
# 關(guān)鍵列非空 → 新行開始
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:
# 關(guān)鍵列空 → 補(bǔ)充到當(dāng)前行(僅填充空值)
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)
核心規(guī)則:僅當(dāng)「新行判斷列」非空時(shí),才新建一行;否則將當(dāng)前行的非空數(shù)據(jù)補(bǔ)充到上一行的空值中,保證一行數(shù)據(jù)完整。
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字典,將原圖片的行號(hào)映射到合并后的新行號(hào),保證圖片跟隨對(duì)應(yīng)數(shù)據(jù)行。
3.5 核心4:導(dǎo)出功能
導(dǎo)出功能封裝在export_with_images方法中,核心是用openpyxl寫入數(shù)據(jù)+圖片:
if file_ext == ".xlsx":
# 創(chuàng)建新工作簿
wb = Workbook()
ws = wb.active
ws.title = "數(shù)據(jù)"
# 1. 寫入DataFrame數(shù)據(jù)(保留表頭)
for r in dataframe_to_rows(df, index=False, header=True):
ws.append(r)
# 2. 插入圖片到對(duì)應(yīng)位置
for img, row, col in images:
new_img = copy.deepcopy(img) # 復(fù)制圖片避免引用沖突
new_img.anchor = f"{chr(64+col)}{row}" # 設(shè)置錨點(diǎn)(如A2、B5)
ws.add_image(new_img)
wb.save(file_path)
wb.close()
關(guān)鍵細(xì)節(jié):
dataframe_to_rows:將DataFrame轉(zhuǎn)換為Excel行格式,保留表頭;- 圖片錨點(diǎn):用
chr(64+col)將列號(hào)(數(shù)字)轉(zhuǎn)換為Excel列字母(如1→A,2→B); - 深拷貝圖片:避免多個(gè)圖片引用同一對(duì)象導(dǎo)致導(dǎo)出失敗。
3.6 輔助:進(jìn)度條+多線程
- 多線程:比對(duì)/整理任務(wù)放在獨(dú)立線程中執(zhí)行(
threading.Thread),設(shè)置daemon=True保證線程隨主程序退出; - 進(jìn)度條更新:通過
update_progress方法實(shí)時(shí)刷新進(jìn)度條,調(diào)用root.update_idletasks()強(qiáng)制刷新UI,避免進(jìn)度條卡住。
四、使用注意事項(xiàng)
- 格式限制:僅支持.xlsx(帶圖片)和.csv(無圖片),.xls格式需先轉(zhuǎn)換為.xlsx;
- 圖片位置:Excel中圖片需「嵌入單元格」(而非浮窗),否則行列映射可能出錯(cuò);
- 關(guān)鍵列選擇:整理功能的「新行判斷列」需選有唯一標(biāo)識(shí)的列(如編號(hào)、名稱),否則合并邏輯會(huì)出錯(cuò);
- 大數(shù)據(jù)量:超過10萬行的表格建議拆分處理,避免內(nèi)存占用過高;
- 編碼問題:CSV文件建議用UTF-8編碼,否則可能出現(xiàn)中文亂碼。
五、總結(jié)
這款工具以「解決實(shí)際痛點(diǎn)」為核心,通過Python+Tkinter實(shí)現(xiàn)了輕量化、可定制的表格處理能力,尤其是「圖片保留」功能填補(bǔ)了常規(guī)工具的空白。代碼完全開源,大家可以根據(jù)自己的需求二次開發(fā)(比如增加批量處理、格式轉(zhuǎn)換、更多比對(duì)規(guī)則等)。
無論是辦公人員快速處理表格,還是開發(fā)者學(xué)習(xí)Tkinter+Excel操作,這款工具都有一定的參考價(jià)值。如果有其他需求(比如支持更多格式、自動(dòng)識(shí)別關(guān)鍵列),也可以基于核心邏輯擴(kuò)展。
六、完整代碼
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
# 全局變量存儲(chǔ)文件數(shù)據(jù)(新增圖片存儲(chǔ)字段)
file_a_data = {"sheets": [], "data": {}, "path": "", "images": {}} # images: {sheet: [(img, row, col), ...]}
file_b_data = {"sheets": [], "data": {}, "path": "", "images": {}}
# 新增:存儲(chǔ)共有數(shù)據(jù)及對(duì)應(yīng)圖片
compare_result = {
"a_not_b": None, "b_not_a": None, "a_common": None, "b_common": None,
"a_common_images": [], "b_common_images": []
}
# 整理表格相關(guān)全局變量(新增圖片存儲(chǔ))
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("表格比對(duì)&整理工具")
self.root.geometry("950x850")
# 進(jìn)度條變量
self.progress_var = tk.DoubleVar()
# 構(gòu)建UI
self._create_widgets()
def _create_widgets(self):
# 1. 文件上傳區(qū)域(原有比對(duì)功能)
upload_frame = ttk.LabelFrame(self.root, text="文件上傳(比對(duì)功能)")
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. 文件基本信息區(qū)域(原有)
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. 比對(duì)配置區(qū)域(原有)
config_frame = ttk.LabelFrame(self.root, text="比對(duì)配置(支持不同列比對(duì))")
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")
# 比對(duì)列選擇
ttk.Label(config_frame, text="A表格比對(duì)列:").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表格比對(duì)列:").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. 操作按鈕區(qū)域(新增:導(dǎo)出共有數(shù)據(jù)按鈕)
compare_btn_frame = ttk.Frame(self.root)
compare_btn_frame.pack(padx=10, pady=10)
self.compare_btn = ttk.Button(compare_btn_frame, text="開始比對(duì)", command=self.start_compare, state="disabled")
self.compare_btn.pack(side="left", padx=5)
self.export_a_btn = ttk.Button(compare_btn_frame, text="導(dǎo)出A有B無的數(shù)據(jù)", 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="導(dǎo)出B有A無的數(shù)據(jù)", command=lambda: self.export_result("b_not_a"), state="disabled")
self.export_b_btn.pack(side="left", padx=5)
# 新增:導(dǎo)出共有數(shù)據(jù)按鈕
self.export_common_btn = ttk.Button(compare_btn_frame, text="導(dǎo)出共有數(shù)據(jù)", command=self.export_common_result, state="disabled")
self.export_common_btn.pack(side="left", padx=5)
# ========== 表格整理功能區(qū)域 ==========
clean_frame = ttk.LabelFrame(self.root, text="表格整理功能(解決一行數(shù)據(jù)占多行問題)")
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")
# 選擇關(guān)鍵列(判斷新行的依據(jù):關(guān)鍵列非空=新行)
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")
# 整理/導(dǎo)出按鈕
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="導(dǎo)出整理后表格", command=self.export_cleaned_table, state="disabled")
self.export_clean_btn.pack(side="left", padx=5)
# ========== 進(jìn)度條區(qū)域(共用) ==========
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="進(jìn)度:0%")
self.progress_label.pack(anchor="center")
# ========== 核心修復(fù):支持讀取圖片 ==========
def load_file(self, file_type):
"""加載比對(duì)用的A/B文件(支持讀取圖片)"""
file_path = filedialog.askopenfilename(
title=f"選擇{file_type}文件",
filetypes=[("Excel文件", "*.xlsx"), ("CSV文件", "*.csv"), ("所有文件", "*.*")]
)
if not file_path:
return
# 初始化數(shù)據(jù)
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 = {} # 存儲(chǔ)每個(gè)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]
# 讀取數(shù)據(jù)到DataFrame
df = pd.DataFrame(ws.values)
# 設(shè)置列名(第一行)
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開始,轉(zhuǎn)換為1-based)
sheet_images = []
for img in ws._images:
row = img.anchor._from.row + 1 # 轉(zhuǎn)換為1-based行號(hào)
col = img.anchor._from.col + 1 # 轉(zhuǎn)換為1-based列號(hào)
sheet_images.append((img, row, col))
images_dict[sheet] = sheet_images
wb.close()
elif file_ext == ".csv":
sheets = ["默認(rèn)表格"]
df = pd.read_csv(file_path)
data_dict["默認(rèn)表格"] = df
images_dict["默認(rèn)表格"] = [] # CSV無圖片
else:
messagebox.showerror("錯(cuò)誤", "僅支持xlsx和csv格式文件!")
return
# 更新數(shù)據(jù)和圖片
target_data["sheets"] = sheets
target_data["data"] = data_dict
target_data["images"] = images_dict
# 更新信息顯示(包含圖片數(shù))
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()} | 子表格數(shù)={len(sheets)} | "
info_text += f"首個(gè)表格行數(shù)={len(first_df)} | 列數(shù)={len(first_df.columns)} | 圖片數(shù)={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()} | 子表格數(shù)={len(sheets)} | "
info_text += f"首個(gè)表格行數(shù)={len(first_df)} | 列數(shù)={len(first_df.columns)} | 圖片數(shù)={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}讀取錯(cuò)誤:{str(e)}")
def load_clean_file(self):
"""加載待整理的表格文件(支持讀取圖片)"""
file_path = filedialog.askopenfilename(
title="選擇待整理的表格文件",
filetypes=[("Excel文件", "*.xlsx"), ("CSV文件", "*.csv"), ("所有文件", "*.*")]
)
if not file_path:
return
# 清空原有整理文件數(shù)據(jù)
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]
# 讀取數(shù)據(jù)到DataFrame
df = pd.DataFrame(ws.values)
# 設(shè)置列名(第一行)
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 = ["默認(rèn)表格"]
df = pd.read_csv(file_path)
data_dict["默認(rèn)表格"] = df
images_dict["默認(rèn)表格"] = []
else:
messagebox.showerror("錯(cuò)誤", "僅支持xlsx和csv格式文件!")
return
# 存儲(chǔ)數(shù)據(jù)和圖片
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)
# 更新關(guān)鍵列下拉框(新行判斷列)
self.update_clean_key_columns()
# 啟用整理按鈕
self.clean_btn.config(state="normal")
# 顯示文件信息(包含圖片數(shù))
first_sheet = sheets[0]
first_df = data_dict[first_sheet]
img_count = len(images_dict[first_sheet])
info_text = f"待整理文件信息:格式={file_ext[1:].upper()} | 子表格數(shù)={len(sheets)} | "
info_text += f"首個(gè)表格行數(shù)={len(first_df)} | 列數(shù)={len(first_df.columns)} | 圖片數(shù)={img_count}"
messagebox.showinfo("文件加載成功", info_text)
except Exception as e:
messagebox.showerror("讀取失敗", f"待整理文件讀取錯(cuò)誤:{str(e)}")
# ========== 通用導(dǎo)出函數(shù)(核心修復(fù):支持導(dǎo)出圖片) ==========
def export_with_images(self, df, images, title):
"""通用導(dǎo)出函數(shù)(支持圖片保留)"""
# 選擇導(dǎo)出路徑
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":
# 創(chuàng)建新工作簿
wb = Workbook()
ws = wb.active
ws.title = "數(shù)據(jù)"
# 寫入DataFrame數(shù)據(jù)(保留表頭)
for r in dataframe_to_rows(df, index=False, header=True):
ws.append(r)
# 插入圖片到對(duì)應(yīng)位置
for img, row, col in images:
# 復(fù)制圖片對(duì)象避免引用沖突
new_img = copy.deepcopy(img)
# 設(shè)置圖片錨點(diǎn)(列字母+行號(hào))
new_img.anchor = f"{chr(64+col)}{row}"
ws.add_image(new_img)
# 保存工作簿
wb.save(file_path)
wb.close()
messagebox.showinfo("導(dǎo)出成功", f"數(shù)據(jù)已導(dǎo)出至:{file_path}\n包含圖片:{len(images)}張")
elif file_ext == ".csv":
# CSV不支持圖片,提示用戶
if images:
messagebox.showwarning("格式提示", "CSV格式不支持存儲(chǔ)圖片,圖片將丟失!")
df.to_csv(file_path, index=False, encoding="utf-8-sig")
messagebox.showinfo("導(dǎo)出成功", f"數(shù)據(jù)已導(dǎo)出至:{file_path}\n注意:CSV格式不包含圖片")
except Exception as e:
messagebox.showerror("導(dǎo)出失敗", f"導(dǎo)出錯(cuò)誤:{str(e)}")
# ========== 比對(duì)功能(修復(fù):處理圖片) ==========
def update_compare_columns(self):
"""更新比對(duì)列下拉框"""
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):
"""檢查比對(duì)按鈕狀態(tài)"""
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):
"""更新進(jìn)度條"""
self.progress_var.set(value)
self.progress_label.config(text=f"進(jìn)度:{int(value)}%")
self.root.update_idletasks()
def compare_task(self):
"""比對(duì)任務(wù)(修復(fù):處理共有數(shù)據(jù)圖片)"""
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. 獲取數(shù)據(jù)和圖片
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. 進(jìn)度條初始化
self.update_progress(0)
time.sleep(0.1)
# 4. 數(shù)據(jù)預(yù)處理
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. 核心比對(duì)邏輯
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)
# 共有數(shù)據(jù)
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)
# 處理共有數(shù)據(jù)的圖片(映射行號(hào))
# A格式共有數(shù)據(jù)圖片
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第0行
if orig_df_row in a_common_original_rows:
new_row = a_common_original_rows.index(orig_df_row) + 2 # 新表行2開始是數(shù)據(jù)
a_common_images.append((img, new_row, col))
# B格式共有數(shù)據(jù)圖片
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. 存儲(chǔ)結(jié)果
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. 完成進(jìn)度
self.update_progress(100)
# 8. 啟用導(dǎo)出按鈕
self.export_a_btn.config(state="normal")
self.export_b_btn.config(state="normal")
self.export_common_btn.config(state="normal")
# 顯示結(jié)果(包含圖片數(shù))
messagebox.showinfo("比對(duì)完成",
f"比對(duì)結(jié)果:\nA有B無的數(shù)據(jù)行數(shù):{len(a_not_b_df)}\n"
f"B有A無的數(shù)據(jù)行數(shù):{len(b_not_a_df)}\n"
f"共有數(shù)據(jù)行數(shù)(A格式):{len(a_common_df)} | 圖片數(shù):{len(a_common_images)}\n"
f"共有數(shù)據(jù)行數(shù)(B格式):{len(b_common_df)} | 圖片數(shù):{len(b_common_images)}")
except Exception as e:
messagebox.showerror("比對(duì)失敗", f"比對(duì)過程出錯(cuò):{str(e)}")
self.update_progress(0)
def start_compare(self):
"""啟動(dòng)比對(duì)"""
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):
"""導(dǎo)出比對(duì)結(jié)果(修復(fù):支持圖片)"""
if compare_result[result_type] is None:
messagebox.showwarning("提示", "暫無可導(dǎo)出的數(shù)據(jù)!")
return
# 篩選對(duì)應(yīng)圖片
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無的數(shù)據(jù)"
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無的數(shù)據(jù)"
else:
return
self.export_with_images(df, images, title)
def export_common_result(self):
"""導(dǎo)出共有數(shù)據(jù)(修復(fù):支持圖片)"""
if compare_result["a_common"] is None or compare_result["b_common"] is None:
messagebox.showwarning("提示", "暫無共有數(shù)據(jù)可導(dǎo)出!")
return
# 選擇導(dǎo)出格式
choice = messagebox.askquestion(
"選擇導(dǎo)出格式",
"請(qǐng)選擇共有數(shù)據(jù)的導(dǎo)出格式:\n【是】= 按A表格格式導(dǎo)出(保留A表格列結(jié)構(gòu))\n【否】= 按B表格格式導(dǎo)出(保留B表格列結(jié)構(gòu))",
icon="question"
)
if choice == "yes":
df = compare_result["a_common"]
images = compare_result["a_common_images"]
title = "保存按A表格格式的共有數(shù)據(jù)"
elif choice == "no":
df = compare_result["b_common"]
images = compare_result["b_common_images"]
title = "保存按B表格格式的共有數(shù)據(jù)"
else:
return
self.export_with_images(df, images, title)
# ========== 表格整理功能(修復(fù):處理圖片) ==========
def update_clean_key_columns(self):
"""更新整理功能的關(guān)鍵列下拉框"""
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):
"""表格整理任務(wù)(修復(fù):合并數(shù)據(jù)+保留圖片)"""
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, [])
# 進(jìn)度條初始化
self.update_progress(0)
time.sleep(0.1)
# 預(yù)處理
self.update_progress(10)
df = df.fillna("")
# 核心整理邏輯
self.update_progress(20)
cleaned_rows = []
current_row = None
orig_to_new_row = {} # 原df行號(hào) → 新df行號(hào)映射
new_row_idx = 0
total_rows = len(df)
for idx, row in df.iterrows():
# 更新進(jìn)度
progress = 20 + (idx / total_rows) * 70
self.update_progress(progress)
# 關(guān)鍵列非空 = 新行開始
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:
# 補(bǔ)充到當(dāng)前行
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
# 轉(zhuǎn)換為DataFrame
self.update_progress(90)
cleaned_table_result = pd.DataFrame(cleaned_rows)
# 處理圖片(映射到新行號(hào))
cleaned_table_images = []
for img, orig_row, col in orig_images:
orig_df_row = orig_row - 2 # Excel行 → df行號(hào)
if orig_df_row in orig_to_new_row:
# 新Excel行號(hào) = 新df行號(hào) + 2(行1是表頭)
new_excel_row = orig_to_new_row[orig_df_row] + 2
cleaned_table_images.append((img, new_excel_row, col))
# 完成進(jìn)度
self.update_progress(100)
# 啟用導(dǎo)出按鈕
self.export_clean_btn.config(state="normal")
messagebox.showinfo("整理完成",
f"表格整理成功!\n原行數(shù):{len(df)} | 整理后行數(shù):{len(cleaned_table_result)}\n"
f"原圖片數(shù):{len(orig_images)} | 整理后圖片數(shù):{len(cleaned_table_images)}")
except Exception as e:
messagebox.showerror("整理失敗", f"表格整理出錯(cuò):{str(e)}")
self.update_progress(0)
def start_clean_table(self):
"""啟動(dòng)表格整理"""
if not self.clean_key_col_combobox.get():
messagebox.showwarning("提示", "請(qǐng)選擇新行判斷列!")
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):
"""導(dǎo)出整理后的表格(修復(fù):保留圖片)"""
global cleaned_table_result, cleaned_table_images
if cleaned_table_result is None:
messagebox.showwarning("提示", "暫無整理后的數(shù)據(jù)!")
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()
以上就是基于Python+Tkinter開發(fā)一個(gè)表格比對(duì)和&整理工具的詳細(xì)內(nèi)容,更多關(guān)于Python Tkinter表格對(duì)比和整理的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Python在Excel單元格中應(yīng)用多種字體樣式的代碼示例
文在數(shù)據(jù)處理和報(bào)表生成場(chǎng)景中,Excel 文件的格式設(shè)置至關(guān)重要,合理的字體格式不僅能提升表格的可讀性,還能突出關(guān)鍵數(shù)據(jù),本文將詳細(xì)介紹如何使用免費(fèi)庫Free Spire.XLS for Python,在 Excel 單元格中靈活應(yīng)用多種字體格式,需要的朋友可以參考下2025-05-05
Python實(shí)現(xiàn)圖片轉(zhuǎn)字符畫的示例代碼
本篇文章主要介紹了Python實(shí)現(xiàn)圖片轉(zhuǎn)字符畫的示例代碼,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-08-08
python 實(shí)現(xiàn)簡(jiǎn)單的吃豆人游戲
這篇文章主要介紹了python 如何實(shí)現(xiàn)簡(jiǎn)單的吃豆人游戲,幫助大家更好的理解和學(xué)習(xí)使用python制作游戲,感興趣的朋友可以了解下2021-04-04
Python疫情確診折線圖實(shí)現(xiàn)數(shù)據(jù)可視化實(shí)例詳解
數(shù)據(jù)可視化是指用圖形或表格的方式來呈現(xiàn)數(shù)據(jù)。圖表能夠清楚地呈現(xiàn)數(shù)據(jù)性質(zhì),?以及數(shù)據(jù)間或?qū)傩蚤g的關(guān)系,可以輕易地讓人看圖釋義。用戶通過探索圖(Exploratory?Graph)可以了解數(shù)據(jù)的特性、尋找數(shù)據(jù)的趨勢(shì)、降低數(shù)據(jù)的理解門檻2022-09-09
Python爬蟲之獲取心知天氣API實(shí)時(shí)天氣數(shù)據(jù)并彈窗提醒
今天我們來學(xué)習(xí)如何獲取心知天氣API實(shí)時(shí)天氣數(shù)據(jù),制作彈窗提醒,并設(shè)置成自啟動(dòng)項(xiàng)目.文中有非常詳細(xì)的代碼示例及介紹,對(duì)正在學(xué)習(xí)python的小伙伴們有非常好的幫助,需要的朋友可以參考下2021-05-05
Python mplfinance庫繪制金融圖表實(shí)現(xiàn)數(shù)據(jù)可視化實(shí)例探究
mplfinance(Matplotlib Finance),它是基于Matplotlib的庫,專門用于創(chuàng)建金融圖表和交互式金融數(shù)據(jù)可視化,本文將深入介紹?mplfinance,包括其基本概念、功能特性以及如何使用示例代碼創(chuàng)建各種金融圖表2024-01-01

