回《驳 <Python正则表达式,请不要再用re.compile了!!!>》

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: 回《驳 <Python正则表达式,请不要再用re.compile了!!!>》

知乎用户@Manjusaka在阅读了我的文章《Python正则表达式,请不要再用re.compile了!!!》以后,写了一篇驳文《驳 Python正则表达式,请不要再用re.compile了!!!》

今天,我在这里回应一下这篇驳文。首先标题里面,我用的是 ,意为回复,而不是继续驳斥@Manjusaka的文章。因为没有什么好驳斥的,他的观点没有什么问题。

首先说明,我自己在公司的代码里面,也会使用 re.compile。但是,我现在仍然坚持我的观点,让看这篇文章的人,不要用 re.compile

你真的在意这点性能?

在公司里面,我使用 re.compile的场景是这样的:

每两小时从10亿条字符串中,筛选出所有不符合特定正则表达式的字符串。

这个程序可以简化为如下结构:

import re
regex_list = ['恭喜玩家:(.*?)获得', '欢迎(.*?)回家', '组队三缺一']
sentence_list = ['字符串1', '字符串2', ..., '字符串10亿']
useful_sentence = []
for sentence in sentence_list:
    for regex in regex_list:
        if re.search(regex, sentence):
            break
    else:
        useful_sentence.append(sentence)

在这个场景下面,对于10亿个字符串,3个正则表达式,需要循环30亿次。虽然读取正则表达式缓存的时间很短,假设只有1毫秒,那么也会浪费833小时。为了实现2小时内处理10亿条数据,我做了很多优化,其中之一就是提前 re.compile

import re
regex_list = ['恭喜玩家:(.*?)获得', '欢迎(.*?)回家', '组队三缺一']
sentence_list = ['字符串1', '字符串2', ..., '字符串10亿']
compiled_regex_list = [re.compile(x) for x in regex_list]
useful_sentence = []
for sentence in sentence_list:
    for regex in compiled_regex_list:
        if regex.search(sentence):
            break
    else:
        useful_sentence.append(sentence)

在这样的场景下,这样的数据量级下面,你是用 re.compile,当然可以。

然而,你日常接触到的工作,都是这个量级吗?知乎上流行一句话:

抛开剂量谈毒性,都是耍流氓。

同样的,在数据处理上也适用:

抛开量级谈性能差异,都是耍流氓

处理几百条数据,还需要担心读取缓存字典的这点小小的性能开销?

我在另一篇文章为什么Python 3.6以后字典有序并且效率更高?中提到,从Python 3.6开始,字典不会再提前申请更多空间了,同时也变得有序了,作为代价就是从字典读取值的过程多了一步。多出来的这一步实际上也会有性能开销,因为它需要先查询 indices,然后再查询 entries。为什么Python愿意放弃性能而要让字典有序?因为新的实现方式,在整体迭代、空间利用率上面都更高。

维护自文档性

回到正则表达式的例子来,Python区别于其他语言的一个非常重要的点是什么?是它的自文档性。

网上有这样一个段子:

问:如何把伪代码改写为Python代码?答:把.txt改成.py即可。

Python的自文档性非常好,即便完全不懂编程的人,看到Python的代码,也能猜的出代码想实现什么功能。

请大家对比下面两种写法:

  1. re.findall('密码: (.*?)$', sentence)

regex = re.compile('密码: (.*?)$')
regex.findall(sentence)

如果让一个完全不会编程的人来看,他看到第一段代码,会猜测:“findall是查找全部,这段代码可能是要从sentence找什么东西”。

而如果让他看第二段代码,他肯定会先问一句:“compile?编译?什么是编译?编写翻译吗?”

而对于刚刚学编程的人来说,如果他看的Python正则表达式入门的文档里面用了 re.compile,他也会很疑惑,为什么要 compile?编译成了什么东西?为什么不能直接查询?于是新人可能会过早去研究底层的东西。

但如果他看的文章直接是 re.findall,那么语义非常明确:正则表达式.查询所有,一目了然,轻轻松松就能理解并学会。

以官方文档的实例入门

当我们学习一门新的语言的时候,第一应该参考的就是它的官方文档。在正则表达式官方文档https://docs.python.org/3/library/re.html#finding-all-adverbs的例子中,无论是 search还是 findall都是使用 re.xxx的形式。如下图所示:

所以网上那些首先使用 pattern=re.compile,再 pattern.xxx的人,要不就是直接从其他语言把先 compile再查询的思维定势带到了Python中,要不就是做正则表达式调优做太久了,思维僵化了,一抬手就是 re.compile

面向接口编程还是面向人类编程?

在我文章的评论里面,有人说,应该面向接口编程,而不是面向实现编程。

对这些人,我想跟你们讲:你们对面向接口编程,理解得太狭隘了!

我们来看看,在Python著名的http库 requests出来之前,使用 urllib2发起一个请求是怎么写的:

import urllib2
gh_url = 'https://api.github.com'
req = urllib2.Request(gh_url)
password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
password_manager.add_password(None, gh_url, 'user', 'pass')
auth_manager = urllib2.HTTPBasicAuthHandler(password_manager)
opener = urllib2.build_opener(auth_manager)
urllib2.install_opener(opener)
handler = urllib2.urlopen(req)

有了 requests以后,实现同样的功能,我们是这样写的:

import requests
r = requests.get('https://api.github.com', auth=('user', 'pass'))

大家自己品位一下, req=urllib2.Request(gh_url)如果类比为 pattern=re.compile('xxxx')handler=urllib2.urlopen(req)类比为 pattern.findall(sentence) 那么, requests.get(xxx)就是 re.findall

为什么我们现在愿意使用 requests而不愿意使用 urllib2

因为 requestsfor human,而 urllibforinterface.

不是问题的问题

在评论里面,有人质疑我使用 re.findall,正则表达式不好维护?

@Manjusaka举出了下面这样的例子:

为什么使用 re.findall,就一定要把正则表达式复制粘贴很多遍?

我单独定义一个文件不行吗:

# regex_str.py
NAME_REGEX = 'name:(.*?),'
AGE_REGEX = 'age:(\d+)'
ADDRESS_REGEX = 'address:(.*?),'

然后我要使用正则表达式的地方直接导入进来:

import re
import regex_str
name = re.findall(regex_str.NAME_REGEX, sentence)
age = re.findall(regex_str.AGE_REGEX, sentence)

请问哪里不好维护了?

总结

我的观点如下:

  1. re.compile很重要,也有用。但是大多数时候你不需要使用它。
  2. 对于初学者,请直接使用 re.findallre.search,不要使用 re.compile
  3. 对于有经验的工程师,在开发项目的时候,请首先使用 re.findallre.search等等上层函数,直到你需要考虑优化正则表达式查询性能的时候,再考虑先 re.compile。因为很多时候,你的代码性能,还不至于需要靠几行 re.compile来提高。
  4. 有人问正则表达式默认缓存512条,这个数字没有写在文档里面,如果哪天改了怎么办?我的回答是:看看你写过的代码,涉及到的正则表达式有几次超过了100条?
  5. 正则表达式基于DFA,在它的原理上,compile这一步确实是必需的。但这并不意味着,在写代码的时候,我们一定要自己手动写compile. 毕竟封装、抽象才是高级语言的一大特征,直接。在其他编程语言里面,没有把compile和查询封装成一个整体接口,但是在Python里面这样做了。那么我们就应该用这个更上层的接口。而不是手动compile再查询。
  6. 为什么Java程序员常常加班,而Python程序员常常提前完成任务?正是因为这种Language Specific的特性提高了生产效率,屏蔽了前期不需要太早关心的实现细节。如果抱着写代码要语言无关,要通用而故意放弃了一些语言特性,那为什么不直接写1010?那才是真正的语言无关,所有语言都是建立于二进制的1010上的。
  7. 我的两篇文章的根本出发点都是:要充分利用语言特性,re.compile是由于有这个出发点,才会引起来的一个表象分歧。所以大家要讨论的话,不用拘泥于re.compile,毕竟这个东西两种写法,表现出来的差异仅仅是多一行少一行代码。

多说一句

以下内容与本次讨论的re.compile无关。

@Manjusaka给出了一个compile需要3秒钟的大型正则表达式,并以此作为例子说明re.compile的合理性。

首先这种情况下,确实需要提前re.compile。

但我所想表达的是,在这种情况下,就不应该使用正则表达式。既然要做Redis的语法校验,那么就应该使用有限状态机。这种使用很多的f表达式拼出来的正则表达式,才是真正的难以维护,难以阅读。

否则为什么里面需要用一个csv文件来存放命令呢?为什么不直接写在正则表达式里面呢?使用CSV文件每行一个命令尚且可以理解,但是 SLOT/ SLOTS/ NODE/ NEWKWY这些正则表达式,可就说不过去了。或条件连接的每一段都要加上这些东西,如果直接写进去,这个正则表达式你们自己都看不下去了,所以才会需要使用拼接的方式生成。

我在读这段代码的时候,首先看到正则表达式里面的 t[xxx],会先去找 t是什么东西,发现t是一个字典,字典是在 commands_csv_loader.py中生成的,然后去到这个文件里面,发现它读的是一个存放Redis命令的CSV文件。然后去项目根目录读取这个csv文件的内容,知道了它的结构,于是推测出t的结构。然后再回到正则表达式里面,继续看这个超大的正则表达式。整个过程会非常费时间和脑子。

但是,我又不能直接打印REDIS_COMMANDS这个变量,因为它多且乱,不同命令长短不一,拼出来以后再打印出来根本没法看。

这个正则表达式只有两位维护者知道什么意思,如果别人想贡献新的Redis命令,那么理解这个超大正则表达式都需要花很久的时间。

如果换成有限状态机,并且t使用Python的data class来表示,而不是使用字典,那么就会简洁很多。有限状态机的一个特点是,只需要关注当前状态和转移条件,可能一开始写起来有点麻烦,但是以后维护和新增,都是直接定位目标,直接修改,不用担心会影响不想干的其他地方。

算上维护时间,正则表达式真是一个非常糟糕的方式。


点击阅读原文,跳转知乎获取正常的文章连接。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
6天前
|
Python
Python 内置正则表达式库re的使用
正则表达式是记录文本规则的代码,用于查找和处理符合特定规则的字符串。在Python中,常通过原生字符串`r&#39;string&#39;`表示。使用`re.compile()`创建正则对象,便于多次使用。匹配字符串有`match()`(从开头匹配)、`search()`(搜索首个匹配)和`findall()`(找所有匹配)。替换字符串用`sub()`,分割字符串则用`split()`。
21 3
|
6天前
|
Python Windows
【Python进阶必备】一文掌握re库:实战正则表达式
【Python进阶必备】一文掌握re库:实战正则表达式
14 0
|
4天前
|
数据采集 Python
python中的正则表达式,Python实习面试经验汇总
python中的正则表达式,Python实习面试经验汇总
|
6天前
|
Python
python正则表达式小结
1. **其他函数:**  `re`模块还提供了其他一些函数,例如 `re.search()`查找字符串中第一个匹配的部分,`re.findall()`查找所有匹配的部分,`re.sub()`替换匹配的部分。
9 0
|
6天前
|
Python
使用Python解析网页和正则表达式
使用Python解析网页涉及`requests`和`re`模块。首先导入这两个模块,然后用`requests.get()`发送HTTP请求获取URL内容。通过`.text`属性得到HTML文本。接着,利用正则表达式和`re.search()`匹配特定模式(如网页标题),并用`.group(1)`获取匹配数据。最后,对提取的信息进行处理,如打印标题。实际操作时,需根据需求调整正则表达式。
19 2
|
6天前
|
缓存 运维 前端开发
第十六章 Python正则表达式
第十六章 Python正则表达式
|
6天前
|
Python
Python正则表达式Regular Expression初探
Python正则表达式Regular Expression初探
28 0
|
6天前
|
数据安全/隐私保护 Python
Python正则表达式:强大的文本处理工具
Python正则表达式:强大的文本处理工具
14 1
|
6天前
|
Python
Python中的正则表达式以及如何使用它们进行文本处理
正则表达式(Regex)是处理字符串的利器,Python通过`re`模块支持Regex操作,包括匹配模式(`re.match()`),查找模式(`re.search()`),替换内容(`re.sub()`),分割内容(`re.split()`),分组提取(使用括号)以及利用特殊字符创建复杂模式。学习和熟练掌握正则表达式能有效提升文本处理效率。
11 1
|
6天前
|
Python
请解释Python中的正则表达式以及如何使用它们进行文本处理。
正则表达式(Regex)是处理字符串的工具,Python通过`re`模块支持Regex操作,如匹配、查找、替换和分割文本。`re.match()`检查字符串是否以指定模式开始,`re.search()`查找模式,`re.sub()`替换匹配内容,`re.split()`按模式分割字符串。使用括号进行分组提取,特殊字符如`.`、`*`、`+`、`?`、`^`和`$`可创建复杂模式。熟练掌握正则表达式需学习和实践。
12 0