Python+pandas条件筛选Excel数据并导出新文件
2026/9/16 13:19:29 网站建设 项目流程

简介:这是一份面向Python办公自动化初学者的完整实操案例,重点演示如何用pandas按条件筛选Excel数据并将结果另存为新工作表。压缩包内共9个文件,含1个py脚本、1个ipynb笔记本、3个xlsx示例表格和4张png效果截图,合计2.46MB,既能直接运行查看输出,又能通过图片比对中间结果。资源围绕读取原始数据、构建筛选条件(如年龄大于30)、利用布尔索引过滤、再写入新excel文件的主线展开,并附有模板与物料表供练习;适合希望快速理解DataFrame条件筛选、to_excel导出逻辑,并进一步掌握批量办公自动化的读者。目前已有687人学习,可作为日常Excel数据处理提速的入门参考。

1. Python 自动办公里最朴素也最高频的 Excel 条件筛选任务

在 Excel 里按条件筛选数据,比如从几千行销售记录里把“华东区且金额大于 5000”的行拎出来,另存成一个新文件,这件事人工做也能完成,无非是点几下筛选按钮再复制粘贴。问题在于,这类需求往往不是一次性的,而是每隔几天、每周甚至每天都要重复,每回筛选条件还会微调,数据量从几千行涨到几万行时,Excel 界面里的下拉筛选和鼠标框选就开始变得拖沓。用 Python 处理这个问题,本质上是把“肉眼找 + 手动复制”换成“写一次脚本 + 以后只改条件”,区别不在第一步,而在第几十次。这篇文章围绕的就是这个场景中最常见的动作:读取 Excel,按条件过滤行,把结果写入新的工作表,同时把 pandas 与 openpyxl 配合时的参数取舍、编码问题、索引处理和文件写入覆盖这些坑一并讲清楚。适合刚接触用 pandas 做办公室自动化的新人,也适合已经写过几个脚本、但总在“筛选完存不进去”或“索引是乱的”这种细节上卡壳的老手。

2. 从读取 Excel 到输出新表:先跑通一列条件的最小闭环

2.1 依赖准备:不要只装 pandas,还要有 openpyxl

写这个需求之前,先确认环境里有什么。pandas 负责数据结构和筛选逻辑,openpyxl 负责读写 xlsx 底层的 XML 文件。两个缺一个,read_excelto_excel就会直接抛错。安装命令如下:

pip install pandas openpyxl

逻辑说明:pandas是数据处理库,openpyxl是 Excel 文件引擎。pandas 读取和写入 xlsx 时,实质上是把工作交给 openpyxl 处理。如果你的机器上是 Python 3.9 以下的老环境,建议顺手把 pandas 升到 2.x,虽然 1.x 也能跑,但性能和对新 Excel 文件格式的支持会差一截。装完后可以在命令行里快速验证:

python -c "import pandas, openpyxl; print(pandas.__version__, openpyxl.__version__)"

如果能输出版本号,说明依赖就位。注意只装 pandas 不装 openpyxl 时,read_excel读 xlsx 会报ImportError: Missing optional dependency 'openpyxl',这是新人最常见的第一道坎。

2.2 读取源表并完成最简单的单列筛选

假设现在有一份销售记录.xlsx,第一张 Sheet 里是原始数据,列包含“区域”“销售员”“金额”“日期”。现在要把“区域等于华东”的所有行筛出来,并存入一个新文件。下面这段是最小可运行版本:

import pandas as pd # 读取 Excel 文件,指定工作表名,数据从第 1 行开始 df = pd.read_excel("销售记录.xlsx", sheet_name="Sheet1") # 单条件筛选:区域列等于"华东" filtered = df[df["区域"] == "华东"] # 写入新文件,不保存索引列 filtered.to_excel("华东区记录.xlsx", index=False) print(f"源数据 {len(df)} 行,筛选后 {len(filtered)} 行")

逻辑说明:df["区域"] == "华东"返回一个布尔型 Series,元素为 True 的行会被保留,False 的行被丢弃。df[布尔条件]是 pandas 的标准过滤方式。to_excelindex=False非常关键,如果省略,生成的 Excel 会多出一列从 0 开始的行号,通常不是你要的东西。

参数说明:sheet_name如果不写,pandas 默认读取第一个工作表;如果你的数据在第二个表,这里要改成对应名字,用错了会报ValueError: Worksheet named 'xxx' not foundindex参数控制是否导出 df 自带的行索引,默认是 True,实际办公场景几乎都应该设成 False。

运行完这段脚本,当前目录下会出现华东区记录.xlsx,打开后应该只有区域、销售员、金额、日期四列,行数比源表少。这里要提醒一个常见的理解偏差:df[df["区域"] == "华东"]并不会修改原来的df,它返回的是一个新的 DataFrame,原数据在内存里原封不动。所以如果你想继续拿原数据做其他条件的筛选,不要担心这一步会污染源数据。

2.3 筛选结果的索引乱序问题与 reset_index

筛选完直接输出,有一个隐藏问题:新 DataFrame 的索引还保留着源数据的位置。比如源表第 1 行、第 5 行、第 8 行满足条件,那么筛选结果的 index 就是[0, 4, 7],带上这些索引导出 Excel 虽然不影响数据内容,但后续如果要在 Python 里继续对结果做循环或合并,index 不连续会带来麻烦。

filtered = df[df["区域"] == "华东"] filtered = filtered.reset_index(drop=True)

逻辑说明:reset_index(drop=True)会把原索引丢弃,重新生成从 0 开始的连续整数索引。drop=False则是把原索引变成一列普通数据,通常用不到。这个操作不会改变行内容,只是重塑索引结构,但它能帮你规避一个大坑:当你拿筛选后的 df 去 join 另一个表时,索引不对齐会导致 NaN 满天飞。

3. 多条件筛选与组合逻辑:把“且”“或”“模糊匹配”写成代码

3.1 用 & | 和括号拼接多条件

实战里条件很少只有一个,最常见的是“区域等于华东,且金额大于 5000”,或者“区域等于华东,或销售员叫张三”。先看代码:

import pandas as pd df = pd.read_excel("销售记录.xlsx", sheet_name="Sheet1") # 多条件且:同时满足区域等于华东、金额大于 5000 cond1 = df["区域"] == "华东" cond2 = df["金额"] > 5000 result_and = df[cond1 & cond2] # 多条件或:区域等于华东,或销售员等于"张三" cond3 = df["销售员"] == "张三" result_or = df[cond1 | cond3] # 取反:区域不是华东 result_not = df[~cond1]

逻辑说明:pandas 里&对应“且”,|对应“或”,~对应“非”。这里的符号和 Python 原生的andor不一样,原因是 pandas 的 DataFrame 在做条件判断时会对整个 Series 进行逐元素运算,&|是位运算符,能逐个元素地做逻辑组合,而and/or只会判断整个对象的真假,直接用在 Series 上会报ValueError: The truth value of a Series is ambiguous,这是新手经常翻车的地方。

参数说明:每个条件必须用括号包起来,即(df["区域"] == "华东") & (df["金额"] > 5000),不能写成df["区域"] == "华东" & df["金额"] > 5000,因为&的优先级高于==,不加括号 Python 会把表达式解析成完全错误的东西。

这段代码运行后,result_and就是同时满足两个条件的行。注意条件里如果列名写错了,比如把“金额”写成“金额(元)”,pandas 会抛KeyError,报错信息会列出所有可用列名,照着改就行。

3.2 用 query() 简化表达:适合条件变量多的场景

如果条件越来越多,每次都写df[df["区域"] == "华东" & df["金额"] > 5000 & ...]会变得冗长。pandas 提供了query()方法,可以直接用字符串表达式筛选,代码更接近自然语言,也更容易动态拼接。

# 使用 query 表达同样的条件 result = df.query("区域 == '华东' and 金额 > 5000")

逻辑说明:query()里的条件用 Python 语法写,但不需要重复写df["列"],直接写列名即可。字符串本身用双引号包起来,内部条件的字符串值用单引号。对于“或”条件,把and换成or即可。

query()的优势在于,你可以在程序里动态构造条件字符串,比如根据用户输入拼出不同的表达式,这在写一个面向多部门、筛选条件随时调整的自动化脚本时很有用。不过要注意,query()内部无法直接引用外部变量,如果需要传值,要用@符号:

min_amount = 5000 region = "华东" result = df.query("区域 == @region and 金额 > @min_amount")

参数说明:@region表示引用外部定义的 Python 变量,这在脚本里比f-string拼接安全,因为不需要处理引号嵌套。

3.3 模糊匹配:excel中字符串包含的筛选

按条件筛选经常不只是精确等于,比如销售员列里存的是“张三是华东区”,想筛出所有姓名包含“张三”的行,用 Excel 里类似A列包含的模糊逻辑。pandas 里用str.contains()

# 销售员列包含"张三"的行 result = df[df["销售员"].str.contains("张三", na=False)]

逻辑说明:str.contains()对 Series 的每个元素做子串匹配,返回布尔 Series。na=False的意思是,如果某行的“销售员”是空值 NaN,不抛错,视为不匹配。如果省略na=False,空值会让整行在筛选时报TypeError

更高级一点,str.contains()还支持正则表达式。想筛出“手机号以 138 开头”的行,可以写成:

# 正则匹配:手机号列以138开头 result = df[df["手机号"].str.contains("^138", na=False, regex=True)]

参数说明:regex=True是默认值,True 表示按正则解析 pattern。^在正则里表示字符串开头。如果你只想做纯文本包含,不想要正则的转义效果,可以显式设regex=False,此时 pattern 里的^$*等符号会被当成普通字符处理。当你的筛选条件里带有括号、星号这类符号时,明确设regex=False是个好习惯,省得被正则语法误伤。

表格里这三个方法经常混在一起用:

方法适用场景典型写法注意事项
df[df["列"] == 值]精确等值df[df["区域"] == "华东"]类型要一致,数字和字符串别混
df.query()多条件、动态拼条件df.query("区域 == '华东' and 金额 > 5000")外部变量用@符号
str.contains()模糊包含df[df["列"].str.contains("关键词")]空值用na=False兜底

4. 写入新表的参数细节与文件覆盖风险

4.1 to_excel 的常用参数与写入多工作表

筛选完成后,写入 Excel 是整个流程的终点。to_excel默认行为是新建一个工作簿,把 DataFrame 写到第一个工作表。假设想把“华东”和“华南”两个结果分别存到同一个 Excel 文件里的两个 Sheet,写法是用pd.ExcelWriter

import pandas as pd df = pd.read_excel("销售记录.xlsx", sheet_name="Sheet1") east = df[df["区域"] == "华东"] south = df[df["区域"] == "华南"] # 使用 ExcelWriter 同时写入多个工作表 with pd.ExcelWriter("分区记录.xlsx", engine="openpyxl") as writer: east.to_excel(writer, sheet_name="华东", index=False) south.to_excel(writer, sheet_name="华南", index=False)

逻辑说明:pd.ExcelWriter是一个上下文管理器,作用是把多个 DataFrame 写入同一个 Excel 文件的不同 Sheet。sheet_name指定工作表名,名字不能包含[]:*?/\这些字符,否则 openpyxl 会报错。engine="openpyxl"是明确指定用哪个引擎写文件,也可以不写,pandas 会自动判断文件后缀,但显式声明能避免一些环境差异问题。

这块有一个很容易踩的坑:如果目标文件分区记录.xlsx已经存在,ExcelWriter在 with 块结束时会把文件整体覆盖。也就是说,你没办法用这个写法往已有 Excel 文件里追加一个 Sheet,它会重新创建一个全新的文件,原文件里之前手动写下的内容会被清空。

如果你确实需要往现有文件里追加工作表,要加上mode="a"参数:

with pd.ExcelWriter("分区记录.xlsx", engine="openpyxl", mode="a", if_sheet_exists="overlay") as writer: east.to_excel(writer, sheet_name="华东新数据", index=False)

参数说明:mode="a"表示 append 模式,打开现有文件而不是新建。if_sheet_exists="overlay"规定如果同名 Sheet 已存在,直接覆盖里面的内容。如果这个名字已经存在且你不写这个参数,pandas 会抛UserWarning或 ValueError,提示你处理 Sheet 重名问题。追加模式在自动办公里很常用,比如每天生成的日报都往同一个 Excel 文件里加一张当天日期的表。

4.2 数值与日期格式在写出后异常的处理

筛选数据写回 Excel,经常会遇到两个格式问题。第一个是金额列是字符串类型,比如从别的系统导出的数据里金额列存的是"1,234""¥5000",这种字符串没法直接和数字比较,筛选时df["金额"] > 5000会报错或结果为空。解决方案是在读取后做类型转换:

# 把金额列转成数字,coerce 让无法转换的值变成 NaN df["金额"] = pd.to_numeric(df["金额"].str.replace(",", "").str.replace("¥", ""), errors="coerce") # 日期列统一转成 datetime df["日期"] = pd.to_datetime(df["日期"], errors="coerce")

逻辑说明:str.replace(",", "")把千分位逗号去掉,str.replace("¥", "")把货币符号去掉,之后再交给pd.to_numericerrors="coerce"表示碰到无法转换的值时,不中断程序,而是填 NaN。这个参数很重要,如果源数据里有一行格式不规范,不写coerce会让整个脚本中断。

日期同理,用pd.to_datetime统一格式,errors="coerce"兜底非法日期。这两个转换完成后再做筛选,df["日期"] >= "2024-01-01"这种比较才靠谱。转换完如果发现金额列有 NaN,说明源数据里有脏数据,你可以用df = df.dropna(subset=["金额"])直接丢掉这些行,或者用df["金额"].fillna(0)填充为 0,看业务需求决定。

第二个问题是写入后日期列在 Excel 里变成一串数字。这其实是 Excel 的日期存储机制,它把日期存成序列号,显示格式由单元格格式决定。想让 Excel 打开后直接看到日期格式,需要在 pandas 侧指定输出格式:

with pd.ExcelWriter("带格式导出.xlsx", engine="openpyxl") as writer: east.to_excel(writer, sheet_name="华东", index=False) # 获取写入后的工作表,设置日期列格式 ws = writer.sheets["华东"] for row in ws.iter_rows(min_row=2, min_col=4, max_col=4): for cell in row: cell.number_format = "YYYY-MM-DD"

逻辑说明:writer.sheets["华东"]返回 openpyxl 的 Worksheet 对象,iter_rows从第 2 行开始(第 1 行是表头)遍历日期所在列,cell.number_format设置单元格的显示格式。这样做的好处是数据本身的类型没变,只是 Excel 的显示方式被指定了。如果列很多,手动调格式会繁琐,只处理必须的日期、金额列即可。

5. 按条件筛选存入新表的完整脚本骨架:从本地文件到文件夹输出

5.1 一个可复用的批量筛选脚本

把前面几节的要点合在一起,可以拼出一个接近实战的脚本骨架。这个脚本做的事情是:读取一个 Excel 工作簿的全部工作表,针对每个工作表应用一组筛选条件,然后把结果写入一个以日期命名的输出目录。

import pandas as pd import os from datetime import date # 配置区:路径和条件都集中在这里改 SOURCE_FILE = "数据源.xlsx" OUTPUT_DIR = f"筛选结果_{date.today().strftime('%Y%m%d')}" REGION = "华东" MIN_AMOUNT = 3000 # 创建输出目录,exist_ok=True 表示目录已存在时不报错 os.makedirs(OUTPUT_DIR, exist_ok=True) # 读取所有工作表,sheet_name=None 返回 dict,key 是 sheet 名 all_sheets = pd.read_excel(SOURCE_FILE, sheet_name=None) # 遍历每个工作表,筛选并单独存文件 for sheet_name, df in all_sheets.items(): if "区域" not in df.columns or "金额" not in df.columns: print(f"跳过 {sheet_name}:缺少必要列") continue # 类型清洗:金额列转数字,日期列转时间 df["金额"] = pd.to_numeric(df["金额"], errors="coerce") df["日期"] = pd.to_datetime(df["日期"], errors="coerce") # 筛选核心逻辑:区域等于指定值,且金额大于阈值 filtered = df[(df["区域"] == REGION) & (df["金额"] > MIN_AMOUNT)] # 重置索引,准备输出 filtered = filtered.reset_index(drop=True) out_path = os.path.join(OUTPUT_DIR, f"{sheet_name}_筛选结果.xlsx") filtered.to_excel(out_path, index=False) print(f"{sheet_name}:共 {len(filtered)} 行 -> {out_path}")

这段脚本的关键点有三个。第一,pd.read_excel(SOURCE_FILE, sheet_name=None)返回一个字典,key 是工作表名,value 是对应的 DataFrame,这样一份数据源里如果有多张表,一张也不用漏。第二,os.makedirs(..., exist_ok=True)确保输出目录存在,exist_ok=True是防止目录已创建时抛错。第三,每一轮循环输出后用len(filtered)打印行数,方便在命令行里直接核对数量。这个打印习惯能帮你快速发现筛选条件是不是写错了,比如本来应该筛出几百行,结果输出 0 行,那就是条件写反了或者类型没转对。

5.2 把条件参数化:筛选条件从外部配置读取

如果这个脚本要交付给不同的人用,或者你自己需要频繁调整条件,硬编码在代码里不是好方案。一种常见做法是把条件放在一个 Python 字典里,或者用一个简单的文本配置文件,脚本启动时读取并解析。

import pandas as pd import os def filter_excel_with_config(source_file, output_file, conditions): df = pd.read_excel(source_file, sheet_name="Sheet1") # 初始化为全 True,相当于不过滤 mask = pd.Series(True, index=df.index) for col, op, value in conditions: if op == "eq": mask &= df[col] == value elif op == "gt": mask &= df[col] > value elif op == "contains": mask &= df[col].astype(str).str.contains(value, na=False) df[mask].to_excel(output_file, index=False) return len(df[mask]) # 调用示例:区域等于华东,且金额大于 5000 conditions = [("区域", "eq", "华东"), ("金额", "gt", 5000)] n = filter_excel_with_config("销售记录.xlsx", "筛选结果.xlsx", conditions) print(f"输出 {n} 行")

逻辑说明:mask初始化为全 True,随后每个条件用&=叠加进去,最终得到一个布尔 Series,再一次性过滤 DataFrame。这种写法比较适合条件数量不固定的场景,加一个条件只需要在列表里加一组元组。op参数的取值可以根据实际需求扩充,比如lt是小于、ne是不等、startswith是前缀匹配。要注意的是,当列是数字类型却用contains操作时,先astype(str)转成字符串再匹配,否则会报TypeError

6. 验证筛选结果的正确性:一次把“存对”做到位

筛选完存进新表,真正的坑不在写代码那一下,而在“怎么确认存进去的数据是对的”。一个残酷的事实是,filtered的行数对不上、金额合计变了、日期格式错乱,这些问题在to_excel完成之后、Excel 打开之前,肉眼是看不见的。所以我的习惯是每次跑完筛选脚本,紧接着跑一段校验,而不是直接打开文件数行数。

import pandas as pd # 读回刚才写入的文件 check_df = pd.read_excel("华东区记录.xlsx", sheet_name="Sheet1") print("写入文件行数:", len(check_df)) print("源数据行数:", len(df)) # 校验 1:行数匹配 assert len(check_df) == len(filtered), "行数不一致,写入或读取有问题" # 校验 2:关键列无空值 assert check_df["金额"].notna().all(), "金额列存在空值" # 校验 3:金额合计与筛选结果一致 assert check_df["金额"].sum() == filtered["金额"].sum(), "金额合计不一致" # 校验 4:区域列的值全部符合条件 assert (check_df["区域"] == "华东").all(), "区域列混入非目标值" print("校验通过")

逻辑说明:assert的作用是断言,如果条件成立什么都不做,条件不成立就抛出异常并附带报错信息。在批处理脚本里用四个断言分门别类检查,比只看一眼行数可靠得多。第一种错误是read_excelto_excel之间列顺序或行数发生了偏移,第二种错误是类型清洗没成功导致写入 NaN,第三种错误是筛选条件里金额比较的边界写错。每一条都指向一个具体的问题,报错时你能直接从提示里判断去哪查。

如果你不想用assert中断脚本,也可以把校验结果收集起来,最后统一打印:

errors = [] if len(check_df) != len(filtered): errors.append(f"行数不一致:预期 {len(filtered)},实际 {len(check_df)}") if not check_df["金额"].notna().all(): errors.append("金额列存在空值") with open("校验报告.txt", "w", encoding="utf-8") as f: f.write("\n".join(errors) if errors else "全部校验通过")

这个写法适合定时任务,脚本跑完后输出一个校验文件,方便直接查看。校验报告的命名可以加上时间戳,避免上次的结果被覆盖。

还有一个容易忽视的验证点:把日期列读回来后再检查一次类型。to_excel写入后再read_excel读回,pandas 偶尔会把日期列识别成字符串,原因可能是 Excel 里单元格格式不统一。遇到这种情况,在读回时加一句解析:

check_df["日期"] = pd.to_datetime(check_df["日期"], errors="coerce")

这样日期列的值就会被重新归一化,若有非法日期会变成 NaT。用check_df["日期"].isna().sum()可以统计出具体有几行有问题。整个验证流程里最有价值的是第二和第三条,它们能拦住九成“筛选完存进去但数据不对”的翻车现场。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询