У меня всегда было впечатление, что вы не можете использовать квантификаторы повторения в утверждениях нулевой ширины (Perl-совместимые регулярные выражения [PCRE]). Однако недавно мне стало известно, что вы можете использовать их в прогнозных утверждениях.

Как работает механизм регулярных выражений PCRE при поиске с просмотром назад нулевой ширины, который исключает использование квантификаторов повторения?

Вот простой пример из PCRE в R:

# Our string
x <- 'MaaabcccM'

##  Does it contain a 'b', preceeded by an 'a' and followed by zero or more 'c',
##  then an 'M'?
grepl( '(?<=a)b(?=c*M)' , x , perl=T )
# [1] TRUE

##  Does it contain a 'b': (1) preceeded by an 'M' and then zero or more 'a' and
##                         (2) followed by zero or more 'c' then an 'M'?
grepl( '(?<=Ma*)b(?=c*M)' , x , perl = TRUE )
# Error in grepl("(?<=Ma*)b(?=c*M)", x, perl = TRUE) :
#   invalid regular expression '(?<M=a*)b(?=c*M)'
# In addition: Warning message:
# In grepl("(?<=Ma*)b(?=c*M)", x, perl = TRUE) : PCRE pattern compilation error
#         'lookbehind assertion is not fixed length'
#         at ')b(?=c*M)'
37
Simon O'Hanlon 31 Май 2014 в 02:39
2
Да, переменную длину могут иметь только утверждения с опережением. Единственным исключением является специальный код \K, представляющий собой особую форму ретроспективного утверждения, которое может быть переменным. Итак, во втором примере в Perl будет работать следующее: /a*\Kb(?=c*)/. Очевидно, что использование утверждения, которое может иметь нулевую ширину, немного бессмысленно, поэтому, возможно, использование + будет лучшим примером
 – 
Miller
31 Май 2014 в 02:45
1
Потому что утверждения переменной длины с просмотром позади - это боль в @$$, когда механизму регулярных выражений необходимо вернуться.
 – 
mob
31 Май 2014 в 02:48
1
Можете ли вы объяснить, почему с ними сложнее иметь дело, чем с утверждениями переменной длины с опережением? С наивной точки зрения, обе операции будут включать просмотр одинакового количества символов, правильно. (Я знаю, что это должно быть неправильно, но как так?)
 – 
Josh O'Brien
31 Май 2014 в 02:51
9
Абзац, начинающийся со слов «Плохие новости» на этой странице, может указывать на причину. Похоже, что движки регулярных выражений действительно могут работать только вперед, так что проверки назад фактически сопоставляются, отступая на n символов и проверяя их с первого символа. С утверждением просмотра назад переменной длины вы не можете знать n заранее, что означало бы, что вам придется тестировать снова и снова, по одному разу для каждого возможного начального символа в строке. Может ли какой-нибудь мастер регулярных выражений подтвердить, правильно ли это +/-?
 – 
Josh O'Brien
31 Май 2014 в 02:59
1
Представление о том, что «b» предшествует ноль или более «a»», довольно нелепо, поскольку оно всегда будет удовлетворено. «b» либо предшествует по крайней мере одна «a» .., либо нет, поэтому наличие нулевой «a» означает, что условие пусто. То же самое для нуля или более "c" после него.
 – 
IRTFM
31 Май 2014 в 07:57

3 ответа

Лучший ответ

Окончательный ответ на такой вопрос находится в коде движка, и в нижней части ответа вы сможете погрузиться в раздел кода движка PCRE, отвечающий за обеспечение фиксированной длины в ретроспективе - если вас интересует зная до мельчайших деталей. А пока давайте постепенно перейдем к вопросу с более высоких уровней.

Поиск назад переменной ширины и просмотр назад бесконечной ширины

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

  • движок знает, что ширина предшествующего элемента должна быть в пределах от 5 до 10 символов (не поддерживается в PCRE)
  • движок знает, что ширина предшествующего элемента должна быть 5 или десяти символов (поддерживается в PCRE)

Напротив, при просмотре назад бесконечной ширины вы можете использовать количественные токены, такие как a+

Механизмы, поддерживающие просмотр назад бесконечной ширины

Для справки, эти движки поддерживают бесконечный просмотр назад:

  • .NET (C #, VB.NET и т. Д.)
  • Модуль Мэттью Барнетта regex для Python
  • JGSoft (EditPad и т. Д .; недоступно для языков программирования).

Насколько я знаю, они единственные.

Поиск назад переменной в PCRE

В PCRE наиболее актуальным разделом документации является следующий:

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

Следовательно, допустим следующий просмотр назад:

(?<=a |big )cat

Однако ни один из них не является:

  • (?<=a\s?|big )cat (стороны чередования не имеют фиксированной ширины)
  • (?<=@{1,10})cat (переменная ширина)
  • (?<=\R)cat (\R не имеет фиксированной ширины, поскольку может соответствовать \n, \r\n и т. Д.)
  • (?<=\X)cat (\X не имеет фиксированной ширины, поскольку кластер графем Unicode может содержать переменное количество байтов.)
  • (?<=a+)cat (явно не исправлено)

Просмотр назад с соответствием нулевой ширины, но бесконечным повторением

Теперь рассмотрим это:

(?<=(?=@+))(cat#+)

На первый взгляд, это ретроспективный просмотр с фиксированной шириной, потому что он может найти только совпадение нулевой ширины (определяемое просмотром вперед (?=@++)). Это уловка, позволяющая обойти бесконечное ограничение просмотра назад?

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

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

Временные решения: жизнь без бесконечного поиска назад

В PCRE два основных решения проблем, при которых может помочь бесконечный просмотр назад, - это \K и группы захвата.

Обходной путь №1: \K

Утверждение \K сообщает движку отбросить то, что было найдено так далеко от окончательного совпадения, которое он возвращает.

Предположим, вам нужен (?<=@+)cat#+, что недопустимо в PCRE. Вместо этого вы можете использовать:

@+\Kcat#+

Обход №2: группы захвата

Другой способ продолжить - сопоставить все, что вы поместили в ретроспективу, и зафиксировать интересующий контент в группе захвата. Затем вы извлекаете совпадение из группы захвата.

Например, вместо незаконного (?<=@+)cat#+ вы должны использовать:

@+(cat#+)

В R это могло выглядеть так:

matches <- regexpr("@+(cat#+)", subject, perl=TRUE);
result <- attr(matches, "capture.start")[,1]
attr(result, "match.length") <- attr(matches, "capture.length")[,1]
regmatches(subject, result)

Для языков, которые не поддерживают \K, это часто единственное решение.

Внутреннее устройство двигателя: что говорит код PCRE?

Окончательный ответ можно найти в pcre_compile.c. Если вы изучите блок кода, который начинается с этого комментария:

Если заглянуть назад, убедитесь, что эта ветвь соответствует строке фиксированной длины

Вы обнаружите, что основная работа выполняется функцией find_fixedlength().

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

static int
find_fixedlength(pcre_uchar *code, BOOL utf, BOOL atend, compile_data *cd)
{
int length = -1;

register int branchlength = 0;
register pcre_uchar *cc = code + 1 + LINK_SIZE;

/* Scan along the opcodes for this branch. If we get to the end of the
branch, check the length against that of the other branches. */

for (;;)
  {
  int d;
  pcre_uchar *ce, *cs;
  register pcre_uchar op = *cc;

  switch (op)
    {
    /* We only need to continue for OP_CBRA (normal capturing bracket) and
    OP_BRA (normal non-capturing bracket) because the other variants of these
    opcodes are all concerned with unlimited repeated groups, which of course
    are not of fixed length. */

    case OP_CBRA:
    case OP_BRA:
    case OP_ONCE:
    case OP_ONCE_NC:
    case OP_COND:
    d = find_fixedlength(cc + ((op == OP_CBRA)? IMM2_SIZE : 0), utf, atend, cd);
    if (d < 0) return d;
    branchlength += d;
    do cc += GET(cc, 1); while (*cc == OP_ALT);
    cc += 1 + LINK_SIZE;
    break;

    /* Reached end of a branch; if it's a ket it is the end of a nested call.
    If it's ALT it is an alternation in a nested call. An ACCEPT is effectively
    an ALT. If it is END it's the end of the outer call. All can be handled by
    the same code. Note that we must not include the OP_KETRxxx opcodes here,
    because they all imply an unlimited repeat. */

    case OP_ALT:
    case OP_KET:
    case OP_END:
    case OP_ACCEPT:
    case OP_ASSERT_ACCEPT:
    if (length < 0) length = branchlength;
      else if (length != branchlength) return -1;
    if (*cc != OP_ALT) return length;
    cc += 1 + LINK_SIZE;
    branchlength = 0;
    break;

    /* A true recursion implies not fixed length, but a subroutine call may
    be OK. If the subroutine is a forward reference, we can't deal with
    it until the end of the pattern, so return -3. */

    case OP_RECURSE:
    if (!atend) return -3;
    cs = ce = (pcre_uchar *)cd->start_code + GET(cc, 1);  /* Start subpattern */
    do ce += GET(ce, 1); while (*ce == OP_ALT);           /* End subpattern */
    if (cc > cs && cc < ce) return -1;                    /* Recursion */
    d = find_fixedlength(cs + IMM2_SIZE, utf, atend, cd);
    if (d < 0) return d;
    branchlength += d;
    cc += 1 + LINK_SIZE;
    break;

    /* Skip over assertive subpatterns */

    case OP_ASSERT:
    case OP_ASSERT_NOT:
    case OP_ASSERTBACK:
    case OP_ASSERTBACK_NOT:
    do cc += GET(cc, 1); while (*cc == OP_ALT);
    cc += PRIV(OP_lengths)[*cc];
    break;

    /* Skip over things that don't match chars */

    case OP_MARK:
    case OP_PRUNE_ARG:
    case OP_SKIP_ARG:
    case OP_THEN_ARG:
    cc += cc[1] + PRIV(OP_lengths)[*cc];
    break;

    case OP_CALLOUT:
    case OP_CIRC:
    case OP_CIRCM:
    case OP_CLOSE:
    case OP_COMMIT:
    case OP_CREF:
    case OP_DEF:
    case OP_DNCREF:
    case OP_DNRREF:
    case OP_DOLL:
    case OP_DOLLM:
    case OP_EOD:
    case OP_EODN:
    case OP_FAIL:
    case OP_NOT_WORD_BOUNDARY:
    case OP_PRUNE:
    case OP_REVERSE:
    case OP_RREF:
    case OP_SET_SOM:
    case OP_SKIP:
    case OP_SOD:
    case OP_SOM:
    case OP_THEN:
    case OP_WORD_BOUNDARY:
    cc += PRIV(OP_lengths)[*cc];
    break;

    /* Handle literal characters */

    case OP_CHAR:
    case OP_CHARI:
    case OP_NOT:
    case OP_NOTI:
    branchlength++;
    cc += 2;
#ifdef SUPPORT_UTF
    if (utf && HAS_EXTRALEN(cc[-1])) cc += GET_EXTRALEN(cc[-1]);
#endif
    break;

    /* Handle exact repetitions. The count is already in characters, but we
    need to skip over a multibyte character in UTF8 mode.  */

    case OP_EXACT:
    case OP_EXACTI:
    case OP_NOTEXACT:
    case OP_NOTEXACTI:
    branchlength += (int)GET2(cc,1);
    cc += 2 + IMM2_SIZE;
#ifdef SUPPORT_UTF
    if (utf && HAS_EXTRALEN(cc[-1])) cc += GET_EXTRALEN(cc[-1]);
#endif
    break;

    case OP_TYPEEXACT:
    branchlength += GET2(cc,1);
    if (cc[1 + IMM2_SIZE] == OP_PROP || cc[1 + IMM2_SIZE] == OP_NOTPROP)
      cc += 2;
    cc += 1 + IMM2_SIZE + 1;
    break;

    /* Handle single-char matchers */

    case OP_PROP:
    case OP_NOTPROP:
    cc += 2;
    /* Fall through */

    case OP_HSPACE:
    case OP_VSPACE:
    case OP_NOT_HSPACE:
    case OP_NOT_VSPACE:
    case OP_NOT_DIGIT:
    case OP_DIGIT:
    case OP_NOT_WHITESPACE:
    case OP_WHITESPACE:
    case OP_NOT_WORDCHAR:
    case OP_WORDCHAR:
    case OP_ANY:
    case OP_ALLANY:
    branchlength++;
    cc++;
    break;

    /* The single-byte matcher isn't allowed. This only happens in UTF-8 mode;
    otherwise \C is coded as OP_ALLANY. */

    case OP_ANYBYTE:
    return -2;

    /* Check a class for variable quantification */

    case OP_CLASS:
    case OP_NCLASS:
#if defined SUPPORT_UTF || defined COMPILE_PCRE16 || defined COMPILE_PCRE32
    case OP_XCLASS:
    /* The original code caused an unsigned overflow in 64 bit systems,
    so now we use a conditional statement. */
    if (op == OP_XCLASS)
      cc += GET(cc, 1);
    else
      cc += PRIV(OP_lengths)[OP_CLASS];
#else
    cc += PRIV(OP_lengths)[OP_CLASS];
#endif

    switch (*cc)
      {
      case OP_CRSTAR:
      case OP_CRMINSTAR:
      case OP_CRPLUS:
      case OP_CRMINPLUS:
      case OP_CRQUERY:
      case OP_CRMINQUERY:
      case OP_CRPOSSTAR:
      case OP_CRPOSPLUS:
      case OP_CRPOSQUERY:
      return -1;

      case OP_CRRANGE:
      case OP_CRMINRANGE:
      case OP_CRPOSRANGE:
      if (GET2(cc,1) != GET2(cc,1+IMM2_SIZE)) return -1;
      branchlength += (int)GET2(cc,1);
      cc += 1 + 2 * IMM2_SIZE;
      break;

      default:
      branchlength++;
      }
    break;

    /* Anything else is variable length */

    case OP_ANYNL:
    case OP_BRAMINZERO:
    case OP_BRAPOS:
    case OP_BRAPOSZERO:
    case OP_BRAZERO:
    case OP_CBRAPOS:
    case OP_EXTUNI:
    case OP_KETRMAX:
    case OP_KETRMIN:
    case OP_KETRPOS:
    case OP_MINPLUS:
    case OP_MINPLUSI:
    case OP_MINQUERY:
    case OP_MINQUERYI:
    case OP_MINSTAR:
    case OP_MINSTARI:
    case OP_MINUPTO:
    case OP_MINUPTOI:
    case OP_NOTMINPLUS:
    case OP_NOTMINPLUSI:
    case OP_NOTMINQUERY:
    case OP_NOTMINQUERYI:
    case OP_NOTMINSTAR:
    case OP_NOTMINSTARI:
    case OP_NOTMINUPTO:
    case OP_NOTMINUPTOI:
    case OP_NOTPLUS:
    case OP_NOTPLUSI:
    case OP_NOTPOSPLUS:
    case OP_NOTPOSPLUSI:
    case OP_NOTPOSQUERY:
    case OP_NOTPOSQUERYI:
    case OP_NOTPOSSTAR:
    case OP_NOTPOSSTARI:
    case OP_NOTPOSUPTO:
    case OP_NOTPOSUPTOI:
    case OP_NOTQUERY:
    case OP_NOTQUERYI:
    case OP_NOTSTAR:
    case OP_NOTSTARI:
    case OP_NOTUPTO:
    case OP_NOTUPTOI:
    case OP_PLUS:
    case OP_PLUSI:
    case OP_POSPLUS:
    case OP_POSPLUSI:
    case OP_POSQUERY:
    case OP_POSQUERYI:
    case OP_POSSTAR:
    case OP_POSSTARI:
    case OP_POSUPTO:
    case OP_POSUPTOI:
    case OP_QUERY:
    case OP_QUERYI:
    case OP_REF:
    case OP_REFI:
    case OP_DNREF:
    case OP_DNREFI:
    case OP_SBRA:
    case OP_SBRAPOS:
    case OP_SCBRA:
    case OP_SCBRAPOS:
    case OP_SCOND:
    case OP_SKIPZERO:
    case OP_STAR:
    case OP_STARI:
    case OP_TYPEMINPLUS:
    case OP_TYPEMINQUERY:
    case OP_TYPEMINSTAR:
    case OP_TYPEMINUPTO:
    case OP_TYPEPLUS:
    case OP_TYPEPOSPLUS:
    case OP_TYPEPOSQUERY:
    case OP_TYPEPOSSTAR:
    case OP_TYPEPOSUPTO:
    case OP_TYPEQUERY:
    case OP_TYPESTAR:
    case OP_TYPEUPTO:
    case OP_UPTO:
    case OP_UPTOI:
    return -1;

    /* Catch unrecognized opcodes so that when new ones are added they
    are not forgotten, as has happened in the past. */

    default:
    return -4;
    }
  }
/* Control never gets here */
}
45
Amal Murali 8 Июл 2014 в 22:26
1
Много полезной информации, но по-прежнему нет ответа на вопрос: как работает механизм регулярных выражений PCRE при поиске с нулевой шириной, что исключает использование квантификаторов повторения?
 – 
HamZa
6 Июл 2014 в 05:07
Приятно слышать от тебя. Я добавил раздел Lookbehind with Zero-Width Match but Infinite Repetition, в котором рассматривается просмотр назад с совпадением нулевой ширины, содержащим бесконечное повторение: (?<=(?=@+)) Вы это имеете в виду? Не уверен, так как из примеров Саймона я не мог точно сказать, является ли это общей идеей (сначала так не казалось, поэтому я не рассматривал этот интересный случай).
 – 
zx81
6 Июл 2014 в 07:58
Кроме того, для всех, кто интересуется внутренностями движка, я отследил раздел pcre_compile.c, отвечающий за проверку фиксированной длины строк просмотра назад. Он вызывает функцию find_fixedlength(), код которой я вставил на тот случай, если кому-то понадобится окончательный ответ с высочайшим уровнем детализации.
 – 
zx81
6 Июл 2014 в 08:16
Спасибо, @анубхава! :)
 – 
zx81
10 Июл 2014 в 11:15

Механизмы регулярных выражений предназначены для работы слева направо .

Для опережения движок сопоставляет весь текст справа от текущей позиции. Однако для просмотра назад механизм регулярных выражений определяет длину строки для возврата, а затем проверяет соответствие (снова слева направо).

Итак, если вы предоставите несколько бесконечных квантификаторов, таких как * или +, просмотр назад не будет работать, потому что движок не знает , на сколько шагов нужно вернуться назад.

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

Предположим, вы хотите сопоставить фамилию Panta, только если имя состоит из 5-7 символов.

Возьмем строку:

Full name is Subigya Panta.

Рассмотрим регулярное выражение:

(?<=\b\w{5,7}\b)\sPanta

Как работает двигатель

Механизм распознает наличие положительного просмотра назад и поэтому сначала выполняет поиск слова Panta (с пробелом перед ним). Это совпадение.

Теперь движок соответствует регулярному выражению в ретроспективе. Он отступает на 7 символов назад (поскольку квантификатор жадный). Граница слова соответствует позиции между пробелом и S. Затем он соответствует всем 7 символам, а затем граница следующего слова соответствует положению между a и пробелом.

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

8
Amal Murali 6 Июл 2014 в 07:23
1
У вас там синтаксическая ошибка: {5-7} должно быть {5,7}. Но ваше объяснение применимо только к разновидностям Java и ICU, которые поддерживают просмотр назад с переменной длиной, если максимально возможная длина может быть определена при компиляции регулярного выражения. Ваш пример также будет работать в .NET, JGSoft и Perl 6 (которые вообще не налагают ограничений), но в большинстве вариантов это только просмотр назад с фиксированной длиной.
 – 
Alan Moore
6 Июл 2014 в 06:08
Спасибо, я сделал правку. Согласен, это также работает с PCRE, и вопрос был связан с PCRE.
 – 
DrGeneral
6 Июл 2014 в 06:52
Библиотека PCRE (это то, что использует R, когда вы указываете perl=TRUE) имитирует поведение Perl 5. Таким образом, она поддерживает просмотр назад, состоящий из нескольких альтернатив фиксированной длины (как упоминалось в других ответах), но это не так. разрешить квантификаторы в просмотре назад.
 – 
Alan Moore
6 Июл 2014 в 08:21

справочная страница pcrepattern документирует ограничение, согласно которому утверждения просмотра назад должны иметь либо фиксированную ширину, либо быть несколькими шаблонами фиксированной ширины, разделенными |, а затем объясняет, что это потому, что:

Реализация утверждений просмотра назад заключается в том, чтобы для каждой альтернативы временно переместить текущую позицию назад на фиксированную длину и затем попытаться сопоставить. Если перед текущей позицией недостаточно символов, утверждение не выполняется.

Я не уверен, почему они делают это таким образом, но я предполагаю, что они потратили много времени на создание хорошего механизма повторного сопоставления с возвратом, который работает вперед, и они не хотели дублировать все эти усилия, чтобы написать другой, который бежит назад. Очевидным подходом было бы перебрать строку в обратном направлении - это легко - при сопоставлении с «обратной» версией вашего утверждения просмотра назад. Обратное преобразование «реального» (совместимого с DFA) RE возможно - обратным от обычного языка является регулярный язык - но «расширенные» RE PCRE являются завершенными по Тьюрингу IIRC, и может быть даже невозможно перевернуть один, чтобы бегать назад эффективно в целом. И даже если бы это было так, вероятно, никто на самом деле не заботился настолько, чтобы беспокоиться. В конце концов, утверждения с ретроспективным просмотром - довольно второстепенная функция в общей схеме вещей.

2
Nathaniel J. Smith 5 Июл 2014 в 17:17