在Python中有没有真正序列化已编译正则表达式的方法?

21 投票
7 回答
3774 浏览
提问于 2025-04-16 06:11

我有一个用Python写的控制台应用程序,里面有300多个正则表达式。这些正则表达式在每次发布时都是固定的。当用户运行这个应用时,所有的正则表达式会被应用一次(很短的任务)到几千次(很长的任务)。

我想通过提前编译这些正则表达式来加快短任务的速度,然后把编译好的正则表达式保存到一个文件里,等应用运行时再加载这个文件。

Python的re模块效率很高,对于长任务来说,编译正则表达式的开销是可以接受的。但是对于短任务来说,这个开销占了整体运行时间的很大一部分。有些用户可能会想要运行很多小任务,以便融入他们现有的工作流程。编译正则表达式大约需要80毫秒,而一个短任务可能只需要20毫秒到100毫秒(不算编译时间)。所以对于短任务来说,开销可能会超过100%。这是在Windows和Linux下使用Python 2.7的情况。

正则表达式必须使用DOTALL标志,所以需要在使用之前先编译。一个大的编译缓存在这种情况下显然没有帮助。正如一些人指出的,默认的序列化编译正则表达式的方法实际上效果不大。

re和sre模块会把模式编译成一种小的自定义语言,里面有自己的操作码和一些辅助数据结构(比如,用于表达式中的字符集)。re.py中的pickle函数采取了简单的方式。它是:

def _pickle(p):
    return _compile, (p.pattern, p.flags)

copy_reg.pickle(_pattern_type, _pickle, _compile)

我认为解决这个问题的一个好办法是更新re.py中_pickle的定义,真正把编译好的模式对象进行序列化。不幸的是,这超出了我的Python技能。不过,我敢打赌这里有人知道怎么做。

我意识到我不是第一个问这个问题的人——但也许你可以成为第一个给出准确和有用回答的人!

非常感谢你的建议。

7 个回答

3

你可以边编译边使用——正则表达式模块会自动缓存已经编译过的正则表达式,即使你不这样做。把 re._MAXCACHE 的值调高到 400 或 500,这样短时间的任务只会编译它们需要的正则表达式,而长时间的任务则能享受到一个大缓存,里面存着很多已经编译好的表达式——大家都能得到好处!

9

正如其他人提到的,你可以简单地把编译好的正则表达式进行“腌制”(也就是序列化)。这样做后,它们可以正常使用。不过,看起来这个“腌制”并没有保存编译的结果。我猜,当你使用这个“腌制”后的结果时,可能还得重新编译一次,这样就会有额外的时间开销。

>>> p.dumps(re.compile("a*b+c*"))
"cre\n_compile\np1\n(S'a*b+c*'\np2\nI0\ntRp3\n."
>>> p.dumps(re.compile("a*b+c*x+y*"))
"cre\n_compile\np1\n(S'a*b+c*x+y*'\np2\nI0\ntRp3\n."

在这两个测试中,你可以看到这两个“腌制”文件之间唯一的区别就是字符串。显然,编译好的正则表达式并没有保存编译的内容,只保存了重新编译所需的字符串。

不过我在想,你的应用整体情况如何:编译正则表达式其实是个很快的操作,那你的任务有多短,竟然让编译正则变得重要呢?一种可能是你编译了300个正则表达式,但只用其中一个来处理一个短任务。在这种情况下,不如一开始就不要全部编译。Python的re模块在使用缓存的编译正则表达式方面做得很好,所以你通常不需要自己去编译,只需使用字符串形式就可以。re模块会在一个编译正则表达式的字典中查找这个字符串,所以自己去获取编译好的形式其实只是省去了查字典的步骤。如果我说错了,请见谅。

12

好的,这个方法看起来不太好,但可能正是你需要的。我查看了Python 2.6中的sre_compile.py模块,提取了一部分代码,把它分成了两半,然后用这两部分来保存和加载编译好的正则表达式:

import re, sre_compile, sre_parse, _sre
import cPickle as pickle

# the first half of sre_compile.compile    
def raw_compile(p, flags=0):
    # internal: convert pattern list to internal format

    if sre_compile.isstring(p):
        pattern = p
        p = sre_parse.parse(p, flags)
    else:
        pattern = None

    code = sre_compile._code(p, flags)

    return p, code

# the second half of sre_compile.compile
def build_compiled(pattern, p, flags, code):
    # print code

    # XXX: <fl> get rid of this limitation!
    if p.pattern.groups > 100:
        raise AssertionError(
            "sorry, but this version only supports 100 named groups"
            )

    # map in either direction
    groupindex = p.pattern.groupdict
    indexgroup = [None] * p.pattern.groups
    for k, i in groupindex.items():
        indexgroup[i] = k

    return _sre.compile(
        pattern, flags | p.pattern.flags, code,
        p.pattern.groups-1,
        groupindex, indexgroup
        )

def pickle_regexes(regexes):
    picklable = []
    for r in regexes:
        p, code = raw_compile(r, re.DOTALL)
        picklable.append((r, p, code))
    return pickle.dumps(picklable)

def unpickle_regexes(pkl):
    regexes = []
    for r, p, code in pickle.loads(pkl):
        regexes.append(build_compiled(r, p, re.DOTALL, code))
    return regexes

regexes = [
    r"^$",
    r"a*b+c*d+e*f+",
    ]

pkl = pickle_regexes(regexes)
print pkl
print unpickle_regexes(pkl)

我其实不太确定这个方法是否有效,或者是否能加快速度。我知道当我尝试时,它会打印出一个正则表达式的列表。不过,这个方法可能只适用于2.6版本,我也不太确定这一点。

撰写回答