1. 办公场景里的数据匹配痛点
1.1 为什么Vlookup总是让人又爱又恨
只要在办公室待过一段时间,大概率都见过这样的场景:两张Excel表,一张是订单明细,一张是客户信息,需要把客户等级、所属区域、联系方式匹配到订单表里。第一反应就是打开Excel,敲一个VLOOKUP公式,往下一拉,完事。但真正操作过的人都知道,这事远没有想象中那么轻松。
VLOOKUP的硬伤其实很明显。第一,它要求查找值必须在目标区域的第一列,稍微有点经验的用户都知道,为了满足这个条件,经常要把列来回挪动,挪完之后原来的格式全乱了。第二,它返回的是值,不是格式。如果源表里某个单元格是红色加粗字体表示异常,匹配过去之后只剩一个干巴巴的值,颜色和字体全丢了。第三,数据量一大,几万行下去,Excel直接卡成幻灯片,改一个单元格要等半天重算。第四,公式一旦写错,排查起来非常痛苦,尤其是跨工作表、跨文件的引用,找半天找不到问题在哪。
更让人头疼的是,很多业务场景下,匹配过去的单元格需要保留原有的格式。比如财务报表里,不同科目有不同的底色标注;比如库存表里,低于安全库存的数量用红色字体显示。这些格式信息本身就是数据的一部分,VLOOKUP完全无能为力。
1.2 用Python做数据匹配的核心优势
换成Python来做这件事,思路就完全不一样了。Python操作Excel文件,本质上是在读写一个结构化的数据对象,你可以精确控制每一个单元格的值、字体、边框、填充色、数字格式,甚至是条件格式规则。匹配逻辑用代码写出来,清晰可控,想怎么改就怎么改,不存在“公式拖错行”这种问题。
而且Python处理几万行数据的速度,比Excel公式快几个数量级。我实测过一个场景:两张表各五万行,用VLOOKUP匹配需要将近两分钟,而且每次修改源数据都要重新计算;用Python脚本处理,从读取到写出结果文件,总共不到十秒。这个差距在批量处理场景下会被进一步放大。
另外,Python方案的可复用性极强。写好一个脚本,下次换两张表,只需要改一下文件路径和列名映射,直接跑就行。不需要重新学一套公式语法,也不需要担心Excel版本差异导致的函数行为不一致。
1.3 这篇文章适合谁来读
如果你每天都在和Excel打交道,经常需要做跨表匹配,又对VLOOKUP的种种限制感到不满,那这篇内容就是写给你的。不需要你有多深的编程基础,只要能看懂基本的Python语法,跟着步骤走,就能把整套方案跑通。
如果你已经会用pandas做数据合并,但不知道怎么保留单元格格式,那这篇文章会补上你知识体系里最关键的一块拼图。如果你是完全的编程新手,也没关系,我会把每一步的操作意图和背后的原理都讲清楚,遇到需要理解的概念会用生活化的例子来解释。
注意:本文所有代码基于Python 3.9以上版本测试通过,用到的核心库是openpyxl,安装命令为
pip install openpyxl。如果你还没有Python环境,建议先装一个Anaconda,省去配置环境的麻烦。
2. 整体方案设计与技术选型
2.1 为什么选openpyxl而不是pandas
提到Python处理Excel,很多人第一反应是pandas。pandas确实强大,read_excel一行代码就能把表读进来,merge一下就能完成匹配,再to_excel写出去。但问题在于,pandas在读写过程中会丢失绝大部分格式信息。单元格的字体、颜色、边框、合并单元格、条件格式,统统保不住。对于只需要数据结果的场景,pandas是首选;但对于需要保留单元格格式的场景,pandas就不够用了。
openpyxl则不同,它直接操作Excel文件的XML结构,能够精确读取和写入每一个单元格的样式属性。你可以把它理解成“用代码模拟人在Excel里的操作”,你选中一个单元格,设置它的字体为红色,openpyxl就能做到一模一样的效果。而且openpyxl支持读取已有的xlsx文件,在保留原有格式的基础上修改内容,这正是我们需要的。
当然,openpyxl也不是没有缺点。它的数据处理能力比pandas弱很多,没有向量化操作,逐行遍历几万行数据会慢一些。但在这个场景下,我们主要的时间花在IO和格式处理上,数据匹配本身的逻辑并不复杂,openpyxl完全够用。
2.2 匹配逻辑的两种实现路径
具体到匹配逻辑,有两种常见的实现路径。第一种是“字典映射法”:先把源表读进来,构建一个字典,key是匹配列的值,value是整行数据或者需要匹配的若干列数据。然后遍历目标表,对每一行去字典里查,查到就把对应的值写过去。这种方法的优点是速度快,字典查找的时间复杂度是O(1),几万行数据几乎瞬间完成。
第二种是“双层循环法”:对目标表的每一行,遍历源表的每一行,找到匹配的行就取值。这种方法逻辑最直观,但效率极低,数据量稍微大一点就慢得无法接受。除非数据量极小(比如几百行),否则不建议用这种方式。
我们采用第一种方案,用字典做映射。具体来说,又分两种情况:如果源表的匹配列没有重复值,直接用单层字典;如果有重复值,需要用嵌套字典或者列表来存储多条匹配记录。大多数业务场景下,匹配列是唯一的,比如订单号、客户编号、产品SKU,所以单层字典就够了。
2.3 格式保留的关键技术点
格式保留是这套方案的核心难点。openpyxl里,每个单元格的样式是通过cell.font、cell.fill、cell.border、cell.alignment、cell.number_format这些属性来控制的。当你从源表读取一个单元格时,可以同时读取它的值和这些样式属性;写入目标表时,再把样式属性赋给目标单元格。
但这里有一个坑:openpyxl的样式对象是共享的,直接赋值可能会导致多个单元格引用同一个样式对象,修改一个会影响其他。正确的做法是使用copy模块的copy()函数,对样式对象做深拷贝,确保每个单元格有独立的样式副本。
另一个需要注意的点是合并单元格。如果源表里有合并单元格,直接读取只有左上角那个单元格有值,其他单元格是None。匹配的时候需要特别处理,否则会漏掉数据。处理方法是先读取合并区域的范围,把值填充到区域内的所有单元格,再做匹配。
2.4 整体流程拆解
整个方案的流程可以拆成五步:第一步,读取源表,构建匹配字典,同时记录每个匹配值对应的格式信息;第二步,读取目标表,确定需要匹配的列和目标写入列;第三步,遍历目标表,逐行查找匹配,写入值和格式;第四步,处理特殊情况,比如匹配不到时的默认值、合并单元格的格式复制;第五步,保存结果文件,验证输出。
这个流程看起来简单,但每一步都有细节需要注意。比如第一步构建字典时,如果匹配列的值有前后空格,需要先做strip处理,否则会出现“看起来一样但匹配不上”的问题。第三步写入格式时,要注意目标单元格原有的格式是否要保留,还是完全覆盖为源表的格式。这些细节我会在后面的实操部分逐一展开。
3. 核心细节解析与实操要点
3.1 读取源表并构建匹配字典
先来看读取源表和构建字典的代码。假设源表叫source.xlsx,匹配列是A列(客户编号),需要匹配的是B列(客户名称)和C列(客户等级),同时要保留B列和C列的格式。
from openpyxl import load_workbook from copy import copy # 加载源表,data_only=True表示读取公式计算后的值 wb_source = load_workbook('source.xlsx', data_only=True) ws_source = wb_source.active # 构建匹配字典 match_dict = {} for row in ws_source.iter_rows(min_row=2, values_only=False): key = row[0].value # A列是匹配键 if key is None: continue key = str(key).strip() # 统一转字符串并去空格 match_dict[key] = { 'name': row[1].value, 'level': row[2].value, 'name_font': copy(row[1].font), 'name_fill': copy(row[1].fill), 'name_border': copy(row[1].border), 'name_alignment': copy(row[1].alignment), 'name_number_format': row[1].number_format, 'level_font': copy(row[2].font), 'level_fill': copy(row[2].fill), 'level_border': copy(row[2].border), 'level_alignment': copy(row[2].alignment), 'level_number_format': row[2].number_format, }这段代码有几个关键点。第一,data_only=True的作用是读取公式的计算结果而不是公式本身。如果你的源表里有公式,不加这个参数读出来就是公式字符串,匹配过去就错了。第二,str(key).strip()是为了处理数字和文本混用的情况。Excel里经常出现“001”和“1”被当成不同值的问题,统一转成字符串再去空格,能避免大部分匹配失败。第三,样式对象用copy()做深拷贝,这是必须的,否则后面写入时会出现样式串扰。
实操心得:如果源表的匹配列有重复值,上面的字典会覆盖前面的记录,只保留最后一条。遇到这种情况,需要把
match_dict[key]改成列表,每次append一条记录,匹配时再根据业务规则选择用哪一条。我一般会加一个判断,如果key已存在,就打印一条警告日志,方便排查数据质量问题。
3.2 遍历目标表并写入匹配结果
接下来是遍历目标表,逐行匹配并写入。假设目标表叫target.xlsx,匹配列也是A列,需要把客户名称写入D列,客户等级写入E列。
wb_target = load_workbook('target.xlsx') ws_target = wb_target.active for row in ws_target.iter_rows(min_row=2): key = row[0].value if key is None: continue key = str(key).strip() if key in match_dict: record = match_dict[key] # 写入客户名称到D列(索引3) cell_d = row[3] cell_d.value = record['name'] cell_d.font = copy(record['name_font']) cell_d.fill = copy(record['name_fill']) cell_d.border = copy(record['name_border']) cell_d.alignment = copy(record['name_alignment']) cell_d.number_format = record['name_number_format'] # 写入客户等级到E列(索引4) cell_e = row[4] cell_e.value = record['level'] cell_e.font = copy(record['level_font']) cell_e.fill = copy(record['level_fill']) cell_e.border = copy(record['level_border']) cell_e.alignment = copy(record['level_alignment']) cell_e.number_format = record['level_number_format'] else: # 匹配不到时的处理 row[3].value = '未匹配' row[4].value = '未匹配' wb_target.save('result.xlsx')这段代码的逻辑很直白:遍历目标表的每一行,拿到匹配键,去字典里查,查到就写入值和格式,查不到就写“未匹配”。但有几个细节值得展开说。
第一,row是一个元组,里面的元素是Cell对象。row[3]拿到的是第4列的Cell对象,直接修改它的value和样式属性,就相当于在Excel里选中这个单元格然后改内容。这种写法比用ws_target.cell(row=i, column=4)更简洁,而且性能更好,因为iter_rows是一次性生成所有行的引用。
第二,样式属性的赋值顺序没有严格要求,但建议先设值再设样式,避免某些情况下样式被重置。number_format单独处理是因为它不是通过copy()来复制的,直接赋值字符串就行。
第三,匹配不到时的处理策略要根据业务需求来定。有些场景下需要保留原值不动,有些场景下需要标红提示,有些场景下需要写默认值。我一般会写一个配置项,让使用者自己决定。
3.3 处理合并单元格的格式复制
合并单元格是Excel里最容易出问题的部分。openpyxl读取合并单元格时,只有左上角的单元格有值,其他单元格的value是None。如果源表的匹配列恰好是合并单元格,直接遍历会漏掉数据。
处理方法是先获取所有合并区域,把值填充到区域内的每个单元格。代码大致如下:
# 获取源表所有合并区域 merged_ranges = list(ws_source.merged_cells.ranges) # 构建一个映射,记录每个单元格属于哪个合并区域 merged_map = {} for merged_range in merged_ranges: min_row, min_col = merged_range.min_row, merged_range.min_col max_row, max_col = merged_range.max_row, merged_range.max_col top_left_value = ws_source.cell(row=min_row, column=min_col).value for r in range(min_row, max_row + 1): for c in range(min_col, max_col + 1): merged_map[(r, c)] = (min_row, min_col, top_left_value)然后在遍历时,如果当前单元格在merged_map里,就用top_left_value作为匹配键。写入目标表时,如果目标表也有对应的合并区域,需要把格式应用到整个区域,而不是单个单元格。
注意:合并单元格的格式复制比普通单元格复杂得多。如果源表的合并区域跨了多行多列,写入目标表时也要创建同样范围的合并区域,否则格式会显示异常。我建议在匹配之前先检查两张表的合并结构是否一致,如果不一致,要么调整源表结构,要么在代码里做映射转换。
3.4 性能优化的几个实用技巧
openpyxl处理大文件时,性能是绕不开的话题。几个实测有效的优化手段:
第一个是使用read_only=True模式读取源表。这个模式下openpyxl不会把整个文件加载到内存,而是流式读取,内存占用大幅降低。但缺点是只能读不能写,而且不能随机访问单元格。对于只需要顺序遍历源表构建字典的场景,read_only模式非常合适。
第二个是关闭目标表的公式计算。load_workbook有一个keep_links参数,设为False可以避免加载外部链接,加快打开速度。另外,如果目标表里有大量公式,写入新值后Excel会重新计算,可以在保存前把wb.calculation.fullCalcOnLoad设为False,避免打开文件时卡顿。
第三个是批量写入。openpyxl的cell()方法每次调用都有开销,如果需要在循环里写大量数据,可以先把数据收集到一个列表里,最后一次性写入。不过在我们的场景下,写入操作本身不多,这个优化的收益有限。
第四个是使用lxml作为XML解析器。openpyxl默认用Python自带的XML解析器,速度一般。安装lxml之后,openpyxl会自动切换过去,读写速度能提升30%以上。安装命令是pip install lxml,装完不用改代码,openpyxl会自动检测。
4. 完整实操流程与代码实现
4.1 环境准备与依赖安装
先把环境搭好。我推荐用Anaconda创建独立的虚拟环境,避免和系统里的其他包冲突。
conda create -n excel_match python=3.10 conda activate excel_match pip install openpyxl lxml如果你不用Anaconda,用系统自带的Python也行,只要版本在3.9以上。安装完依赖后,建一个项目文件夹,把源表和目标表放进去,再新建一个match.py文件,代码就写在这里面。
实操心得:建议把源表、目标表和脚本放在同一个目录下,用相对路径引用文件。这样整个文件夹拷到别的电脑上也能直接跑,不需要改路径。如果文件在别的目录,用
os.path.join拼接路径,不要用字符串硬拼,避免Windows和Mac的路径分隔符差异。
4.2 完整代码实现与逐段解析
下面是完整的代码实现,我把整个逻辑封装成了一个函数,方便复用。
import os from copy import copy from openpyxl import load_workbook def match_excel(source_path, target_path, output_path, source_key_col=1, target_key_col=1, match_columns=None, default_value='未匹配'): """ 源表与目标表数据匹配,保留单元格格式 参数: source_path: 源表文件路径 target_path: 目标表文件路径 output_path: 输出文件路径 source_key_col: 源表匹配列索引(从1开始) target_key_col: 目标表匹配列索引(从1开始) match_columns: 匹配列映射,格式为[(源表列索引, 目标表列索引), ...] default_value: 匹配不到时的默认值 """ if match_columns is None: match_columns = [(2, 4), (3, 5)] # 默认匹配第2列到第4列,第3列到第5列 # 第一步:读取源表,构建匹配字典 wb_source = load_workbook(source_path, data_only=True, read_only=True) ws_source = wb_source.active match_dict = {} for row in ws_source.iter_rows(min_row=2): key_cell = row[source_key_col - 1] key = key_cell.value if key is None: continue key = str(key).strip() record = {} for src_col, tgt_col in match_columns: cell = row[src_col - 1] record[tgt_col] = { 'value': cell.value, 'font': copy(cell.font), 'fill': copy(cell.fill), 'border': copy(cell.border), 'alignment': copy(cell.alignment), 'number_format': cell.number_format, } match_dict[key] = record wb_source.close() # 第二步:读取目标表,逐行匹配写入 wb_target = load_workbook(target_path) ws_target = wb_target.active matched_count = 0 unmatched_count = 0 for row in ws_target.iter_rows(min_row=2): key_cell = row[target_key_col - 1] key = key_cell.value if key is None: continue key = str(key).strip() if key in match_dict: record = match_dict[key] for tgt_col, style_info in record.items(): cell = row[tgt_col - 1] cell.value = style_info['value'] cell.font = copy(style_info['font']) cell.fill = copy(style_info['fill']) cell.border = copy(style_info['border']) cell.alignment = copy(style_info['alignment']) cell.number_format = style_info['number_format'] matched_count += 1 else: for _, tgt_col in match_columns: row[tgt_col - 1].value = default_value unmatched_count += 1 # 第三步:保存结果 wb_target.save(output_path) wb_target.close() print(f'匹配完成:成功 {matched_count} 条,未匹配 {unmatched_count} 条') print(f'结果已保存至:{output_path}') if __name__ == '__main__': match_excel( source_path='source.xlsx', target_path='target.xlsx', output_path='result.xlsx', source_key_col=1, target_key_col=1, match_columns=[(2, 4), (3, 5)], default_value='未匹配' )这段代码把整个流程封装成了一个函数,参数化程度比较高。match_columns是一个列表,每个元素是一个元组,表示“源表的第几列匹配到目标表的第几列”。这样设计的好处是灵活,不管你要匹配几列,只需要改这个列表就行,不需要动核心逻辑。
read_only=True模式读取源表,内存占用低,适合大文件。但要注意,read_only模式下不能随机访问单元格,只能顺序遍历,所以构建字典时必须用iter_rows。另外,read_only模式下ws_source.close()是必须的,否则文件句柄不会释放。
4.3 运行结果验证与格式对比
跑完脚本后,打开result.xlsx,重点检查几个地方。第一,匹配到的单元格值是否正确,特别是数字和文本混用的列,看看有没有出现“001”变成“1”的情况。第二,格式是否保留,包括字体、颜色、边框、对齐方式、数字格式。第三,匹配不到的单元格是否按预期写了默认值。
我一般会做一个抽样对比:从源表随机抽10条记录,在结果表里找到对应的行,逐项核对值和格式。如果发现格式不对,大概率是copy()漏了某个属性,或者目标单元格原有的条件格式覆盖了手动设置的格式。
注意:如果目标表本身有条件格式规则,手动设置的格式可能会被条件格式覆盖。这种情况下,需要先清除目标区域的条件格式,或者调整条件格式的优先级。openpyxl对条件格式的支持有限,复杂的条件格式规则建议在Excel里手动处理。
4.4 批量处理多个文件的扩展思路
实际工作中,经常需要批量处理多个文件。比如一个文件夹里有几十个目标表,都需要和同一个源表做匹配。这时候只需要在外面加一层循环,遍历文件夹里的所有xlsx文件,逐个调用match_excel函数。
import glob source_file = 'source.xlsx' target_folder = 'targets' output_folder = 'results' os.makedirs(output_folder, exist_ok=True) for target_file in glob.glob(os.path.join(target_folder, '*.xlsx')): filename = os.path.basename(target_file) output_file = os.path.join(output_folder, f'matched_{filename}') match_excel(source_file, target_file, output_file) print(f'已处理:{filename}')这个扩展思路很实用,尤其是月度报表、季度汇总这类场景。把源表更新一下,脚本一跑,所有目标表的结果文件就都生成了,省去大量重复劳动。
5. 常见问题与排查技巧实录
5.1 匹配不上的五大原因及排查方法
匹配不上是最高频的问题。根据我的经验,90%以上的匹配失败都逃不出下面五种情况。
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 明明有对应数据却匹配不上 | 匹配列有前后空格 | 用len()对比字符串长度 | 统一用strip()处理 |
| 数字和文本混用导致匹配失败 | 源表是数字,目标表是文本 | 用type()查看数据类型 | 统一转字符串再匹配 |
| 大小写不一致 | 源表大写,目标表小写 | 肉眼对比或打印出来看 | 统一用upper()或lower() |
| 特殊字符不可见 | 存在换行符、制表符 | 用repr()打印字符串 | 用replace()清除特殊字符 |
| 日期格式不一致 | 一个是日期,一个是文本 | 查看单元格数字格式 | 统一转成相同格式的字符串 |
我踩过最坑的一次是源表里的客户编号后面跟了一个不可见的换行符,肉眼完全看不出来,但就是匹配不上。后来用repr()打印出来才发现是'C001\n'。从那以后,我在构建字典之前都会加一步清洗:key = str(key).strip().replace('\n', '').replace('\t', '')。
5.2 格式丢失的典型场景与修复
格式丢失通常发生在几个地方。第一种是copy()漏了某个属性,比如只复制了font和fill,忘了border和alignment。这种问题好排查,对照源表和结果表的单元格格式设置,缺什么补什么。
第二种是目标单元格原有的格式覆盖了写入的格式。openpyxl在写入时,如果目标单元格已经有样式,新赋的样式会替换掉旧的。但如果你只赋了部分属性,比如只赋了font,那fill和border还是原来的。所以要么全部覆盖,要么全部保留,不要只改一部分。
第三种是合并单元格的格式没有正确应用。合并区域的格式由左上角单元格控制,如果你只设置了左上角的格式,其他单元格看起来没变,但实际上是合并区域在起作用。处理方法是先取消合并,设置好格式后再重新合并。
实操心得:建议在写入格式之前,先把目标单元格的样式重置为默认值,然后再赋新样式。这样可以避免旧样式残留导致的显示异常。重置的方法是
cell.style = 'Normal',不过这个操作会清除所有格式,慎用。
5.3 大文件处理的性能瓶颈与优化
处理几万行以上的文件时,性能问题会变得明显。我实测过一个十万行的文件,用默认配置读取需要将近一分钟,写入又要一分钟。优化之后,读取降到十几秒,写入降到二十秒左右。
优化手段主要有三个。第一,源表用read_only=True模式读取,内存占用从几百兆降到几十兆,速度也有提升。第二,安装lxml库,openpyxl会自动用它替代默认的XML解析器,读写速度提升30%以上。第三,目标表如果不需要保留公式,可以在加载时设置data_only=True,避免加载公式链。
还有一个容易被忽略的点是打印输出。如果在循环里频繁print,IO开销会拖慢整体速度。建议只在最后打印汇总信息,循环里不要打印。
5.4 特殊数据类型的处理注意事项
Excel里的数据类型比想象中复杂。日期、时间、百分比、货币、科学计数法,每种类型在openpyxl里的表现都不一样。
日期类型在openpyxl里是datetime对象,直接写入目标单元格时,需要确保目标单元格的number_format也是日期格式,否则会显示成一串数字。百分比类型本质上是小数,比如50%在openpyxl里是0.5,写入时需要把number_format设为'0.00%'才能正确显示。
货币类型要注意符号和精度。如果源表是人民币符号,目标表是美元符号,直接复制值会导致显示混乱。建议在匹配之前先统一货币类型,或者在代码里做转换。
科学计数法是个大坑。长数字(比如身份证号、银行卡号)在Excel里默认会显示成科学计数法,openpyxl读取时拿到的是浮点数,精度可能丢失。处理方法是把单元格的number_format设为'@'(文本格式),强制按文本读取。
注意:如果源表里的长数字已经被Excel转成了科学计数法,openpyxl读出来的值可能已经不准确了。这种情况下,建议在Excel里先把列格式设为文本,重新输入数据,或者从原始数据源重新导出。
6. 进阶技巧与场景延展
6.1 多条件匹配的实现思路
单条件匹配用字典就够了,但实际业务中经常需要多条件匹配。比如同时按“客户编号”和“产品编号”两个条件来匹配价格。这时候字典的key就不能是单个值了,需要组合成一个元组。
key = (str(row[0].value).strip(), str(row[1].value).strip()) match_dict[key] = {...}查找的时候也传元组:if (key1, key2) in match_dict。这种方式的效率和单条件差不多,因为元组的哈希计算很快。唯一需要注意的是元组里每个元素的类型要一致,不能一个是字符串一个是数字。
如果条件更多,比如三个、四个,同样用元组组合就行。但条件太多时,字典的key会变得很长,内存占用会增加。这种情况下可以考虑用pandas做预处理,把多条件合并成一个辅助列,再用openpyxl做格式处理。
6.2 模糊匹配与近似匹配的处理
精确匹配搞不定的时候,就需要模糊匹配。比如客户名称有“某某科技有限公司”和“某某科技(北京)有限公司”,严格来说不是同一个,但业务上认为是同一家。这时候可以用difflib库做相似度计算。
from difflib import SequenceMatcher def find_best_match(key, candidates, threshold=0.8): best_score = 0 best_match = None for candidate in candidates: score = SequenceMatcher(None, key, candidate).ratio() if score > best_score and score >= threshold: best_score = score best_match = candidate return best_match这种方式的缺点是性能差,每次查找都要遍历所有候选值。优化方法是先用精确匹配查一遍,查不到的再走模糊匹配。另外,可以先把候选值按首字母分组,缩小查找范围。
6.3 与pandas配合的混合方案
openpyxl擅长格式处理,pandas擅长数据运算。把两者结合起来,能发挥各自的优势。思路是用pandas做数据匹配和清洗,用openpyxl做格式写入。
具体做法是:先用pandas读取源表和目标表,用merge完成匹配,得到一个包含匹配结果的数据框。然后用openpyxl打开目标表,遍历数据框,把匹配结果写入对应的单元格,同时从源表读取格式信息一并写入。
这种方案适合数据量大、匹配逻辑复杂的场景。pandas的向量化运算比openpyxl的逐行遍历快得多,而openpyxl的格式控制能力又是pandas不具备的。两者结合,既快又灵活。
6.4 自动化定时任务的搭建思路
如果这个匹配任务需要定期执行,比如每天早上跑一次,可以把它做成定时任务。Windows下用“任务计划程序”,Mac和Linux下用crontab。
核心是把脚本包装成一个可执行的命令,然后配置定时触发。需要注意的是,定时任务运行时的环境变量和手动执行可能不一样,建议在脚本里用绝对路径引用文件,避免相对路径找不到文件的问题。
另外,建议在脚本里加日志记录,每次运行的结果写入一个日志文件,方便排查问题。日志内容包括运行时间、处理文件数、匹配成功数、失败数、异常信息等。这样即使无人值守,也能通过日志了解运行情况。
6.5 从匹配到数据校验的延伸
数据匹配做完之后,自然延伸出来的需求是数据校验。比如匹配完之后,检查目标表里有没有重复的匹配键,检查源表里有没有未被匹配到的记录,检查匹配后的数据是否符合业务规则。
这些校验逻辑都可以加在匹配脚本里,匹配完成后自动执行,输出一份校验报告。报告里列出异常记录的行号、异常类型、异常值,方便人工复核。这样一来,整个流程就从“数据匹配”升级成了“数据质量管控”,价值提升了一个档次。
我个人的习惯是,任何数据匹配任务都配上校验环节。匹配结果直接用的风险太大,尤其是财务、库存这类敏感数据,多一道校验就多一层保障。校验规则可以根据业务需求灵活配置,比如“匹配率低于90%时报警”、“存在重复匹配键时报警”、“匹配后的数值超出合理范围时报警”等等。