我有一个包含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技能。不过,我打赌,这里有人知道该怎么做。
我意识到我不是第一个提出这个问题的人 - 但也许你可以成为第一个对它做出准确而有用的回应的人!
非常感谢您的建议。
答案 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文件就会缓存它们。你不需要酸洗。