前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >回《驳 <Python正则表达式,请不要再用re.compile了!!!>》

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

作者头像
青南
发布2019-08-20 13:16:19
1.4K0
发布2019-08-20 13:16:19
举报
文章被收录于专栏:未闻Code未闻Code
摄影:kingname

厨师:kingname

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

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

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

你真的在意这点性能?

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

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

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

代码语言:javascript
复制
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

代码语言:javascript
复制
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的代码,也能猜的出代码想实现什么功能。

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

代码语言:javascript
复制
re.findall('密码: (.*?)$', sentence)

代码语言:javascript
复制
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发起一个请求是怎么写的:

代码语言:javascript
复制
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以后,实现同样的功能,我们是这样写的:

代码语言:javascript
复制
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,就一定要把正则表达式复制粘贴很多遍?

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

代码语言:javascript
复制
# regex_str.py

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

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

代码语言:javascript
复制
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.findall re.search,不要使用 re.compile
  3. 对于有经验的工程师,在开发项目的时候,请首先使用 re.findall re.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来表示,而不是使用字典,那么就会简洁很多。有限状态机的一个特点是,只需要关注当前状态和转移条件,可能一开始写起来有点麻烦,但是以后维护和新增,都是直接定位目标,直接修改,不用担心会影响不想干的其他地方。

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

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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-08-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 未闻Code 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 你真的在意这点性能?
  • 维护自文档性
  • 以官方文档的实例入门
  • 面向接口编程还是面向人类编程?
  • 不是问题的问题
  • 总结
  • 多说一句
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档