2025, Dec 31 18:02

Как парсить тикеры NATURALGAS с CE/PE/FUT без regex

Сравниваем regex и срезы строк при парсинге тикеров NATURALGAS: извлечение даты DDMMM[YY], страйка и типа CE/PE/FUT в Python с упором на скорость и масштаб.

Разбор тикеров опционов и фьючерсов в больших объёмах кажется простой задачей, пока не упираешься в редкие случаи и пределы производительности. Типичное имя файла вроде NATURALGAS21FEB25270CE.MCX включает дату, цену и тип, но дата может быть как в формате DDMMMYY, так и DDMMM. Когда нужно обработать сотни CSV с десятками тысяч строк в каждом, вопрос становится практическим: регулярные выражения или срезы по фиксированным смещениям? Цель — быстро и надёжно извлечь дату, трёхзначную цену и тип инструмента (CE, PE или FUT), чтобы решение не развалилось под реальной нагрузкой.

Базовый вариант: простая реализация на базе регулярных выражений

Часто начинают с векторизованного извлечения через regex в pandas. Это работает и читается легко, но на крупных датасетах возникают сомнения насчёт времени выполнения.

import pandas as pd
# Столбцы: symbol содержит значения вроде NATURALGAS21FEB25270CE.MCX
# Извлекаем тип опциона (CE, PE или FUT)
tbl["opt_kind"] = tbl["symbol"].str.extract(r'(PE|CE|FUT)', expand=False)
# Извлекаем страйк; изначально допускалось 2–5 цифр; цена ожидается из 3 цифр
# а часть с датой может быть DDMMM или DDMMMYY
tbl['strike_val'] = tbl['symbol'].str.extract(
    r'NATURALGAS\d{2}[A-Z]{3}(?:\d{2})?(\d{2,5})(?=CE|PE)', expand=False)
tbl['strike_val'] = pd.to_numeric(tbl['strike_val'], errors='coerce')
# Извлекаем дату только для строк с CE/PE
tbl['exp_date'] = None
sel_opts = tbl['opt_kind'].isin(['PE', 'CE'])
tbl.loc[sel_opts, 'exp_date'] = tbl.loc[sel_opts, 'symbol'].str.extract(
    r'NATURALGAS(\d{2}[A-Z]{3}(?:\d{2})?)(?=\d{2,5}(?:CE|PE))')[0]

Что здесь действительно сложно

Имена файлов имеют жёсткую структуру: сначала NATURALGAS, затем дата в формате DDMMMYY или DDMMM, потом трёхзначная цена и тип инструмента CE, PE или FUT, после чего идёт .MCX. Формат выглядит регулярным, но переменная длина даты создаёт неоднозначность для наивных срезов и подталкивает к regex. При этом, когда расположение элементов предсказуемо и строка простая, регулярные выражения — не самый эффективный инструмент. Здесь выручают срезы по позициям.

Есть надёжное правило для снятия неоднозначности, основанное на общей длине имени. Если длина — 26 или 27 символов, значит дата в формате DDMMMYY; иначе — DDMMM. Выполняется и ещё одно свойство: если общая длина нечётная, тип — FUT (три буквы), в противном случае — CE или PE (две буквы). Зная эти два инварианта и постоянный префикс NATURALGAS, можно детерминированно посчитать смещения и извлекать нужные части простыми подстроками.

Более быстрый подход: срезы по индексам с фиксированными смещениями

Когда формат предсказуем, извлечение подстрок по индексам обычно быстрее, чем regex. Здесь длина имени файла подсказывает, должна ли дата занимать пять или семь символов, а также состоит ли тип инструмента из двух или трёх букв.

def split_parts(file_name):
    # Допущения по умолчанию: DDMMM (5) и двухбуквенный тип (CE/PE)
    date_width = 5
    type_width = 2
    # Выбираем DDMMMYY или DDMMM по общей длине
    if len(file_name) >= 26:
        date_width = 7
    # Определяем FUT или CE/PE по чётности общей длины
    if len(file_name) % 2 == 1:
        type_width = 3
    # NATURALGAS имеет длину 10, значит дата начинается с индекса 10
    date_val = file_name[10:10 + date_width]
    price_val = file_name[10 + date_width:13 + date_width]
    kind_val = file_name[13 + date_width:13 + date_width + type_width]
    return date_val, price_val, kind_val
# Примеры
x_date, x_price, x_kind = split_parts("NATURALGAS21FEB25270CE.MCX")
print(x_date)
print(x_price)
print(x_kind)
x_date, x_price, x_kind = split_parts("NATURALGAS21FEB270CE.MCX")
print(x_date)
print(x_price)
print(x_kind)
x_date, x_price, x_kind = split_parts("NATURALGAS21FEB25270FUT.MCX")
print(x_date)
print(x_price)
print(x_kind)
x_date, x_price, x_kind = split_parts("NATURALGAS21FEB270FUT.MCX")
print(x_date)
print(x_price)
print(x_kind)

Почему это работает и когда это применять

Подход опирается на стабильный формат именования. В начале всегда NATURALGAS. Длину даты можно вывести из общей длины строки. Ширина типа инструмента определяется по чётности длины. Как только эти два решения приняты, позиции остальных подстрок становятся фиксированными. Нет ни бэктрекинга, ни сканирования — значит, нет и накладных расходов движка регулярных выражений. Для описанного масштаба — сотни файлов и десятки тысяч строк — срезы по индексам оправданы, если схема имен стабильна.

Есть и аспект поддерживаемости. Для столь регулярных имён явные срезы делают замысел прозрачным: видно, какие диапазоны байтов читаются. Если формат эволюционирует или появляются новые варианты, достаточно поправить небольшой набор условий, определяющих длины. Если не уверены, где пропадает время в конвейере, сначала измерьте. Не стоит заниматься микрооптимизацией, если парсинг не является узким местом.

Почему это важно

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

Вывод

Если у вас предсказуемая схема тикера вроде NATURALGASDDMMM[YY]PPP[CE|PE|FUT].MCX, отдавайте предпочтение прямому извлечению подстрок вместо regex. Определите ширину даты и ширину типа по длине имени, а затем сделайте один срез. Базовый вариант с регулярными выражениями можно держать для быстрых экспериментов, но решения принимайте по измерениям, а не на глаз. Профилируйте весь джоб, убедитесь, что парсинг — действительно горячая точка, и только после этого меняйте подход. Стабильные форматы вознаграждают простой код — пусть структуру данных диктует выбор инструментов.