Подтвердить что ты не робот

Есть ли способ действительно рассортировать скомпилированные регулярные выражения в python?

У меня есть консольное приложение на python, которое содержит более 300 регулярных выражений. Набор регулярных выражений фиксирован для каждой версии. Когда пользователи запускают приложение, весь набор регулярных выражений будет применяться где угодно от одного раза (очень короткая работа) до тысяч раз (длинная работа).

Я хотел бы ускорить выполнение более коротких заданий, скомпилировав регулярные выражения вперед, рассортировать скомпилированные регулярные выражения в файл и затем загружать этот файл при запуске приложения.

Модуль python re эффективен, а служебные данные компиляции регулярных выражений вполне приемлемы для длинных заданий. Однако для коротких заданий это большая часть общего времени выполнения. Некоторые пользователи захотят запустить множество небольших заданий, чтобы вписаться в их существующие рабочие процессы. Компиляция регулярных выражений занимает около 80 мс. Короткая работа может занять 20 мс-100 мс, исключая компиляцию регулярных выражений. Таким образом, для коротких заданий накладные расходы могут составлять 100% или более. Это с Python27 под Windows и Linux.

Регулярные выражения должны применяться с флагом DOTALL, поэтому их необходимо скомпилировать перед использованием. В этом случае большой кеш компиляции явно не помогает. Как указывали некоторые, метод по умолчанию для сериализации скомпилированного регулярного выражения на самом деле мало что делает.

Модули re и sre компилируют шаблоны на небольшой пользовательский язык со своими кодами операций и некоторыми вспомогательными структурами данных (например, для кодировок, используемых в выражении). Функция рассола в re.py делает простой выход. Это:

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

copy_reg.pickle(_pattern_type, _pickle, _compile)

Я думаю, что хорошим решением проблемы будет обновление определения _pickle в re.py, которое фактически мариновали скомпилированный объект шаблона. К сожалению, это выходит за рамки моих навыков питона. Уверен, однако, что кто-то здесь знает, как это сделать.

Я понимаю, что я не первый человек, задающий этот вопрос, но, возможно, вы можете быть первым, кто может дать ему точный и полезный ответ!

Ваш совет будет очень благодарен.

4b9b3361

Ответ 1

ОК, это не очень, но это может быть то, что вы хотите. Я просмотрел модуль sre_compile.py из Python 2.6 и немного вырвал его, нарезал его пополам и использовал две части для рассолачивания и сглаживания скомпилированных регулярных выражений:

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, я также не знаю этого.

Ответ 2

Как уже упоминалось, вы можете просто рассортировать скомпилированное регулярное выражение. Они будут мариновать и рассыпать просто отлично и могут быть использованы. Однако это не похоже на то, что рассол фактически содержит результат компиляции. Я подозреваю, что вы снова воспользуетесь компиляцией, когда будете использовать результат рассыпания.

>>> 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 будет искать строку в словаре скомпилированных регулярных выражений, поэтому захват скомпилированной формы самостоятельно сохраняет ваш словарь. Я могу быть совершенно вне базы, извините, если так.

Ответ 3

Просто скомпилируйте, когда вы перейдете - модуль будет кэшировать скомпилированные данные, даже если вы этого не сделаете. Bump re._MAXCACHE до 400 или 500, короткие задания будут компилировать только те, которые им нужны, а длинные рабочие места выигрывают от большого жира кеша скомпилированных выражений - все счастливы!

Ответ 4

Некоторые наблюдения и размышления:

Вам не нужно компилировать, чтобы получить эффект флага re.DOTALL(или любого другого флага) - все, что вам нужно сделать, это вставить (?s) в начале строки шаблона... re. DOTALL → re.S → s in (? S). Сделайте поиск Ctrl-F для sux (sic) в re синтаксисе docs.

80ms кажется очень коротким, даже когда умножается на "много" (сколько?) коротких заданий.

Требуется ли для каждой работы новый процесс Python? Если да, то это не 80 мс по сравнению с начальным запуском и отключением процесса? В противном случае, пожалуйста, объясните, почему это невозможно, когда пользователь хочет запустить "много" небольших заданий, чтобы сделать re.compiles один раз за каждую партию заданий.

Ответ 5

В аналогичном случае (где каждый раз, когда какой-то ввод должен выполняться через ВСЕ из регулярных выражений), мне пришлось разбить Python script в настройке ведущего-ведомого с помощью * nix-сокетов; в первый раз, когда вызывается script, запускается мастер -дополнение всех дорогостоящих компиляций регулярных выражений, и ведомое устройство для этого и все последующие вызовы обмениваются данными с ведущим. Мастер остается в режиме ожидания не более N секунд.

В моем случае эта настройка мастера/подчиненного была найдена быстрее во всех случаях, чем простой способ (многие вызовы против относительно небольших данных каждый раз, также это должно было быть script, потому что оно вызывается из внешнее приложение без каких-либо привязок Python). Я не знаю, применимо ли это к вашей ситуации.

Ответ 6

У меня была такая же проблема, и вместо того, чтобы исправлять модуль python re, я решил создать долговременную службу "regex". Базовый код прилагается ниже. Обратите внимание: он не предназначен для обработки нескольких клиентов параллельно, то есть сервер доступен только после закрытия клиента.

сервер

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

Ответ 7

Пока вы создаете их при запуске программы, файл pyc будет кэшировать их. Вам не нужно приводить к травлению.