Python Excel 比较 sheet 之间的差异

研究目的

实现快速输出比较两个excel(可能是xlsx、xls、xlsb、xlsm或者csv)中指定的sheet中列内容是否一致。

假定每个sheet的第一行是标题行,如果一致则输出相同的信息,如果有不一致则输出一个结果分析的xlsx文件。

V 1.0

python 复制代码
import os
import pandas as pd
from openpyxl import Workbook
from openpyxl.styles import PatternFill
from openpyxl.utils.dataframe import dataframe_to_rows
from openpyxl.utils import get_column_letter

# 颜色定义(淡红、淡绿)
LIGHT_RED = PatternFill(start_color='FFC0C0C0', end_color='FFC0C0C0', fill_type='solid')
LIGHT_GREEN = PatternFill(start_color='C0FFC0C0', end_color='C0FFC0C0', fill_type='solid')

def normalize_value(value):
    """标准化值:处理空值并去除前后空格"""
    if pd.isna(value):
        return ''
    return str(value).strip()

def read_excel_sheet(file_path, sheet_name):
    """读取指定文件和Sheet的数据,返回处理后的DataFrame"""
    ext = os.path.splitext(file_path)[1].lower()
    
    try:
        if ext == '.csv':
            df = pd.read_csv(file_path, header=0)
        elif ext in ('.xls', '.xlsx', '.xlsm'):
            engine = 'openpyxl' if ext in ('.xlsx', '.xlsm') else None
            df = pd.read_excel(file_path, sheet_name=sheet_name, header=0, engine=engine)
        elif ext == '.xlsb':
            df = pd.read_excel(file_path, sheet_name=sheet_name, header=0, engine='pyxlsb')
        else:
            raise ValueError(f"不支持的文件格式: {ext}")
    except Exception as e:
        raise RuntimeError(f"读取文件失败: {str(e)}")
    
    # 确保标题行存在
    if len(df) == 0 or df.columns[0] == 'Unnamed: 0':
        raise ValueError("Sheet第一行应为标题行,或数据为空")
    
    return df

def compare_sheets(file_a, sheet_a, file_b, sheet_b, output_file):
    """主函数:比较两个Sheet并生成报告"""
    # 读取数据
    df_a = read_excel_sheet(file_a, sheet_a)
    df_b = read_excel_sheet(file_b, sheet_b)
    
    # 标准化列名(去除前后空格)
    df_a.columns = [normalize_value(col) for col in df_a.columns]
    df_b.columns = [normalize_value(col) for col in df_b.columns]
    
    # 获取列信息
    cols_a = set(df_a.columns)
    cols_b = set(df_b.columns)
    common_cols = sorted(cols_a & cols_b)
    only_a = sorted(cols_a - cols_b)
    only_b = sorted(cols_b - cols_a)
    all_columns = only_a + common_cols + only_b
    
    # 生成总结表数据
    summary_data = []
    for col in all_columns:
        a_col = col if col in cols_a else ''
        b_col = col if col in cols_b else ''
        
        if col in common_cols:
            # 计算共同列的差异行数
            max_len = max(len(df_a), len(df_b))
            diff_count = 0
            for i in range(max_len):
                a_val = normalize_value(df_a[col].iloc[i]) if i < len(df_a) else ''
                b_val = normalize_value(df_b[col].iloc[i]) if i < len(df_b) else ''
                if a_val != b_val:
                    diff_count += 1
        elif col in only_a:
            diff_count = len(df_a) - 1  # 数据行数(总行数-标题行)
        else:  # only_b
            diff_count = len(df_b) - 1
        
        summary_data.append([col, b_col if col in cols_a else a_col, diff_count])
    
    # 生成差异细节数据(A视角)
    a_diff_data = []
    # 生成差异细节数据(B视角)
    b_diff_data = []
    
    max_rows = max(len(df_a), len(df_b))
    for row_idx in range(max_rows):
        # 原始行号(标题行+1开始)
        original_row_num = row_idx + 2  # 标题行是第1行,数据行从第2行开始
        
        # 获取A和B的当前行数据(标准化后)
        a_row = {}
        b_row = {}
        for col in all_columns:
            # A的列数据
            if col in df_a.columns:
                if row_idx < len(df_a):
                    a_val = normalize_value(df_a[col].iloc[row_idx])
                else:
                    a_val = ''
            else:
                a_val = ''  # B的独有列在A中视为空
            
            # B的列数据
            if col in df_b.columns:
                if row_idx < len(df_b):
                    b_val = normalize_value(df_b[col].iloc[row_idx])
                else:
                    b_val = ''
            else:
                b_val = ''  # A的独有列在B中视为空
            
            a_row[col] = a_val
            b_row[col] = b_val
        
        # 判断是否需要加入A差异表
        a_has_row = row_idx < len(df_a)
        b_has_row = row_idx < len(df_b)
        is_diff = (a_row != b_row)
        
        if a_has_row:
            a_row['索引位置'] = original_row_num
            # 补充B的独有列(已处理为空)
            a_diff_data.append(a_row)
        
        if b_has_row:
            b_row['索引位置'] = original_row_num
            # 补充A的独有列(已处理为空)
            b_diff_data.append(b_row)
    
    # 创建输出工作簿
    wb = Workbook()
    ws_summary = wb.active
    ws_summary.title = "列名总结"
    
    # 写入总结表
    ws_summary.append(["列名", "对应另一文件的列名", "不一样个数"])
    for row in summary_data:
        ws_summary.append(row)
    
    # 调整总结表列宽
    for col in ws_summary.columns:
        max_length = 0
        column = get_column_letter(col[0].column)
        for cell in col:
            try:
                if len(str(cell.value)) > max_length:
                    max_length = len(str(cell.value))
            except:
                pass
        adjusted_width = (max_length + 2) * 1.2
        ws_summary.column_dimensions[column].width = adjusted_width
    
    # 写入A差异表(第二个工作表)
    ws_a_diff = wb.create_sheet("A差异行")
    if a_diff_data:
        # 写入标题行
        headers = ['索引位置'] + all_columns
        ws_a_diff.append(headers)
        # 写入数据行
        for row in a_diff_data[1:]:  # 跳过标题行(因为a_diff_data的第一个是标题?不,前面是手动构造的)
            # 注意:前面的a_diff_data构造可能有误,需要重新检查
            # 正确的构造方式应该是从原始数据构建
            # 这里可能需要重新组织代码,确保正确获取行数据
            pass  # 实际实现需要补充
        
        # 设置淡红格式(A多的行)
        for idx, row in enumerate(a_diff_data):
            if idx == 0:
                continue  # 跳过标题行
            original_row_num = row['索引位置']
            a_has = original_row_num <= len(df_a) + 1  # 标题行+1
            b_has = original_row_num <= len(df_b) + 1
            if not b_has:
                for cell in ws_a_diff[idx+1]:
                    cell.fill = LIGHT_RED
    
    # 写入B差异表(第三个工作表)
    ws_b_diff = wb.create_sheet("B差异行")
    # 类似A差异表的处理...
    
    # 保存结果
    wb.save(output_file)
    print(f"报告已生成:{output_file}")

if __name__ == "__main__":
    # 示例用法
    compare_sheets(
        file_a="path/to/fileA.xlsx",
        sheet_a="Sheet1",
        file_b="path/to/fileB.xlsx",
        sheet_b="Sheet1",
        output_file="comparison_result.xlsx"
    )

使用说明

依赖安装 :需要安装pandas, openpyxl, pyxlsb(处理xlsb格式):

bash 复制代码
pip install pandas openpyxl pyxlsb

功能特点

  • 支持多种Excel格式(xls/xlsx/xlsm/xlsb/csv)
  • 自动处理标题行和前后空格
  • 生成三张工作表:列名总结、A差异行、B差异行
  • 差异行标记(淡红/A多,淡绿/B多)

注意事项

  • 确保被比较的Sheet存在且包含标题行
  • 数值和字符串会统一按内容比较(如123'123'视为相同)
  • 空单元格(包括NaN)统一视为空字符串''
  • 行号从标题行下一行开始计数(索引位置=原数据行号)

待完善点

  • 差异行数据写入逻辑需要完整实现(当前示例中部分代码未完成)
  • 可添加更多错误处理(如Sheet不存在、文件无权限等)
  • 支持自定义列映射(如指定某些列作为主键比较)
  • 优化大数据量下的性能(当前逐行比较可能较慢)

V 1.5

python 复制代码
import pandas as pd
import numpy as np
from openpyxl import Workbook
from openpyxl.styles import PatternFill, Font
from openpyxl.utils.dataframe import dataframe_to_rows
import os

def read_excel_file(file_path, sheet_name=None):
    """读取Excel文件,支持多种格式"""
    ext = os.path.splitext(file_path)[1].lower()
    
    if ext == '.csv':
        return pd.read_csv(file_path)
    elif ext == '.xlsb':
        # 对于xlsb格式,使用pyxlsb引擎
        return pd.read_excel(file_path, sheet_name=sheet_name, engine='pyxlsb')
    else:
        # 对于xlsx、xls、xlsm格式
        return pd.read_excel(file_path, sheet_name=sheet_name)

def compare_excel_sheets(file1, file2, sheet1_name=None, sheet2_name=None, output_file='comparison_result.xlsx'):
    """
    比较两个Excel文件中指定sheet的列内容
    
    参数:
    file1: 第一个Excel文件路径
    file2: 第二个Excel文件路径
    sheet1_name: 第一个文件的sheet名称,如果为None则使用第一个sheet
    sheet2_name: 第二个文件的sheet名称,如果为None则使用第一个sheet
    output_file: 输出结果文件名
    """
    
    # 读取数据
    try:
        df1 = read_excel_file(file1, sheet1_name)
        df2 = read_excel_file(file2, sheet2_name)
    except Exception as e:
        print(f"读取文件时出错: {e}")
        return
    
    # 确保数据框有列名
    if df1.columns.empty or df2.columns.empty:
        print("错误: 至少一个数据框没有列名")
        return
    
    # 清理列名(去除前后空格)
    df1.columns = df1.columns.str.strip()
    df2.columns = df2.columns.str.strip()
    
    # 获取共同列和独有列
    common_cols = sorted(set(df1.columns) & set(df2.columns))
    only_in_df1 = sorted(set(df1.columns) - set(df2.columns))
    only_in_df2 = sorted(set(df2.columns) - set(df1.columns))
    
    # 创建结果工作簿
    wb = Workbook()
    
    # 创建标题比较工作表
    ws_summary = wb.active
    ws_summary.title = "标题比较"
    
    # 添加标题行
    ws_summary.append(["SheetA列名", "SheetB列名", "不一样个数", "备注"])
    
    # 添加共同列
    diff_counts = {}
    for col in common_cols:
        # 比较两列内容(忽略NaN和空格)
        col1_clean = df1[col].astype(str).str.strip().replace('nan', np.nan)
        col2_clean = df2[col].astype(str).str.strip().replace('nan', np.nan)
        
        # 找出不同的行
        diff_mask = col1_clean != col2_clean
        # 排除两个都是NaN的情况
        both_nan = col1_clean.isna() & col2_clean.isna()
        diff_count = (diff_mask & ~both_nan).sum()
        
        diff_counts[col] = diff_count
        ws_summary.append([col, col, diff_count, ""])
    
    # 添加只在df1中的列
    for col in only_in_df1:
        ws_summary.append([col, "", "", "只在SheetA中存在"])
    
    # 添加只在df2中的列
    for col in only_in_df2:
        ws_summary.append(["", col, "", "只在SheetB中存在"])
    
    # 设置标题行样式
    for cell in ws_summary[1]:
        cell.font = Font(bold=True)
    
    # 调整列宽
    for column in ws_summary.columns:
        max_length = 0
        column_letter = column[0].column_letter
        for cell in column:
            try:
                if len(str(cell.value)) > max_length:
                    max_length = len(str(cell.value))
            except:
                pass
        adjusted_width = min(max_length + 2, 50)
        ws_summary.column_dimensions[column_letter].width = adjusted_width
    
    # 创建详细比较工作表 - SheetA
    ws_detail_a = wb.create_sheet("SheetA差异详情")
    
    # 添加索引列
    detail_df1 = df1.copy()
    detail_df1.insert(0, 'Index', detail_df1.index)
    
    # 写入数据
    for r in dataframe_to_rows(detail_df1, index=False, header=True):
        ws_detail_a.append(r)
    
    # 创建详细比较工作表 - SheetB
    ws_detail_b = wb.create_sheet("SheetB差异详情")
    
    # 添加索引列
    detail_df2 = df2.copy()
    detail_df2.insert(0, 'Index', detail_df2.index)
    
    # 写入数据
    for r in dataframe_to_rows(detail_df2, index=False, header=True):
        ws_detail_b.append(r)
    
    # 设置样式 - 定义颜色
    red_fill = PatternFill(start_color="FFCCCB", end_color="FFCCCB", fill_type="solid")
    green_fill = PatternFill(start_color="90EE90", end_color="90EE90", fill_type="solid")
    
    # 比较数据并标记差异
    for col in common_cols:
        if col not in detail_df1.columns or col not in detail_df2.columns:
            continue
            
        # 获取列索引
        col_idx_df1 = detail_df1.columns.get_loc(col) + 1  # +1因为openpyxl从1开始计数
        col_idx_df2 = detail_df2.columns.get_loc(col) + 1
        
        # 清理数据进行比较
        col1_clean = detail_df1[col].astype(str).str.strip().replace('nan', np.nan)
        col2_clean = detail_df2[col].astype(str).str.strip().replace('nan', np.nan)
        
        # 找出不同的行
        diff_mask = col1_clean != col2_clean
        both_nan = col1_clean.isna() & col2_clean.isna()
        diff_rows = diff_mask & ~both_nan
        
        # 标记SheetA中的差异
        for row_idx in detail_df1.index[diff_rows]:
            cell = ws_detail_a.cell(row=row_idx+2, column=col_idx_df1)  # +2因为第一行是标题
            # 不设置背景色(无色)
    
    # 识别只在A或B中存在的行
    # 这里使用一个简单的基于索引的比较,实际可能需要更复杂的行匹配逻辑
    max_rows = max(len(df1), len(df2))
    
    for row_idx in range(max_rows):
        # 标记只在A中存在的行(淡红色)
        if row_idx >= len(df2) and row_idx < len(df1):
            for col_idx in range(1, len(detail_df1.columns) + 1):
                cell = ws_detail_a.cell(row=row_idx+2, column=col_idx)
                cell.fill = red_fill
        
        # 标记只在B中存在的行(淡绿色)
        if row_idx >= len(df1) and row_idx < len(df2):
            for col_idx in range(1, len(detail_df2.columns) + 1):
                cell = ws_detail_b.cell(row=row_idx+2, column=col_idx)
                cell.fill = green_fill
    
    # 设置标题行样式
    for cell in ws_detail_a[1]:
        cell.font = Font(bold=True)
    
    for cell in ws_detail_b[1]:
        cell.font = Font(bold=True)
    
    # 调整详细工作表的列宽
    for ws in [ws_detail_a, ws_detail_b]:
        for column in ws.columns:
            max_length = 0
            column_letter = column[0].column_letter
            for cell in column:
                try:
                    if len(str(cell.value)) > max_length:
                        max_length = len(str(cell.value))
                except:
                    pass
            adjusted_width = min(max_length + 2, 50)
            ws.column_dimensions[column_letter].width = adjusted_width
    
    # 保存工作簿
    wb.save(output_file)
    print(f"比较完成,结果已保存到: {output_file}")
    
    # 检查是否有差异
    total_differences = sum(diff_counts.values()) + len(only_in_df1) + len(only_in_df2)
    if total_differences == 0:
        print("两个文件的内容完全一致")
    else:
        print(f"发现 {total_differences} 处差异")

# 使用示例
if __name__ == "__main__":
    # 请替换为您的文件路径和sheet名称
    file1_path = "file1.xlsx"
    file2_path = "file2.xlsx"
    sheet1_name = "Sheet1"  # 如果为None则使用第一个sheet
    sheet2_name = "Sheet1"  # 如果为None则使用第一个sheet
    output_filename = "comparison_result.xlsx"
    
    compare_excel_sheets(file1_path, file2_path, sheet1_name, sheet2_name, output_filename)

使用说明

  1. 安装必要的库:
bash 复制代码
pip install pandas openpyxl pyxlsb
  1. 修改代码底部的文件路径和sheet名称:
python 复制代码
file1_path = "file1.xlsx"  # 替换为第一个文件路径
file2_path = "file2.xlsx"  # 替换为第二个文件路径
sheet1_name = "Sheet1"     # 第一个文件的sheet名称
sheet2_name = "Sheet1"     # 第二个文件的sheet名称
output_filename = "comparison_result.xlsx"  # 输出文件名
  1. 运行脚本,将会生成比较结果文件。

功能特点

  1. 支持多种Excel格式:xlsx、xls、xlsb、xlsm和csv
  2. 自动比较两个sheet的列标题和内容
  3. 生成包含三个工作表的比较结果:
    • 标题比较:显示列名差异和不同单元格数量
    • SheetA差异详情:显示第一个文件的差异,多出的行标记为淡红色
    • SheetB差异详情:显示第二个文件的差异,多出的行标记为淡绿色
  4. 比较时自动忽略单元格前后的空格
  5. 自动调整列宽以便阅读

注意事项

  1. 对于大型文件,处理可能需要一些时间
  2. 当前实现基于行索引比较,如果需要更复杂的行匹配逻辑,可能需要进一步修改代码
  3. 确保已安装所有必需的库,特别是对于xlsb格式需要pyxlsb库
相关推荐
曾经的三心草3 小时前
基于阿里云系列平台的python微服务设计与DevOps实践
python·阿里云·微服务
nyf_unknown3 小时前
(vue)前端下载本地excel文件
前端·vue.js·excel
脑花儿3 小时前
ABAP EXCEL模板数据上传 及 注意事项
开发语言·excel
一叶飘零_sweeeet3 小时前
攻克 大 Excel 上传难题:从异步处理到并发去重的全链路解决方案
java·excel·大文件上传
咖啡星人k3 小时前
MonkeyCode+Excel混合双打:3步把表格变成可视化大屏
人工智能·excel
吴声子夜歌3 小时前
Excel——常用函数三
excel
Waitind_3 小时前
Excel常用函数
excel
揭老师高效办公3 小时前
WPS表格和Excel中快速选择有批注的全部单元格
excel·wps表格
mudtools3 小时前
.NET操作Excel:高效数据读写与批量操作
c#·.net·excel·wps