Python技术迷

比正则快M倍以上!Python替换字符串的新姿势

哎我跟你说,我昨晚在公司楼下那儿喝着便利店买的那罐冰美式…脑子突然被一个需求给点炸了,你们不是老问我那个“Python 字符串替换怎么搞得更快”嘛,对吧?我之前一直都拿正则糊弄事儿,懒得想别的,但那天我在调一个日志清洗脚本的时候,正则慢得跟蜗牛一样,我那会儿眼皮都开始抖了,真的。

结果我突然想起小李前几天在茶水间说了句啥“你别老用 re.sub,你看看那个 replace,多快啊”,当时我还不信,昨晚一测,卧槽,还真是快得离谱…快了不止一两倍,是那种你能明显看到脚本跑飞的那种。

我这不是边等外卖边琢磨,就随便写了个小 demo(代码我给你们贴一下,你们看看就懂了):

import re
import time

text = "abc123xyz" * 50000

# 正则
t1 = time.time()
for _ in range(1000):
    re.sub(r"\d+", "NUM", text)
print("regex:", time.time() - t1)

# replace
t2 = time.time()
for _ in range(1000):
    text.replace("123", "NUM")
print("replace:", time.time() - t2)

我当时在那儿的反应就是“靠,这差距也太大了吧”。主要你们要搞懂一点:replace 是 C 实现的纯字符串替换,而 re.sub 每次都要走一次正则引擎,要编译、要匹配、要回溯…你知道正则那玩意儿有时候像抠脚一样慢,对吧?

后来我吃着外卖鸡腿又继续想,实际业务里我们这种替换场景特别多,比如:

  • 日志清洗把手机号替换成星号
  • 批量替换 URL 参数
  • 文本里过滤敏感词
  • 模板渲染里的简单占位符替换

这些大多数都是“确定的字符串 → 确定的字符串”,不需要花里胡哨的表达式,结果我们一堆人偏要用正则,怪不得脚本慢得跟死机似的。

我给你看个真实一点的例子哈,就是我之前改的那个小工具,原始版本这样写:

clean = re.sub(r"token=[a-zA-Z0-9]+", "token=***", line)

我后来直接换成:

clean = line.replace("token=", "token=***")

顺便把后面那串字符切一下:

if"token="in line:
    prefix, _, suffix = line.partition("token=")
    clean = prefix + "token=***"

这玩意儿比我原来那个正则快得不是一点点,是成倍往上翻的那种,我估计能有 M 倍以上(反正我当时测出来比正则快 6~20 倍不等),我都懒得精确算了。

不过你们别光觉得 replace 香,用的时候也得注意一些坑,不然又开始问我“为啥替换不对啊东哥”。

像下面这种你们肯定踩过:

text = "abcabcabc"
print(text.replace("ab", "abX"))

你以为它会跳着替换,其实不会,它是从左往右来一刀一刀砍的:

输出:

abXcabXcabXc

我当时被这玩意儿弄晕过一次,你们要是要跳着替换,自己写逻辑,不要怪 replace。

又比如有人问那可不可以一次替多个?当然可以,就是写得稍微难看点,比如链式:

text = text.replace("foo", "A").replace("bar", "B")

要是你替换特别多,那你最好弄个表,用循环:

mapping = {
"foo": "A",
"bar": "B",
"xyz": "C"
}

for k, v in mapping.items():
    text = text.replace(k, v)

这比正则的多模式匹配还是快一截的。

我前几天还试了个更牛逼的写法,就是用 str.translate,尤其做大量单字符替换的时候,高得吓人:

table = str.maketrans({"a": "A", "b": "B"})
print("abcabc".translate(table))

这个写起来看着怪怪的,但性能是真猛。我那天在工位上试着把一个 5MB 的文本跑了几轮,用 translate 大概比 replace 还要快一倍多一点,当然具体场景看情况。

不过说到这里,我突然想到…哎等一下我电话响了,我回一下… …… 好了继续继续,我刚说到哪儿了?哦对,性能问题。

你们要明白一点:性能差距最大的不是单次替换,而是“循环 + 正则”这种死亡组合。就像我那天那个日志清洗脚本,里面 10 万行日志,每行都 re.sub 一次,那正则引擎不累死才怪。

优化的方法其实很简单,就是:

能不用正则,就不用正则

比如你要替换所有数字成 X,你可能写:

re.sub(r"\d", "X", text)

其实你完全可以:

import string

table = str.maketrans({c: "X"for c in string.digits})
clean = text.translate(table)

那速度真的是飞起,我测过,比正则快十几倍。

你要是非得用正则,比如那种复杂匹配,多条件、多分组、多行模式的,那当然还是乖乖用 re.sub,只不过你提前把 pattern 编译一下,能快不少:

pattern = re.compile(r"\d+")
pattern.sub("NUM", text)

反正你们千万别再像我一样,平时嫌麻烦就直接 re.sub 上去,结果把机器 CPU 打到 90%,最后还被运维问是不是脚本死循环…

行了,我现在咖啡也喝完了,午饭也该点了,等会儿我还得改个接口,不扯了。

你要实在用不明白某种替换场景,你扔我一句就行,我给你看看用不用正则、用哪种最快。

-END-

我为大家打造了一份RPA教程,完全免费:songshuhezi.com/rpa.html

🔥虎哥私藏精品🔥

虎哥作为一名老码农,整理了全网最全《python高级架构师资料合集》,总量高达650GB