有没有办法在python中真正挑选编译的正则表达式?

时间:2010-10-27 20:21:14

标签: python regex pickle

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

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

python re模块是高效的,正则表达式编译开销对于长作业来说是完全可以接受的。然而,对于短期工作,它占整个运行时间的很大一部分。一些用户希望运行许多小作业以适应他们现有的工作流程。编译正则表达式大约需要80毫秒。除正则表达式编译外,短作业可能需要20ms-100ms。因此,对于短期工作,开销可以是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技能。不过,我打赌,这里有人知道该怎么做。

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

非常感谢您的建议。

7 个答案:

答案 0 :(得分:11)

好的,这不是很好,但它可能就是你想要的。我查看了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 :(得分:9)

正如其他人所提到的,你可以简单地挑选已编译的正则表达式。他们会腌制和去腌制得很好,并且可以使用。但是,它看起来并不像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模块将在编译的正则表达式的字典中查找字符串,因此自己抓取编译的表单只会节省字典查找。我可能完全偏离基地,对不起,如果是这样的话。

答案 2 :(得分:3)

随着你的去编译 - 即使你不这样做,模块也会缓存已编译的re。将re._MAXCACHE压缩到400或500,短工作只会编译他们需要的工作,而长工作会从编译表达式的大量缓存中受益 - 每个人都很高兴!

答案 3 :(得分:2)

一些观察和思考:

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

80ms似乎是一个非常短的时间,即使乘以“很多”(多少??)短期工作。

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

答案 4 :(得分:1)

在类似的情况下(每次需要通过所有正则表达式运行某些输入时),我必须使用* nix套接字在主从设置中拆分Python脚本;第一次调用脚本时,主机 - 所有时间昂贵的正则表达式编译 - 启动,并且该从站和所有后续调用的从站与主站交换数据。主机最长保持空闲N秒。

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

答案 5 :(得分:0)

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

服务器

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()

<强> TestClient的

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: (.*)

答案 6 :(得分:-2)

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