你接手了一份客户数据,名字前后有空格,手机号格式五花八门(有的带
+86-,有的只有10位),邮箱有的缺@有的缺.com,城市字段混入了" 广州 "这样的脏数据。如果靠Excel一行行手动改,今天就可以准备下班后通宵了。学会.str访问器和正则表达式,500行数据只需5行代码。
文中涉及到的数据文件可以联系我发给你,因为我还不知道怎么在公众号插入文件
Pandas 的 .str 访问器让你能像处理单个字符串一样处理整列字符串。它本质上是把Python的字符串方法"广播"到Series的每个元素。
import pandas as pd
df = pd.read_csv('data_python/customer_info.csv')
# .str不是属性,是访问器——只有object或string类型的列才能用
print(df['full_name'].dtype) # object → 可以用 .str
print(df['customer_id'].dtype) # int64 → 不能用 .str核心思想:Python中字符串有 .strip()、.lower()、.replace() 等方法,Pandas中对应的就是 .str.strip()、.str.lower()、.str.replace()。前面加个 .str 就行。
注意:
.str访问器遇到NaN会自动返回NaN,不会报错。这是它比Python原生字符串方法更"宽容"的地方。
最常见的脏数据问题:前后空格、大小写混乱、需要统一替换的内容。
# 去除首尾空格(最常用的清洗操作)
df['city_clean'] = df['city'].str.strip()
# 去除左侧或右侧空格
df['name_lstrip'] = df['full_name'].str.lstrip() # 只去左边
df['name_rstrip'] = df['full_name'].str.rstrip() # 只去右边
# 统一大小写
df['city_upper'] = df['city'].str.upper() # 全部大写
df['city_lower'] = df['city'].str.lower() # 全部小写
df['city_title'] = df['city'].str.title() # 首字母大写
# 替换字符串
df['clean_city'] = df['city'].str.strip().str.replace(' ', '')
# 将所有空字符串和'nan'替换为真正的NaN
import numpy as np
df['city'] = df['city'].str.strip().replace({'': np.nan, 'nan': np.nan})
# 链式调用:一行完成多个清洗步骤
df['clean_name'] = (
df['full_name']
.str.strip() # 去首尾空格
.str.replace(' ', ' ') # 合并多余空格
)实用技巧:
.str方法支持链式调用。你可以把多个清洗步骤像流水线一样串联起来,一气呵成。
很多时候我们需要判断字符串是否包含某个模式,这比完全匹配更灵活。
# contains:是否包含(最常用)
has_qq = df['email'].str.contains('qq', na=False)
qq_users = df[has_qq]
# 查找包含特定关键词的备注
vip_customers = df[df['notes'].str.contains('VIP', na=False)]
complaint_customers = df[df['notes'].str.contains('投诉', na=False)]
# startswith / endswith:判断开头和结尾
gmail_users = df[df['email'].str.endswith('@gmail.com', na=False)]
beijing_users = df[df['full_name'].str.startswith('客户_1', na=False)]
# match:从字符串开头匹配正则表达式
starts_with_number = df['full_name'].str.match(r'\d+', na=False)
# len:计算字符串长度
df['name_len'] = df['full_name'].str.len()
df['phone_len'] = df['phone'].str.len()
# 统计各内容出现次数
print(df['notes'].str.contains('投诉', na=False).sum(), '条投诉')
print(df['notes'].str.contains('VIP', na=False).sum(), '个VIP')contains 常用参数:
na=False | ||
case=False | ||
regex=True | str.contains(r'\d{11}') | |
regex=False |
一个字段里塞了多个信息时,用 str.split() 拆开。
# 基本拆分:把email按@拆成username和domain
split_result = df['email'].str.split('@', expand=True)
split_result.columns = ['username', 'domain']
df = pd.concat([df, split_result], axis=1)
# 没有@的email拆分后第二列为None,需要处理
df['domain'] = df['domain'].fillna('unknown')
# 拆分城市地址(如果有省市区信息)
# 例如:"北京-朝阳-三里屯"
addr_parts = df['full_name'].str.split('_', expand=True)
print(addr_parts.head())
# 取拆分后的第n个元素
df['first_part'] = df['full_name'].str.split('_').str[0]
# 限制拆分次数(只拆第一次出现的位置)
df[['provider', 'domain_part']] = df['email'].str.split('@', n=1, expand=True)关键参数:
expand=True让拆分结果变成DataFrame(多列),expand=False(默认)返回的是Series,每个元素是一个列表。
str.extract() 是正则表达式的入门利器。你只需要用括号 () 标记要提取的部分。
# 提取email中的用户名和域名(一步到位)
df[['email_user', 'email_domain']] = df['email'].str.extract(
r'([\w.]+)@([\w.]+)',
expand=True
)
# 提取手机号的前三位(运营商号段)
df['phone_prefix'] = df['phone'].str.extract(r'(\d{3})')
# extractall:提取所有匹配(不止第一个)
# 比如notes中可能提了多个关键词
all_keywords = df['notes'].str.extractall(r'(VIP|投诉|发票)')
print(all_keywords.head(10))extract vs extractall:
extract():只取第一个匹配,返回一行一个结果extractall():取所有匹配,返回多行(一个原始行可能对应多行结果)正则表达式(regex)看起来像天书,但掌握几个常用模式就能处理80%的场景。
\d | \d{11} | |
\w | \w+ | |
\s | \s+ | |
. | .+ | |
[abc] | [男女] | |
+ | \d+ | |
* | \s* | |
? | -? | |
{n,m} | \d{2,4} | |
^$ | ^1 | |
() | (\d{3})-(\d{4}) |
import pandas as pd
# 手机号(中国大陆):1开头 + 3-9的号段 + 9位数字
PHONE_PATTERN = r'1[3-9]\d{9}'
# 验证手机号是否合法
df['valid_phone'] = df['phone'].str.match(
PHONE_PATTERN, na=False
)
# 邮箱(宽松匹配)
EMAIL_PATTERN = r'[\w.+-]+@[\w-]+\.[\w.]+'
# 提取邮箱域名(@后面的部分)
df['email_domain'] = df['email'].str.extract(r'@(.+)$', expand=False)
# 身份证号(18位):17位数字 + 数字或X
ID_PATTERN = r'\d{17}[\dXx]'
# 日期:YYYY-MM-DD 或 YYYY/MM/DD
DATE_PATTERN = r'\d{4}[-/]\d{1,2}[-/]\d{1,2}'
# 中文姓名
CHINESE_NAME_PATTERN = r'[一-龥]{2,4}'
# 提取固定电话区号
LANDLINE_PATTERN = r'0\d{2,3}-\d{7,8}'# 从notes中找出所有提到的电话号码
df['phones_found'] = df['notes'].str.findall(PHONE_PATTERN)
# 从备注中提取所有数字
df['numbers_in_notes'] = df['notes'].str.findall(r'\d+')学习建议:不用背正则语法。记住
\d(数字)、\w(字母数字)、()分组 三个就够解决一大半问题了。遇到复杂需求再去查。
# cat:拼接字符串
df['full_info'] = df['full_name'].str.cat(df['city'], sep=' - ')
# 也可以拼接多列
df['contact'] = df['full_name'].str.cat(
[df['phone'].fillna('无电话'), df['email'].fillna('无邮箱')],
sep=' | '
)
# 条件拼接
df['label'] = '普通客户'
df.loc[df['notes'].str.contains('VIP', na=False), 'label'] = 'VIP客户'
df.loc[df['notes'].str.contains('投诉', na=False), 'label'] = '需关注客户'
# pad / zfill:填充到指定长度(如编号补零)
# 确保客户编号是6位
df['customer_id_str'] = df['customer_id'].astype(str).str.zfill(6)
# get_dummies:独热编码(把分类字符串变成0/1列)
dummies = df['city'].str.strip().str.get_dummies()
print(dummies.head())import pandas as pd
import numpy as np
df = pd.read_csv('data_python/customer_info.csv')
print(f"清洗前:{df.shape[0]} 行")
# ========== 第1步:清洗姓名和城市(去空格) ==========
df['full_name'] = df['full_name'].str.strip()
df['city'] = df['city'].str.strip().replace('', np.nan)
df['city'] = df['city'].replace('nan', np.nan)
# ========== 第2步:清洗手机号 ==========
# 去掉+86-前缀和空格
df['phone'] = df['phone'].str.replace('+86-', '', regex=False)
df['phone'] = df['phone'].str.replace('-', '', regex=False)
df['phone'] = df['phone'].str.replace(' ', '', regex=False)
# 只保留纯数字的手机号(去掉非法字符)
df['phone'] = df['phone'].str.extract(r'(\d+)', expand=False)
# 小贴士:变成NaN的说明原数据里根本没有数字
df['phone'] = df['phone'].replace('nan', np.nan)
# 验证手机号合法性(11位 + 1开头)
PHONE_PATTERN = r'^1[3-9]\d{9}$'
df['phone_valid'] = df['phone'].str.match(PHONE_PATTERN, na=False)
# ========== 第3步:清洗邮箱 ==========
# 先去空格
df['email'] = df['email'].str.strip()
# 提取用户名和域名
df[['email_username', 'email_domain']] = df['email'].str.extract(
r'([\w.]+)@([\w.]+)',
expand=True
)
# 验证邮箱(必须有@和域名)
df['email_valid'] = df['email'].str.match(
r'^[\w.+-]+@[\w-]+\.[\w.]+$', na=False
)
# ========== 第4步:日期转换 ==========
df['signup_date'] = pd.to_datetime(df['signup_date'], errors='coerce')
# ========== 第5步:汇总清洗结果 ==========
print(f"\n--- 清洗结果 ---")
print(f"city缺失:{df['city'].isna().sum()} 行")
print(f"phone有效:{df['phone_valid'].sum()} 行")
print(f"phone无效/缺失:{(~df['phone_valid']).sum()} 行")
print(f"email有效:{df['email_valid'].sum()} 行")
print(f"email无效/缺失:{(~df['email_valid']).sum()} 行")
# 看看清洗前后的对比
print("\n清洗前后对比(前5行):")
comparison = df[['full_name', 'phone', 'phone_valid', 'email', 'email_valid', 'city']]
print(comparison.head()).str.strip() | ||
.str.lower().str.upper() | ||
.str.replace() | ||
.str.contains() | ||
.str.startswith().str.endswith() | ||
.str.split(expand=True) | ||
.str.extract(r'...') | ||
.str.len() | ||
.str.cat() |
下一篇我们将学数据筛选、过滤与排序——用布尔索引精准锁定目标行,.query() 像SQL一样写筛选条件,再按任意列自由排序,把数据"拿捏"得恰如其分。
#字符串处理 #正则表达式 #文本清洗 #Pandas #str访问器 #数据清洗
觉得有用?点赞、在看、转发给正在和Excel斗争的同事吧!