乐于分享
好东西不私藏

Excel多Sheet合并工具:彻底解放双手,告别无效加班

Excel多Sheet合并工具:彻底解放双手,告别无效加班

真的搞不懂,为什么合并Excel表格永远是职场里最磨人的“体力活”?

明明数据都在,却要一个个打开文件,复制粘贴再对齐。稍微遇到表头不一致、格式不一样的情况,整个人都陷入僵局——改格式、对列名,来来回回折腾半天,大文件稍微大一点,Excel直接卡死,前功尽弃。这种反复重复的机械操作,既耗精力又易出错,完全就是在浪费时间。

这款Excel多Sheet合并工具,就是为了解决这些糟心事而生的。它不用你手动操作,选好文件夹、配好参数,一键就能搞定所有表格合并,把你从繁琐的工作里解放出来。

一、核心功能,精准直击需求

  1. 1. 批量识别合并:自动扫描指定文件夹,一次性识别.xlsx、.xls、.csv三种格式文件,无需逐个筛选,直接批量处理,几十上百个文件也能快速搞定。
  2. 2. 表头智能适配:支持自动识别所有表格的共有表头,也能手动指定需要合并的表头列,彻底解决“表头名称不一致”“列错位”的问题,合并后数据整齐不乱。
  3. 3. 灵活跳过设置:可根据表格格式,自定义跳过表头行数(比如跳过1-3行标题),避免重复表头干扰,适配不同规范的表格文件。
  4. 4. 进度实时可视:合并过程中同步显示进度条和操作日志,清晰记录每个文件的处理状态,让你随时掌握进度,不用干等。
  5. 5. 保留原格式:合并时自动保留各表格的单元格格式、字体、边框、颜色等,合并完成后无需二次调整格式,直接可用。
  6. 6. 便捷参数备份:支持配置导入/导出,下次使用相同设置时直接加载,不用重复配置,省心又高效。

二、使用操作说明

1. 准备工作

打开工具后,先确认目标文件夹里,存放了需要合并的所有Excel/CSV文件,整理好放在同一目录下,避免文件混乱。

2. 选择目标文件夹

点击工具界面上的“浏览”按钮,在电脑中找到存放表格的文件夹,选中并确认,工具会自动填充文件夹路径。

3. 基础参数设置

  • • 跳过行数:根据表格表头情况填写,比如表格第一行是标题、第二行是真正表头,就填“2”;没有多余标题,直接填“1”。
  • • 包含CSV:如果文件夹里有CSV文件,勾选这个选项,工具会自动识别并合并;只有Excel文件,就保持默认不勾选。
  • • 合并模式:选择“全列合并”,工具会自动匹配所有相同列名的数据,精准合并。

4. 表头配置

如果表格表头名称差异较大,可手动添加需要合并的表头名称:

  • • 点击“+ 添加”按钮,输入表头名称,比如“姓名”“销售额”;
  • • 多余的表头可点击“- 删除”或“清空”按钮调整,确保配置的表头和表格中列名一致。

5. 启动合并

确认所有参数设置无误后,点击绿色的“开始合并”按钮,工具会自动开始处理文件,此时耐心等待进度条走完,日志区域会显示每个文件的处理结果。

6. 查看结果

合并完成后,工具会弹出提示框告知“合并成功”,并显示合并后文件的保存路径(默认在目标文件夹下,命名为“合并后的总表.xlsx”),直接打开即可使用。

7. 异常处理

如果合并过程中出现提示“读取文件失败”,检查文件是否损坏、是否有读取权限;若提示“无有效数据”,确认文件夹内是否有符合格式的表格文件,简单调整后重新尝试即可。

8.完整代码

"""Excel 表格合并工具 V2.0支持多 sheet、自定义表头、跳过行数配置、共同列/全列合并模式增强对各种表格格式的兼容性"""import sysimport osimport pandas as pdfrom pathlib import Pathfrom datetime import datetimefrom typing importListDictOptionalSetimport warningsfrom PySide6.QtWidgets import (    QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,    QLabel, QPushButton, QLineEdit, QTableWidget, QTableWidgetItem,    QCheckBox, QSpinBox, QGroupBox, QTextEdit, QFileDialog, QMessageBox,    QHeaderView, QAbstractItemView, QSplitter, QFrame, QProgressBar,    QComboBox, QDialog, QDialogButtonBox, QListWidget, QListWidgetItem,    QScrollArea, QSizePolicy, QStyledItemDelegate, QStyleOptionProgressBar,    QStyle, QStyleFactory)from PySide6.QtCore import Qt, QThread, Signal, QSettings, QSize, QTimerfrom PySide6.QtGui import QFont, QIcon, QColor, QPalette, QPainter, QBrush# 忽略所有警告warnings.filterwarnings('ignore')classMergeWorker(QThread):"""后台合并工作线程 - 增强版,支持多种表格格式"""    progress = Signal(int)    log = Signal(str)    finished_signal = Signal(boolstr)def__init__(self, folder_path: str, headers: List[str], skip_rows: int                 merge_mode: str, include_csv: bool = False, parent=None):super().__init__(parent)self.folder_path = folder_pathself.headers = headersself.skip_rows = skip_rowsself.merge_mode = merge_mode  # 'common' 或 'all'self.include_csv = include_csvself.is_running = Truedefstop(self):self.is_running = Falsedefclean_column_name(self, col):"""清理列名 - 处理各种格式"""if pd.isna(col):returnf"未命名列_{id(col)}"ifisinstance(col, (intfloat)):returnstr(int(col)) iffloat(col).is_integer() elsestr(col)ifisinstance(col, datetime):return col.strftime("%Y-%m-%d %H:%M:%S")# 转为字符串并去除空格        col_str = str(col).strip()# 处理空白字符串ifnot col_str or col_str.lower() in ['nan''none''null''']:returnf"未命名列_{hash(str(col))}"return col_strdefclean_dataframe_values(self, df):"""清理 DataFrame 中的所有值"""        df_clean = df.copy()for col in df_clean.columns:# 将 NaN 和 None 替换为空字符串            df_clean[col] = df_clean[col].apply(lambda x: ''if pd.isna(x) orstr(x).lower() in ['nan''none''null'''else x            )# 转换日期等特殊类型            df_clean[col] = df_clean[col].apply(lambda x: x.strftime("%Y-%m-%d %H:%M:%S"ifisinstance(x, datetime) else x            )return df_cleandefread_excel_safely(self, file_path, sheet_name, skip_rows):"""安全读取 Excel 文件"""try:# 首先尝试使用引擎读取for engine in ['openpyxl''xlrd']:try:ifstr(file_path).lower().endswith('.csv'):                        df = pd.read_csv(str(file_path), header=skip_rows, encoding='utf-8-sig')else:                        df = pd.read_excel(str(file_path), sheet_name=sheet_name,                                           header=skip_rows, engine=engine)return dfexcept Exception:continue# 最后的备选方案ifstr(file_path).lower().endswith('.csv'):return pd.read_csv(str(file_path), header=skip_rows, encoding='utf-8-sig'                                 on_bad_lines='skip')else:return pd.read_excel(str(file_path), sheet_name=sheet_name,                                    header=skip_rows, on_bad_lines='skip')except Exception as e:raise Exception(f"读取失败: {str(e)}")defrun(self):try:self.log.emit("🔍 开始扫描目录...")# 获取所有支持的文件            file_patterns = ['*.xlsx''*.xls']ifself.include_csv:                file_patterns.append('*.csv')            all_files = []for pattern in file_patterns:                all_files.extend(Path(self.folder_path).rglob(pattern))# 去重(处理大小写不同但实际相同的文件)            seen = set()            excel_files = []for f in all_files:                normalized = str(f).lower()if normalized notin seen:                    seen.add(normalized)                    excel_files.append(f)ifnot excel_files:self.finished_signal.emit(False"未找到支持的文件(.xlsx, .xls" +                                         (", .csv)"ifself.include_csv else")"))returnself.log.emit(f"📁 找到 {len(excel_files)} 个文件")            all_data = []            all_columns: Set[str] = set()            file_columns: Dict[strSet[str]] = {}            stats = {'success'0,'failed'0,'empty_sheets'0,'total_rows'0            }# 第一遍:收集所有列信息self.log.emit("\n📊 正在分析文件结构...")for i, file_path inenumerate(excel_files):ifnotself.is_running:self.finished_signal.emit(False"⚠️ 操作已取消")returntry:# 处理不同文件类型ifstr(file_path).lower().endswith('.csv'):# CSV 文件处理try:# 尝试不同编码for encoding in ['utf-8-sig''utf-8''gbk''gb2312']:try:                                    df_sample = pd.read_csv(str(file_path), nrows=5, encoding=encoding)breakexcept:continue                            file_cols = set()for col in df_sample.columns:                                file_cols.add(self.clean_column_name(col))                            file_columns[str(file_path)] = file_cols                            all_columns.update(file_cols)self.log.emit(f"  📄 {file_path.name} ({len(file_cols)} 列)")except Exception as e:self.log.emit(f"  ❌ {file_path.name}{str(e)}")                            stats['failed'] += 1else:# Excel 文件处理                        xl = pd.ExcelFile(str(file_path))                        file_cols = set()for sheet_name in xl.sheet_names:try:# 只读取表头行                                df_sample = pd.read_excel(str(file_path),                                     sheet_name=sheet_name,                                    header=self.skip_rows,                                    nrows=0                                )# 清理列名for col in df_sample.columns:                                    cleaned = self.clean_column_name(col)                                    file_cols.add(cleaned)except Exception as e:self.log.emit(f"    ⚠️ Sheet '{sheet_name}': {str(e)}")if file_cols:                            file_columns[str(file_path)] = file_cols                            all_columns.update(file_cols)self.log.emit(f"  📊 {file_path.name} ({len(file_cols)} 列)")else:self.log.emit(f"  ⚠️ {file_path.name}: 未找到有效列")                            stats['failed'] += 1except Exception as e:self.log.emit(f"  ❌ {file_path.name}{str(e)}")                    stats['failed'] += 1# 更新进度                progress_value = int((i + 1) / len(excel_files) * 25)self.progress.emit(progress_value)# 处理列名            all_columns_clean = {self.clean_column_name(col) for col in all_columns if col}ifnot all_columns_clean:self.finished_signal.emit(False"未找到任何有效的列名")return# 确定要合并的列self.log.emit(f"\n📋 共发现 {len(all_columns_clean)} 个不同列")ifself.merge_mode == 'common':# 共同列模式if file_columns:iflen(file_columns) == 1:                        common_cols_clean = set(list(file_columns.values())[0])self.log.emit(f"ℹ️ 单文件模式,使用所有 {len(common_cols_clean)} 列")else:                        common_cols_clean = set.intersection(*file_columns.values())self.log.emit(f"✓ 多文件模式,找到 {len(common_cols_clean)} 个共同列")ifself.headers:                        headers_clean = [self.clean_column_name(h) for h inself.headers]                        target_cols = [h for h in headers_clean if h in common_cols_clean]ifnot target_cols:self.log.emit(f"⚠️ 配置表头不在共同列中,使用共同列")                            target_cols = sorted(list(common_cols_clean))else:self.log.emit(f"✓ 使用 {len(target_cols)} 个配置的表头")else:                        target_cols = sorted(list(common_cols_clean))else:                    target_cols = []else:# 全列模式ifself.headers:                    headers_clean = [self.clean_column_name(h) for h inself.headers]                    target_cols = [h for h in headers_clean if h in all_columns_clean]if target_cols:self.log.emit(f"✓ 使用 {len(target_cols)} 个配置的表头")else:                    target_cols = sorted(list(all_columns_clean))self.log.emit(f"✓ 使用全部 {len(target_cols)} 列")# 显示要合并的列if target_cols:self.log.emit(f"\n✅ 将合并以下 {len(target_cols)} 列:")for i inrange(0len(target_cols), 6):self.log.emit(f"   • {', '.join(target_cols[i:i+6])}")else:                error_msg = "❌ 未找到可合并的列\n\n可能原因:\n"                error_msg += "1. 配置的表头与实际列名不匹配\n"                error_msg += "2. 跳过行数设置不当\n"                error_msg += f"\n实际列名 ({len(all_columns_clean)} 个):\n"                error_msg += '\n'.join([f"  • {c}"for c insorted(list(all_columns_clean))[:20]])iflen(all_columns_clean) > 20:                    error_msg += f"\n  ... 还有 {len(all_columns_clean) - 20} 列"self.finished_signal.emit(False, error_msg)return# 第二遍:读取实际数据self.log.emit("\n📥 正在读取数据...")            total_rows = 0for i, file_path inenumerate(excel_files):ifnotself.is_running:self.finished_signal.emit(False"⚠️ 操作已取消")returntry:ifstr(file_path).lower().endswith('.csv'):# CSV 文件try:                            df = pd.read_csv(str(file_path), encoding='utf-8-sig'                                           on_bad_lines='skip')                            df.columns = [self.clean_column_name(col) for col in df.columns]                            df = self.clean_dataframe_values(df)                            available_cols = [col for col in target_cols if col in df.columns]if available_cols:                                df_filtered = df[available_cols].copy()                                df_filtered['__来源文件__'] = file_path.name                                df_filtered['__来源Sheet__'] = 'CSV'                                all_data.append(df_filtered)                                total_rows += len(df_filtered)                                stats['success'] += 1self.log.emit(f"  ✓ {file_path.name}{len(df_filtered)} 行")else:self.log.emit(f"  ⚠️ {file_path.name}: 无匹配列")                                stats['empty_sheets'] += 1except Exception as e:self.log.emit(f"  ❌ {file_path.name}{str(e)}")                            stats['failed'] += 1else:# Excel 文件                        xl = pd.ExcelFile(str(file_path))for sheet_name in xl.sheet_names:try:                                df = self.read_excel_safely(file_path, sheet_name, self.skip_rows)# 清理列名                                df.columns = [self.clean_column_name(col) for col in df.columns]                                df = self.clean_dataframe_values(df)# 只保留目标列                                available_cols = [col for col in target_cols if col in df.columns]if available_cols andlen(df) > 0:                                    df_filtered = df[available_cols].copy()                                    df_filtered['__来源文件__'] = file_path.name                                    df_filtered['__来源Sheet__'] = sheet_name                                    all_data.append(df_filtered)                                    total_rows += len(df_filtered)                                    stats['success'] += 1self.log.emit(f"  ✓ {file_path.name} - {sheet_name}{len(df_filtered)} 行")else:                                    stats['empty_sheets'] += 1except Exception as e:self.log.emit(f"  ❌ {file_path.name} - {sheet_name}{str(e)}")except Exception as e:self.log.emit(f"  ❌ {file_path.name}{str(e)}")                    stats['failed'] += 1# 更新进度                progress_value = int(25 + (i + 1) / len(excel_files) * 55)self.progress.emit(progress_value)ifnot all_data:self.finished_signal.emit(False"未读取到有效数据,请检查文件格式")return# 合并数据self.log.emit(f"\n🔄 正在合并 {len(all_data)} 个数据块...")            merged_df = pd.concat(all_data, ignore_index=True)# 生成输出文件名            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")            output_path = Path(self.folder_path) / f"合并结果_{timestamp}.xlsx"# 保存self.log.emit(f"💾 正在保存到: {output_path.name}")try:with pd.ExcelWriter(str(output_path), engine='openpyxl'as writer:                    merged_df.to_excel(writer, sheet_name='合并数据', index=False)# 添加汇总信息                    summary_data = [                        ['统计项目''数值'],                        ['合并文件数'len(excel_files)],                        ['成功处理', stats['success']],                        ['处理失败', stats['failed']],                        ['空表格数', stats['empty_sheets']],                        ['总数据行数', total_rows],                        ['合并列数'len(target_cols)],                        ['合并模式''共同列'ifself.merge_mode == 'common'else'全列'],                        ['跳过行数'self.skip_rows],                        ['生成时间', datetime.now().strftime("%Y-%m-%d %H:%M:%S")]                    ]                    summary_df = pd.DataFrame(summary_data[1:], columns=summary_data[0])                    summary_df.to_excel(writer, sheet_name='汇总信息', index=False)self.progress.emit(100)# 成功日志self.log.emit("\n" + "="*60)self.log.emit(f"✅ 合并成功!")self.log.emit(f"   📁 输出文件: {output_path.name}")self.log.emit(f"   📊 总行数: {total_rows}")self.log.emit(f"   📋 列数: {len(target_cols)}")self.log.emit(f"   ✓ 成功: {stats['success']} | ✗ 失败: {stats['failed']} | ⚠ 空表: {stats['empty_sheets']}")self.log.emit("="*60)self.finished_signal.emit(Truestr(output_path))except Exception as e:self.finished_signal.emit(Falsef"保存失败: {str(e)}")except Exception as e:import tracebackself.finished_signal.emit(Falsef"合并失败: {str(e)}\n{traceback.format_exc()}")classHeaderDialog(QDialog):"""添加表头对话框"""def__init__(self, parent=None):super().__init__(parent)self.setWindowTitle("添加表头")self.setMinimumWidth(300)self.setup_ui()defsetup_ui(self):        layout = QVBoxLayout(self)        layout.addWidget(QLabel("表头名称:"))self.header_input = QLineEdit()self.header_input.setPlaceholderText("请输入表头名称")        layout.addWidget(self.header_input)        buttons = QDialogButtonBox(            QDialogButtonBox.Ok | QDialogButtonBox.Cancel        )        buttons.accepted.connect(self.accept)        buttons.rejected.connect(self.reject)        layout.addWidget(buttons)defget_header(self) -> str:returnself.header_input.text().strip()classExcelMergerApp(QMainWindow):def__init__(self):super().__init__()self.setWindowTitle("Excel 表格合并工具 V2.0")# DPI 适配:根据屏幕缩放设置合适的窗口大小        screen = QApplication.primaryScreen()if screen:            dpi = screen.logicalDotsPerInch()            scale_factor = dpi / 96.0# 基准 DPIelse:            scale_factor = 1.0# 根据缩放因子调整窗口大小(紧凑设计)        base_width = int(900 * min(scale_factor, 1.3))        base_height = int(750 * min(scale_factor, 1.3))self.setMinimumSize(base_width, base_height)self.resize(base_width, base_height)self.settings = QSettings("ExcelMerger""Config")self.worker = Noneself.setup_ui()self.load_settings()self.center_window()# 自动调整字体大小以适配 DPIself.adjust_font_for_dpi(scale_factor)defadjust_font_for_dpi(self, scale_factor):"""根据 DPI 调整字体大小"""        base_font_size = 10        new_size = max(8int(base_font_size * scale_factor))        font = self.font()        font.setPointSize(new_size)self.setFont(font)defsetup_ui(self):# 主窗口部件        central_widget = QWidget()self.setCentralWidget(central_widget)# 主布局(紧凑设计)        main_layout = QVBoxLayout(central_widget)        main_layout.setSpacing(int(10 * self.get_scale_factor()))        main_layout.setContentsMargins(int(15 * self.get_scale_factor()),int(12 * self.get_scale_factor()),int(15 * self.get_scale_factor()),int(10 * self.get_scale_factor())        )# 标题区域        title_widget = QWidget()        title_layout = QVBoxLayout(title_widget)        title_layout.setContentsMargins(000int(8 * self.get_scale_factor()))        title_top_layout = QHBoxLayout()        title_top_layout.addStretch()        about_btn = QPushButton("ℹ️ 关于")        about_btn.setFont(QFont("Microsoft YaHei"int(8 * self.get_scale_factor())))        about_btn.setStyleSheet(self.get_button_style('secondary'))        about_btn.setFixedSize(int(70 * self.get_scale_factor()), int(24 * self.get_scale_factor()))        about_btn.clicked.connect(self.show_about)        title_top_layout.addWidget(about_btn)        title_layout.addLayout(title_top_layout)        title_label = QLabel("📊 Excel 表格合并工具")        title_font = QFont("Microsoft YaHei"int(16 * self.get_scale_factor()), QFont.Bold)        title_label.setFont(title_font)        title_label.setAlignment(Qt.AlignCenter)        title_label.setStyleSheet("color: #2196F3; margin-bottom: 3px;")        title_layout.addWidget(title_label)        dev_label = QLabel("【昨天软件】开发")        dev_font = QFont("Microsoft YaHei"int(8 * self.get_scale_factor()))        dev_label.setFont(dev_font)        dev_label.setAlignment(Qt.AlignCenter)        dev_label.setStyleSheet("color: #FF5722; font-weight: bold;")        title_layout.addWidget(dev_label)        version_label = QLabel("V2.0 - 增强版")        version_font = QFont("Microsoft YaHei"int(8 * self.get_scale_factor()))        version_label.setFont(version_font)        version_label.setAlignment(Qt.AlignCenter)        version_label.setStyleSheet("color: #999999;")        title_layout.addWidget(version_label)        main_layout.addWidget(title_widget)# 分隔线        line = QFrame()        line.setFrameShape(QFrame.HLine)        line.setStyleSheet("background-color: #E0E0E0;")        line.setFixedHeight(1)        main_layout.addWidget(line)# 上部分:配置区域        config_group = QGroupBox("⚙️ 配置选项")        config_group.setStyleSheet(self.get_group_style())        config_layout = QVBoxLayout(config_group)        config_layout.setSpacing(int(8 * self.get_scale_factor()))# 目录选择        dir_layout = QHBoxLayout()        dir_label = QLabel("📁 目标目录:")        dir_label.setStyleSheet("font-weight: bold; font-size: 10px;")        dir_layout.addWidget(dir_label)self.dir_input = QLineEdit()self.dir_input.setPlaceholderText("请选择包含 Excel/CSV 文件的目录...")self.dir_input.setStyleSheet(self.get_input_style())self.dir_input.setFixedHeight(int(28 * self.get_scale_factor()))        dir_layout.addWidget(self.dir_input)        browse_btn = QPushButton("浏览...")        browse_btn.setStyleSheet(self.get_button_style('primary'))        browse_btn.setFont(QFont("Microsoft YaHei"int(9 * self.get_scale_factor())))        browse_btn.setFixedSize(int(80 * self.get_scale_factor()), int(28 * self.get_scale_factor()))        browse_btn.clicked.connect(self.browse_directory)        dir_layout.addWidget(browse_btn)        config_layout.addLayout(dir_layout)# 选项区域(更紧凑)        options_layout = QHBoxLayout()        options_layout.setSpacing(int(20 * self.get_scale_factor()))# 跳过行数self.skip_spin = QSpinBox()self.skip_spin.setRange(020)self.skip_spin.setValue(1)self.skip_spin.setFixedSize(int(70 * self.get_scale_factor()), int(28 * self.get_scale_factor()))self.skip_spin.setStyleSheet(self.get_spinbox_style())        skip_label = QLabel("⏭️ 跳过行数:")        skip_label.setStyleSheet("font-weight: bold; font-size: 10px;")        skip_layout = QHBoxLayout()        skip_layout.addWidget(skip_label)        skip_layout.addWidget(self.skip_spin)        options_layout.addLayout(skip_layout)# 合并模式self.merge_mode_combo = QComboBox()self.merge_mode_combo.addItem("📌 共同列""common")self.merge_mode_combo.addItem("📋 全列""all")self.merge_mode_combo.setFixedSize(int(130 * self.get_scale_factor()), int(28 * self.get_scale_factor()))self.merge_mode_combo.setStyleSheet(self.get_combobox_style())        mode_label = QLabel("🔀 合并模式:")        mode_label.setStyleSheet("font-weight: bold; font-size: 10px;")        mode_layout = QHBoxLayout()        mode_layout.addWidget(mode_label)        mode_layout.addWidget(self.merge_mode_combo)        options_layout.addLayout(mode_layout)# CSV 选项self.csv_checkbox = QCheckBox("📄 包含CSV")self.csv_checkbox.setStyleSheet("font-size: 10px;")        options_layout.addWidget(self.csv_checkbox)        options_layout.addStretch()        config_layout.addLayout(options_layout)        main_layout.addWidget(config_group)# 中间部分:表头配置        header_group = QGroupBox("📋 表头配置(可选)")        header_group.setStyleSheet(self.get_group_style())        header_layout = QVBoxLayout(header_group)        header_layout.setSpacing(int(6 * self.get_scale_factor()))# 第一行:提示 + 按钮水平布局        top_layout = QHBoxLayout()# 左侧提示        hint_label = QLabel("💡 提示:手动添加需要合并的表头,留空则自动识别所有表头")        hint_label.setStyleSheet(f"color: #666666; font-size: {int(9*self.get_scale_factor())}px;")        hint_label.setWordWrap(True)        top_layout.addWidget(hint_label, 1)# 右侧按钮(更小更紧凑)        btn_layout = QHBoxLayout()        btn_layout.setSpacing(int(5 * self.get_scale_factor()))        add_header_btn = QPushButton("➕")        add_header_btn.setStyleSheet(self.get_button_style('primary'))        add_header_btn.setFont(QFont("Microsoft YaHei"int(8 * self.get_scale_factor())))        add_header_btn.setFixedSize(int(55 * self.get_scale_factor()), int(24 * self.get_scale_factor()))        add_header_btn.clicked.connect(self.add_header)        btn_layout.addWidget(add_header_btn)        import_header_btn = QPushButton("📥")        import_header_btn.setStyleSheet(self.get_button_style('secondary'))        import_header_btn.setFont(QFont("Microsoft YaHei"int(8 * self.get_scale_factor())))        import_header_btn.setFixedSize(int(55 * self.get_scale_factor()), int(24 * self.get_scale_factor()))        import_header_btn.clicked.connect(self.import_headers)        import_header_btn.setToolTip("从Excel文件导入表头")        btn_layout.addWidget(import_header_btn)        clear_header_btn = QPushButton("🗑️")        clear_header_btn.setStyleSheet(self.get_button_style('danger'))        clear_header_btn.setFont(QFont("Microsoft YaHei"int(8 * self.get_scale_factor())))        clear_header_btn.setFixedSize(int(55 * self.get_scale_factor()), int(24 * self.get_scale_factor()))        clear_header_btn.clicked.connect(self.clear_headers)        btn_layout.addWidget(clear_header_btn)        top_layout.addLayout(btn_layout)        header_layout.addLayout(top_layout)# 第二行:表头表格self.header_table = QTableWidget()self.header_table.setColumnCount(2)self.header_table.setHorizontalHeaderLabels(["表头名称""操作"])self.header_table.horizontalHeader().setStretchLastSection(True)self.header_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)self.header_table.setSelectionBehavior(QAbstractItemView.SelectRows)self.header_table.setStyleSheet(self.get_table_style())self.header_table.verticalHeader().setDefaultSectionSize(int(28 * self.get_scale_factor()))self.header_table.setFixedHeight(int(140 * self.get_scale_factor()))        header_layout.addWidget(self.header_table)        main_layout.addWidget(header_group)# 下部分:日志和进度        log_group = QGroupBox("📝 合并日志")        log_group.setStyleSheet(self.get_group_style())        log_layout = QVBoxLayout(log_group)        log_layout.setSpacing(int(6 * self.get_scale_factor()))self.log_text = QTextEdit()self.log_text.setReadOnly(True)self.log_text.setStyleSheet(self.get_log_style())self.log_text.setFixedHeight(int(150 * self.get_scale_factor()))        log_layout.addWidget(self.log_text)# 进度条self.progress_bar = QProgressBar()self.progress_bar.setRange(0100)self.progress_bar.setValue(0)self.progress_bar.setTextVisible(True)self.progress_bar.setFixedHeight(int(20 * self.get_scale_factor()))self.progress_bar.setStyleSheet(self.get_progress_style())        log_layout.addWidget(self.progress_bar)        main_layout.addWidget(log_group)# 底部按钮(更紧凑)        bottom_layout = QHBoxLayout()        bottom_layout.setSpacing(int(10 * self.get_scale_factor()))self.merge_btn = QPushButton("🚀 开始合并")self.merge_btn.setFont(QFont("Microsoft YaHei"int(11 * self.get_scale_factor()), QFont.Bold))self.merge_btn.setFixedHeight(int(38 * self.get_scale_factor()))self.merge_btn.setStyleSheet(self.get_button_style('success'))self.merge_btn.clicked.connect(self.start_merge)        bottom_layout.addWidget(self.merge_btn)self.cancel_btn = QPushButton("❌")self.cancel_btn.setFixedHeight(int(38 * self.get_scale_factor()))self.cancel_btn.setFixedWidth(int(50 * self.get_scale_factor()))self.cancel_btn.setEnabled(False)self.cancel_btn.setStyleSheet(self.get_button_style('danger'))self.cancel_btn.clicked.connect(self.cancel_merge)        bottom_layout.addWidget(self.cancel_btn)        clear_log_btn = QPushButton("🧹")        clear_log_btn.setFixedHeight(int(38 * self.get_scale_factor()))        clear_log_btn.setFixedWidth(int(50 * self.get_scale_factor()))        clear_log_btn.setStyleSheet(self.get_button_style('secondary'))        clear_log_btn.clicked.connect(self.clear_log)        bottom_layout.addWidget(clear_log_btn)        main_layout.addLayout(bottom_layout)# 状态栏self.statusBar().showMessage("✅ 就绪 - 准备就绪")self.statusBar().setStyleSheet(self.get_statusbar_style())# 初始日志self.log("欢迎使用 Excel 表格合并工具 V2.0!")self.log("支持处理 .xlsx, .xls 和 .csv 文件")self.log("-" * 60)defget_scale_factor(self):"""获取缩放因子"""        screen = QApplication.primaryScreen()if screen:            dpi = screen.logicalDotsPerInch()returnmax(0.8min(dpi / 96.01.5))return1.0defget_group_style(self):"""获取分组框样式"""return"""            QGroupBox {                font-weight: bold;                font-size: 11px;                border: 1px solid #E0E0E0;                border-radius: 6px;                margin-transform: translateY( 8px;                padding: 12px 8px 8px 8px;                background-color: white;            }            QGroupBox::title {                subcontrol-origin: margin;                left: 8px;                padding: 0 4px;                color: #2196F3;            }        """defget_input_style(self):"""获取输入框样式"""return"""            QLineEdit {                padding: 5px 8px;                border: 1px solid #CCCCCC;                border-radius: 3px;                background-color: #FAFAFA;                font-size: 10px;            }            QLineEdit:focus {                border: 2px solid #2196F3;                background-color: white;            }        """defget_button_style(self, btn_type='primary'):"""获取按钮样式"""        styles = {'primary'"""                QPushButton {                    padding: 4px 10px;                    background-color: #2196F3;                    color: white;                    border: none;                    border-radius: 3px;                    font-weight: bold;                }                QPushButton:hover {                    background-color: #1976D2;                }                QPushButton:pressed {                    background-color: #1565C0;                }            """,'success'"""                QPushButton {                    padding: 6px 20px;                    background-color: #4CAF50;                    color: white;                    border: none;                    border-radius: 4px;                    font-weight: bold;                }                QPushButton:hover {                    background-color: #45A049;                }                QPushButton:pressed {                    background-color: #3E8E41;                }                QPushButton:disabled {                    background-color: #CCCCCC;                    color: #888888;                }            """,'danger'"""                QPushButton {                    padding: 4px 10px;                    background-color: #f44336;                    color: white;                    border: none;                    border-radius: 3px;                    font-weight: bold;                }                QPushButton:hover {                    background-color: #d32f2f;                }                QPushButton:pressed {                    background-color: #C62828;                }                QPushButton:disabled {                    background-color: #CCCCCC;                    color: #888888;                }            """,'secondary'"""                QPushButton {                    padding: 4px 10px;                    background-color: #757575;                    color: white;                    border: none;                    border-radius: 3px;                    font-weight: bold;                }                QPushButton:hover {                    background-color: #616161;                }                QPushButton:pressed {                    background-color: #424242;                }            """        }return styles.get(btn_type, styles['primary'])defget_spinbox_style(self):"""获取微调框样式"""return"""            QSpinBox {                padding: 3px 5px;                border: 1px solid #CCCCCC;                border-radius: 3px;                font-size: 10px;            }            QSpinBox:focus {                border: 2px solid #2196F3;            }        """defget_combobox_style(self):"""获取下拉框样式"""return"""            QComboBox {                padding: 3px 5px;                border: 1px solid #CCCCCC;                border-radius: 3px;                font-size: 10px;            }            QComboBox:focus {                border: 2px solid #2196F3;            }            QComboBox::drop-down {                border: none;                width: 20px;            }            QComboBox::down-arrow {                image: none;                border-left: 4px solid transparent;                border-right: 4px solid transparent;                border-top: 4px solid #666666;                margin-right: 4px;            }        """defget_table_style(self):"""获取表格样式"""return"""            QTableWidget {                border: 1px solid #E0E0E0;                border-radius: 3px;                gridline-color: #E0E0E0;                font-size: 10px;            }            QHeaderView::section {                background-color: #F5F5F5;                padding: 5px;                border: none;                border-bottom: 2px solid #E0E0E0;                font-weight: bold;                color: #333333;                font-size: 10px;            }            QTableWidget::item) {                padding: 3px;            }            QTableWidget::item:selected {                background-color: #E3F2FD;                color: #333333;            }        """defget_log_style(self):"""获取日志样式"""return"""            QTextEdit {                border: 1px solid #E0E0E0;                border-radius: 3px;                background-color: #FAFAFA;                font-family: 'Consolas', 'Monaco', monospace;                font-size: 10px;                padding: 8px;            }        """defget_progress_style(self):"""获取进度条样式"""return"""            QProgressBar {                border: 1px solid #E0E0E0;                border-radius: 3px;                text-align: center;                font-weight: bold;                font-size: 9px;            }            QProgressBar::chunk {                background-color: qlineargradient(                    x1: 0, y1: 0, x2: 1, y2: 0,                    stransform: translateY( 0 #4CAF50, stop: 1 #8BC34A                );                border-radius: 2px;            }        """defget_statusbar_style(self):"""获取状态栏样式"""return"""            QStatusBar {                background-color: #F5F5F5;                padding: 3px;                font-size: 9px;            }        """defshow_about(self):"""显示关于对话框"""        about_text = """<h2 style="color: #2196F3;">📊 Excel 表格合并工具 V2.0</h2><p style="color: #FF5722; font-weight: bold;">由【昨天软件】开发</p><br/><h3 style="color: #333333;">👤 作者介绍</h3><p>专注于桌面应用开发,提供高质量的办公效率工具。</p><p>致力于打造简洁、易用、高效的软件解决方案。</p><br/><h3 style="color: #333333;">📖 功能说明</h3><p><b>1. 批量合并Excel文件</b></p><p>• 支持 .xlsx、.xls 和 .csv 格式</p><p>• 自动遍历目录下的所有文件</p><p>• 支持多Sheet表格合并</p><br/><p><b>2. 灵活的配置选项</b></p><p>• 跳过行数:可设置跳过表格开头的行数</p><p>• 共同列模式:只合并所有文件共有的列</p><p>• 全列模式:合并所有出现的列</p><p>• 包含CSV:可选择是否处理CSV文件</p><br/><p><b>3. 表头配置</b></p><p>• 手动添加:逐个添加需要合并的表头</p><p>• 导入表头:从Excel文件自动导入表头</p><p>• 清空表头:一键清除所有配置的表头</p><br/><p><b>4. 其他功能</b></p><p>• 实时日志:显示合并进度和详细信息</p><p>• 配置记忆:自动保存和恢复设置</p><p>• 美观界面:适配不同DPI缩放</p><br/><p style="color: #888888;">感谢您的使用!</p>        """        msg_box = QMessageBox(self)        msg_box.setWindowTitle("关于")        msg_box.setText(about_text)        msg_box.setIcon(QMessageBox.Information)        msg_box.setStyleSheet("""            QMessageBox {                background-color: white;            }            QLabel {                font-size: 11px;            }        """)        msg_box.exec()defcenter_window(self):"""窗口居中显示 - 适配多显示器和高DPI"""# 确保窗口在可见屏幕上        screen = QApplication.screenAt(self.mapToGlobal(self.rect().center()))if screen isNone:            screen = QApplication.primaryScreen()if screen:            screen_geometry = screen.geometry()            window_geometry = self.geometry()# 计算居中位置            x = screen_geometry.left() + (screen_geometry.width() - window_geometry.width()) // 2            y = screen_geometry.top() + (screen_geometry.height() - window_geometry.height()) // 2# 确保窗口完全在屏幕内            x = max(screen_geometry.left(), min(x, screen_geometry.right() - window_geometry.width()))            y = max(screen_geometry.top(), min(y, screen_geometry.bottom() - window_geometry.height()))self.move(x, y)defbrowse_directory(self):"""浏览目录"""        folder = QFileDialog.getExistingDirectory(self"选择包含 Excel 文件的目录")if folder:self.dir_input.setText(folder)self.log(f"已选择目录: {folder}")defadd_header(self):"""添加表头"""        dialog = HeaderDialog(self)if dialog.exec() == QDialog.Accepted:            header = dialog.get_header()if header:self.add_header_to_table(header)self.save_settings()defimport_headers(self):"""从Excel文件导入表头"""        folder = self.dir_input.text().strip()ifnot folder ornot os.path.exists(folder):            QMessageBox.warning(self"提示""请先选择包含Excel文件的目录")return# 查找Excel文件        excel_files = list(Path(folder).glob("*.xlsx")) + list(Path(folder).glob("*.xls"))ifnot excel_files:            QMessageBox.warning(self"提示""目录中没有找到Excel文件")return# 获取用户设置的跳过行数        skip_rows = self.skip_spin.value()# 读取第一个文件的第一个sheet的表头,使用用户设置的跳过行数try:# 使用 header=skip_rows 来读取表头(skip_rows 指定表头所在的行)            df = pd.read_excel(str(excel_files[0]), header=skip_rows, nrows=0)            headers = [str(col).strip() for col in df.columns ifstr(col).strip()]if headers:# 询问是否清空现有表头ifself.header_table.rowCount() > 0:                    reply = QMessageBox.question(self"确认",f"是否清空现有表头并导入 {len(headers)} 个新表头?\n(基于跳过 {skip_rows} 行)",                        QMessageBox.Yes | QMessageBox.No                    )if reply == QMessageBox.Yes:self.header_table.setRowCount(0)# 添加表头for header in headers:if header:self.add_header_to_table(header)self.log(f"✓ 已从 {excel_files[0].name} 导入 {len(headers)} 个表头(跳过 {skip_rows} 行)")self.save_settings()else:                QMessageBox.warning(self"提示"f"未找到有效表头\n\n可能原因:\n"f"• 跳过行数 ({skip_rows}) 设置不正确\n"f"• 请尝试调整跳过行数后重新导入")except Exception as e:            QMessageBox.warning(self"错误"f"读取Excel文件失败:\n{str(e)}")defadd_header_to_table(self, header: str):"""添加表头到表格"""ifnot header ornotisinstance(header, str):return# 检查是否已存在for row inrange(self.header_table.rowCount()):            item) = self.header_table.item(row, 0)if item and item.text() == header:return        row = self.header_table.rowCount()self.header_table.insertRow(row)# 表头名称        name_item = QTableWidgetItem(header)        name_item.setToolTip(header)self.header_table.setItem(row, 0, name_item)# 删除按钮        delete_btn = QPushButton("🗑️")        delete_btn.setStyleSheet(self.get_button_style('danger'))        delete_btn.setProperty("row", row)        delete_btn.clicked.connect(self.on_delete_header_clicked)self.header_table.setCellWidget(row, 1, delete_btn)defon_delete_header_clicked(self):"""删除表头按钮点击处理"""        btn = self.sender()if btn:            row = btn.property("row")if row isnotNone:self.remove_header(row)defremove_header(self, row: int):"""删除表头"""self.header_table.removeRow(row)self.save_settings()defclear_headers(self):"""清空所有表头"""self.header_table.setRowCount(0)self.save_settings()defget_headers(self) -> List[str]:"""获取所有配置的表头"""        headers = []for row inrange(self.header_table.rowCount()):            item = self.header_table.item(row, 0)if item:                headers.append(item.text())return headersdeflog(self, message: str):"""添加日志"""        timestamp = datetime.now().strftime("%H:%M:%S")self.log_text.append(f"[{timestamp}{message}")# 滚动到底部        scrollbar = self.log_text.verticalScrollBar()        scrollbar.setValue(scrollbar.maximum())defclear_log(self):"""清空日志"""self.log_text.clear()defstart_merge(self):"""开始合并"""        folder_path = self.dir_input.text().strip()ifnot folder_path:            QMessageBox.warning(self"警告""请先选择目标目录")returnifnot os.path.exists(folder_path):            QMessageBox.warning(self"警告""所选目录不存在")return        headers = self.get_headers()        skip_rows = self.skip_spin.value()        merge_mode = self.merge_mode_combo.currentData()        include_csv = self.csv_checkbox.isChecked()self.log("=" * 60)self.log("🚀 开始合并操作")self.log(f"📂 目标目录: {folder_path}")self.log(f"⏭️ 跳过行数: {skip_rows}")self.log(f"🔀 合并模式: {'共同列'if merge_mode == 'common'else'全列'}")self.log(f"📄 包含CSV: {'是'if include_csv else'否'}")if headers:self.log(f"📋 指定表头 ({len(headers)}个): {', '.join(headers[:5])}{'...'iflen(headers) > 5else''}")else:self.log("📋 表头: 自动识别")# 更新UI状态self.merge_btn.setEnabled(False)self.cancel_btn.setEnabled(True)self.progress_bar.setValue(0)self.statusBar().showMessage("⚙️ 正在合并...")# 启动工作线程self.worker = MergeWorker(folder_path, headers, skip_rows, merge_mode, include_csv)self.worker.progress.connect(self.update_progress)self.worker.log.connect(self.log)self.worker.finished_signal.connect(self.merge_finished)self.worker.start()defcancel_merge(self):"""取消合并"""ifself.worker andself.worker.isRunning():self.worker.stop()self.log("正在取消操作...")defupdate_progress(self, value: int):"""更新进度条"""self.progress_bar.setValue(value)defmerge_finished(self, success: bool, message: str):"""合并完成回调"""self.merge_btn.setEnabled(True)self.cancel_btn.setEnabled(False)if success:self.log("=" * 60)self.log(f"✅ 合并成功完成!")self.progress_bar.setValue(100)self.statusBar().showMessage("✅ 合并完成")            reply = QMessageBox.question(self"合并完成"f"✅ 合并成功!\n\n📁 输出文件:\n{message}\n\n是否打开所在目录?",                QMessageBox.Yes | QMessageBox.No            )if reply == QMessageBox.Yes:                os.startfile(os.path.dirname(message))else:self.log(f"❌ {message}")self.statusBar().showMessage("❌ 合并失败")            QMessageBox.critical(self"错误", message)self.progress_bar.setValue(0)self.save_settings()defsave_settings(self):"""保存设置"""self.settings.setValue("last_directory"self.dir_input.text())self.settings.setValue("skip_rows"self.skip_spin.value())self.settings.setValue("merge_mode"self.merge_mode_combo.currentIndex())self.settings.setValue("include_csv"self.csv_checkbox.isChecked())self.settings.setValue("headers"self.get_headers())defload_settings(self):"""加载设置"""        last_dir = self.settings.value("last_directory""")if last_dir:self.dir_input.setText(last_dir)        skip_rows = self.settings.value("skip_rows"1)self.skip_spin.setValue(int(skip_rows))        merge_mode = self.settings.value("merge_mode"0)self.merge_mode_combo.setCurrentIndex(int(merge_mode))        include_csv = self.settings.value("include_csv"False)if include_csv and include_csv != "false":self.csv_checkbox.setChecked(True)        headers = self.settings.value("headers", [])if headers:for header in headers:self.add_header_to_table(header)defcloseEvent(self, event):"""关闭事件"""ifself.worker andself.worker.isRunning():            reply = QMessageBox.question(self"确认退出""合并操作正在进行中,确定要退出吗?",                QMessageBox.Yes | QMessageBox.No            )if reply == QMessageBox.Yes:self.worker.stop()self.worker.wait(2000)                event.accept()else:                event.ignore()else:self.save_settings()            event.accept()defmain():    app = QApplication(sys.argv)# 设置 Fusion 主题    app.setStyle('Fusion')# 设置全局调色板    palette = QPalette()    palette.setColor(QPalette.Window, QColor(250250250))    palette.setColor(QPalette.WindowText, QColor(333333))    palette.setColor(QPalette.Base, QColor(255255255))    palette.setColor(QPalette.AlternateBase, QColor(245245245))    palette.setColor(QPalette.ToolTipBase, QColor(255255255))    palette.setColor(QPalette.ToolTipText, QColor(333333))    palette.setColor(QPalette.Text, QColor(333333))    palette.setColor(QPalette.Button, QColor(240240240))    palette.setColor(QPalette.ButtonText, QColor(333333))    palette.setColor(QPalette.BrightText, QColor(25500))    palette.setColor(QPalette.Highlight, QColor(33150243))    palette.setColor(QPalette.HighlightedText, QColor(255255255))    app.setPalette(palette)# 设置全局字体    font = QFont("Microsoft YaHei"10)    app.setFont(font)    window = ExcelMergerApp()    window.show()    sys.exit(app.exec())if __name__ == "__main__":    main()

如果需要打包后文件的 请关注后留言