Профилирование, кэширование, типичные проблемы и техники оптимизации
Производительность регулярных выражений критична при обработке больших объёмов данных. В этой теме изучим техники оптимизации и профилирования.
import re
import time
pattern = re.compile(r'\d+')
text = 'abc123def456' * 1000
start = time.perf_counter()
for _ in range(1000):
pattern.findall(text)
end = time.perf_counter()
print(f'Время: {end - start:.4f} сек')import timeit
# Паттерн 1: жадный
pattern1 = r'.*<div>.*'
# Паттерн 2: нежадный
pattern2 = r'.*?<div>.*?'
# Паттерн 3: конкретный
pattern3 = r'[^<]*<div>[^<]*'
text = '<div>content</div>' * 100
time1 = timeit.timeit(lambda: re.search(pattern1, text), number=1000)
time2 = timeit.timeit(lambda: re.search(pattern2, text), number=1000)
time3 = timeit.timeit(lambda: re.search(pattern3, text), number=1000)
print(f'Жадный: {time1:.4f}')
print(f'Нежадный: {time2:.4f}')
print(f'Конкретный: {time3:.4f}')Модуль re автоматически кэширует до 512 скомпилированных паттернов:
# Автоматическое кэширование
for i in range(100):
re.search(r'\d+', text) # Компилируется один раз, затем из кэша
# Явная компиляция для важных паттернов
pattern = re.compile(r'\d+')
for i in range(100):
pattern.search(text) # Из кэшаimport re
# Размер кэша (по умолчанию 512)
print(re._cache.__len__())
# Очистка кэша
re.purge()# Плохо: .соответствует любой символ
pattern = r'.*<div>.*'
# Хорошо: конкретный класс
pattern = r'[^<]*<div>[^<]*'# Плохо: поиск по всей строке
pattern = r'\d{4}-\d{2}-\d{2}'
# Хорошо: с якорями, если известно положение
pattern = r'^\d{4}-\d{2}-\d{2}$'# Плохо: неограниченный квантификатор
pattern = r'.*'
# Хорошо: с ограничением
pattern = r'.{0,100}'
# Или: конкретный класс
pattern = r'[^\n]{0,100}'# Плохо: catastrophic backtracking
pattern = r'(a+)+b'
# Хорошо: упрощённый
pattern = r'a+b'# Плохо: захватывающие группы, если не нужны
pattern = r'(\d+)-(\d+)-(\d+)'
# Хорошо: non-capturing
pattern = r'(?:\d+)-(?:\d+)-(?:\d+)'# Плохо: компиляция при каждом вызове
def find_emails(text):
return re.findall(r'[\w.-]+@[\w.-]+\.[a-zA-Z]{2,}', text)
# Хорошо: компиляция один раз
EMAIL_PATTERN = re.compile(r'[\w.-]+@[\w.-]+\.[a-zA-Z]{2,}')
def find_emails(text):
return EMAIL_PATTERN.findall(text)# Плохо: поиск по всей строке
if re.search(r'^ERROR', log_line):
process(log_line)
# Хорошо: startswith быстрее для простых префиксов
if log_line.startswith('ERROR'):
process(log_line)# Плохо: частые варианты в конце
pattern = r'cat|dog|bird|fish|elephant'
# Хорошо: частые варианты в начале
pattern = r'elephant|fish|bird|dog|cat'
# Или: сортировка по длине (длинные primero)
pattern = r'elephant|bird|fish|cat|dog'def process_large_file(filename, chunk_size=8192):
pattern = re.compile(r'\d+')
with open(filename, 'r') as f:
buffer = ''
while True:
chunk = f.read(chunk_size)
if not chunk:
break
buffer += chunk
lines = buffer.split('\n')
buffer = lines[-1] # Оставляем последнюю неполную строку
for line in lines[:-1]:
matches = pattern.findall(line)
process(matches)# Плохо: создаёт весь список в памяти
matches = re.findall(r'\d+', large_text)
for match in matches:
process(match)
# Хорошо: итератор
for match in re.finditer(r'\d+', large_text):
process(match.group())from concurrent.futures import ProcessPoolExecutor
def process_chunk(args):
text, pattern = args
return re.findall(pattern, text)
def parallel_findall(text, pattern, num_chunks=4):
chunk_size = len(text) // num_chunks
chunks = [text[i:i+chunk_size] for i in range(0, len(text), chunk_size)]
with ProcessPoolExecutor() as executor:
results = executor.map(process_chunk, [(c, pattern) for c in chunks])
return [match for chunk_results in results for match in chunk_results]# Плохо: .*соответствует до конца строки
text = 'a' * 10000 + 'b'
re.search(r'a*b', text) # Медленно
# Хорошо: конкретный класс
re.search(r'[a]*b', text) # Быстрее# Плохо: лишние группы
pattern = r'(\d{4})-(\d{2})-(\d{2})'
# Если группы не нужны:
pattern = r'(?:\d{4})-(?:\d{2})-(?:\d{2})'# Плохо: редкие варианты primero
pattern = r'zebra|elephant|cat|dog'
# Хорошо: частые варианты primero
pattern = r'cat|dog|elephant|zebra'import re
# Отладка скомпилированного паттерна
pattern = re.compile(r'\d+')
print(pattern.pattern) # Исходный паттернimport cProfile
import re
def benchmark():
pattern = re.compile(r'\d+')
text = 'abc123def456' * 1000
for _ in range(1000):
pattern.findall(text)
cProfile.run('benchmark()')# Модуль уровня
EMAIL_PATTERN = re.compile(r'^[\w.-]+@[\w.-]+\.[a-zA-Z]{2,}$')
PHONE_PATTERN = re.compile(r'^\+7\d{10}$')
def validate_email(email):
return bool(EMAIL_PATTERN.match(email))class PatternCache:
_cache = {}
@classmethod
def get(cls, pattern):
if pattern not in cls._cache:
cls._cache[pattern] = re.compile(pattern)
return cls._cache[pattern]
# Использование
pattern = PatternCache.get(r'\d+')import timeit
# Измерьте текущую производительность
baseline = timeit.timeit(lambda: current_pattern.findall(text), number=1000)
# Измерьте оптимизированную
optimized = timeit.timeit(lambda: new_pattern.findall(text), number=1000)
print(f'Улучшение: {baseline / optimized:.2f}x'). — используйте конкретные^, $ для раннего завершения{0,100} вместо *(a+)+(?:...) если не нужен захватfinditer() для больших текстовcat|dog|birdre кэширует до 512 паттернов автоматическиВопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.