姓名有四种脏写法,就得写四层嵌套替换。再来两种,就六层——这就是书里说的「太麻烦」。 正则换一个思路:不列举坏字符,而是描述「只要汉字」。一句字符类把四种写法全干掉。 但正则也不是万能的:它会顺手删掉少数民族名字里的间隔号,规则得再改一句。
上一篇是「删掉固定字符」,这一篇处理「一类字符」。两种活,两种工具。 书里说 R 能用正则表达式处理复杂模式。这里用 Python 的 re,语法几乎一样。
一、嵌套替换会爆炸
假设姓名有四种脏写法:「王^小明」「王 小明」「王 小明」(全角空格)「王-小明」。 用 replace 一个个删,就得写四层嵌套。再来两种写法,就是六层。 每多一种写法就多一层,而且顺序还可能互相影响——先删哪个后删哪个,结果可能不一样。
这种写法的成本不是「长」,是「每加一条规则都要重新想一遍顺序」。 现实里的脏数据永远比你列的规则多一种。所以这条路走到头一定是崩溃的。
二、正则换一个方向
正则的思路不是「删掉这几种字符」,而是「只保留汉字」。 一个字符类写出来:不是汉字的,全换成空。 ^、半角空格、全角空格、横杠,一次全没了——因为它们的共同点是「都不是汉字」。
这是两种完全不同的思路:一个是列举坏东西,一个是描述好东西。 列举永远列不全,描述才收敛。这是正则真正的价值所在。
三、但正则也会删错
上面那个「只保留汉字」的正则,会把少数民族名字里的间隔号(·)也删掉。 「买买提·艾力」变成「买买提艾力」——这就删错了。 因为 · 确实不是汉字。规则没错,是规则不够细。
改法很简单:保留汉字,另外保留 ·。一句话的事。 正则的力量在于「改一句话就能改规则」,代价是这句话必须写对。
四、用哪个:一句话能不能说清
要删的东西是固定几个字符,用 replace:一眼看懂,不会写错。 要删的东西是「一类字符」,用正则:写得短,但要先想清楚「保留什么」。 判断标准:规则能不能一句话说清。能,用正则;不能,用 replace 一条条列。
「只要汉字和间隔号」——一句话说清了,用正则。 「去掉井号、去掉 +86、去掉前后空格」——三件不同的事,用三个 replace。
五、两者不是对立的
真实项目里两个都会用:能一句话说清的用正则,剩下零碎的用 replace。 顺序也有讲究:先用正则把「一类字符」清掉,再用 replace 处理剩余的特例。 反过来做,特例被正则顺手删掉了,你就再也找不到它。
清洗的顺序本身就是一种判断,不是随手排的。
六、动手 Demo:嵌套公式与正则表达式
同一批脏姓名,先用四层嵌套 replace 洗一遍,再用一个正则洗一遍,比结果和代码长度。 再拿少数民族名字试一次,看正则会在哪儿删错。
import re
MESSY = ["王^小明", "李 伟", "张\u3000芳", "刘-娜", "陈强", "买买提·艾力"]
# 四层嵌套 replace
nest = []
for x in MESSY:
x = x.replace("^", "").replace(" ", "").replace("\u3000", "").replace("-", "")
nest.append(x)
# 只保留汉字
pat1 = re.compile("[^\u4e00-\u9fff]")
reg1 = [pat1.sub("", x) for x in MESSY]
# 保留汉字与间隔号
pat2 = re.compile("[^\u4e00-\u9fff\u00b7]")
reg2 = [pat2.sub("", x) for x in MESSY]
for a, b, c, d in zip(MESSY, nest, reg1, reg2):
print("%-12s | %-10s | %-10s | %s" % (a, b, c, d))
print()
print("嵌套版一行长度 %d 字符,正则版 %d 字符"
% (len("x.replace('^','').replace(' ','').replace('\u3000','').replace('-','')"),
len("re.sub('[^\u4e00-\u9fff\u00b7]', '', x)")))
实跑输出:
--------------------------------------------------------------------
嵌套公式与正则表达式
--------------------------------------------------------------------
① 嵌套替换会爆炸
--------------------------------------------------------------------
假设姓名有四种写法:「王^小明」「王 小明」「王 小明」(全角空格)「王-小明」。
用 replace 一个个删,就得写四层嵌套。再来两种写法,就得六层。
每多一种写法就多一层,而且顺序还可能互相影响。这就是书里说的「太麻烦了」。
② 嵌套版本真的跑一遍
--------------------------------------------------------------------
| 原始 | 四层替换后 |
|---|---|
| 王^小明 | 王小明 |
| 李 伟 | 李伟 |
| 张 芳 | 张芳 |
| 刘-娜 | 刘娜 |
| 陈强 | 陈强 |
五行都清干净了。但注意:这一行代码已经长得看不清先后顺序了。
③ 正则版本:一句话说清「只要汉字」
--------------------------------------------------------------------
| 原始 | 只保留汉字后 |
|---|---|
| 王^小明 | 王小明 |
| 李 伟 | 李伟 |
| 张 芳 | 张芳 |
| 刘-娜 | 刘娜 |
| 陈强 | 陈强 |
一个字符类就把 ^、半角空格、全角空格、横杠全干掉了——因为规则写的是「保留汉字」,
不是「删掉这四种字符」。
这是两种完全不同的思路:一个是列举坏东西,一个是描述好东西。
! 列举永远列不全,描述才收敛。
④ 正则也不是万能的
--------------------------------------------------------------------
上面这个正则会把少数民族名字里的间隔号(·)也删掉,因为 · 不是汉字。
「买买提·艾力」会变成「买买提艾力」——这就删错了。
所以真正的规则是:保留汉字,另外保留 ·。正则改一下就行:汉字或 ·。
用改过的正则处理「买买提·艾力」:买买提·艾力。
正则的力量在于「改一句话就能改规则」,代价是这句话必须写对。
⑤ 用哪个
--------------------------------------------------------------------
要删的东西是固定几个字符,用 replace:一眼看懂,不会写错。
要删的东西是「一类字符」,用正则:写得短,但要先想清楚「保留什么」。
判断标准:规则能不能一句话说清。能,用正则;不能,用 replace 一条条列。
第三列和第四列只在最后一行不同:只保留汉字会把「买买提·艾力」变成「买买提艾力」,加一个 · 就对了。 两列一比就看出来:不是正则更好,是「描述要保留什么」这个思路更好。 列举式的思路永远差最后一种写法。
30 秒实验
- 如果要保留「欧阳」这种复姓中间的连接符(如果有的话),正则该怎么改?
- 把「只保留汉字」用在地址字段上,会删掉哪些有用的东西?
- 想一想:为什么「列举坏东西」这条路注定走不到头?
常见误区
- 无限嵌套 replace:每加一种写法加一层,最后没人看得懂顺序。
- 正则写完不测边界:少数民族名字、带生僻字的名字,都是最容易翻车的地方。
- 以为正则一定更好:三个固定字符的活,replace 更清楚也更快。
- 清洗顺序随意排:先用宽规则再用窄规则,特例会被宽规则顺手吃掉。
自测三问(点开对答案)
1. 嵌套 replace 的根本问题是什么? —— 写法是列举式的:每多一种脏写法就多一层,而且层与层之间的顺序会互相影响。 2. 正则的思路和 replace 有什么不同? —— replace 列举「要删什么」,正则描述「要留什么」。描述能收敛,列举不能。 3. 什么时候该用 replace 而不是正则? —— 要处理的是几个固定字符、规则一句话说不清的时候。replace 更直白,也不容易写错。工具选好了,还剩一件麻烦事:汉语姓名没有分隔符,怎么拆姓和名?
- ☐ (本篇完,下一篇 13.6 sub 指令与交货)