Есть ли способ определить пользовательские сокращения в регулярных выражениях?

Я имею регулярное выражение вида

def parse(self, format_string):
    for m in re.finditer(
        r"""(?: \$ \( ( [^)]+ ) \) )   # the field access specifier
          | (
                (?:
                    \n | . (?= \$ \( ) # any one single character before the '$('
                )
              | (?:
                    \n | . (?! \$ \( ) # any one single character, except the one before the '$('
                )*
            )""",
        format_string,
        re.VERBOSE):
    ...

Я хотел бы заменить все повторяющиеся последовательности (\$ \() на некоторую пользовательскую сокращенную "константу", которая будет выглядеть так:

def parse(self, format_string):
    re.<something>('\BEGIN = \$\(')
    for m in re.finditer(
        r"""(?: \BEGIN ( [^)]+ ) \) )   # the field access specifier
          | (
                (?:
                    \n | . (?= \BEGIN ) # any one single character before the '$('
                )
              | (?:
                    \n | . (?! \BEGIN ) # any one single character, except the one before the '$('
                )*
            )""",
        format_string,
        re.VERBOSE):
    ...

Есть ли способ сделать это с помощью самих регулярных выражений (т.е. не использовать форматирование строк Python для замены \BEGIN на \$\()?

Уточнение: источник Python предназначен исключительно для контекста и иллюстрации. Я ищу решение RE, которое будет доступно на каком-то диалекте RE (возможно, не на Python), а не на решении для Python.

Ответ 1

Я не думаю, что это возможно в аромате регулярного выражения Python. Вам понадобится рекурсия (или, скорее, повторное использование шаблонов), которая поддерживается только PCRE. Фактически, PCRE даже упоминает, как определяющие сокращения работают в своей странице man (поиск "Определение подшаблонов" ).

В PCRE вы можете использовать синтаксис рекурсии аналогично обратным ссылкам - за исключением того, что шаблон применяется снова, вместо того, чтобы пытаться получить тот же литеральный текст, что и от обратной ссылки. Пример:

/(\d\d)-(?1)-(?1)/

Совпадает с чем-то вроде даты (где (?1) будет заменено на \d\d и снова оценено). Это действительно мощно, потому что, если вы используете эту конструкцию в самой ссылочной группе, вы получаете рекурсию, но нам здесь даже не нужно. Вышеупомянутое также работает с именованными группами:

/(?<my>\d\d)-(?&my)-(?&my)/

Теперь мы уже очень близки, но это определение также является первым использованием шаблона, который несколько загромождает выражение. Хитрость заключается в том, чтобы использовать шаблон сначала в позиции, которая никогда не оценивается. Страницы руководства предполагают, что условие зависит от (несуществующей) группы DEFINE:

/
(?(DEFINE)
  (?<my>\d\d)
)
(?&my)-(?&my)-(?&my)
/x

Конструкция (?(group)true|false) применяет шаблон true, если раньше использовалась группа group, и (необязательный) шаблон false в противном случае. Поскольку нет группы DEFINE, условие всегда будет ложным, а шаблон true будет пропущен. Следовательно, мы можем поставить там всевозможные определения, не опасаясь, что они когда-либо будут применены и испортят наши результаты. Таким образом, мы получаем их в шаблон, не используя их.

И альтернатива - это негативный взгляд, который никогда не достигает точки, в которой определяется выражение:

/
(?!
  (?!)     # fail - this makes the surrounding lookahead pass unconditionally
  # the engine never gets here; now we can write down our definitions
  (?<my>\d\d) 
)
(?&my)-(?&my)-(?&my)
/x

Однако вам действительно нужна эта форма, если у вас нет условных выражений, но у меня есть повторное использование шаблонов (и я не думаю, что такой аромат существует). Другой вариант имеет то преимущество, что использование DEFINE делает очевидным, для чего предназначена группа, в то время как подход lookahead немного запутан.

Итак, вернемся к вашему оригинальному примеру:

/
# Definitions
(?(DEFINE)
  (?<BEGIN>[$][(])
)
# And now your pattern
  (?: (?&BEGIN) ( [^)]+ ) \) ) # the field access specifier
|
  (
    (?: # any one single character before the '$('
      \n | . (?= (?&BEGIN) ) 
    )
  | 
    (?: # any one single character, except the one before the '$('
      \n | . (?! (?&BEGIN) ) 
    )*
  )
/x

Для этого подхода есть два основных оговорки:

  • Рекурсивные ссылки atomic. То есть, как только ссылка сопоставит что-то, она никогда не будет возвращена. В некоторых случаях это может означать, что вы должны быть немного умны в создании своего выражения, так что первое совпадение всегда будет тем, которое вы хотите.
  • Вы не можете использовать захват внутри определенных шаблонов. Если вы используете что-то вроде (?<myPattern>a(b)c) и повторно его используете, b никогда не будет захвачен - при повторном использовании шаблона все группы не захватываются.

Самое важное преимущество перед любым типом интерполяции или конкатенации состоит в том, что вы никогда не сможете создавать недопустимые шаблоны с этим, и вы не можете испортить количество ваших захваченных групп.