【问题标题】:Is there a way to really pickle compiled regular expressions in python?有没有办法在python中真正腌制编译的正则表达式?
【发布时间】:2010-10-27 20:21:14
【问题描述】:

我有一个包含 300 多个正则表达式的 Python 控制台应用程序。每个版本的正则表达式集都是固定的。当用户运行应用程序时,整套正则表达式将被应用到任何地方,从一次(一个非常短的工作)到数千次(一个很长的工作)。

我想通过预先编译正则表达式来加速较短的作业,将编译后的正则表达式腌制到一个文件中,然后在应用程序运行时加载该文件。

python re 模块是高效的,正则表达式的编译开销对于长时间的工作来说是完全可以接受的。然而,对于短作业,它占整个运行时间的很大一部分。一些用户会希望运行许多小型作业以适应他们现有的工作流程。编译正则表达式大约需要 80 毫秒。一个简短的工作可能需要 20 毫秒到 100 毫秒,不包括正则表达式编译。因此,对于短期工作,开销可能是 100% 或更多。这适用于 Windows 和 Linux 下的 Python27。

正则表达式必须与 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 技能。不过,我敢打赌,这里有人知道怎么做。

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

您的建议将不胜感激。

【问题讨论】:

  • 有了这么多正则表达式,考虑将 re._MAXCACHE 从其默认值 100 提高。
  • @user489276:您实际上并没有问问题。您实际上并没有提供太多有用的信息。这里有一些问题要问你:在什么平台上的什么版本的 Python 上编译你的 300 多个正则表达式需要多长时间?作业中是否使用了所有 300 多个正则表达式?
  • “如何在 python 中腌制已编译的正则表达式”缺少标点符号,但仍可识别为问题。
  • 如果每个版本的集合都是固定的,并且正则表达式花费了不可接受的时间,那么我会用适当的解析器替换它们。
  • 在目前的状态下,这是一个很好的第一个问题。欢迎加入社区,我们希望你能坚持下去。

标签: python regex pickle


【解决方案1】:

好的,这不是很漂亮,但它可能是你想要的。我查看了 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 版,我也不知道。

【讨论】:

  • 这看起来很有希望。我期待着尝试一下,并将报告结果。
  • +1。注意:我记得在某处读到过,正则表达式未以编译形式存储的原因是 Python 版本间兼容性(存储的 re 形式可以并且确实在 Python 版本之间发生变化),所以是的,你可能是对的,这可能是仅适用于 2.6 的解决方案。
【解决方案2】:

正如其他人所提到的,您可以简单地腌制已编译的正则表达式。他们会腌制和解封就好了,并且可以使用。但是,pickle 看起来并不实际包含编译结果。我怀疑你在使用 unpickling 的结果时会再次产生编译开销。

>>> 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 个正则表达式,然后仅将一个用于短期工作。在这种情况下,不要预先编译它们。 re 模块非常擅长使用已编译的正则表达式的缓存副本,因此您通常不必自己编译它们,只需使用字符串形式即可。 re 模块将在已编译的正则表达式字典中查找字符串,因此自己获取已编译的表单只会为您节省字典查找。我可能完全偏离了基地,如果是这样,对不起。

【讨论】:

  • 泡菜不包含编译结果是正确的。这就是问这个问题的原因。我更新了原始帖子,表明正则表达式编译在短期工作(可能很多)上引入了高达 100% 的开销。
  • 我之前做过分析,结果表明只使用一个正则表达式,在使用前编译它比使用字符串要快得多。我不确定正则表达式缓存的效率到底有多高。
【解决方案3】:

随用随编译 - 即使你不这样做,re 模块也会缓存已编译的 re。将 re._MAXCACHE 提高到 400 或 500,短作业只会编译他们需要的 re,而长作业会受益于已编译表达式的大缓存 - 每个人都开心!

【讨论】:

  • 如果短期工作使用所有正则表达式一次,则不会。
  • 即使是很短的工作也需要应用所有正则表达式。
  • 我的错误——我把“短期工作”误认为是“只需要一些正则表达式”——我会在下次回答之前尝试阅读整个问题。
【解决方案4】:

一些观察和思考:

你不需要编译来获得 re.DOTALL 标志(或任何其他标志)的效果——你需要做的就是在模式字符串的开头插入(?s) ... re。 DOTALL -> re.S -> (?s) 中的 s。在re syntax docs 中按Ctrl-F 搜索sux(原文如此)。

80 毫秒似乎很短,即使乘以“许多”(多少??)短作业也是如此。

是否每个作业都需要启动一个新的 Python 进程?如果是这样,与进程启动和关闭开销相比,80ms 不是很小吗?否则,请解释为什么当用户想要运行“许多”小作业时,每批作业执行一次 re.compiles 是不可能的。

【讨论】:

  • 1. (?s) - 而不是编译标志。这很有帮助。我在文档中错过了这一点。 2.进程启动和基础模块加载耗时390ms,80ms增加20%以上。 3. 很多可能是数万。
  • @Adam:与其试图让每个作业的 80 毫秒消失,不如专注于让每个作业的 390+80=470 毫秒消失。我再问一次:请解释为什么当用户想要运行“许多”小作业时,每批作业执行一次 re.compiles 是不可能的。
  • 数万 * 八十毫秒 = 八十秒 -- 可能是几分钟。在宏伟的计划中似乎并不多。
【解决方案5】:

在类似的情况下(每次需要通过所有正则表达式运行某些输入),我不得不使用 *nix 套接字将 Python 脚本拆分为主从设置;第一次调用脚本时,主服务器(进行所有耗时的正则表达式编译)启动,而从服务器和所有后续调用都与主服务器交换数据。 master 最多保持空闲 N 秒。

在我的例子中,发现这种主/从设置在所有情况下都比直接方式更快(每次调用相对较少的数据;另外,它必须是脚本,因为它是从外部应用程序调用的没有任何 Python 绑定)。我不知道这是否适用于你的情况。

【讨论】:

    【解决方案6】:

    我遇到了同样的问题,我没有修补 python 的 re 模块,而是选择创建一个长时间运行的正则表达式“服务”。下面附上基本代码。请注意:它并非旨在并行处理多个客户端,即服务器仅在客户端关闭连接后才可用。

    服务器

    from multiprocessing.connection import Client
    from multiprocessing.connection import Listener
    import re
    
    class RegexService(object):
        patternsByRegex = None
    
        def __init__(self):
            self.patternsByRegex = {}
    
        def processMessage(self, message):
            regex = message.get('regex')
            result = {"error": None}
            if regex == None:
                result["error"] = "no regex in message - something is wrong with your client"
                return result
            text = message.get('text')
            pattern = self.patternsByRegex.get(regex)
            if pattern == None:
                print "compiling previously unseen regex: %s" %(regex)
                pattern = re.compile(regex, re.IGNORECASE)
                self.patternsByRegex[regex] = pattern
            if text == None:
                result["error"] = "no match"
                return result
            match = pattern.match(text)
            result["matchgroups"] = None
            if match == None:
                return result
            result["matchgroups"] = match.groups()
            return result
    
    workAddress = ('localhost', 6000)
    resultAddress = ('localhost', 6001)
    listener = Listener(workAddress, authkey='secret password')
    service = RegexService()
    patterns = {}
    while True:
        connection = listener.accept()
        resultClient = Client(resultAddress, authkey='secret password')
        while True:
            try:
                message = connection.recv()
                resultClient.send(service.processMessage(message))
            except EOFError:
                resultClient.close()
                connection.close()
                break
    listener.close()
    

    测试客户端

    from multiprocessing.connection import Client
    from multiprocessing.connection import Listener
    
    
    workAddress = ('localhost', 6000)
    resultAddress = ('localhost', 6001)
    regexClient = Client(workAddress, authkey='secret password')
    resultListener = Listener(resultAddress, authkey='secret password')
    resultConnection = None
    
    def getResult():
        global resultConnection
        if resultConnection == None:
            resultConnection = resultListener.accept()
        return resultConnection.recv()
    
    regexClient.send({
        "regex": r'.*'
    })
    print str(getResult())
    regexClient.send({
        "regex": r'.*',
        "text": "blub"
    })
    print str(getResult())
    regexClient.send({
        "regex": r'(.*)',
        "text": "blub"
    })
    print str(getResult())
    resultConnection.close()
    regexClient.close()
    

    测试客户端运行 2 次的输出

    $ python ./regexTest.py 
    {'error': 'no match'}
    {'matchgroups': (), 'error': None}
    {'matchgroups': ('blub',), 'error': None}
    $ python ./regexTest.py 
    {'error': 'no match'}
    {'matchgroups': (), 'error': None}
    {'matchgroups': ('blub',), 'error': None}
    

    两次测试运行期间服务进程的输出

    $ python ./regexService.py
    compiling previously unseen regex: .*
    compiling previously unseen regex: (.*)
    

    【讨论】:

      【解决方案7】:

      只要在程序启动时创建它们,pyc 文件就会缓存它们。你不需要酸洗。

      【讨论】:

      • 嗯,不。 picklemarshal(以及因此 pyc 文件)都不存储已编译的正则表达式。这就是问题提出的全部问题。
      猜你喜欢
      • 2010-10-31
      • 1970-01-01
      • 2013-02-26
      • 2021-07-21
      • 2013-01-23
      • 2021-03-19
      • 2019-02-24
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多