

在日常办公、数据分析、财务对账、销售统计等工作中,你是否每天都在重复着枯燥又耗时的操作?打开两个Excel文件,手动筛选数据、制作数据透视表、逐行核对数值差异、调整格式导出报表……不仅效率低下,还极易出现人为错误,加班加点成了常态。尤其是面对多维度数据对比、批量聚合计算时,传统Excel的操作门槛高、灵活性差,稍不注意就会打乱数据结构,让前期的工作全部白费。
有没有一款工具,能一键读取Excel文件,可视化配置数据透视表,自动对比两份数据的差异,还能一键导出美观的Excel/PDF报表?不用复杂的函数公式,不用繁琐的拖拽设置,界面精致高级、操作简单易懂,哪怕是零基础的办公小白也能轻松上手?今天就给大家带来一款基于Python+PyQt5开发的双文件数据透视&智能对比工具!
这款工具完美融合了数据处理的核心需求与高颜值UI设计,采用清新不透明的配色方案,左右分栏双面板独立操作,支持自定义行、列、值字段与六种聚合函数,自动计算数值差异/百分比差异,差异结果自动上色标注,还能一键导出带格式的专业报表。从数据导入、透视分析、智能对比到报表输出,全流程一站式搞定,彻底解放你的双手,让数据分析从繁琐加班变成高效轻松,职场竞争力直接翻倍!
这款工具整体分为三大核心模块,每个模块各司其职,完美覆盖数据处理全流程,功能齐全且操作极简:
这是整个工具的数据心脏,我们没有依赖笨重的Pandas库,而是纯手写实现了轻量级数据框,完美适配Excel读写、数据透视表核心功能,轻量化无冗余,运行速度更快。
这是工具的操作核心,采用左右分栏设计,绿色+蓝色清新配色,不透明高级质感,支持两个Excel文件独立透视分析,互不干扰:
这是工具的总控中心,整合双面板数据,实现智能对比与专业报表导出,功能闭环:
import sysimport randomfrom datetime import datetimefrom PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QHBoxLayout, QVBoxLayout, QGroupBox, QPushButton, QComboBox, QLineEdit, QSpinBox, QListWidget, QTableWidget, QTableWidgetItem, QCheckBox, QFileDialog, QMessageBox, QSplitter, QLabel, QDialog)from PyQt5.QtCore import Qtfrom PyQt5.QtGui import QColorfrom fpdf import FPDF# 聚合函数映射AGG_FUNCTIONS = {"sum": "求和","mean": "平均值","count": "计数","max": "最大值","min": "最小值","std": "标准差"}# 轻量级数据框,实现Excel读写和透视表classMiniDataFrame:def__init__(self, columns=None, data=None): self.columns = columns or [] self.data = data or []def_col_idx(self, col_name):return self.columns.index(col_name)# 核心:数据透视表实现defpivot_table(self, index, columns, values, aggfunc, fill_value=0):from collections import defaultdict index_keys = [] col_keys = [] data_dict = defaultdict(lambda: defaultdict(list))for row in self.data: idx_vals = tuple(row[self._col_idx(c)] for c in index) col_vals = tuple(row[self._col_idx(c)] for c in columns)for v in values:try: num = float(row[self._col_idx(v)])except: num = 0 data_dict[idx_vals][col_vals].append(num)# 聚合函数映射 agg_map = {"sum": sum,"mean": lambda x: sum(x)/len(x) if x else0,"count": len,"max": max,"min": min,"std": lambda x: ((sum((i-sum(x)/len(x))**2for i in x)/len(x))**0.5) if len(x)>=1else0 } agg = agg_map[aggfunc] all_idx = sorted(data_dict.keys()) all_col = sorted({c for vs in data_dict.values() for c in vs.keys()})# 构建新表头 new_cols = list(index) val_headers = [f"{v}|{'|'.join(c)}"for v in values for c in all_col] new_cols += val_headers# 构建新数据 new_data = []for idx in all_idx: row = list(idx)for c in all_col: vals = data_dict[idx].get(c, []) row.append(agg(vals) if vals else fill_value) new_data.append(row)return MiniDataFrame(new_cols, new_data)# 导出Exceldefto_excel(self, path, index=False):import xlwt wb = xlwt.Workbook() ws = wb.add_sheet("Sheet1")for i, col in enumerate(self.columns): ws.write(0, i, col)for r, row in enumerate(self.data):for c, val in enumerate(row): ws.write(r+1, c, val) wb.save(path)# 读取Excel @staticmethoddefread_excel(path, sheet_name=0, header=0):import xlrd wb = xlrd.open_workbook(path) ws = wb.sheet_by_name(sheet_name) if isinstance(sheet_name, str) else wb.sheet_by_index(sheet_name) headers = [ws.cell_value(header, i) for i in range(ws.ncols)] data = [[ws.cell_value(r, i) for i in range(ws.ncols)] for r in range(header+1, ws.nrows)]return MiniDataFrame(headers, data)# 数据拷贝defcopy(self):return MiniDataFrame(self.columns.copy(), [row.copy() for row in self.data])# 透视表操作面板(左右双面板)classPivotPanel(QWidget):def__init__(self, color="#e8f5e9"): super().__init__() self.df = None self.pivot_df = None self.init_ui(color)definit_ui(self, color): main_layout = QVBoxLayout()# 文件控制面板 file_group = QGroupBox("文件1 - 控制面板"if color == "#e8f5e9"else"文件2 - 控制面板") file_group.setStyleSheet(f"QGroupBox {{ background-color: {color}; }}") file_layout = QHBoxLayout() file_group.setLayout(file_layout) self.btn_select_file = QPushButton("选择Excel文件") self.file_path_edit = QLineEdit(readOnly=True) self.combo_sheet = QComboBox() self.spin_header = QSpinBox(minimum=1, value=1) file_left = QVBoxLayout() file_left.addWidget(QLabel("文件选择")) file_left.addWidget(self.btn_select_file) file_left.addWidget(self.file_path_edit) file_right = QHBoxLayout() file_right.addWidget(QLabel("工作表:")) file_right.addWidget(self.combo_sheet) file_right.addWidget(QLabel("标题行:")) file_right.addWidget(self.spin_header) file_layout.addLayout(file_left) file_layout.addLayout(file_right) main_layout.addWidget(file_group)# 字段配置区 field_group = QGroupBox() field_group.setStyleSheet(f"QGroupBox {{ background-color: {color}; }}") field_layout = QHBoxLayout() field_group.setLayout(field_layout) self.list_available = QListWidget(selectionMode=QListWidget.ExtendedSelection) self.list_row = QListWidget(selectionMode=QListWidget.ExtendedSelection) self.list_col = QListWidget(selectionMode=QListWidget.ExtendedSelection) self.list_value = QListWidget(selectionMode=QListWidget.ExtendedSelection)# 可用字段 la = QVBoxLayout() la.addWidget(QLabel("可用字段")) la.addWidget(self.list_available) field_layout.addLayout(la)# 行字段 lr = QVBoxLayout() lr.addWidget(QLabel("行字段")) lr.addWidget(self.list_row) ar = QHBoxLayout() btn_r_add = QPushButton("添加") btn_r_del = QPushButton("移除") ar.addWidget(btn_r_add) ar.addWidget(btn_r_del) lr.addLayout(ar) field_layout.addLayout(lr)# 列字段 lc = QVBoxLayout() lc.addWidget(QLabel("列字段")) lc.addWidget(self.list_col) ac = QHBoxLayout() btn_c_add = QPushButton("添加") btn_c_del = QPushButton("移除") ac.addWidget(btn_c_add) ac.addWidget(btn_c_del) lc.addLayout(ac) field_layout.addLayout(lc)# 值字段+聚合函数 lv = QVBoxLayout() lv.addWidget(QLabel("值字段")) lv.addWidget(self.list_value) av = QHBoxLayout() btn_v_add = QPushButton("添加") btn_v_del = QPushButton("移除") av.addWidget(btn_v_add) av.addWidget(btn_v_del) lv.addLayout(av) agg_layout = QHBoxLayout() agg_layout.addWidget(QLabel("聚合函数:")) self.combo_agg = QComboBox() self.combo_agg.addItems(AGG_FUNCTIONS.values()) agg_layout.addWidget(self.combo_agg) lv.addLayout(agg_layout) field_layout.addLayout(lv) main_layout.addWidget(field_group)# 操作按钮 btn_layout = QHBoxLayout() self.btn_preview = QPushButton("预览透视表") self.btn_export = QPushButton("导出到Excel") btn_layout.addWidget(self.btn_preview) btn_layout.addWidget(self.btn_export) main_layout.addLayout(btn_layout)# 原始数据表格 g1 = QGroupBox("原始数据") g1.setStyleSheet(f"background-color: {color};") t1 = QVBoxLayout() self.table_raw = QTableWidget() t1.addWidget(self.table_raw) g1.setLayout(t1) main_layout.addWidget(g1)# 透视表结果 g2 = QGroupBox("数据透视表") g2.setStyleSheet(f"background-color: {color};") t2 = QVBoxLayout() self.table_pivot = QTableWidget() t2.addWidget(self.table_pivot) g2.setLayout(t2) main_layout.addWidget(g2) self.setLayout(main_layout)# 信号绑定 self.btn_select_file.clicked.connect(self.select_file) self.combo_sheet.currentIndexChanged.connect(self.on_sheet_changed) self.spin_header.valueChanged.connect(self.on_sheet_changed) self.btn_preview.clicked.connect(self.preview_pivot) self.btn_export.clicked.connect(self.export_pivot)# 字段移动按钮 btn_r_add.clicked.connect(lambda: self.move(self.list_available, self.list_row)) btn_r_del.clicked.connect(lambda: self.move(self.list_row, self.list_available)) btn_c_add.clicked.connect(lambda: self.move(self.list_available, self.list_col)) btn_c_del.clicked.connect(lambda: self.move(self.list_col, self.list_available)) btn_v_add.clicked.connect(lambda: self.move(self.list_available, self.list_value)) btn_v_del.clicked.connect(lambda: self.move(self.list_value, self.list_available))# 字段移动方法defmove(self, src, dst):for item in src.selectedItems(): dst.addItem(item.text()) src.takeItem(src.row(item))# 选择Excel文件defselect_file(self): path, _ = QFileDialog.getOpenFileName(filter="Excel (*.xlsx *.xls)")ifnot path:return self.file_path_edit.setText(path)try:import xlrd xl = xlrd.open_workbook(path) self.combo_sheet.clear() self.combo_sheet.addItems(xl.sheet_names())except Exception as e: QMessageBox.critical(self, "错误", f"读取失败:{str(e)}")# 工作表切换事件defon_sheet_changed(self):ifnot self.file_path_edit.text():returntry: self.df = MiniDataFrame.read_excel( self.file_path_edit.text(), sheet_name=self.combo_sheet.currentText(), header=self.spin_header.value() - 1 ) self.list_available.clear() self.list_available.addItems(self.df.columns) self.show_raw()except Exception as e: QMessageBox.critical(self, "错误", f"加载失败:{str(e)}")# 显示原始数据defshow_raw(self): self.table_raw.setRowCount(len(self.df.data)) self.table_raw.setColumnCount(len(self.df.columns)) self.table_raw.setHorizontalHeaderLabels(self.df.columns)for r, row in enumerate(self.df.data):for c, val in enumerate(row): self.table_raw.setItem(r, c, QTableWidgetItem(str(val)))# 获取列表所有字段defget_list(self, lst):return [lst.item(i).text() for i in range(lst.count())]# 预览透视表defpreview_pivot(self): rows = self.get_list(self.list_row) cols = self.get_list(self.list_col) vals = self.get_list(self.list_value)ifnot vals: QMessageBox.warning(self, "提示", "请选择值字段")returntry: agg = next(k for k, v in AGG_FUNCTIONS.items() if v == self.combo_agg.currentText()) self.pivot_df = self.df.pivot_table(index=rows, columns=cols, values=vals, aggfunc=agg, fill_value=0) self.show_pivot()except Exception as e: QMessageBox.critical(self, "错误", f"透视表失败:{str(e)}")# 显示透视表结果defshow_pivot(self): self.table_pivot.setRowCount(len(self.pivot_df.data)) self.table_pivot.setColumnCount(len(self.pivot_df.columns)) self.table_pivot.setHorizontalHeaderLabels(self.pivot_df.columns)for r, row in enumerate(self.pivot_df.data):for c, val in enumerate(row): self.table_pivot.setItem(r, c, QTableWidgetItem(str(val)))# 导出透视表defexport_pivot(self):if self.pivot_df isNone: QMessageBox.warning(self, "提示", "请先生成透视表")return path, _ = QFileDialog.getSaveFileName(filter="Excel (*.xlsx)")if path: self.pivot_df.to_excel(path, index=False) QMessageBox.information(self, "成功", "导出完成")# 获取透视表数据defget_pivot_data(self):return self.pivot_df# 主窗口classMainWindow(QMainWindow):def__init__(self): super().__init__() self.setWindowTitle("Excel数据透视&对比工具") self.setGeometry(100, 100, 1600, 900) self.init_ui()definit_ui(self): w = QWidget() self.setCentralWidget(w) main_layout = QVBoxLayout(w)# 左右双面板 splitter = QSplitter(Qt.Horizontal) self.left = PivotPanel("#e8f5e9") self.right = PivotPanel("#e3f2fd") splitter.addWidget(self.left) splitter.addWidget(self.right) main_layout.addWidget(splitter)# 对比设置 g = QGroupBox("对比设置") g.setStyleSheet("background-color: #f3e5f5;") h = QHBoxLayout(g) h.addWidget(QLabel("对比类型:")) self.cb_type = QComboBox() self.cb_type.addItems(["数值差异", "百分比差异"]) h.addWidget(self.cb_type) self.cb_auto = QCheckBox("自动匹配字段", checked=True) h.addWidget(self.cb_auto) main_layout.addWidget(g)# 对比&导出按钮 btn_layout = QHBoxLayout() self.btn_cmp = QPushButton("对比两个透视表") self.btn_exp = QPushButton("导出对比结果(Excel/PDF)") btn_layout.addWidget(self.btn_cmp) btn_layout.addWidget(self.btn_exp) main_layout.addLayout(btn_layout)# 对比结果表格 self.table_cmp = QTableWidget() main_layout.addWidget(self.table_cmp) self.result = None# 信号绑定 self.btn_cmp.clicked.connect(self.do_compare) self.btn_exp.clicked.connect(self.export_cmp_with_style)# 执行数据对比defdo_compare(self): a = self.left.get_pivot_data() b = self.right.get_pivot_data()if a isNoneor b isNone: QMessageBox.warning(self, "提示", "请先生成透视表")return common = [c for c in a.columns if c in b.columns]ifnot common: QMessageBox.warning(self, "提示", "无共同字段")returntry: res_cols = [] res_data = []for a_row, b_row in zip(a.data, b.data): new_row = []for c in common: ai = a.columns.index(c) bi = b.columns.index(c) av = float(a_row[ai]) if str(a_row[ai]).replace('.','').replace('-','').isdigit() else0 bv = float(b_row[bi]) if str(b_row[bi]).replace('.','').replace('-','').isdigit() else0 new_row += [av, bv]# 计算差异if self.cb_type.currentText() == "数值差异": new_row.append(av - bv)else: new_row.append(((av - bv) / bv) * 100if bv != 0else0) res_data.append(new_row)# 构建结果表头for c in common: res_cols += [f"文件1_{c}", f"文件2_{c}", f"差异_{c}"] self.result = MiniDataFrame(res_cols, res_data) self.show_cmp() QMessageBox.information(self, "成功", "对比完成")except Exception as e: QMessageBox.critical(self, "错误", f"对比失败:{str(e)}")# 显示对比结果(自动上色)defshow_cmp(self): df = self.result self.table_cmp.setRowCount(len(df.data)) self.table_cmp.setColumnCount(len(df.columns)) self.table_cmp.setHorizontalHeaderLabels(df.columns)for r, row in enumerate(df.data):for c, val in enumerate(row):try: txt = f"{float(val):.2f}"except: txt = str(val) item = QTableWidgetItem(txt) item.setTextAlignment(Qt.AlignCenter)# 差异列自动上色if"差异"in df.columns[c]:try: num = float(val)if num > 0: item.setBackground(QColor(144,238,144)) # 绿色elif num < 0: item.setBackground(QColor(250,128,114)) # 红色else: item.setBackground(QColor(211,211,211)) # 灰色except:pass self.table_cmp.setItem(r, c, item)# 生成唯一文件名defgenerate_filename(self): time_str = datetime.now().strftime("%Y%m%d%H%M%S") rand_str = str(random.randint(10000,99999))returnf"对比结果_{time_str}_{rand_str}"# 导出选择弹窗defexport_cmp_with_style(self):if self.result isNoneor self.table_cmp.rowCount() == 0: QMessageBox.warning(self, "提示", "请先生成对比结果")return dialog = QDialog(self) dialog.setWindowTitle("选择导出格式") dialog.setFixedSize(300,150) layout = QVBoxLayout(dialog) layout.addWidget(QLabel("请选择导出格式:")) hbox = QHBoxLayout() btn_xls = QPushButton("导出 Excel") btn_pdf = QPushButton("导出 PDF") hbox.addWidget(btn_xls) hbox.addWidget(btn_pdf) layout.addLayout(hbox)defexp_xls(): dialog.close(); self.export_to_excel()defexp_pdf(): dialog.close(); self.export_to_pdf() btn_xls.clicked.connect(exp_xls) btn_pdf.clicked.connect(exp_pdf) dialog.exec_()# 导出Exceldefexport_to_excel(self): folder = QFileDialog.getExistingDirectory()ifnot folder: return path = f"{folder}/{self.generate_filename()}.xlsx" self.result.to_excel(path) QMessageBox.information(self, "成功", f"Excel已保存:\n{path}")# 导出PDF(带颜色格式)defexport_to_pdf(self): folder = QFileDialog.getExistingDirectory()ifnot folder: return path = f"{folder}/{self.generate_filename()}.pdf" pdf = FPDF() pdf.add_page() pdf.set_font("Arial", size=10) pdf.cell(200, 10, txt="数据对比报表", ln=True, align='C') pdf.ln(5) col_width = 28 headers = self.result.columns# 表头for h in headers: pdf.cell(col_width, 10, h[:8], border=1, align='C') pdf.ln()# 数据行for row in self.result.data:for i, val in enumerate(row): txt = f"{val:.2f}"if isinstance(val, (int, float)) else str(val)if"差异"in headers[i]:try: num = float(val)if num > 0: pdf.set_fill_color(144,238,144)elif num < 0: pdf.set_fill_color(250,128,114)else: pdf.set_fill_color(211,211,211) pdf.cell(col_width, 10, txt, border=1, align='C', fill=True)except: pdf.cell(col_width, 10, txt, border=1, align='C')else: pdf.cell(col_width, 10, txt, border=1, align='C') pdf.ln() pdf.output(path) QMessageBox.information(self, "成功", f"PDF已保存:\n{path}")if __name__ == "__main__": app = QApplication(sys.argv) win = MainWindow() win.show() sys.exit(app.exec_())运行前需要安装第三方库,打开命令提示符执行以下命令:
pip install PyQt5 xlrd xlwt fpdf这款工具将Python的强大数据处理能力与高颜值GUI完美结合,无需编程基础就能使用,彻底解决职场数据处理的痛点。不管是日常办公还是专业数据分析,都能大幅提升效率,让你从繁琐的重复劳动中解放出来!