十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用Python pandas高效筛选Excel数据:从手工操作到脚本化

用Python pandas高效筛选Excel数据:从手工操作到脚本化 简介这份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.py和12.ipynb就是同一套逻辑的两种载体problem.PNG和result.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_name0, # 第 1 个工作表也可以写工作表名比如 Sheet1 dtype{物料编码: str} # 该列强制按文本读取 ) print(df.shape) # (行数, 列数)先确认有没有读全 print(df.head(3)) # 打印前 3 行看看列名和内容是否和表头一致参数说明sheet_name接受工作表名称字符串或索引整数。如果 Excel 文件里有多个 sheet这里指定错误会直接抛异常。建议用工作表名字不容易因为页签顺序调整而读错。dtype把指定列强制读成某种数据类型。物料编码这类长数字列如果不加这个参数pandas 会按 int64 读数字超过 15 位时前面的有效位会丢后面变成60012e13之类的浮点表示再写回去数据就错了。header默认header0也就是第一行作为列名。如果你的表前面有几行标题说明就得改成header2或其他索引然后用skiprows配合跳过多余行。读进来之后别急着筛选先做一步体检。df.info()会列出每一列的非空值数量和数据类型一眼就能看出哪些列有缺失、哪些列类型和预期不符。这个习惯能省掉后面大量排查问题的时间。2.3 数据体检筛选结果不对八成是读入时类型出了问题打开problem.PNG看到的筛选结果少了几行或筛选结果为空最常见的原因不是筛选条件写错而是数据列的类型根本不对。举例来说Excel 里的金额列如果有些单元格是文本格式pandas 读进来后整列会变成object类型里面混着字符串和数字。此时用df[金额] 1000比较字符串和数字之间的比较规则和预期完全不一样。print(df.dtypes) # 查看所有列的数据类型 print(df[金额].head(10)) # 打印前 10 个值人工检查是否有 1,234.00 这类文本如果发现金额列是 object里面还带着千分位逗号用下面的方式清洗# errorscoerce 会把无法转换的文本变成 NaN方便后续处理 df[金额] pd.to_numeric(df[金额], errorscoerce) # 转完之后检查有多少个空值如果数量不多就直接删掉这些行 print(df[金额].isna().sum()) df df.dropna(subset[金额])pd.to_numeric的作用是把 object 类型的列转换成数值类型errorscoerce的意思是转换失败不报错而是填上NaN。这里的取舍是如果原表里确实有脏数据直接删掉比让它干扰后续计算更稳妥。3. 条件筛选的三种写法以及布尔索引的优先级陷阱3.1 最基础的布尔索引df[条件] 到底做了什么pandas 筛选数据的核心机制是布尔索引先构造一个和 DataFrame 行数相同的布尔 SeriesTrue保留、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(螺丝, naFalse)) ]两个要点每个独立条件必须用括号包起来然后才能用与、|或、~非连接。和|是位运算符优先级高于比较运算符所以不加大括号的话Python 会先算再算比较结果完全不是你要的。str.contains里加naFalse是为了让空值直接按False处理否则某一行的物料名称是空值时整个筛选会得到NaNpandas 会把NaN当作True保留筛选结果里混进一堆空行。3.3 字符串、日期和介于区间的筛选不同数据类型的筛选条件写法不太一样整理成一张表方便对照场景写法说明金额大于 1000df[df[金额] 1000]数值列直接比较金额在 1000 到 5000 之间df[(df[金额] 1000) (df[金额] 5000)]左闭右开注意括号物料名称包含螺丝df[df[物料名称].str.contains(螺丝, naFalse)]子串匹配默认支持正则日期晚于某个时间点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 基础写入indexFalse 之外的隐藏问题筛选完成之后把结果写到新文件filtered.to_excel( 每月(大于1K).xlsx, sheet_name大于1K, indexFalse, engineopenpyxl )indexFalse是必写的如果不写pandas 会把行号0、1、2……当成第一列写进 Excel输出文件多一列没有任何业务意义的序号别人拿到表还要手动删。engineopenpyxl是写入 .xlsx 格式的默认引擎一般不用显式指定但如果文件后缀是 .xls需要换成enginexlwt。有一个经常被忽略的点to_excel默认会覆盖整个目标文件。如果你之前已经生成了一个每月(大于1K).xlsx再次运行不会有任何提示直接把旧文件冲掉。所以在写代码时建议加上存在性检查或者输出文件名带时间戳from datetime import datetime out_name f每月(大于1K)_{datetime.now().strftime(%Y%m%d)}.xlsx filtered.to_excel(out_name, indexFalse) print(f已生成 {out_name})4.2 多个条件结果写入同一文件的多个 sheet实际业务中同一个源表可能要按不同阈值切分成多个 sheet比如大于 5000 的、1000 到 5000 的、小于 1000 的。这种情况下不能多次调用to_excel因为第二次调用会覆盖第一次的结果。正确做法是用ExcelWriter把多个 DataFrame 写进同一个文件的不同 sheetwith pd.ExcelWriter(每月分级汇总.xlsx, engineopenpyxl) as writer: df[df[金额] 5000].to_excel(writer, sheet_name大于5K, indexFalse) df[(df[金额] 1000) (df[金额] 5000)].to_excel(writer, sheet_name1K到5K, indexFalse) df[df[金额] 1000].to_excel(writer, sheet_name小于1K, indexFalse)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(dropTrue)), 物料编码列不一致 print(f验证通过{len(verify)} 行)reset_index(dropTrue)是为了消除索引错位——filtered是原表筛选出来的子集索引是原表的下标不是从 0 开始的连续整数重新读入的文件索引是连续的。直接用equals()比较会误报重置索引后再比就对了。5. 从单文件脚本到批量工具给筛选逻辑加上路径参数和异常兜底5.1 用 pathlib 遍历目录批量处理多个 Excel 文件12.py处理的是单个文件但实际工作中往往要处理整个目录下的所有物料表。用pathlib的glob方法可以一次性拿到所有匹配的文件路径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, indexFalse)file_path.stem是不带后缀的文件名这样生成的结果文件名自然带上源文件的名字。跳过~$开头的临时文件是因为 Excel 打开文件时会生成隐藏的临时副本glob(*.xlsx)会匹配到它直接读取会报错。5.2 用 argparse 做命令行参数不用再改代码里的路径每次换条件就改脚本里的路径会让代码越改越乱。argparse可以把输入文件、筛选列、阈值都变成命令行参数脚本本身保持不变import argparse parser argparse.ArgumentParser(description按条件筛选 Excel 并输出新文件) parser.add_argument(--input, requiredTrue, help输入的 Excel 文件路径) parser.add_argument(--column, default金额, help筛选依据的列名) parser.add_argument(--min, typefloat, default1000, help筛选阈值大于该值保留) parser.add_argument(--output, defaultNone, 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], errorscoerce) df df.dropna(subset[args.column]) df[df[args.column] args.min].to_excel(out, indexFalse)使用方式python filter_excel.py --input 每月物料表.xlsx --column 金额 --min 1000 --output 每月(大于1K).xlsxtypefloat会把命令行的字符串参数转成浮点数保证args.min能参与数值比较。5.3 一个小技巧ipynb 转 py让代码脱离 Notebook 运行项目包里同时给了12.ipynb和12.py很多人是先在 Jupyter Notebook 里调试代码调通了之后再用命令行跑。手动从 Notebook 复制代码到 .py 文件很容易漏掉中间某个单元格或者把调试用的print输出也复制进去。用 nbconvert 一条命令完成转换jupyter nbconvert --to script 12.ipynb转换后的12.py包含所有代码块--to script会自动去掉输出结果只保留代码本身。之后每次在 Notebook 里改完代码重新执行这一条命令就能把脚本同步更新到 .py 文件。配合 Windows 计划任务或 Linux 的 cron这个脚本就能在每月固定时间自动运行从手动打开 Excel、点筛选、另存为的工作流中彻底解放出来。本文还有配套的精品资源点击获取
返回列表