简介:这份资源面向需要处理表格数据的Python学习者与开发者,聚焦Excel文件的读取与写入这一常见任务,帮助解决数据分析、数据清洗与自动化报告中的表格操作问题。压缩包共4个文件,全部为py脚本,整体约6KB,涵盖读取、按条件筛选、写入以及读写结合等不同操作示例,便于按需查阅与直接运行。内容围绕pandas与openpyxl两个常用库展开:pandas部分演示read_excel读取、条件筛选及to_excel写入,openpyxl部分则涉及工作簿加载、单元格遍历与新建保存,可帮助读者理解两种方案在简单表格与复杂格式场景下的差异。目前已有702人学习,适合希望快速上手Excel自动化处理、积累脚本模板的初中级Python使用者参考。
1. 用 Python 读取 Excel 并写入 zip:一个被低估的自动化刚需
财务同事每周五下午都要把 12 个区域的销售明细表汇总,再打包成一个压缩包发给总部。手动操作是:打开每个 xlsx,复制粘贴到总表,另存为,右键压缩。一套流程 40 分钟,错一个单元格就得重来。这种场景下,用 Python 读取 Excel 并写入 zip 不是炫技,而是把重复劳动压缩成一条命令的刚需。
这个标题拆开看是两件事:读 Excel(pandas/openpyxl 负责)和写 zip(标准库 zipfile 负责)。难点不在单个库怎么用,而在两者衔接时的内存管理、路径处理和编码问题。适合有基础 Python 语法、日常要处理报表打包的运营、财务、数据分析从业者。下面按「读进来 → 处理好 → 压出去」的链路,把每一步的参数和坑讲透。
2. 读取 Excel 的三种姿势:pandas、openpyxl 与 calamine 怎么选
2.1 先搞清楚你的 Excel 到底长什么样
选库之前,先确认三件事:文件是 .xlsx 还是 .xls、有没有合并单元格、单表数据量多大。.xls 是旧二进制格式,pandas 需要 xlrd 才能读,而新版 xlrd 已经放弃了对 xlsx 的支持,这个历史包袱经常让人翻车。合并单元格在 pandas 里读出来是 NaN,需要额外填充。数据量超过 10 万行时,pandas 默认的全量加载会吃掉大量内存。
我一般先用一条命令探底:
python -c "import pandas as pd; df = pd.read_excel('sample.xlsx', nrows=5); print(df.shape); print(df.columns.tolist())"nrows=5只读前 5 行,快速看列名和结构,避免大文件全量加载。df.columns.tolist()打印列名列表,确认表头在第几行——很多报表前两行是标题和空行,直接读会把标题当列名。
2.2 pandas 读取的核心参数与适用边界
pandas 的read_excel是最省事的选择,适合结构规整、单表不超过几十万行的场景。关键参数有四个:
import pandas as pd df = pd.read_excel( "sales_2024.xlsx", sheet_name="Sheet1", # 指定工作表,None 表示读全部 header=0, # 表头所在行,从 0 开始计数 usecols="A:F", # 只读 A 到 F 列,减少内存 dtype={"订单号": str}, # 强制订单号为字符串,防止前导零丢失 engine="openpyxl" # 显式指定引擎 ) print(df.head())sheet_name传字符串读单表,传None返回一个字典,键是表名、值是 DataFrame,适合批量处理多 sheet。usecols用列字母或列名列表限定范围,能显著降低内存。dtype是血泪经验:订单号、身份证号这类字段如果被 pandas 推断成整数,前导零会消失,必须显式声明为 str。engine在 xlsx 场景下用 openpyxl,xls 场景用 xlrd,不指定时 pandas 会自动选,但自动选偶尔会选错。
读进来的 DataFrame 可以直接做筛选、合并、计算,处理完再决定怎么写出去。
2.3 openpyxl 直接操作单元格:需要保留格式时的选择
pandas 读进来的是纯数据,原表的字体、颜色、公式全部丢失。如果压缩包里的 Excel 需要保留格式,就得用 openpyxl 直接操作工作簿对象:
from openpyxl import load_workbook wb = load_workbook("template.xlsx", data_only=True) ws = wb["Sheet1"] # 遍历所有行,跳过表头 for row in ws.iter_rows(min_row=2, values_only=True): order_id, amount, region = row[0], row[1], row[2] if amount and amount > 10000: print(f"大额订单: {order_id} 金额 {amount} 区域 {region}") wb.save("filtered.xlsx")data_only=True表示读取公式的计算结果而不是公式本身,这个参数在读取别人发来的报表时特别重要,否则你拿到的是=SUM(A1:A10)这样的字符串。iter_rows的values_only=True直接返回值元组,比逐个访问cell.value快很多。openpyxl 的代价是速度慢,10 万行以上会明显卡顿,适合小批量、重格式的场景。
2.4 大文件场景:calamine 引擎的提速效果
数据量到几十万行时,pandas 默认的 openpyxl 引擎会慢到让人怀疑人生。python-calamine 是一个用 Rust 写的读取引擎,pandas 2.2 之后可以直接通过engine="calamine"调用:
import pandas as pd df = pd.read_excel( "big_data.xlsx", engine="calamine", usecols=["日期", "金额", "区域"] ) print(f"读取完成,共 {len(df)} 行")实测在 50 万行、20 列的 xlsx 上,calamine 比 openpyxl 快 3 到 5 倍,内存占用也更低。代价是需要额外安装python-calamine包,且对某些冷门格式支持不如 openpyxl 完善。我的习惯是:小文件用 openpyxl,大文件先试 calamine,遇到格式报错再退回 openpyxl。
3. 把处理结果写进 zip:zipfile 的四种写入模式
3.1 从内存直接写入:避免落盘的 BytesIO 方案
最直觉的做法是先把 DataFrame 存成 xlsx 文件,再压缩。但中间文件多了之后,清理是个麻烦。更好的方式是用BytesIO在内存里完成 Excel 的序列化,直接写进 zip:
import pandas as pd from io import BytesIO from zipfile import ZipFile, ZIP_DEFLATED df = pd.read_excel("source.xlsx") # 在内存中生成 xlsx buffer = BytesIO() with pd.ExcelWriter(buffer, engine="openpyxl") as writer: df.to_excel(writer, index=False, sheet_name="汇总") buffer.seek(0) # 关键:把指针拨回开头 # 写入 zip with ZipFile("output.zip", "w", compression=ZIP_DEFLATED) as zf: zf.writestr("汇总表.xlsx", buffer.read()) print("打包完成")BytesIO是一个内存中的二进制流,ExcelWriter把 DataFrame 写进去之后,指针停在末尾,必须seek(0)拨回开头,否则read()读到的是空内容——这个坑我踩过不止一次。writestr的第一个参数是压缩包内的文件名,可以带路径如"2024/汇总表.xlsx",zipfile 会自动创建目录结构。ZIP_DEFLATED是压缩算法,对 xlsx 这种本身已压缩的格式,压缩率有限但聊胜于无。
3.2 多文件批量打包:循环写入与命名规范
实际场景往往是把多个处理结果打进一个包。用循环配合writestr即可:
import pandas as pd from io import BytesIO from zipfile import ZipFile, ZIP_DEFLATED regions = ["华东", "华南", "华北", "西南"] source = pd.read_excel("all_sales.xlsx") with ZipFile("regional_reports.zip", "w", compression=ZIP_DEFLATED) as zf: for region in regions: subset = source[source["区域"] == region] if subset.empty: continue # 空数据跳过,避免生成空文件 buf = BytesIO() with pd.ExcelWriter(buf, engine="openpyxl") as writer: subset.to_excel(writer, index=False, sheet_name=region) buf.seek(0) zf.writestr(f"{region}_销售明细.xlsx", buf.read()) print(f"{region}: {len(subset)} 行已写入")subset.empty判断很关键,某些区域可能没有数据,生成一个空 xlsx 既占空间又让接收方困惑。文件名用 f-string 拼接区域名,保证压缩包内文件可读。循环内每次新建BytesIO,不要复用同一个 buffer,否则会读到上一次的残留数据。
3.3 压缩级别与文件名的三个细节
ZipFile的compresslevel参数控制压缩级别,取值 0 到 9,默认是 None(相当于 6):
from zipfile import ZipFile, ZIP_DEFLATED # 高压缩,适合归档 with ZipFile("archive.zip", "w", compression=ZIP_DEFLATED, compresslevel=9) as zf: zf.write("report.xlsx", arcname="2024年度报告.xlsx")compresslevel=9压缩率最高但耗时最长,compresslevel=1最快但文件大。对 xlsx 来说,因为内部已经是 zip 格式,再压缩收益很小,我一般用默认值。arcname参数指定文件在压缩包内的名字,不指定则用原路径,容易带上一长串本地目录。中文文件名在 zipfile 里默认用 UTF-8 编码,Windows 自带解压工具可能显示乱码,接收方用 7-Zip 或 Bandizip 打开正常。
3.4 追加与更新:a 模式与重复写入的陷阱
ZipFile的"a"模式可以在已有压缩包上追加文件:
from zipfile import ZipFile, ZIP_DEFLATED with ZipFile("output.zip", "a", compression=ZIP_DEFLATED) as zf: zf.write("new_report.xlsx", arcname="补充报表.xlsx")注意:如果压缩包里已经存在同名文件,追加不会覆盖,而是出现两个同名条目,解压时后者覆盖前者,行为取决于解压工具。要更新文件,必须先读出全部内容、删掉旧条目、重新写入,或者干脆重建整个 zip。我一般不在原包上追加,而是每次生成新包,避免这种玄学问题。
4. 避坑指南:编码、内存与路径的五个真实翻车现场
4.1 中文文件名在 Windows 解压后变乱码
现象:Python 生成的 zip 在 Windows 资源管理器里解压,中文文件名显示为乱码,用 7-Zip 打开却正常。原因是 zipfile 默认用 UTF-8 编码文件名,而 Windows 自带解压工具按本地代码页解析。解决方式是在写入时显式指定编码,或者干脆用英文文件名加一个中文映射表。我的做法是压缩包内文件名用拼音或英文,在包内附一个manifest.txt说明对应关系,接收方一看就懂。
4.2 BytesIO 忘记 seek(0) 导致写入空文件
现象:压缩包生成了,但里面的 xlsx 打开是空白。原因就是前面提到的,ExcelWriter写完后指针在末尾,read()返回空字节。解决:每次read()之前调用buffer.seek(0)。这个错误不报异常,只是静默生成空文件,排查起来很费时间。养成习惯:凡是 BytesIO 读写切换,先 seek。
4.3 大文件全量加载导致内存溢出
现象:处理 100 万行的 xlsx 时进程被系统杀掉。原因是 pandas 默认全量加载,100 万行 30 列大约占用 2 到 3 GB 内存。解决:用read_excel的usecols限定列,用chunksize分块读取(注意 read_excel 的 chunksize 返回的是迭代器),或者换 calamine 引擎降低内存占用。如果只是筛选后打包,根本不需要全量加载,分块处理完直接写 zip 即可。
4.4 日期列读出来变成数字
现象:Excel 里的日期2024-01-15读进来变成45306。原因是 Excel 内部用序列号存储日期,openpyxl 在某些配置下不自动转换。解决:读取后用pd.to_datetime(df["日期"], unit="D", origin="1899-12-30")转换,或者在read_excel里指定parse_dates=["日期"]。注意 origin 是 1899-12-30 而不是 1900-01-01,因为 Excel 有一个著名的 1900 闰年 bug。
4.5 压缩包内路径用了反斜杠
现象:在 Windows 上开发,压缩包内文件名写成"reports\\summary.xlsx",在 Linux 上解压变成一个名字里带反斜杠的文件。原因是 zip 规范要求路径分隔符用正斜杠/。解决:统一用os.path.join或 pathlib 生成路径后,把反斜杠替换成正斜杠,或者直接用 f-string 写"reports/summary.xlsx"。跨平台代码里,路径拼接永远用正斜杠。
5. 进阶技巧:用上下文管理器封装一个可复用的打包函数
把前面的逻辑封装成一个函数,是让这套方案真正可复用的关键。下面这个函数接收一个 Excel 路径和分组列名,按分组拆表并打包,全程内存操作,不落中间文件:
import pandas as pd from io import BytesIO from zipfile import ZipFile, ZIP_DEFLATED from pathlib import Path def excel_to_zip_by_group( excel_path: str, group_col: str, output_zip: str, sheet_name: str = "Sheet1", compress_level: int = 6 ) -> int: """ 按指定列分组,将每组数据写入独立 xlsx 并打包成 zip。 返回写入的文件数量。 """ df = pd.read_excel(excel_path, sheet_name=sheet_name) if group_col not in df.columns: raise ValueError(f"列 {group_col} 不存在,实际列: {df.columns.tolist()}") count = 0 with ZipFile(output_zip, "w", compression=ZIP_DEFLATED, compresslevel=compress_level) as zf: for name, group in df.groupby(group_col): buf = BytesIO() with pd.ExcelWriter(buf, engine="openpyxl") as writer: group.to_excel(writer, index=False, sheet_name=str(name)[:31]) buf.seek(0) safe_name = str(name).replace("/", "_").replace("\\", "_") zf.writestr(f"{safe_name}.xlsx", buf.read()) count += 1 return count if __name__ == "__main__": n = excel_to_zip_by_group("all_sales.xlsx", "区域", "regional.zip") print(f"共打包 {n} 个文件")几个设计细节值得说。sheet_name=str(name)[:31]是因为 Excel 工作表名上限 31 个字符,超长会报错。safe_name把分组名里的斜杠替换掉,防止路径穿越或非法文件名。groupby默认会排除 NaN 分组,如果分组列有空值,这些行会被静默丢弃,需要提前用fillna("未知")处理。返回文件数量方便调用方做校验。
验证方法很简单:生成 zip 后用ZipFile.namelist()打印条目列表,再逐个read()出来用 pandas 读一遍,确认行数和原数据一致:
from zipfile import ZipFile import pandas as pd from io import BytesIO with ZipFile("regional.zip") as zf: for name in zf.namelist(): data = pd.read_excel(BytesIO(zf.read(name))) print(f"{name}: {len(data)} 行, 列: {data.columns.tolist()}")这套流程我用了两年多,最大的教训是:不要相信 Excel 的表头一定在第一行,也不要想当然认为分组列没有空值。每次处理新报表,先跑一遍探查命令,确认结构再写正式逻辑。另一个习惯是压缩包内永远附一个manifest.txt,记录生成时间、数据行数和分组依据,接收方出问题时能快速定位。希望帮到你。
本文还有配套的精品资源,点击获取