<think>我们之前合并表格时,每个表格只保留了数据行,现在要求每个表格的字段行(标题行)也需要保留。
但是要注意,去重是针对数据行(根据Passport字段),标题行不需要去重,需要保留每个表格的标题行。
同时,每个表格的标题行之前还要保留我们添加的第一列(文件名-工作表名)和Passport列。
调整思路:
1. 在收集数据时,将标题行也收集进来,但标记为标题行(不参与去重)。
2. 在合并时,每个表格的标题行原样输出(不参与去重)。
3. 去重只针对数据行(非标题行)。
具体步骤:
第一阶段(收集数据):
- 对于每个工作表,首先处理标题行(假设是第一行)并标记为标题行('is_header': True)
- 然后处理数据行(从第二行开始)并标记为非标题行('is_header': False)
- 标题行不参与去重,但需要保留在输出中
第二阶段(去重):
- 只对非标题行进行去重(按Passport值分组去重)
- 标题行直接保留
第三阶段(写入输出):
- 按照原始顺序,先写入标题行,紧接着写入该表格的数据行(去重后的)
- 注意:每个表格的标题行只写入一次(即使该表格的数据行被去重后可能没有数据行,也要保留标题行)
- 同时,每个表格的标题行之前,我们也要添加第一列(文件名-工作表名)和Passport列。但标题行在第一列显示什么?可以显示为“文件名-工作表名(标题行)”或者留空?根据需求,我们可以显示为“字段行”
修改方案:
- 在收集数据时,对于标题行,我们记录:
'is_header': True
'passport' 可以设为None(因为标题行没有Passport值)
- 在去重阶段,跳过标题行,只对数据行去重
- 在写入时,标题行单独处理:第一列显示为“文件名-工作表名(字段行)”,Passport列显示为“Passport”(或者留空?)这里我们显示为“Passport”字段名,但注意每个表格的Passport列名可能不同,所以统一显示为“Passport”可能不合适。我们可以从标题行的Passport列对应的单元格获取字段名,但这样每个表格的标题行Passport列名可能不同。这里我们统一为“Passport”标题。
另一种做法:保留原始表格的标题行不变,即不添加第一列和Passport列到标题行?但是我们的输出表格结构是:
第一列:Source(用于显示来源)
第二列:Passport(用于显示Passport值)
后面的列:原始表格的列
那么对于标题行,Source列应该显示什么?我们可以显示为“Source”标题(即标题行自己的Source列标题)。但是这样就会和我们的数据行的Source列内容不一致(数据行显示的是文件名-工作表名)。
因此,我们需要统一:
- 在输出表格中,第一列标题为“Source”,第二列标题为“Passport”,后面的列标题为原始表格的列标题。
所以我们在输出表格的标题行(第一行)固定为:
A1: "Source"
B1: "Passport"
C1, D1, ...: 原始表格的列标题(从第一个表格获取?但每个表格列标题可能不同)
但是,现在要求保留每个表格的标题行,这意味着每个表格的标题行都要输出一遍。这会导致输出表格中出现多个标题行(每个表格前有一个标题行)。所以我们需要:
- 在输出表格中,每个表格的数据前先输出该表格的标题行(但要注意,我们的输出表格已经有了一个总的标题行(第一行)?)
需求是:每个原始表格的字段行(标题行)也要保留在合并后的表格中。
因此,合并后的表格结构应该是:
总标题行(可选?) + 多个表格块(每个表格块包含:该表格的标题行 + 该表格的数据行(去重后的))
但注意:我们之前已经有一个总标题行(第一行)了,现在每个表格块前又要加一个标题行(原始表格的标题行)。这样就会有两个标题行?
我们可以这样设计:
第一行:固定的总标题行(Source, Passport, 原始表格的列名(第一个表格的列名))—— 这个总标题行我们保留。
然后,对于每个原始表格:
写入一条记录:Source列显示文件名-工作表名(字段行),Passport列显示“Passport”,其他列为该表格的原始列标题(即该表格的标题行内容)
接着写入该表格的数据行(去重后的)
但是这样会导致Passport列在标题行显示为“Passport”,而数据行显示具体值。看起来不一致?
或者,我们不在每个表格块前加标题行,而是将每个表格的标题行当作一条特殊记录输出(不参与去重),并且Source列显示为“文件名-工作表名(字段行)”,Passport列留空。这样每个表格的标题行就变成了合并后表格中的一行。
这样设计后,合并后表格的结构:
第一行:总标题行(Source, Passport, 列1, 列2, ...)—— 这个我们保留,作为整个表格的标题行。
然后,每个表格的数据块:
一行标题行(来源信息标注为“文件名-工作表名(字段行)”,Passport列为空,其他列为原始表格的标题)
若干行数据行(去重后的)
这样,用户就能区分哪些是字段行,哪些是数据行。
因此,我们修改代码:
1. 在收集数据时,将标题行作为一条特殊记录收集,并设置标志('is_header': True),同时记录该标题行的所有单元格。
2. 标题行不参与去重,直接保留。
3. 在写入时,先写入总标题行(即输出表格的标题行,第一行)。
4. 然后遍历每个表格的数据块(注意:我们收集时是按行收集的,但我们需要保留表格之间的结构,即同一个表格的标题行和数据行要连续输出,不同表格之间可以有空行分隔?但需求没有要求空行,所以可以连续输出)。
但是,我们收集数据时已经打乱了原始表格的顺序(先收集所有表格的标题行,再收集数据行)。为了保持每个表格的标题行紧跟着该表格的数据行,我们需要在收集时按表格分组记录。
因此,调整数据结构:
all_tables = [] # 每个元素是一个表格块:{ 'file': 文件名, 'sheet': 工作表名, 'headers': [标题行数据], 'data_rows': [数据行列表] }
然后在每个表格块内,对数据行进行去重(按Passport去重)。
具体步骤:
第一阶段:收集每个表格的数据块
for 每个excel文件:
for 每个工作表:
找到Passport列
收集标题行(第一行)-> 作为该表格块的标题行
收集数据行(从第二行开始)-> 放入该表格块的数据行列表
第二阶段:对每个表格块的数据行进行去重(同一个表格块内去重)
for table in all_tables:
# 对table['data_rows']进行去重(按Passport值)
# 去重规则:优先保留有图片的行,若都没有则保留第一行
第三阶段:写入输出
先写入总标题行(第一行):Source, Passport, 列名(这里列名我们可以使用第一个表格块的标题行?但是每个表格的列数可能不同,列名也可能不同。所以我们使用输出表格的列结构:第一列Source,第二列Passport,后面列名来自第一个表格块的标题行?但这样后续表格块的列名可能对不上?)
由于每个表格块的列数、列名可能不同,我们无法统一列名。所以需要动态调整列数?
另一种做法:不预先设置总标题行,而是每个表格块用自己的标题行(即每个表格块有自己的列名)。这样输出表格的列数可能会变化。
考虑到需求是保留每个表格的字段行,所以合并后的表格中,不同表格块的列结构可能不同。因此,我们不做总标题行,而是每个表格块前用自己的标题行。
因此,输出表格将没有总标题行,只有每个表格块的标题行(显示为来源信息+该表格的原始标题行)。
那么输出表格的结构:
行1:表格块1的标题行(第一列显示“文件名1-工作表名1(字段行)”,第二列Passport列显示该表格Passport列标题,后面是其他列标题)
行2~行N:表格块1的数据行(去重后的)
行N+1:表格块2的标题行
行N+2~行M:表格块2的数据行
这样我们就避免了列名不统一的问题。
因此,我们需要修改:
- 不再预先写入总标题行
- 每个表格块先写入标题行(注意:标题行我们也要添加第一列(来源)和第二列(Passport)?)
对于标题行,我们如何处理新增的两列(Source列和Passport列)?
Source列:显示为“文件名-工作表名(字段行)”
Passport列:显示为该表格的Passport列标题(通过之前的find_passport_column找到的列位置,就能知道该列在原始标题行中的值)
但是,其他列怎么办?我们之前收集标题行时,只收集了原始表格的列。现在输出表格的结构是:
A列(Source列) -> 我们添加的
B列(Passport列) -> 我们添加的
C列开始 -> 原始表格的列(保持原始顺序)
所以标题行写入时:
A列:显示“文件名-工作表名(字段行)”(并应用标题样式)
B列:显示原始表格中Passport列的标题内容(即原始标题行中Passport列位置的值)
C列开始:原始标题行的其他列(从第一列开始,跳过Passport列?不对,原始标题行每个列都要写入,包括Passport列?)
但是注意:原始表格的Passport列在输出表格中已经变成了B列(我们的Passport列),所以我们不应该再重复输出原始表格的Passport列。所以原始表格的标题行中,Passport列的位置应该跳过,其他列依次写入。
然而,这样会导致列数错位:因为我们新增了两列,所以原始表格的列需要整体右移两列,并且Passport列的位置被我们新增的Passport列替代。
因此,在写入标题行时:
原始表格的标题行有N列(假设列索引从1到N)
我们已知Passport列在原始表格中的列索引为passport_col_idx(假设是3)
那么我们在输出表格中写入标题行:
A列(1列):来源信息
B列(2列):原始表格中第3列的值(即Passport列标题)
C列(3列):原始表格中第1列的值
D列(4列):原始表格中第2列的值
E列(5列):原始表格中第4列的值
...(跳过原始表格的第3列,因为已经放在B列了)
这样逻辑会很混乱,因为打乱了原始表格的列顺序。
考虑到需求是保留原始表格的字段行,并且每个表格的字段行结构可能不同,我们不如完全保留原始表格的列顺序,而将Source列和Passport列作为新增的两列放在最前面。这样原始表格的标题行在输出中保持不变(除了新增的两列)。
因此,对于标题行,我们这样写入:
A列:来源信息(如“file1-sheet1(字段行)”)
B列:字符串“Passport”(因为我们新增的Passport列,在标题行中我们不知道原始表格Passport列标题是什么,所以我们就写“Passport”)
C列开始:原始标题行的所有列(包括原始Passport列标题)
这样,标题行中就会有两个Passport列:一个是新增的B列(标题为“Passport”),一个是原始表格的Passport列(在C列之后的某个位置)。这看起来重复了。
所以,为了避免重复,我们可以在写入标题行时,将原始表格的Passport列标题省略不写(因为已经在新列B列写了“Passport”)。但是这样会丢失原始表格的列信息。
权衡之后,我们决定:
A列:来源信息
B列:原始表格的Passport列标题(这样保留了原始列名,避免与后面的原始表格列重复)
C列开始:原始表格的其他列(不包括Passport列)
这样,标题行中Passport列只出现一次(在B列),其他列按顺序写入。
但在数据行中,Passport列的值我们放在B列,原始表格Passport列的位置(假设是第3列)我们就跳过,其他列依次填入。
因此,在收集数据行时,我们就要跳过原始表格的Passport列吗?不对,数据行中Passport列的值我们是从原始表格的Passport列提取的,然后单独放在B列。但是原始表格Passport列位置的数据我们不再显示(因为已经在B列显示了)。所以原始表格Passport列的位置在输出中应该被跳过。
所以,我们在收集数据行时,对于原始表格的每一行,除了Passport列以外的列我们都要收集(因为Passport列的值已经单独提取放到B列了)。
这样,在收集标题行时,我们也要跳过Passport列。
因此,修改收集数据的方式:
标题行收集:
A列:来源信息(字符串)
B列:原始表格Passport列标题(字符串)
后面:原始表格标题行中除了Passport列以外的其他列(按顺序)
数据行收集:
A列:来源信息(文件名-工作表名)
B列:Passport值
后面:原始表格该行中除了Passport列以外的其他列(按顺序)
这样,输出表格的结构就一致了。
但是,这样修改会导致代码复杂度增高,因为要跳过Passport列。
考虑到时间,我们可以采用另一种简单方法:
不跳过Passport列,而是在输出表格中保留原始表格的所有列(包括Passport列),同时新增Source列和Passport列(我们的Passport列)。这样就会有两列Passport:一列是我们新增的(放在B列),一列是原始表格的(在后面的列中)。
在标题行中,原始表格的Passport列标题保留,我们的Passport列标题写“Passport”。这样用户可以看到原始表格的Passport列标题是什么。
在数据行中,我们的Passport列(B列)写入Passport值,原始表格的Passport列位置写入原始值(实际上就是同一个值,重复了)。这样虽然重复,但信息完整。
为了简单起见,我们采用重复列的方法。
因此,在收集数据时,标题行和数据行都不跳过Passport列,而是完整保留原始表格的所有列。
写入标题行时:
A列:来源信息(标题行我们写“文件名-工作表名(字段行)”)
B列:字符串“Passport”(作为我们新增Passport列的标题)
C列开始:原始表格的标题行(包括Passport列)
这样,在标题行中,原始表格的Passport列标题会出现在C列之后的某个位置(假设原始表格Passport列是第1列,那么在C列就会显示原始表格Passport列标题,而B列是我们写的“Passport”)。
在数据行中:
A列:来源信息(文件名-工作表名)
B列:Passport值(从原始表格Passport列提取)
C列开始:原始表格该行的所有列(包括Passport列的值)
这样,Passport值会在B列和原始表格Passport列位置重复出现。
用户可以根据需要删除重复列。
基于以上思路,我们修改代码。
由于改动较大,我们重新组织代码:
数据结构:
tables = [] # 每个元素是一个字典,代表一个表格块
table = {
'file': 文件名,
'sheet': 工作表名,
'passport_col_idx': Passport列索引(从1开始),
'headers': 标题行(第一行)的单元格数据列表(每个单元格包含值和样式),
'data_rows': [ # 数据行列表
{
'passport': Passport值,
'has_image': 是否有图片,
'images': 图片列表,
'row_data': 该行的单元格数据列表(包括所有列,每个单元格包含值、样式),
'row_idx': 行号
},
...
]
}
处理步骤:
1. 遍历每个文件和工作表,提取表格块
2. 对每个表格块的data_rows按passport去重(规则如前)
3. 写入输出表格:
current_row = 1 # 当前行
for table in tables:
# 写入该表格块的标题行
# A列: f"{table['file']}-{table['sheet']} (字段行)"
# B列: "Passport" (这里我们固定写)
# C列开始: 原始标题行的所有单元格(包括Passport列)
# 写入该表格块的数据行(去重后的)
# A列: f"{table['file']}-{table['sheet']}"
# B列: passport值
# C列开始: 该行原始数据
4. 处理图片:在写入数据行时,如果该行有图片,则添加图片(注意位置偏移)
注意:由于每个表格块都有自己的标题行,所以不需要总标题行。
具体代码实现如下:
</think>以下代码实现了合并带有图片的Excel表格,保留每个表格的字段行(标题行),并根据"Passport"关键字字段去重。合并后的表格会保留所有原始表格的标题行,并对数据行按Passport值去重(保留有图片的行,若无图片则保留第一行)。
```python
import os
import glob
import zipfile
from openpyxl import load_workbook, Workbook
from openpyxl.drawing.image import Image
from openpyxl.drawing.spreadsheet_drawing import AnchorMarker, TwoCellAnchor, OneCellAnchor
from openpyxl.styles import PatternFill, Border, Side, Alignment, Font, colors
from openpyxl.utils import get_column_letter
import copy
import re
def is_valid_excel_file(file_path):
"""检查是否为有效的Excel文件"""
try:
with zipfile.ZipFile(file_path, 'r') as zip_ref:
required_files = ['xl/workbook.xml', 'xl/styles.xml', 'xl/worksheets/']
return all(name in zip_ref.namelist() for name in required_files[:-1])
except zipfile.BadZipFile:
return False
except Exception:
return False
def copy_cell_style(src_cell, dest_cell):
"""复制单元格样式"""
if src_cell.font:
dest_cell.font = copy.copy(src_cell.font)
if src_cell.fill:
dest_cell.fill = copy.copy(src_cell.fill)
if src_cell.border:
dest_cell.border = copy.copy(src_cell.border)
if src_cell.alignment:
dest_cell.alignment = copy.copy(src_cell.alignment)
if src_cell.number_format:
dest_cell.number_format = src_cell.number_format
def find_passport_column(ws):
"""
查找包含'passport'关键字的列(不区分大小写)
返回列字母和列索引(从1开始)
"""
# 检查标题行(第一行)
for cell in ws[1]:
if cell.value and re.search(r'passport', str(cell.value), re.IGNORECASE):
return cell.column_letter, cell.column
# 如果第一行没找到,检查所有单元格
for row in ws.iter_rows(min_row=1, max_row=min(10, ws.max_row)):
for cell in row:
if cell.value and re.search(r'passport', str(cell.value), re.IGNORECASE):
return cell.column_letter, cell.column
# 仍未找到,默认第一列
return 'A', 1
def create_header_style():
"""创建第一列的标题样式"""
return (
Font(bold=True, color=colors.WHITE),
PatternFill(start_color="4F81BD", end_color="4F81BD", fill_type="solid"),
Alignment(horizontal='center', vertical='center')
)
def merge_excel_with_headers_and_deduplication(input_dir, output_file):
"""
合并Excel文件并保留所有字段行(标题行)
基于Passport字段去重数据行
"""
# 创建新工作簿
wb_output = Workbook()
ws_output = wb_output.active
ws_output.title = "Merged with Headers"
# 创建第一列标题的样式
header_font, header_fill, header_alignment = create_header_style()
ws_output.column_dimensions['A'].width = 40
# 存储所有处理的表格块
table_blocks = []
# 用于去重的Passport字典
passport_map = {}
current_row = 1
processed_files = 0
processed_sheets = 0
try:
# 遍历目录中所有Excel文件
for excel_file in glob.glob(os.path.join(input_dir, "*.xlsx")):
file_name = os.path.basename(excel_file)
if not is_valid_excel_file(excel_file):
print(f"跳过无效文件: {file_name}")
continue
print(f"处理文件: {file_name}")
try:
wb_source = load_workbook(excel_file, keep_vba=False, data_only=True)
processed_files += 1
except Exception as e:
print(f" 加载失败: {str(e)}")
continue
for ws_name in wb_source.sheetnames:
processed_sheets += 1
print(f" 处理工作表: {ws_name}")
ws_source = wb_source[ws_name]
# 跳过空工作表
if ws_source.max_row == 0:
print(" 空工作表,跳过")
continue
# 查找Passport列
passport_col_letter, passport_col_idx = find_passport_column(ws_source)
print(f" 检测到Passport列: {passport_col_letter}")
# 收集图片信息
image_rows = {}
if hasattr(ws_source, '_images'):
for img in ws_source._images:
# 获取图片所在行
if isinstance(img.anchor, (TwoCellAnchor, OneCellAnchor)):
anchor = img.anchor
img_row = anchor._from.row if hasattr(anchor, '_from') else anchor.row
if img_row not in image_rows:
image_rows[img_row] = []
image_rows[img_row].append(img)
# 存储当前表格块的数据
table_block = {
'file': file_name,
'sheet': ws_name,
'passport_col': passport_col_idx,
'header_row': [], # 标题行数据
'data_rows': [], # 数据行数据
'source_images': image_rows,
'start_row': current_row # 在输出中的起始行
}
# 处理标题行(第一行)
header_row = []
for cell in ws_source[1]:
cell_data = {
'value': cell.value,
'font': copy.copy(cell.font),
'fill': copy.copy(cell.fill),
'border': copy.copy(cell.border),
'alignment': copy.copy(cell.alignment),
'number_format': cell.number_format
}
header_row.append(cell_data)
table_block['header_row'] = header_row
# 处理数据行(从第二行开始)
for row_idx in range(2, ws_source.max_row + 1):
row_data = []
passport_value = None
has_image = row_idx in image_rows
for col_idx, cell in enumerate(ws_source[row_idx], 1):
# 获取Passport值
if col_idx == passport_col_idx:
passport_value = str(cell.value).strip() if cell.value else ""
cell_data = {
'value': cell.value,
'font': copy.copy(cell.font),
'fill': copy.copy(cell.fill),
'border': copy.copy(cell.border),
'alignment': copy.copy(cell.alignment),
'number_format': cell.number_format
}
row_data.append(cell_data)
# 添加数据行信息
table_block['data_rows'].append({
'row_idx': row_idx,
'passport': passport_value,
'row_data': row_data,
'has_image': has_image
})
# 添加表格块到列表
table_blocks.append(table_block)
# 更新当前行计数器(标题行 + 数据行 + 分隔行)
current_row += len(table_block['data_rows']) + 2
except Exception as e:
print(f"收集数据时出错: {str(e)}")
raise
# 第二阶段:处理去重并写入输出
current_row = 1 # 从头开始写入
# 1. 写入全局标题行(可选)
# 这里跳过,每个表格保留自己的标题行
# 2. 处理每个表格块
passport_count = {}
for block in table_blocks:
source_info = f"{block['file']}-{block['sheet']}"
# 写入表格标题标识
title_cell = ws_output.cell(row=current_row, column=1)
title_cell.value = f"表格来源: {source_info}"
title_cell.font = Font(bold=True, size=12, color="000000")
title_cell.fill = PatternFill(start_color="D9E1F2", end_color="D9E1F2", fill_type="solid")
current_row += 1
# 写入原始标题行
for col_idx, cell_data in enumerate(block['header_row'], 1):
dest_cell = ws_output.cell(row=current_row, column=col_idx + 1) # 第二列开始
# 添加源标识到第一列
if col_idx == 1:
source_cell = ws_output.cell(row=current_row, column=1)
source_cell.value = source_info + " (字段行)"
source_cell.font = header_font
source_cell.fill = header_fill
source_cell.alignment = header_alignment
# 复制标题行数据
if 'value' in cell_data:
dest_cell.value = cell_data['value']
# 应用样式
if cell_data['font']:
dest_cell.font = cell_data['font']
if cell_data['fill']:
dest_cell.fill = cell_data['fill']
if cell_data['border']:
dest_cell.border = cell_data['border']
if cell_data['alignment']:
dest_cell.alignment = cell_data['alignment']
if 'number_format' in cell_data:
dest_cell.number_format = cell_data['number_format']
current_row += 1
# 处理数据行(应用去重规则)
for data_row in block['data_rows']:
passport_value = data_row['passport']
# 跳过空Passport行
if not passport_value:
continue
# 检查是否已存在相同的Passport
if passport_value in passport_map:
# 比较图片存在情况
existing_row = passport_map[passport_value]
new_has_image = data_row['has_image']
# 规则:优先保留有图片的行
if new_has_image and not existing_row['has_image']:
# 新行有图片而旧行没有,替换
passport_map[passport_value] = data_row
passport_count[passport_value] = passport_count.get(passport_value, 0) + 1
else:
# 新Passport值,直接添加
passport_map[passport_value] = data_row
passport_count[passport_value] = 1
# 写入去重后的数据行
for passport_value, data_row in passport_map.items():
# 只写入当前表格块的数据行
if data_row not in block['data_rows']:
continue
# 写入源标识
source_cell = ws_output.cell(row=current_row, column=1)
source_cell.value = source_info
source_cell.font = header_font
source_cell.fill = header_fill
source_cell.alignment = header_alignment
# 写入Passport值
passport_cell = ws_output.cell(row=current_row, column=block['passport_col'] + 1)
passport_cell.value = passport_value
# 写入其他数据
for col_idx, cell_data in enumerate(data_row['row_data'], 1):
dest_cell = ws_output.cell(row=current_row, column=col_idx + 1) # 第二列开始
if 'value' in cell_data:
dest_cell.value = cell_data['value']
# 应用样式
if cell_data['font']:
dest_cell.font = cell_data['font']
if cell_data['fill']:
dest_cell.fill = cell_data['fill']
if cell_data['border']:
dest_cell.border = cell_data['border']
if cell_data['alignment']:
dest_cell.alignment = cell_data['alignment']
if 'number_format' in cell_data:
dest_cell.number_format = cell_data['number_format']
# 处理图片
if data_row['has_image'] and data_row['row_idx'] in block['source_images']:
for img in block['source_images'][data_row['row_idx']]:
img_copy = copy.deepcopy(img)
anchor = img_copy.anchor
# 调整锚点位置
if isinstance(anchor, TwoCellAnchor):
new_anchor = TwoCellAnchor(
_from=AnchorMarker(
col=anchor._from.col,
row=anchor._from.row + current_row - data_row['row_idx'],
colOff=anchor._from.colOff,
rowOff=anchor._from.rowOff
),
to=AnchorMarker(
col=anchor.to.col,
row=anchor.to.row + current_row - data_row['row_idx'],
colOff=anchor.to.colOff,
rowOff=anchor.to.rowOff
)
)
img_copy.anchor = new_anchor
elif isinstance(anchor, OneCellAnchor):
new_anchor = OneCellAnchor(
_from=AnchorMarker(
col=anchor._from.col,
row=anchor._from.row + current_row - data_row['row_idx'],
colOff=anchor._from.colOff,
rowOff=anchor._from.rowOff
)
)
img_copy.anchor = new_anchor
# 添加图片
ws_output.add_image(img_copy)
current_row += 1
# 添加表格块分隔行
current_row += 2
# 保存结果
try:
wb_output.save(output_file)
print(f"\n合并完成! 输出文件: {output_file}")
print(f"处理文件: {processed_files}, 工作表: {processed_sheets}")
print(f"去重后Passport数量: {len(passport_map)}")
# 统计重复项
duplicates = {k: v for k, v in passport_count.items() if v > 1}
if duplicates:
print(f"发现 {len(duplicates)} 个重复Passport值:")
for passport, count in list(duplicates.items())[:5]:
print(f" - {passport}: {count}次")
if len(duplicates) > 5:
print(f" 和另外 {len(duplicates)-5} 个重复值...")
except Exception as e:
print(f"保存文件时出错: {str(e)}")
raise
if __name__ == "__main__":
input_directory = "./input_excels" # Excel文件所在目录
output_filename = "merged_with_headers.xlsx"
merge_excel_with_headers_and_deduplication(input_directory, output_filename)
```
### 关键功能说明:
#### 1. 保留所有表格的字段行(标题行)
```python
# 处理标题行(第一行)
header_row = []
for cell in ws_source[1]:
cell_data = {
'value': cell.value,
'font': copy.copy(cell.font),
'fill': copy.copy(cell.fill),
'border': copy.copy(cell.border),
'alignment': copy.copy(cell.alignment),
'number_format': cell.number_format
}
header_row.append(cell_data)
table_block['header_row'] = header_row
# 写入输出表格
for col_idx, cell_data in enumerate(block['header_row'], 1):
dest_cell = ws_output.cell(row=current_row, column=col_idx + 1)
# ... 复制值和样式 ...
```
- 每个表格的第一行被识别为标题行(字段行)
- 标题行的样式和数据被完整保留
- 在输出表格中,每个表格块前会显示自己的标题行
#### 2. 表格块结构可视化
```python
# 写入表格标题标识
title_cell = ws_output.cell(row=current_row, column=1)
title_cell.value = f"表格来源: {source_info}"
title_cell.font = Font(bold=True, size=12, color="000000")
title_cell.fill = PatternFill(start_color="D9E1F2", end_color="D9E1F2", fill_type="solid")
```
- 每个表格块前添加醒目标识行
- 使用浅蓝色背景和加粗字体突出显示
- 清晰展示表格来源(文件名和工作表名)
#### 3. 去重逻辑优化
```python
# 处理数据行(应用去重规则)
for data_row in block['data_rows']:
passport_value = data_row['passport']
if not passport_value:
continue # 跳过空值
if passport_value in passport_map:
# 比较图片存在情况
existing_row = passport_map[passport_value]
new_has_image = data_row['has_image']
# 规则:优先保留有图片的行
if new_has_image and not existing_row['has_image']:
passport_map[passport_value] = data_row # 替换
else:
passport_map[passport_value] = data_row # 新值
```
- 使用字典进行高效去重
- 优先保留有图片的行
- 若无图片则保留第一个出现的行
- 空Passport值会被跳过
#### 4. 图片处理优化
```python
# 调整图片锚点位置
new_anchor = TwoCellAnchor(
_from=AnchorMarker(
col=anchor._from.col,
row=anchor._from.row + current_row - data_row['row_idx'],
...
),
...
)
```
- 精确计算行偏移量
- 保持图片与原始行的相对位置
- 使用深拷贝确保图片数据完整
### 输出文件结构示例:
```
行1: [表格来源: file1.xlsx-Sheet1] <- 表格块标识行
行2: [源信息] | 字段1 | 字段2 | ... | Passport字段 | ... <- 标题行
行3: 数据行1(Passport: ABC123)
行4: 数据行2(Passport: DEF456)
...
行N: [空行分隔]
行N+1: [表格来源: file2.xlsx-Sheet2]