Модуль Python re становится в 20 раз медленнее при циклировании более чем 100 различных регулярных выражений

Моя проблема заключается в анализе файлов журнала и удалении переменных в каждой строке, чтобы сгруппировать их. Например:

s = re.sub(r'(?i)User [_0-9A-z]+ is ', r"User .. is ", s)
s = re.sub(r'(?i)Message rejected because : (.*?) \(.+\)', r'Message rejected because : \1 (...)', s)

У меня есть примерно 120+ правил соответствия, как указано выше.

Я не нашел проблем с производительностью при одновременном поиске по 100 различным регулярным выражениям. Но при применении 101 регулярных выражений происходит значительное замедление.

Точное поведение наблюдается при замене моих правил на

for a in range(100):
    s = re.sub(r'(?i)caught here'+str(a)+':.+', r'( ... )', s)

При использовании диапазона (101) он был в 20 раз медленнее.

# range(100)
% ./dashlog.py file.bz2
== Took  2.1 seconds.  ==

# range(101)
% ./dashlog.py file.bz2
== Took  47.6 seconds.  ==

Почему такое происходит? И есть ли какое-нибудь известное обходное решение?

(происходит на Python 2.6.6/2.7.2 на Linux/Windows.)

Ответ 1

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

Угадайте, сколько элементов может хранить кеш?

>>> import re
>>> re._MAXCACHE
100

В момент превышения размера кеша Python 2 очищает все кэшированные выражения и начинает с чистого кеша. Python 3 увеличил предел до 512, но все еще полностью очистил.

Работа для вас заключается в том, чтобы вы сами кэшировали компиляцию:

compiled_expression = re.compile(r'(?i)User [_0-9A-z]+ is ')

compiled_expression.sub(r"User .. is ", s)

Вы можете использовать functools.partial() для объединения вызова sub() вместе с заменяющим выражением:

from functools import partial

compiled_expression = re.compile(r'(?i)User [_0-9A-z]+ is ')
ready_to_use_sub = partial(compiled_expression.sub, r"User .. is ")

а затем использовать ready_to_use_sub(s) для использования скомпилированного шаблона регулярного выражения вместе с определенным шаблоном замены.