在做表格数据处理时,经常遇到一种需求:在一张表里同时筛选多个关键词,只要某一列包含其中任意一个关键词,就把这一行保留下来。很多人第一反应是手动一列一列筛、一个词一个词查,再复制粘贴结果。数据量小还好,一旦表格有几千行、关键词有十几个,手动操作不仅容易漏,而且效率极低。
这篇文章就来系统梳理“全表同时筛选多个关键词”的完整方案,以 Python + pandas 为核心,覆盖单列多关键词筛选、全表多关键词筛选、关键词自动转义、空值处理、大小写处理等场景。文中所有代码都可以直接复制运行,看完之后你可以把方案封装成自己的工具函数,以后遇到同类需求一次解决。
适合读者:经常处理表格数据、Excel 筛选效率太低、想用 Python 提升数据处理效率的同学,以及刚接触 pandas 想找实战案例的新手。
1. 背景与核心概念
1.1 什么是全表多关键词筛选
全表多关键词筛选,指的是在一张二维表结构中,不限定某一列,而是对整个表格范围进行检索:只要某一个关键词出现在任意一列的任意一个单元格中,就认为这一行数据与关键词匹配。
举个例子,假设有一张客户反馈表,包含“客户姓名”“反馈内容”“处理人”三列。现在需要筛选出所有提到“退款”“投诉”“差评”这三个词的记录。这三个词可能出现在“反馈内容”里,也可能出现在“处理人”备注里,甚至客户姓名里。手动操作时,需要分别对每一列执行包含筛选,再把多列结果合并,逻辑繁琐,而且多个关键词之间是“或”关系时特别容易漏掉数据。
pandas 提供了字符串向量化操作,可以在一行代码内完成这种多关键词匹配,不需要写 for 循环,也不需要一个词一个词地查。
1.2 适用的典型场景
这种筛选方式在以下场景中非常常见:
- 日志分析:从大量日志行中筛出包含 error、exception、failed 的记录。
- 客户反馈分类:按关键词归类投诉、建议、咨询内容。
- 商品信息清洗:筛选描述中命中多个品牌词或品类词的商品。
- 运营数据质检:快速定位异常备注、特殊标记的记录。
- 多表合并前的脏数据排查:找出包含缺失标记、异常字符的行。
1.3 常用的实现思路对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| Excel 自动筛选逐列操作 | 不需要写代码 | 多列、多关键词时效率低,容易遗漏 |
| Excel 高级筛选 | 支持条件区域 | 配置复杂,条件区域维护麻烦 |
| pandas 正则筛选 | 灵活、可复用、支持复杂条件 | 需要基础正则知识 |
| pandas 逐行遍历判断 | 逻辑直观 | 数据量大时性能差 |
这篇文章重点讲 pandas 正则筛选方案,因为在“多关键词 + 全表范围”这个组合条件下,它的代码最短、可维护性最好、执行效率也最高。
2. 环境准备与数据说明
2.1 运行环境
本文示例代码基于以下环境:
- 操作系统:Windows / macOS / Linux 均可
- Python:3.8 及以上版本
- pandas:1.3 及以上版本
如果你的电脑还没安装 pandas,可以在命令行中执行:
pip install pandas如果你使用的是 Anaconda,pandas 通常已经预装。可以通过下面命令确认版本:
python -c "import pandas; print(pandas.__version__)"版本不同可能导致个别 API 行为有差异,但本文用到的方法在 pandas 1.x 各版本中表现一致,可以放心使用。
2.2 示例数据结构
为了方便演示,先构造一张模拟表,内容是一份简化的工单记录:
import pandas as pd data = { "单号": ["A001", "A002", "A003", "A004", "A005"], "客户": ["张三", "李四", "王五", "赵六", "孙七"], "反馈内容": [ "产品使用过程中遇到了闪退问题", "物流配送速度很快,服务态度好", "申请退款,希望尽快处理", "客服电话一直打不通,投诉", "收到货后发现有破损,差评" ], "处理人": ["小明", "小红", "小刚", "小丽", "小华"] } df = pd.DataFrame(data) print(df)运行结果:
单号 客户 反馈内容 处理人 0 A001 张三 产品使用过程中遇到了闪退问题 小明 1 A002 李四 物流配送速度很快,服务态度好 小红 2 A003 王五 申请退款,希望尽快处理 小刚 3 A004 赵六 客服电话一直打不通,投诉 小丽 4 A005 孙七 收到货后发现有破损,差评 小华接下来所有的筛选需求,都会基于这张表展开。后面第 4 章还会扩展一张更接近真实业务的三列表格,用来演示全表检索。
3. 核心方法拆解
3.1 str.contains 方法:字符串筛选的基础
pandas 的 Series 对象有一个str.contains方法,用于判断字符串中是否包含指定内容。它返回一个布尔型 Series,可以当作筛选条件的索引。
基本语法:
df["反馈内容"].str.contains("退款")返回结果:
0 False 1 False 2 True 3 False 4 False Name: 反馈内容, dtype: bool这个布尔 Series 可以直接传给 DataFrame 的索引位置:
result = df[df["反馈内容"].str.contains("退款")] print(result)只保留“反馈内容”中包含“退款”的行。
str.contains有几个关键参数,日常使用频率很高:
| 参数 | 作用 | 默认值 |
|---|---|---|
| pat | 匹配模式,可以传普通字符串,也可以传正则表达式 | 无 |
| case | 是否区分大小写 | True |
| na | 遇到 NaN 空值时返回什么值 | False 会报错,建议显式指定 |
| regex | 是否把 pat 当作正则表达式解析 | True |
| flags | 正则表达式标志位,如 re.IGNORECASE | 0 |
这里特别提醒:na参数非常重要。当某列存在空值时,如果不指定na=False,筛选会直接把空值行排除掉并抛出一个ValueError,导致程序中断。后面常见问题部分会演示这个报错。
3.2 正则表达式中的竖线:多关键词的天然表达
str.contains的pat参数支持正则表达式。正则表达式中,竖线|表示“或”关系。因此,要判断一列是否包含多个关键词中的任意一个,只需要把关键词用竖线拼接成一个字符串。
例如:
df["反馈内容"].str.contains("退款|投诉|差评", na=False)返回结果:
0 False 1 False 2 True 3 True 4 True Name: 反馈内容, dtype: bool这一行代码就完成了三个关键词的“或”匹配。关键词再多,也只是往竖线拼接的字符串里继续追加而已。
这是“多关键词同时筛选”最核心的原理:用正则表达式的或运算,把多个关键词简化成一个匹配模式,然后交给 pandas 一次执行。
3.3 动态拼接关键词模式
实际业务中,关键词通常不是写死在代码里的,而是来自一个列表。比如从配置文件中读取,或者从另一张表中读取。这时候需要把列表动态拼接成正则模式。
keywords = ["退款", "投诉", "差评"] pattern = "|".join(keywords) print(pattern)输出:
退款|投诉|差评拼接完成后再传给str.contains:
result = df[df["反馈内容"].str.contains(pattern, na=False)]这样做的好处是:以后增减关键词,只需要改keywords列表,不用改动筛选逻辑本身。
3.4 特殊字符转义:关键词里有括号、点号、竖线怎么办
这是很多人容易踩坑的地方。比如关键词是“v1.0”,这里的点号在正则表达式中表示“匹配任意字符”,而不是字面上的小数点。又比如关键词本身包含竖线“|”,会和正则表达式的“或”语法冲突。
解决方法是使用re.escape对关键词做转义:
import re keywords = ["v1.0", "C++", "Windows|Linux"] pattern = "|".join(re.escape(k) for k in keywords) print(pattern)输出:
v1\.0|C\+\+|Windows\|Linux转义之后,点号、加号、竖线都变成了普通字符,匹配时不会再被解析成正则语法。这一点在关键词包含版本号、文件路径、括号、星号时尤其重要。
3.5 全表检索:如何把布尔判断扩展到整张表
上面说的都是对“某一列”做匹配。全表多关键词筛选,需要把“每一列是否包含某个关键词”的判断结果组合起来。
pandas 中有一个简单做法:对每一列调用str.contains,再用逻辑或把各列结果合并。如果只有三列,可以这样写:
mask = ( df["客户"].str.contains(pattern, na=False) | df["反馈内容"].str.contains(pattern, na=False) | df["处理人"].str.contains(pattern, na=False) )但列数多了以后,这种写法非常冗余。更通用的做法是用apply对每一列执行str.contains,得到一个由布尔 Series 组成的 DataFrame,再沿行方向取“任意一个为 True”:
mask = df.apply( lambda col: col.astype(str).str.contains(pattern, na=False) ).any(axis=1)这里分两步理解:
df.apply(lambda col: ...)遍历每一列,返回一个布尔 Series。any(axis=1)沿行方向判断:只要这一行的任意一列匹配成功,就保留这一行。
这是全表检索最简单的实现方式。
3.6 使用 reduce 合并多列条件
除了any(axis=1),还有一种写法使用 Python 内置的functools.reduce把多个列的布尔条件逐层合并:
from functools import reduce mask_list = [ df[col].astype(str).str.contains(pattern, na=False) for col in df.columns ] mask = reduce(lambda a, b: a | b, mask_list)这种写法适合后续还想对条件做二次加工的场景,比如有的列用“或”关系、有的列用“与”关系。不过对大多数全表检索需求来说,apply+any(axis=1)已经足够清晰。
4. 完整实战案例
4.1 创建项目结构
先建立一个工作目录,例如multi_keyword_filter/,目录下放一个 Python 脚本文件filter_demo.py。后续所有代码都可以写在这个脚本里,也可以直接在 Jupyter Notebook 中逐段运行。
multi_keyword_filter/ └── filter_demo.py4.2 案例一:单列多关键词筛选
先实现最简单、最常用的需求:只针对“反馈内容”一列,筛选出包含“退款”“投诉”“差评”任意一个关键词的行。
完整代码如下:
import pandas as pd import re data = { "单号": ["A001", "A002", "A003", "A004", "A005"], "客户": ["张三", "李四", "王五", "赵六", "孙七"], "反馈内容": [ "产品使用过程中遇到了闪退问题", "物流配送速度很快,服务态度好", "申请退款,希望尽快处理", "客服电话一直打不通,投诉", "收到货后发现有破损,差评" ], "处理人": ["小明", "小红", "小刚", "小丽", "小华"] } df = pd.DataFrame(data) keywords = ["退款", "投诉", "差评"] pattern = "|".join(re.escape(k) for k in keywords) result = df[df["反馈内容"].str.contains(pattern, na=False)] print(result)运行结果:
单号 客户 反馈内容 处理人 2 A003 王五 申请退款,希望尽快处理 小刚 3 A004 赵六 客服电话一直打不通,投诉 小丽 4 A005 孙七 收到货后发现有破损,差评 小华这里有两个细节需要说明:
第一,re.escape(k)对关键词做转义。虽然“退款”“投诉”“差评”本身没有特殊字符,但养成转义的习惯可以避免以后关键词变化时踩正则的坑。
第二,na=False处理空值。如果“反馈内容”列存在 NaN,不写这个参数程序会直接报错。
4.3 案例二:全表多关键词筛选
现在把需求升级:不限定任何一列,全表只要出现“退款”“投诉”“差评”“打不通”任意一个关键词,就保留该行。
假设有一张新的业务表,包含“客户姓名”“联系备注”“对话内容”“分配客服”四列:
import pandas as pd import re business_data = { "客户姓名": ["张三", "李四", "王五", "赵六", "孙七", "周八"], "联系备注": ["老客户", "新客户", "VIP", "投诉重点客户", "普通客户", "老客户"], "对话内容": [ "询问产品价格", "反馈物流慢", "要求更换配送地址", "电话投诉客服态度", "咨询优惠活动", "申请退款" ], "分配客服": ["小明", "小红", "小明", "小丽", "小红", "小华"] } df = pd.DataFrame(business_data) print(df)原始数据:
客户姓名 联系备注 对话内容 分配客服 0 张三 老客户 询问产品价格 小明 1 李四 新客户 反馈物流慢 小红 2 王五 VIP 要求更换配送地址 小明 3 赵六 投诉重点客户 电话投诉客服态度 小丽 4 孙七 普通客户 咨询优惠活动 小红 5 周八 老客户 申请退款 小华现在用全表检索方式过滤:
keywords = ["退款", "投诉", "打不通", "更换"] pattern = "|".join(re.escape(k) for k in keywords) mask = df.apply( lambda col: col.astype(str).str.contains(pattern, na=False) ).any(axis=1) result = df[mask] print(result)运行结果:
客户姓名 联系备注 对话内容 分配客服 2 王五 VIP 要求更换配送地址 小明 3 赵六 投诉重点客户 电话投诉客服态度 小丽 5 周八 老客户 申请退款 小华逐行分析一下这些命中的逻辑:
- 第 2 行“王五”,关键词“更换”命中了“要求更换配送地址”。
- 第 3 行“赵六”,关键词“投诉”在“投诉重点客户”和“电话投诉客服态度”两个单元格中都有出现,属于多重命中。
- 第 5 行“周八”,关键词“退款”命中了“申请退款”。
“打不通”在全表中没有出现,因此不影响结果。
这里的col.astype(str)非常关键。它把每一列都先转换成字符串,目的是防止某些列本身就是数字类型。如果不做转换,数字列调用str.contains虽然不会报错,但行为可能和预期不一致。统一转换后,所有列都以字符串方式参与匹配,逻辑一致。
4.4 案例三:关键词来源于列表文件
真实项目中,关键词往往需要经常调整。把关键词维护在 Python 列表里可读性好,但需要改代码。更规范的方案是把关键词放在独立的配置文件中,程序启动时读入。
这里给出一个把关键词放在文本文件里的示例。
先创建keywords.txt,每行一个关键词:
退款 投诉 差评 打不通读取并拼接模式的代码:
import pandas as pd import re def load_keywords(path): with open(path, "r", encoding="utf-8") as f: keywords = [line.strip() for line in f if line.strip()] return keywords keywords = load_keywords("keywords.txt") pattern = "|".join(re.escape(k) for k in keywords) print("关键词列表:", keywords) print("正则模式:", pattern)输出:
关键词列表: ['退款', '投诉', '差评', '打不通'] 正则模式: 退款|投诉|差评|打不通后续筛选逻辑不变,只是关键词不再硬编码在脚本中,维护人员只需要编辑keywords.txt即可。
这种方式的工程价值在于:筛选条件和代码逻辑完全解耦。业务人员改关键词不需要碰代码,也不会因为改错缩进或引号导致程序崩溃。
4.5 案例四:所有关键词必须同时出现
前面讲的需求都是“或”关系:任意一个关键词命中即保留。还有一些场景要求“与”关系:多个关键词必须同时出现在同一行中。
比如:筛选“对话内容”中同时包含“退款”和“希望”的行。
实现方式是把多个条件用&连接:
condition_1 = df["对话内容"].str.contains("退款", na=False) condition_2 = df["对话内容"].str.contains("希望", na=False) result = df[condition_1 & condition_2] print(result)如果关键词数量不固定,可以用循环动态拼接:
from functools import reduce keywords_and = ["退款", "希望"] conditions = [ df["对话内容"].str.contains(k, na=False) for k in keywords_and ] mask = reduce(lambda a, b: a & b, conditions) result = df[mask] print(result)运行结果为空是正常的,因为原始数据里没有同时包含“退款”和“希望”的记录。你可以往数据里加一行测试数据再验证。
这里强调一点:&是位运算符,用于 pandas 布尔 Series 的逻辑与;Python 里的and不能直接作用于 pandas Series,这是新手经常犯的错误。
4.6 案例五:全表所有列同时满足多个关键词
“全表筛选”和“与关系”可以结合:要求整行的任意列中既出现关键词 A,又出现关键词 B。比如筛选出“联系备注”或“对话内容”或“分配客服”中同时含“投诉”和“客户”的行。
实现方式:
import pandas as pd import re df = pd.DataFrame(business_data) keyword_a = "投诉" keyword_b = "客户" mask_a = df.apply( lambda col: col.astype(str).str.contains(keyword_a, na=False) ).any(axis=1) mask_b = df.apply( lambda col: col.astype(str).str.contains(keyword_b, na=False) ).any(axis=1) result = df[mask_a & mask_b] print(result)运行结果:
客户姓名 联系备注 对话内容 分配客服 3 赵六 投诉重点客户 电话投诉客服态度 小丽这一行中,关键词 A“投诉”出现在“联系备注”和“对话内容”中,关键词 B“客户”出现在“联系备注”和“客户姓名”中,两个条件均满足,所以被保留。
这种写法的意义在于:它把“全表范围”和“多关键词组合”两个维度解耦开来。每个关键词先在全表范围内生成一个布尔掩码,最后用&组合所有关键词掩码。无论网页端也好,后端也好,只需要维护一个关键词掩码列表,逻辑可以复用。
4.7 封装成通用函数
上面的案例已经覆盖了主要场景。为了以后使用方便,可以把最常用的“全表多关键词筛选”封装成一个函数。
import pandas as pd import re from typing import List, Optional def filter_rows_by_keywords( df: pd.DataFrame, keywords: List[str], match: str = "any", columns: Optional[List[str]] = None ) -> pd.DataFrame: """ 全表或指定列多关键词筛选。 参数说明: df: 输入 DataFrame keywords: 关键词列表 match: 'any' 表示任意关键词命中即保留,'all' 表示所有关键词都必须命中 columns: 指定参与筛选的列,默认 None 表示全表检索 """ if columns is None: columns = df.columns.tolist() df_subset = df[columns] pattern_list = [re.escape(k) for k in keywords] masks = [] for pat in pattern_list: col_mask = df_subset.apply( lambda col: col.astype(str).str.contains(pat, na=False) ).any(axis=1) masks.append(col_mask) if match == "any": combined_mask = masks[0] for m in masks[1:]: combined_mask = combined_mask | m else: combined_mask = masks[0] for m in masks[1:]: combined_mask = combined_mask & m return df[combined_mask]使用示例:
keywords = ["退款", "投诉", "差评"] result = filter_rows_by_keywords(df, keywords, match="any") print(result)这个函数可以应对三种常见需求:
match="any"且columns=None:全表任意关键词命中。match="all"且columns=None:全表所有关键词都命中。- 指定
columns:只在某些列中检索。
函数参数化之后,后续业务只需要调用函数,传入不同的关键词和匹配模式即可。
4.8 运行与验证
把 4.7 的函数和 4.3 的数据放进同一个脚本中运行,预期输出如下:
客户姓名 联系备注 对话内容 分配客服 2 王五 VIP 要求更换配送地址 小明 3 赵六 投诉重点客户 电话投诉客服态度 小丽 5 周八 老客户 申请退款 小华如果输出与预期不一致,优先检查关键词是否拼写正确、文本文件编码是否为 UTF-8、以及na=False是否添加。
5. 常见问题与排查思路
以下问题都是实际使用中高频出现的,整理成表格方便检索:
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
re.error: missing ), unterminated subpattern | 关键词包含括号、星号等正则特殊字符,未转义 | 对关键词使用re.escape再拼接 |
| 筛选结果为空 | 关键词大小写不匹配 | 设置case=False或使用flags=re.IGNORECASE |
报错ValueError: cannot mask with array containing NA / NaN values | 某一列存在空值,na未设置 | 在str.contains中指定na=False |
关键词本身包含竖线|,筛选结果异常 | 竖线被识别为正则“或”语法 | 使用re.escape转义关键词 |
| 数字列筛选不到内容 | 数字列未转为字符串 | 使用astype(str)后再匹配 |
| 全表筛选时只返回部分列匹配的结果 | any(axis=1)用法错误,取了axis=0 | 确认any的 axis 参数,沿行方向应为axis=1 |
| 数据量大时筛选很慢 | 正则模式复杂或逐行循环处理 | 优先使用向量化str.contains,避免 Python 原生 for 循环 |
代码中使用and连接两个 boolean Series 报错 | pandas 的布尔 Series 不支持and/or | 使用&和|,并注意加括号 |
下面单独展开两个高频问题。
5.1 str.contains 报错如何处理
错误场景:
df = pd.DataFrame({"备注": ["正常", None, "异常"]}) df[df["备注"].str.contains("异常")]运行后会直接抛错,因为 None 值导致了 NaN。解决方案是加na=False:
df[df["备注"].str.contains("异常", na=False)]建议在写筛选逻辑时无条件加上na=False,这是一个成本极低但收益很高的习惯。
5.2 正则特殊字符导致匹配失败
假设关键词里有C++,不转义时+会被解析为“前面的字符重复一次或多次”,效果完全不符合预期。正确做法:
import re keywords = ["C++"] pattern = "|".join(re.escape(k) for k in keywords) print(pattern)输出:
C\+\+加上转义之后,str.contains(pattern)才能正确匹配字面意义上的C++。
6. 最佳实践与工程建议
6.1 关键词统一做转义
无论是单列筛选还是全表筛选,建议所有关键词在拼接正则模式之前统一经过re.escape处理。这样可以避免因关键词变化引入的正则语法冲突,尤其是当关键词来自业务人员维护的配置文件时,转义能显著减少线上问题。
import re keywords = ["v1.0", "C++", "退款(部分)"] pattern = "|".join(re.escape(k) for k in keywords)6.2 关键词独立维护
不要把关键词硬编码在核心筛选逻辑中。推荐放在独立的.txt或.yaml、.json配置文件中,程序启动时加载。
以 JSON 配置为例:
{ "positive_keywords": ["退款", "投诉", "差评"], "negative_keywords": ["测试", "模拟"] }后续可以扩展出“正向关键词”“负向关键词”组合过滤的逻辑,比如保留包含正向关键词、但不包含负向关键词的行。这种设计在运营数据清洗任务中非常实用。
6.3 大数据量下避免 for 循环
很多人第一次看到“多关键词”会本能地写 for 循环,一个词一个词地筛选再合并结果。在小数据集上没问题,但数据量大时效率很低。pandas 的向量化str.contains底层是 C 实现的,性能远优于逐行 Python 循环。
如果数据量达到百万行级别,还可以先用str.contains做一次粗筛,再用更精确的逻辑处理,尽量减少正则引擎的处理范围。
6.4 使用日志记录筛选条件
在一个长期运行的脚本或服务中,建议把筛选使用的关键词列表、匹配模式、命中行数记录下来。这样即使筛选结果异常,也可以快速定位是关键词写错、数据源变化,还是逻辑调整导致的问题。
import logging logging.basicConfig(level=logging.INFO) logging.info("筛选关键词:%s", keywords) logging.info("匹配模式:%s", pattern) logging.info("命中行数:%d", len(result))6.5 结果导出时注意编码
筛选结果如果需要保存为 CSV 并用 Excel 打开,推荐使用utf-8-sig编码,避免中文乱码:
result.to_csv("filter_result.csv", index=False, encoding="utf-8-sig")6.6 区分业务语义:包含还是模糊匹配
str.contains默认做的是“子串包含”判断。如果业务要求更复杂的匹配,比如忽略标点符号、拼音匹配、同义词匹配、分词匹配,那str.contains就不够用了,需要考虑引入分词工具或正则的加强匹配。
一句话建议:简单需求用str.contains快速解决;复杂需求要先明确匹配口径,再选择技术方案,不要从一开始就陷入复杂的正则表达式。
7. 总结与下一步学习方向
本文围绕“全表同时筛选多个关键词”这一需求,把 pandas 的str.contains、正则表达式的|或运算、re.escape特殊字符转义、apply全表遍历、布尔掩码组合等技术点完整串通了一遍。
从实际用途来看,读完这篇文章后你应该至少掌握以下能力:
- 针对单列做多关键词“或”筛选。
- 针对全表做多关键词“或”筛选。
- 针对全表做多关键词“与”筛选。
- 把关键词从代码中抽离出来,放到配置文件维护。
- 封装自己的多关键词筛选函数,方便项目复用。
- 遇到空值、大小写、特殊字符导致的筛选异常时,能够独立排查并解决。
如果之后实际项目中还需要更复杂的文本匹配能力,可以继续了解这几个方向:
df.query()的字符串表达式用法,适合写简单查询但不如布尔掩码灵活。- pandas 正则的
flags参数,比如re.IGNORECASE做忽略大小写匹配。 - 中文分词工具,例如 jieba,配合关键词表做更细粒度的文本匹配。
- pyqt 或 streamlit 等工具,把筛选能力做成一个小型可视化工具,非技术同事也能使用。
无论以后表格规模多大、关键词数量多少,解决问题的核心思路是不变的:把多个关键词构造成一个正则模式,用 pandas 的向量化方法一次性匹配全表数据。掌握了这个思路,下次再遇到同类需求,你就可以直接把方案拿出来用,不用再手动一列一列筛选了。