Python 处理 Excel 办公自动化:pandas 数据加工 + openpyxl 排版实战

📅 发布时间:2026/9/3 3:12:32
Python 处理 Excel 办公自动化:pandas 数据加工 + openpyxl 排版实战 Python 处理 Excel 做办公自动化真正能拉开效率差距的不是谁会写更复杂的循环而是能不能把“数据加工”和“表格排版”分开处理。很多人拿一批表格过来就直接用 pandas 读读出来之后统计、筛选、分组都做完了结果导出的 Excel 一打开列宽是默认的日期变成一串数字有空值的单元格还残留着“NaN”文本最后还是花十几分钟手动改格式。这篇内容围绕办公自动化里的 Excel 高级操作展开适合已经能用 Python 写基础脚本、但还没把 Excel 场景跑顺的人。下面我会按实际落地的顺序拆先判断需求属于哪一类再准备环境和依赖接着处理高频的数据清洗场景然后处理格式、公式、批量文件最后说大文件性能和报错排查。整个过程里会给出可复现的代码片段和判断标准但不会把“能跑通”说成“一定适合生产”有些边界要等你用真实数据验证过才能确认。1. 先把需求分成两类数据加工和表格排版办公自动化里的 Excel 任务表面看都是“用 Python 操作表格”实际差别很大。如果不先分清需求类型很容易选错库、写错流程甚至花了半天做出来的东西跟手动做没区别。1.1 数据加工类需求核心是减少重复劳动这类需求的特征是输入是一张或很多张表你要做的是清洗、筛选、拆分、合并、统计最后得到一个结果表。比如从销售明细中统计每个部门每个月的总金额或者把一列“姓名电话”拆成两列再或者按多个条件筛出目标数据。这类任务最适合 pandas。pandas 处理的是“表”这个概念而不是某个 Excel 文件里的某个单元格。读进来之后你可以按列名操作可以按布尔条件筛选可以分组聚合可以合并多个表。判断这类需求是否做好的标准也很简单输出结果的行数、列数、汇总值是否和手工核对一致而不是看代码写得多漂亮。1.2 表格排版类需求核心是输出后能不能直接使用还有一类需求输入数据基本已经处理好了但最终交付的 Excel 需要带标题、表头颜色、边框、合并单元格、固定列宽、冻结首行。如果数据本身还要从多张表里汇总那其实是一个“先数据加工、后排版”的组合需求。排版这类任务用 openpyxl 更合适。你可以精确控制某个单元格的字体、背景色、边框、对齐方式也能控制整列的宽度、某个区域是否合并、筛选按钮是否开启。判断标准是打开生成文件时不需要人工补格式直接能转发给别人。两类需求用一张表区分会更直观需求类型典型场景优先使用的库最简单的判断标准数据加工清洗、筛选、去重、分组统计、按条件汇总pandas结果表和手工核对结果一致表格排版标题合并、表头颜色、列宽、边框、冻结窗格openpyxl打开文件后不需要再手动调格式数据加工 排版多表汇总后生成对外日报pandas 先算openpyxl 后写数据和格式都能直接交付这里最常犯的错误是一上来就写 openpyxl 逐行逐列循环。数据有几千行时这种写法又慢又难维护。正确顺序是先让 pandas 把数据处理干净再一次性把结果交给 openpyxl 做样式调整。顺序反了后面改需求会很痛苦。2. 环境和依赖准备先解决“读不到、导不进去”的问题Excel 办公自动化最容易出问题的不是功能代码而是环境。好几年前我带过一个小项目脚本在别人电脑上跑得好好的换一台机器就报错原因不是代码改了而是别人电脑上没有装依赖库。2.1 Python 环境和虚拟环境要提前固定我建议新建项目时先创建虚拟环境别一股脑把库装到全局 Python 里。全局环境里的包版本一乱今天这个脚本能用明天安装另一个库之后可能就冲突了。常规操作是这样python -m venv venvWindows 下激活虚拟环境venv\Scripts\activatemacOS 或 Linux 下激活source venv/bin/activate激活后安装依赖pip install pandas openpyxl安装完成后我建议先确认版本能正常导入不要直接跑完整脚本import pandas as pd import openpyxl print(pd.__version__) print(openpyxl.__version__)这一步看着多余但能避免“昨天还能用今天突然报 module not found”这类问题。原始材料没有给出固定的版本号所以这里不写死某个版本要求。你只要保证 pandas 和 openpyxl 都能 import 成功就说明基础环境没问题。2.2 pandas 和 openpyxl 的分工不同很多新手会问到底是学 pandas 还是 openpyxl。我的回答是两个都要装但脑子里要把分工理清。pandas 底层依赖 openpyxl 或 xlrd 来读写 Excel 文件。你在 pandas 里调用pd.read_excel()时实际上它要借助 openpyxl 去解析 .xlsx 文件。所以安装 pandas 后单独安装 openpyxl 是合理的两者不是替代关系而是配合关系。文件格式也要提前确认.xlsx 是现代 Excel 默认格式pandas openpyxl 处理最稳。.xls 是老版本格式openpyxl 不支持读取 .xls需要额外用 xlrd 或先把文件另存为 .xlsx。.xlsm 带宏读取数据通常可以但写回宏会有很多限制不建议直接当普通表格改。读文件时还要注意路径。如果文件路径里包含中文或空格用 pandas 通常没问题但建议用 raw string 或 pathlib 来避免反斜杠转义问题。路径写错了很容易出现FileNotFoundError而这类报错不是 Excel 库的问题是路径本身没写对。我一般会先这样确认文件是否存在from pathlib import Path file_path Path(data/销售明细.xlsx) print(file_path.exists()) print(file_path.resolve())能打印出True和完整绝对路径再往下读文件。3. 第一个稳定流程读取、检查、清洗办公自动化脚本想长期复用第一步不是写出花哨的统计代码而是把读取和检查做扎实。拿到一张 Excel我总是先看几行再看字段类型最后才决定怎么处理。3.1 先读入并观察数据不直接清洗用 pandas 读取 Excel 文件的基本写法import pandas as pd df pd.read_excel(data/销售明细.xlsx, sheet_name明细) print(df.head()) print(df.info())sheet_name可以传 sheet 名称也可以传索引。如果不确定文件里有几张 sheet可以先用sheets pd.read_excel(data/销售明细.xlsx, sheet_nameNone) print(sheets.keys())sheet_nameNone会把所有 sheet 读成一个字典key 是 sheet 名。这样能快速知道整个工作簿的结构。如果只想读某个 sheet再单独用sheet_name指定。df.info()会输出每列的名称、非空数量、数据类型。这一步非常关键因为 Excel 里“看起来是数字”的列读进来之后很可能是object类型这是最常见的坑之一。3.2 空值、重复值和类型转换要分开处理空值处理前先要搞清楚空值在哪里。比如合并单元格会导致某些行在 pandas 里显示为NaN因为原始 Excel 中只有左上角单元格有值其他合并区域是空的。这时直接dropna()会删掉大量有效数据。正确做法是先判断这个空值是不是由合并单元格造成的如果是可以先用前向填充把值补上。类型转换也要按列来不能一概而论。比如金额列可能带有千分位逗号读进来后实际上是文本df[金额] df[金额].astype(str).str.replace(,, , regexFalse) df[金额] pd.to_numeric(df[金额], errorscoerce)先转成字符串再移除逗号最后转数字。errorscoerce的意思是遇到无法转换的内容时变成NaN而不是直接抛错。这样你能通过统计NaN的数量定位到原始数据里哪些行格式不正常。我自己处理时不会直接修改原文件而是先复制一份再加清洗结果clean_df df.copy() clean_df[日期] pd.to_datetime(clean_df[日期], errorscoerce) clean_df clean_df.dropna(subset[姓名, 金额])dropna(subset...)只检查指定列不会因为某些无关列缺失就把整行删掉。判断清洗是否成功的标准是清洗前后总行数、各列非空数量、金额总和是否有明显变化。如果金额总和突然少了一大截多数是空值或类型转换把某些行弄丢了。4. 高频办公场景筛选、拆分、分组汇总数据清洗干净之后就可以进入真正的高频场景了。下面这几个需求几乎每天都会在表格工作里出现用 Python 处理它们的核心不是代码难而是清楚每个操作背后的判断标准。4.1 多条件筛选先写条件再写数据“找出部门是销售部且金额大于 1000 的记录”这种需求写作上很简单但新手常见的报错是把条件表达式写错。推荐先构造布尔条件再用loc筛选mask (clean_df[部门] 销售部) (clean_df[金额] 1000) result clean_df.loc[mask]这里要注意括号。在 pandas 表达式里是逐位与操作运算优先级容易踩坑。如果漏掉括号可能会得到错误结果甚至报 ValueError。每次写这类条件时先把 mask 单独打印出来看 True/False 的数量再筛选数据。如果条件里还包含“或”的关系记得用|同时也要加括号mask ( (clean_df[部门] 销售部) (clean_df[金额] 1000) ) | (clean_df[客户等级] A)过滤结果以后别忘了统计一下行数再预览前几行。行数是否符合业务预期比代码逻辑“看起来对”更值得确认。4.2 姓名和电话拆分先看分隔符再写正则“姓名和电话分开”也是常见需求。很多人会直接写复杂正则结果遇到几十个异常格式就懵了。我建议先看原始数据的实际格式再决定拆分方式。如果原始数据像“张三 13800138000”一个空格分隔可以先 splitdf[[姓名, 电话]] df[联系方式].str.split(expandTrue, n1)但如果分隔符可能是空格、全角逗号、半角逗号、竖线那简单 split 就不可靠。这时可以用正则提取姓名和手机号pattern r^(?P姓名.*?)[\s,|](?P电话1[3-9]\d{9})$ split_result df[联系方式].str.extract(pattern) df2 pd.concat([df, split_result], axis1)这个正则的含义是.*?非贪婪匹配姓名部分[\s,|]匹配一个或多个分隔符1[3-9]\d{9}匹配中国内地手机号的常见格式正则不是万能钥匙。如果原始数据里根本没有规律的起止位置比如写成“张先生电话13800138000”正则就很难一次提取完整。这时候更稳妥的做法是先把“电话”字段提取出来再反推剩余部分作为姓名。判断标准是提取后姓名列和电话列不能有错位空值数量要能说清楚原因。4.3 分组汇总和跨表合并做月度汇总、部门汇总这类需求时groupby很常用summary ( clean_df.groupby([部门, 月份], as_indexFalse)[金额] .agg([sum, count]) .reset_index() )as_indexFalse可以避免分组列变成索引后续操作更直观。.agg([sum, count])会同时得到汇总金额和记录数。汇总之后列名通常会变得有点乱比如金额下面出现两层列名这时可以手动重命名再导出。跨表合并也很常见比如把“订单表”和“客户表”按客户编号关联merged pd.merge(order_df, customer_df, on客户编号, howleft)howleft的意思是以左边表为基础能匹配到的客户信息拼进来匹配不到的显示为NaN。合并后你应该检查一下行数是否等于左边表的行数因为如果客户表里有重复客户编号会导致行数膨胀。5. 做一份能直接交付的 Excel格式、合并单元格、冻结窗格数据处理结束后如果交付对象不是程序员那你就不能只给一张“能看的 CSV”而是要给他们一份打开后不尴尬的 Excel。这里我一般会采用“先让 pandas 写值再用 openpyxl 调样式”的组合流程。5.1 写入数据再加载工作簿调样式先正常导出数据summary.to_excel(output/销售汇总.xlsx, indexFalse, sheet_name汇总)然后加载这个文件调整样式from openpyxl import load_workbook from openpyxl.styles import Font, PatternFill, Alignment, Border, Side wb load_workbook(output/销售汇总.xlsx) ws wb.active为什么不直接在 pandas 里用 ExcelWriter 写样式因为 pandas 的Styler方案在导出 Excel 时能做的事情有限而且不同版本表现有差异。用 openpyxl 加载再修改代码更直白也能看到每一步操作的对象到底是哪个单元格。给标题设置字体和背景色title_font Font(name微软雅黑, size14, boldTrue) header_fill PatternFill(start_color4472C4, end_color4472C4, fill_typesolid) header_font Font(name微软雅黑, size11, boldTrue, colorFFFFFF) for cell in ws[1]: cell.font header_font cell.fill header_fill cell.alignment Alignment(horizontalcenter, verticalcenter)如果要在第一行上方再加一个合并标题可以先插入一行再合并ws.insert_rows(1) ws.merge_cells(start_row1, start_column1, end_row1, end_columnws.max_column) ws.cell(row1, column1, value销售汇总报表) ws.cell(row1, column1).font title_font ws.cell(row1, column1).alignment Alignment(horizontalcenter, verticalcenter)合并单元格时一定要想清楚合并范围。合并后只有合并区域左上角的单元格能正常写值其他区域的内容会被清空。所以先确定好标题要跨多少列再传end_column。5.2 列宽、边框、冻结首行和筛选按钮列宽如果不设置Excel 打开后会按默认宽度显示中文字符很容易被截断。可以这样设置width_mapping { A: 20, B: 15, C: 12, D: 18, } for col, width in width_mapping.items(): ws.column_dimensions[col].width width如果字段很多更通用的方式是按表头内容大致估算宽度或直接设置一个统一宽度。这一步别追求完美重点是让中文列名和主要数据不会被隐藏。冻结首行ws.freeze_panes A2freeze_panes是“冻结窗格”的位置。A2表示第一行固定不动向下滚动时表头始终可见。如果前面插入了标题行那表头行号会变化写完代码后记得打开文件确认一下。给数据区域加边框thin_border Border( leftSide(stylethin), rightSide(stylethin), topSide(stylethin), bottomSide(stylethin), ) for row in ws.iter_rows(min_row2, max_rowws.max_row, max_colws.max_column): for cell in row: cell.border thin_border如果数据量很大给每一行加边框会拖慢脚本速度。我的经验是对外交付的小报表可以逐格加边框几百行以内问题不大几千行以上的大表通常不需要加全边框加个自动筛选和冻结首行就够了。最后是自动筛选按钮可以用ws.auto_filter.ref指定区域ws.auto_filter.ref ws.dimensionsws.dimensions是当前有数据的区域范围它能自动包含所有列。6. 公式、跨文件处理和批量任务设计一个办公自动化流程能不能真正替代手工要看它能不能处理批量文件而不只是处理一个文件。6.1 openpyxl 写公式的边界如果你需要在某个单元格写 Excel 公式openpyxl 可以直接赋值ws[F21] SUM(F2:F20)但是这里有一个必须知道的边界openpyxl 只负责把公式写进文件并不会帮你计算公式结果。也就是说你用 pandas 读这个文件时可能读不到 F21 的计算值只会读到公式字符串或None。等用户用 Excel 打开文件时Excel 通常会重新计算公式并显示结果。如果下游脚本要用 pandas 再读这个 Excel我不建议把计算逻辑交给 Excel 公式。更好的做法是用 pandas 先把汇总值算出来直接写数值。需要公式只是为了“在 Excel 里能联动”那再考虑写公式。判断标准是下游还要不要读取这个文件。如果需要读用数值更稳如果只是给人看的用公式可以。6.2 批量处理文件夹下的多个 Excel批量处理时第一步不是写处理逻辑而是确认“要处理哪些文件”。用 pathlib 遍历目录比较方便from pathlib import Path input_dir Path(data) output_dir Path(output) output_dir.mkdir(exist_okTrue) for file_path in input_dir.glob(*.xlsx): print(开始处理:, file_path.name) try: df pd.read_excel(file_path, sheet_name0) # 这里放你的清洗和统计逻辑 result df.groupby(部门, as_indexFalse)[金额].sum() out_path output_dir / f{file_path.stem}_汇总.xlsx result.to_excel(out_path, indexFalse) except Exception as e: print(f处理失败: {file_path.name}, 错误: {e})这个流程里有三个容易被忽略的点。第一输出文件名一定要基于输入文件名生成不能所有文件都写成汇总.xlsx否则后面的文件会覆盖前面的。第二异常捕获不能只打印还要带上文件名。如果你把异常捕获写在循环外面一个文件出错就会中断整批任务。建议在每个文件级别捕获异常这样单个文件失败不影响其他文件。第三处理完后要检查输出文件数量是否等于输入文件数量。数量对不上时根据日志定位是哪些文件失败了不要直接重跑全部任务。批量任务的“成功”不是代码不报错而是输出文件数量、文件名、行数、汇总值都符合预期。我在批量运行前会先处理一个文件看输出结果对不对确认无误后再放开整个文件夹。6.3 跨文件数据合并如果需求是把多个结构相同的 Excel 合成一张总表可以先循环读取再用pd.concat合并all_data [] for file_path in input_dir.glob(*.xlsx): df pd.read_excel(file_path, sheet_name0) all_data.append(df) combined pd.concat(all_data, ignore_indexTrue) combined.to_excel(output_dir / 合并结果.xlsx, indexFalse)pd.concat默认按列名对齐。如果每个文件的列名不完全一致合并后会出现很多NaN列。这时先检查每个文件的列名是否一致是更重要的前提。7. 数据量变大时不要直接撑爆内存办公自动化场景里有个常见错觉小文件能用大文件也能用。实际不是这样。用 pandas 读取 Excel 时文件本身可能只有 50MB但读进内存后会膨胀好几倍。如果你在脚本里反复复制 DataFrame内存占用还会更高。7.1 先用小数据试跑再决定是否全量处理面对大文件我一般先读前几十行确认结构df_sample pd.read_excel(big_file.xlsx, sheet_name明细, nrows50) print(df_sample.head()) print(df_sample.columns)nrows参数只读前面若干行读取速度很快。确认列名、类型和样例数据没问题后再全量读取。如果你的机器配置不高全量读取时关注三个指标内存占用、读取耗时、处理耗时。打开任务管理器观察 Python 进程的内存如果内存涨到接近物理内存上限就要想办法减少数据体积或分批处理。7.2 openpyxl 的 read_only 和 write_only 模式如果你不需要用 pandas 做复杂统计只是要把某个 Excel 里的内容遍历一遍可以用 openpyxl 的只读模式from openpyxl import load_workbook wb load_workbook(large_file.xlsx, read_onlyTrue) ws wb[明细] for row in ws.iter_rows(values_onlyTrue): # 每一行都是元组 pass wb.close()read_onlyTrue不会一次性把所有数据加载到内存而是按行流式读取内存占用明显更低。写文件时也可以用只写模式from openpyxl import Workbook wb Workbook(write_onlyTrue) ws wb.create_sheet(结果) ws.append([姓名, 金额]) for row in some_iterable: ws.append(row) wb.save(large_output.xlsx)write_onlyTrue模式不支持反向修改已经写入的内容适合一次性顺序写入大量行。判断是否使用这种模式要看任务是不是“批量写入 不需要频繁定位单元格”。如果你的脚本需要反复改某个固定单元格还是用普通模式方便。7.3 超过 Excel 行数上限时要考虑其他方案.xlsx 格式的行数上限大约是 104 万行左右这个限制不是 Python 造成的而是 Excel 文件格式本身的上限。如果你的数据量接近这个范围不建议硬塞进 Excel。更合适的做法是把处理结果输出成 CSV或者导入到数据库里再做查询分析。低配置机器能跑通小文件不代表适合批量跑大文件。如果你要处理的目标是几百 MB 的 Excel先考虑把源数据按月份或按部门拆成多个文件分批处理后再合并会比一次性读完更稳。8. 报错定位链路按这个顺序排查不要乱改参数最后一部分专门说问题排查。办公自动化脚本出问题时很多人的第一反应是去改代码参数但实际有一半问题出在文件、路径、权限或数据格式上。我建议按“先看现象再看输入再看环境再看代码”的顺序排查。8.1 常见报错现象和排查方向现象优先排查的方向FileNotFoundError路径是否正确、文件是否存在、目录大小写是否一致PermissionError文件是否正在被 Excel 打开、输出目录是否有写权限IndexError/ 列名报错表头是否在最上面一行、有没有多级表头、列名是否匹配读出来的数据全是 NaN是不是 sheet 名选错、文件是不是图片或 PDF 改名伪装成 xlsx日期变成数字或时间戳Excel 单元格是否是日期格式还是本身存的文本数字带千分位不能求和数据是否被存成了文本要先清洗再转换合并单元格导致大量空值先处理合并单元格造成的空值再决定是否 dropna处理速度极慢是否在用 openpyxl 逐单元格遍历大量行是否能转 pandas 或只读模式输出文件打不开文件是否被其他程序占用或者 write_only 模式下忘了保存8.2 通用的排查顺序第一步看报错在哪个阶段。它是发生在读取文件时、清洗数据时、写入文件时还是生成样式时。因为 Excel 文件的错误往往会在最后写入时才暴露比如某些单元格里有非法字符。第二步看输入文件本身。用 Excel 打开文件检查表头、sheet 名称、合并单元格、单元格格式不要只盯着代码看。很多“Python 读不到”的问题其实是源文件第一行并不是表头或者文件里存在多个 sheet你默认读的 sheet 不是你以为的那个。第三步看环境和依赖。先确认 pandas 和 openpyxl 能正常 import再确认文件路径没有因为目录结构变化而失效。如果代码之前能跑现在不能跑先想想是不是有人移动了文件或升级了依赖版本。第四步看你的数据操作逻辑。比如筛选条件里的括号是不是写错了groupby之后是不是忘了重置索引合并单元格时是不是写错了行列范围。这些逻辑错误不报错但结果就是不对。第五步看库本身的功能边界。openpyxl 不计算公式、宏链路会破坏、.xls 旧格式读不了、合并单元格会挡住部分操作这些都属于工具限制不是你的代码 bug。遇到这些情况要么换方案要么换工具不要硬调参数。8.3 几个我实战中经常踩的细节处理 Excel 时我最常犯的一个错误是忘记处理“文件已经打开”的状态。脚本写入时报PermissionError十有八九是你自己在 Excel 里开着同一个文件。排查时先关掉 Excel 再跑一次。第二个容易被忽略的问题是 sheet 名称里可能有空格。比如 sheet 名字叫“销售明细 ”末尾带一个看不见的空格用sheet_name销售明细读取就会报错。可以用pd.ExcelFile先打印所有 sheet 名称确认有没有看不见的字符。第三个问题是 pandas 的空值处理。df.fillna()是常用的补空操作但如果某列本身是数字类型强行填空字符串会把整列变成文本后续统计就会出错。我的建议是先想清楚每个空值在业务上应该怎么处理是删除整行、填 0、填“未知”还是保留 NaN然后再动手。第四个问题是格式和数据的顺序。不要先合并单元格再写数据很容易覆盖内容。应该先写完所有数据再统一调格式。格式操作和数据处理分开代码维护起来会轻松很多。这个方案真正落地时最该盯住的不是功能列表而是输入格式、资源占用和失败重试。先让单文件流程跑通再做文件夹批量先看日志再改参数先确认数据没有异常再放心交付给业务方。办公自动化不是把代码写出来就结束而是把重复劳动稳定地降低到一个可以接受的程度。