用Python pandas高效筛选Excel数据:从手工操作到脚本化
2026/9/16 15:46:50 网站建设 项目流程

简介:这份Python自动办公实例包面向需要提升Excel数据处理效率的办公人员和数据分析初学者,演示了如何基于pandas按条件筛选数据并自动写入新工作表,帮助读者告别手工筛选与导出流程。压缩包共含9个文件,主要包含1个可直接运行的Python脚本、1份Jupyter Notebook操作笔记、3份Excel数据表格与4张示意截图,整体大小仅2.46MB,结构清晰,方便下载与对照学习。目前已有687人学习该资源,适合在日常报表处理、月度数据拆分等场景中快速应用。通过运行脚本和笔记,可以完整掌握读取原始表格、构造筛选条件、定位满足要求的数据行以及导出到新工作表的操作思路;配套的Excel文件和截图则有助于核对每一步结果。在此基础上,还可进一步将pandas技巧拓展到数据分析、网络爬虫脚本编写乃至游戏开发中的数值处理任务中,提升自动化办公的综合能力。

1. 一个 Excel 筛选需求,为什么值得用 Python 重写

手里有一份每月物料表.xlsx,一万多行,月底要筛出金额大于 1000 的物料明细,单独存成一个文件发出去。用 Excel 自带的筛选功能,点三下鼠标就能出结果,但如果每个月都要做一次,每次还要换条件、换文件名、发不同的人,手工操作就容易漏行、忘改条件、发错文件。这个实例给了一套完整的思路:用 pandas 读入 Excel,按条件筛选,再写入新的工作表,全程脚本化。项目里的12.py12.ipynb就是同一套逻辑的两种载体,problem.PNGresult.PNG记录了筛选前后的对照。适合手里有重复性 Excel 处理任务、想从手工操作切换到脚本化处理的人。

2. read_excel 读入数据:从文件到 DataFrame 之间发生了什么

2.1 为什么是 pandas,而不是 openpyxl 或 Excel 的 VBA

单看"筛选数据"这件事,openpyxl 也能做:循环每一行、判断条件、写入新表。但一旦数据量过万,或者后续还要做分组统计、多表合并,openpyxl 的行级循环就会拖慢速度,而且代码会越写越长。pandas 的 DataFrame 是二维表格结构,筛选、聚合、去重这类操作是向量化的,底层用 C 实现,数据量几万行基本感觉不到延迟。

另一个常见选择是 Excel VBA,但它绑定 Office 环境,没法脱离 Windows 运行,也不方便接入其他数据源。pandas 的代码写好之后放在任何装了 Python 的机器上都能跑,甚至接到定时任务里。这也是为什么网络爬虫、数据分析这类方向的教程里,pandas 总是绕不开的一环——它不只是处理 Excel 顺手,处理一切表格型数据都是核心组件。

2.2 read_excel 的主要参数:别只写一个文件路径

pd.read_excel()是这个实例的第一步,很多人在这一步就埋了坑。看下面的代码:

import pandas as pd # 读取每月物料表,物料编码按字符串读入,避免变成科学计数法 df = pd.read_excel( "每月物料表.xlsx", sheet_name=0, # 第 1 个工作表;也可以写工作表名,比如 "Sheet1" dtype={"物料编码": str} # 该列强制按文本读取 ) print(df.shape) # (行数, 列数),先确认有没有读全 print(df.head(3)) # 打印前 3 行,看看列名和内容是否和表头一致

参数说明:

  • sheet_name:接受工作表名称(字符串)或索引(整数)。如果 Excel 文件里有多个 sheet,这里指定错误会直接抛异常。建议用工作表名字,不容易因为页签顺序调整而读错。
  • dtype:把指定列强制读成某种数据类型。物料编码这类长数字列,如果不加这个参数,pandas 会按 int64 读,数字超过 15 位时前面的有效位会丢,后面变成60012e13之类的浮点表示,再写回去数据就错了。
  • header:默认header=0,也就是第一行作为列名。如果你的表前面有几行标题说明,就得改成header=2或其他索引,然后用skiprows配合跳过多余行。

读进来之后别急着筛选,先做一步体检。df.info()会列出每一列的非空值数量和数据类型,一眼就能看出哪些列有缺失、哪些列类型和预期不符。这个习惯能省掉后面大量排查问题的时间。

2.3 数据体检:筛选结果不对,八成是读入时类型出了问题

打开problem.PNG看到的"筛选结果少了几行"或"筛选结果为空",最常见的原因不是筛选条件写错,而是数据列的类型根本不对。举例来说,Excel 里的金额列如果有些单元格是文本格式,pandas 读进来后整列会变成object类型,里面混着字符串和数字。此时用df["金额"] > 1000比较,字符串和数字之间的比较规则和预期完全不一样。

print(df.dtypes) # 查看所有列的数据类型 print(df["金额"].head(10)) # 打印前 10 个值,人工检查是否有 "1,234.00" 这类文本

如果发现金额列是 object,里面还带着千分位逗号,用下面的方式清洗:

# errors="coerce" 会把无法转换的文本变成 NaN,方便后续处理 df["金额"] = pd.to_numeric(df["金额"], errors="coerce") # 转完之后检查有多少个空值,如果数量不多,就直接删掉这些行 print(df["金额"].isna().sum()) df = df.dropna(subset=["金额"])

pd.to_numeric的作用是把 object 类型的列转换成数值类型,errors="coerce"的意思是转换失败不报错,而是填上NaN。这里的取舍是:如果原表里确实有脏数据,直接删掉比让它干扰后续计算更稳妥。

3. 条件筛选的三种写法,以及布尔索引的优先级陷阱

3.1 最基础的布尔索引:df[条件] 到底做了什么

pandas 筛选数据的核心机制是布尔索引:先构造一个和 DataFrame 行数相同的布尔 Series,True保留、False丢弃。df["金额"] > 1000返回的正是这样一个布尔 Series,把它放进df[...]的方括号里,pandas 会按位置取出所有True对应的行。

# 筛选金额大于 1000 的物料记录 condition = df["金额"] > 1000 filtered = df[condition] print(f"原表 {len(df)} 行,筛选后 {len(filtered)} 行")

这段代码是这个实例的核心逻辑,12.py里的实现大同小异。注意condition是一个独立的中间变量,不要在一行里写太复杂的表达式,格式问题是次要的,关键是为了确认条件本身正确——可以先print(condition.value_counts())看看 True 和 False 的分布,再决定要不要执行筛选。

3.2 多条件组合:加号不行,必须用 & 、 | 和括号

实际业务很少只有一个筛选条件,常见的场景是"金额大于 1000 且物料名称包含 '螺丝'"。这时候新手最容易犯的错是写df[df["金额"] > 1000 and df["物料名称"].str.contains("螺丝")],然后报错ValueError: The truth value of a Series is ambiguous。原因是 Python 的and会把两端转成布尔值,而 Series 是多元素的,根本没法判断真假。

# 多条件筛选:金额大于 1000 且物料名称包含 "螺丝" filtered_multi = df[ (df["金额"] > 1000) & (df["物料名称"].str.contains("螺丝", na=False)) ]

两个要点:

  • 每个独立条件必须用括号包起来,然后才能用&(与)、|(或)、~(非)连接。
  • &|是位运算符,优先级高于比较运算符,所以不加大括号的话,Python 会先算&再算比较,结果完全不是你要的。

str.contains里加na=False是为了让空值直接按False处理,否则某一行的物料名称是空值时,整个筛选会得到NaN,pandas 会把NaN当作True保留,筛选结果里混进一堆空行。

3.3 字符串、日期和"介于区间"的筛选

不同数据类型的筛选条件写法不太一样,整理成一张表方便对照:

场景写法说明
金额大于 1000df[df["金额"] > 1000]数值列直接比较
金额在 1000 到 5000 之间df[(df["金额"] >= 1000) & (df["金额"] < 5000)]左闭右开,注意括号
物料名称包含"螺丝"df[df["物料名称"].str.contains("螺丝", na=False)]子串匹配,默认支持正则
日期晚于某个时间点df[pd.to_datetime(df["日期"]) > "2024-06-01"]先把字符串列转成 datetime 再比
筛选的结果取反df[~df["金额"] > 1000]~放在条件前面取反

这里最容易忽略的是日期筛选。Excel 里的日期列读进来经常是datetime64或字符串,字符串直接和"2024-06-01"比较是按字典序,结果通常不可用。先用pd.to_datetime()转一遍再比较,时间开销不大,但能避免边界日的判断出错。

3.4 筛选相关的坑:空值、type 混乱和重复列名

这一节集中说三个实际工作中踩过的坑,problem.PNG里展示的问题大概率就是其中之一。

第一个坑是空值筛选失效。如果某列有空值,df[df["金额"] > 1000]不会保留空值行,也不会报错,只是静默丢弃。所以要确认筛选后行数是否符合业务预期,不要想当然认为"没筛出来就是没数据"。

第二个坑是列名有不可见字符。Excel 表头里如果带空格或换行符,读进来之后列名显示为"金额 ",直接写df["金额"]会报KeyError。用df.columns.tolist()打印一遍,肉眼对齐一下列名,比反复试错更快。

第三个坑是重复列名。两张表合并后可能出现两列都叫"金额",df["金额"]返回的是一个 DataFrame 而不是 Series,后面的> 1000再操作就会形态不匹配。这种情况用df.iloc[:, 4]按位置取列,或者合并后立即重命名列,不要拖到筛选阶段。

4. to_excel 写入新表:参数陷阱、多 sheet 导出和格式边界

4.1 基础写入:index=False 之外的隐藏问题

筛选完成之后,把结果写到新文件:

filtered.to_excel( "每月(大于1K).xlsx", sheet_name="大于1K", index=False, engine="openpyxl" )

index=False是必写的:如果不写,pandas 会把行号(0、1、2……)当成第一列写进 Excel,输出文件多一列没有任何业务意义的序号,别人拿到表还要手动删。engine="openpyxl"是写入 .xlsx 格式的默认引擎,一般不用显式指定,但如果文件后缀是 .xls,需要换成engine="xlwt"

有一个经常被忽略的点:to_excel默认会覆盖整个目标文件。如果你之前已经生成了一个每月(大于1K).xlsx,再次运行不会有任何提示,直接把旧文件冲掉。所以在写代码时建议加上存在性检查,或者输出文件名带时间戳:

from datetime import datetime out_name = f"每月(大于1K)_{datetime.now().strftime('%Y%m%d')}.xlsx" filtered.to_excel(out_name, index=False) print(f"已生成 {out_name}")

4.2 多个条件结果写入同一文件的多个 sheet

实际业务中,同一个源表可能要按不同阈值切分成多个 sheet,比如大于 5000 的、1000 到 5000 的、小于 1000 的。这种情况下不能多次调用to_excel,因为第二次调用会覆盖第一次的结果。正确做法是用ExcelWriter把多个 DataFrame 写进同一个文件的不同 sheet:

with pd.ExcelWriter("每月分级汇总.xlsx", engine="openpyxl") as writer: df[df["金额"] > 5000].to_excel(writer, sheet_name="大于5K", index=False) df[(df["金额"] >= 1000) & (df["金额"] <= 5000)].to_excel(writer, sheet_name="1K到5K", index=False) df[df["金额"] < 1000].to_excel(writer, sheet_name="小于1K", index=False)

ExcelWriter作为上下文管理器使用,with块结束时自动保存文件。注意每个to_excel的第一个参数是writer对象,不是文件名。这样生成的文件里只有一个 Excel 文件、三个 sheet 页签,方便后续用筛选功能查看,也方便发给别人时不用打包多个附件。

4.3 to_excel 不会保留原表格式,要保留格式就换 openpyxl

这里要明确一个边界:pandas 的to_excel只负责数据,列宽、填充色、边框、合并单元格这些原始格式全部丢弃。如果你筛出来的数据是给人直接看的报表,而不是给下游程序处理的中间产物,输出文件首行没有加粗、列宽挤在一起,观感很差。

需要保留原表格式时,常见做法是换用 openpyxl 直接操作行:

from openpyxl import load_workbook from openpyxl.utils import get_column_letter # 读取原始文件,保留模板样式 wb = load_workbook("模板.xlsx") ws = wb.active # 假设要筛选 A 列金额大于 1000 的行 # 根据数据量循环判断,把满足条件的行复制到一张新表

这个方案的问题是代码量明显变大,而且要自己按行号处理逻辑,不如 pandas 直观。我的取舍标准是:数据作为中间产物交给其他程序处理,直接用to_excel;数据是给人看的正式报表,那我会考虑用 openpyxl 从模板复制样式,或者直接把筛选逻辑做进模板表里。

4.4 写入后的自检方法:别手动打开 Excel 数行数

写完文件之后,手动打开 Excel 数行数来验证结果,最慢也最容易出错。更可靠的做法是用 pandas 把刚写入的文件再读出来,和内存里的 DataFrame 做断言比较:

# 重新读入写入的文件,验证行数和数据一致 verify = pd.read_excel("每月(大于1K).xlsx") assert len(verify) == len(filtered), f"行数不一致:{len(verify)} vs {len(filtered)}" assert verify["物料编码"].equals(filtered["物料编码"].reset_index(drop=True)), "物料编码列不一致" print(f"验证通过:{len(verify)} 行")

reset_index(drop=True)是为了消除索引错位——filtered是原表筛选出来的子集,索引是原表的下标,不是从 0 开始的连续整数;重新读入的文件索引是连续的。直接用equals()比较会误报,重置索引后再比就对了。

5. 从单文件脚本到批量工具:给筛选逻辑加上路径参数和异常兜底

5.1 用 pathlib 遍历目录,批量处理多个 Excel 文件

12.py处理的是单个文件,但实际工作中往往要处理整个目录下的所有物料表。用pathlibglob方法可以一次性拿到所有匹配的文件路径:

from pathlib import Path data_dir = Path("./data") for file_path in data_dir.glob("*.xlsx"): # 跳过 Excel 临时文件 if file_path.name.startswith("~$"): continue df = pd.read_excel(file_path) filtered = df[df["金额"] > 1000] if len(filtered) == 0: print(f"跳过 {file_path.name}:无满足条件的记录") continue filtered.to_excel(f"{file_path.stem}_大于1K.xlsx", index=False)

file_path.stem是不带后缀的文件名,这样生成的结果文件名自然带上源文件的名字。跳过~$开头的临时文件是因为 Excel 打开文件时会生成隐藏的临时副本,glob("*.xlsx")会匹配到它,直接读取会报错。

5.2 用 argparse 做命令行参数,不用再改代码里的路径

每次换条件就改脚本里的路径,会让代码越改越乱。argparse可以把输入文件、筛选列、阈值都变成命令行参数,脚本本身保持不变:

import argparse parser = argparse.ArgumentParser(description="按条件筛选 Excel 并输出新文件") parser.add_argument("--input", required=True, help="输入的 Excel 文件路径") parser.add_argument("--column", default="金额", help="筛选依据的列名") parser.add_argument("--min", type=float, default=1000, help="筛选阈值,大于该值保留") parser.add_argument("--output", default=None, help="输出文件路径,默认自动生成") args = parser.parse_args() out = args.output or "筛选结果.xlsx" df = pd.read_excel(args.input) df[args.column] = pd.to_numeric(df[args.column], errors="coerce") df = df.dropna(subset=[args.column]) df[df[args.column] > args.min].to_excel(out, index=False)

使用方式:

python filter_excel.py --input 每月物料表.xlsx --column 金额 --min 1000 --output 每月(大于1K).xlsx

type=float会把命令行的字符串参数转成浮点数,保证args.min能参与数值比较。

5.3 一个小技巧:ipynb 转 py,让代码脱离 Notebook 运行

项目包里同时给了12.ipynb12.py,很多人是先在 Jupyter Notebook 里调试代码,调通了之后再用命令行跑。手动从 Notebook 复制代码到 .py 文件很容易漏掉中间某个单元格,或者把调试用的print输出也复制进去。用 nbconvert 一条命令完成转换:

jupyter nbconvert --to script 12.ipynb

转换后的12.py包含所有代码块,--to script会自动去掉输出结果,只保留代码本身。之后每次在 Notebook 里改完代码,重新执行这一条命令,就能把脚本同步更新到 .py 文件。配合 Windows 计划任务或 Linux 的 cron,这个脚本就能在每月固定时间自动运行,从手动打开 Excel、点筛选、另存为的工作流中彻底解放出来。

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

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

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

立即咨询