现实数据里字符串无处不在:地址、姓名、商品描述、日志……本篇文章系统讲解 pandas 的文本处理武器库:str 访问器、大小写与去空格、拆分与拼接、查找替换、正则表达式、提取结构化信息。
目录
1. str 访问器是什么
2. 大小写与空白处理
3. 包含与匹配判断
4. 拆分与拼接
5. 查找、替换与删除
6. 正则表达式提取
7. 类型转换与长度统计
8. 处理 object 与 string 类型的差异
9. 文本清洗实战案例
10. 常见坑与注意事项
11. 本章小结与练习
1. str 访问器是什么
pandas 通过 `.str` 访问器把 Python 字符串方法"向量化":对 Series 的每个元素应用同一个字符串操作。
import pandas as pd
s = pd.Series([" Hello ", "WORLD", "pandas", "Data Science"])
print(s.str.lower())
# 0 hello
# 1 world
# 2 pandas
# 3 data science
注意:.str 只对字符串类型的 Series有效(object 或 string 类型)。对数值 Series 调用会报错。# 报错示例
# pd.Series([1, 2, 3]).str.lower()
# AttributeError: Can only use .str accessor with string values!
2. 大小写与空白处理
s = pd.Series([" Apple ", "banana", "CHERRY", None])
print(s.str.lower()) # 全部小写
print(s.str.upper()) # 全部大写
print(s.str.title()) # 每个单词首字母大写
print(s.str.capitalize()) # 首字母大写其余小写
print(s.str.strip()) # 去除两端空白(strip/lstrip/rstrip)
print(s.str.swapcase()) # 大小写互换
print(s.str.casefold()) # 更强的小写(处理特殊字符)
strip()系列是清洗数据的头号工具:" 北京 "→"北京"。
# 缺失值在 str 方法中保持 NaN,不报错
print(s.str.strip())
# 0 Apple
# 1 banana
# 2 CHERRY
# 3 NaN
3. 包含与匹配判断
返回布尔 Series,可用于筛选。
s = pd.Series(["apple pie", "banana split", "cherry cake", "apple juice"])
# 是否包含子串
print(s.str.contains("apple"))
# 0 True
# 1 False
# 2 False
# 3 True
# 是否以某开头 / 结尾
print(s.str.startswith("a"))
print(s.str.endswith("cake"))
# 是否匹配正则
print(s.str.contains("^(apple|cherry)"))
# 0 True
# 1 False
# 2 True
# 3 True
# 精确匹配
print(s.str.match("apple")) # 从开头匹配
print(s.str.fullmatch("apple")) # 完全一致
# 缺失值默认返回 False?不,默认返回 NaN(na 参数控制)
print(s.str.contains("apple", na=False))
| 方法 | 含义 |
|------|------|
| contains(pat, case=, regex=) | 是否包含(默认支持正则) |
| startswith(pat) | 是否以 pat 开头 |
| endswith(pat) | 是否以 pat 结尾 |
| match(pat) | 开头是否匹配正则 |
| fullmatch(pat) | 是否完全匹配 |
| isalnum/isalpha/isnumeric | 字符类型判断 |
# 筛选应用:找出含"apple"的行
df = pd.DataFrame({"food": ["apple pie", "banana", "cherry"], "price": [5, 3, 4]})
print(df[df["food"].str.contains("apple")])
contains的case=False可忽略大小写;regex=False则把模式当普通字符串。
4. 拆分与拼接
4.1 拆分:str.split
s = pd.Series(["张三-北京", "李四-上海", "王五-广州"])
# 拆成列表
print(s.str.split("-"))
# 0 [张三, 北京]
# 1 [李四, 上海]
# 2 [王五, 广州]
# 直接展开成多列
print(s.str.split("-", expand=True))
# 0 1
# 0 张三 北京
# 1 李四 上海
# 2 王五 广州
# 指定列名
df_split = s.str.split("-", expand=True)
df_split.columns = ["姓名", "城市"]
# 只取第一部分
print(s.str.split("-").str[0])
# 限制拆分数
print("a-b-c".split("-")) # ['a', 'b', 'c']
print(pd.Series(["a-b-c"]).str.split("-", n=1, expand=True))
# 0 a b-c
4.2 按位置切分:str.slice / str[]
s = pd.Series(["abcdef", "uvwxyz"])
print(s.str[0]) # 第一个字符
print(s.str[1:4]) # 切片:bcd / vwx
print(s.str.slice(2, 5)) # 等价写法
print(s.str.slice(0, 4, 2)) # 带步长:ac / uw
4.3 拼接:str.cat
s1 = pd.Series(["张", "李", "王"])
s2 = pd.Series(["三", "四", "五"])
# 元素级拼接
print(s1.str.cat(s2))
# 0 张三
# 1 李四
# 2 王五
# 加分隔符
print(s1.str.cat(s2, sep="-"))
# 0 张-三
# 整列拼成一个字符串
print(s1.str.cat(sep="、"))
# 张、李、王
# 与缺失值:na_rep 处理
s3 = pd.Series(["a", None, "c"])
print(s1.str.cat(s3, na_rep="?"))
# 0 a? <- 不对,这里是逐元素:0->张+a? 需要注意
拼接多列更常用的写法:df["姓名"] = df["姓"] + df["名"]或df["姓名"] = df["姓"].astype(str) + "-" + df["名"]。
df = pd.DataFrame({"姓": ["张", "李"], "名": ["三", "四"]})
df["全名"] = df["姓"] + df["名"] # 直接 +
df["带分隔"] = df["姓"] + "-" + df["名"]
4.4 重复与填充
s = pd.Series(["ab", "cd"])
print(s.str.repeat(2)) # abab cdcd
print(s.str.pad(width=5, side="left", fillchar="0")) # 左侧补 0 到 5 位
print(s.str.zfill(5)) # 数字补零:000ab
5. 查找、替换与删除
5.1 查找
s = pd.Series(["apple123", "banana456", "cherry"])
# 子串位置
print(s.str.find("a")) # 第一次出现位置,找不到 -1
print(s.str.rfind("a")) # 最后一次出现位置
# 提取匹配内容
print(s.str.extract(r"(\d+)")) # 提取数字(正则,见下节)
5.2 替换:str.replace
s = pd.Series(["北京-朝阳", "上海-浦东", "广州-天河"])
# 普通替换
print(s.str.replace("-", "市"))
# 0 北京市朝阳
# 1 上海市浦东
# 2 广州市天河
# 正则替换(默认支持正则)
print(s.str.replace(r"-\w+", "")) # 删除 -后面内容
# 0 北京
# 1 上海
# 2 广州
# 忽略大小写
print(pd.Series(["Apple", "apple"]).str.replace("apple", "fruit", case=False))
# 正则替换函数
s2 = pd.Series(["a1b2", "c3d4"])
print(s2.str.replace(r"(\d)", lambda m: f"[{m.group(1)}]"))
# 0 a[1]b[2]
# 1 c[3]d[4]
5.3 删除字符:translate / removeprefix
s = pd.Series(["ab12cd", "xy78zw"])
# 删除数字
print(s.str.replace(r"\d", "", regex=True))
# 0 abcd
# 1 xyzw
# 删除指定字符集
print(s.str.translate(str.maketrans("", "", "0123456789")))
# 去掉前缀 / 后缀
print(pd.Series(["cat_1", "cat_2"]).str.removeprefix("cat_"))
print(pd.Series(["a.txt", "b.txt"]).str.removesuffix(".txt"))
6. 正则表达式提取
正则(regex)是文本处理的终极武器。pandas 的 .str.extract 和 .str.extractall 能直接从字符串中提取结构化字段。
6.1 提取单个字段
s = pd.Series(["订单A100-500元", "订单B200-800元", "无效记录"])
# 提取金额
print(s.str.extract(r"(\d+)元"))
# 0
# 0 500
# 1 800
# 2 NaN
# 提取多个字段(每个分组一列)
df_ext = s.str.extract(r"订单([A-Z])(\d+)-(\d+)元")
df_ext.columns = ["类别", "编号", "金额"]
print(df_ext)
# 类别 编号 金额
# 0 A 100 500
# 1 B 200 800
# 2 NaN NaN NaN
6.2 提取所有匹配(extractall)
s = pd.Series(["a=1;b=2", "c=3"])
print(s.str.extractall(r"(\w)=(\d)"))
# 0 1
# match
# 0 0 a 1
# 1 b 2
# 1 0 c 3
6.3 常用正则速查
| 模式 | 含义 | 示例 |
|------|------|------|
| \d | 数字 | \d+ 连续数字 |
| \w | 字母数字下划线 | \w+ 单词 |
| \s | 空白 | \s+ 连续空白 |
| . | 任意字符 | .* 任意内容 |
| ^ / $ | 开头 / 结尾 | ^北京 |
| [abc] | 字符集合 | [0-9] 数字 |
| (a\|b) | 或 | (苹果\|香蕉) |
| {n,m} | 重复次数 | \d{4} 四位数字 |
6.4 正则筛选
s = pd.Series(["13800138000", "021-12345678", "10086"])
# 手机号(1 开头 11 位)
print(s.str.contains(r"^1\d{10}$"))
# 0 True
# 1 False
# 2 False
# 带区号电话
print(s[s.str.contains(r"^\d{3}-\d{8}$")])
7. 类型转换与长度统计
s = pd.Series(["apple", "banana", "cherry"])
print(s.str.len()) # 每元素长度:5 6 6
print(s.str.upper()) # 已在前面学过
# 字符串与数值互转
s_num = pd.Series(["1", "2", "3"])
print(pd.to_numeric(s_num)) # 转数值(见第 8 章)
print(s_num.str.isdigit()) # 是否全是数字
8. 处理 object 与 string 类型的差异
# object 类型
s_obj = pd.Series(["a", "b", None])
# string 类型(推荐)
s_str = s_obj.astype("string")
# 差异 1:缺失值表示
print(s_obj.isna()) # True(内部是 np.nan)
print(s_str.isna()) # True(内部是 pd.NA)
# 差异 2:拼接行为
print(s_obj + "-x") # 会拼出 'nan-x'(坑!)
print(s_str + "-x") # 缺失保持 <NA>
# 差异 3:contains 的 na 处理
print(s_obj.str.contains("a", na=False)) # 需要 na=False 参数
print(s_str.str.contains("a")) # 直接得到 False?实际是 <NA>
建议:新代码优先用 astype("string"),避免 object 拼接 NaN 的经典坑。9. 文本清洗实战案例
import pandas as pd
# 模拟脏数据
raw = pd.DataFrame({
"姓名": [" 张三 ", "李四", " 王五 ", "赵六"],
"电话": ["138-0013-8000", "13911112222", "021-5566-7788", "137-5555-9999"],
"邮箱": ["Zhang@Example.com", "li.si@qq.com", "wangwu@163.com", "INVALID"],
"备注": ["VIP客户", "普通客户", "VIP客户,重点维护", "普通"],
})
def clean_text(df):
df = df.copy()
df["姓名"] = df["姓名"].str.strip() # 1. 去空白
df["电话"] = df["电话"].str.replace("-", "", regex=False) # 2. 去分隔符
df["邮箱"] = df["邮箱"].str.lower() # 3. 统一小写
df["是否VIP"] = df["备注"].str.contains("VIP", na=False) # 4. 提取标记
df["电话有效"] = df["电话"].str.fullmatch(r"1\d{10}") # 5. 校验格式
return df
print(clean_text(raw))
输出:
姓名 电话 邮箱 备注 是否VIP 电话有效
0 张三 13800138000 zhang@example.com VIP客户 True True
1 李四 13911112222 li.si@qq.com 普通客户 False True
2 王五 02155667788 wangwu@163.com VIP客户,重点维护 True False
3 赵六 13755559999 invalid 普通 False True
10. 常见坑与注意事项
| 坑 | 现象 | 解决办法 |
|----|------|----------|
| 对数值列用 .str | AttributeError | 先 astype(str) 或 astype("string") |
| 正则未转义 | . 匹配任意字符 | 需要字面量时用 \. 或 regex=False |
| replace 默认当正则 | "-" 没问题但 "." 出意外 | 明确 regex=False 做纯文本替换 |
| 拼接时 NaN | "nan-x" 出现 | 用 string 类型或 fillna("") |
| extract 找不到 | 返回 NaN 不是报错 | 先 str.contains 校验再提取 |
| 中文正则 | 某些写法不生效 | 确保 \w 覆盖中文或改用 [\\u4e00-\\u9fa5] |
11. 本章小结与练习
小结
.str访问器把字符串操作向量化,缺失值自动传递;- 清洗三件套:
strip()、lower()、replace(); - 拆分
split(expand=True)展开多列,+/cat拼接; - 正则:
contains(筛选)、extract(提取字段)、replace(批量替换); - 推荐使用
string类型替代object。
练习题
1. 对 [" Apple ", " banana ", "cherry"] 去空白并统一小写。
2. 用 split(expand=True) 把 "2025-01-01 08:30:00" 拆成日期和时间两列。
3. 从 "订单号: A100, 金额: 250.5元" 中提取订单号和金额。
4. 将一列电话中的 - 和空格全部删除。
5. 用正则筛选出所有合法的邮箱地址。
下一篇预告:第 10 章 数据变换:map / apply / applymap —— 掌握 pandas 逐元素、逐行、逐列变换的三大神器。
文章回复
0 条公开回复