Жадность ведет себя по-разному в JavaScript?

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

![(.*?)*]

(я знаю, что * здесь избыточно, но я обнаружил, что следующее за этим довольно интересное поведение).

и если мы попробуем матч против:

![][][]

я ожидал, что первая группа захвата будьте пусты, потому что (.*?) ленив и остановится на первом ] это произошло через. Это действительно то, что происходит в:

я посмотрел вокруг с некоторыми другими языками, например Рубин, java, C# но все ведут себя так, как я ожидал (т. е. возвращают пустые группы захвата).

(regexplanet это golang вкус, по-видимому, также получает непустые группы захвата)

кажется, что движок регулярных выражений JavaScript интерпретирует второй * преобразование .*? С ленивым жадным. Обратите внимание, что преобразование второго * to *? кажется, что регулярное выражение работает так, как я ожидал (как и полное удаление квантора, потому что я знайте, что это избыточно в этой ситуации, но дело не в этом).

* использовался в регулярном выражении, но это поведение похоже на +, ? или {m,n} и преобразование их в ленивую версию дает те же результаты, что и с *?.

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

3 ответов


короткий ответ:

поведение определяется в ECMA-262 спецификации в разделе 15.10.2 Семантика Шаблон, особенно 15.10.2.5 где обсуждается семантика производства Term:: Atom Quantifier.

как небольшое обобщение: пусть E шаблон, который может соответствовать пустая строка. Если существует входная строка S, где пустая строка является первым подходящим выбором в E, шаблоны, которые содержат жадина повторение шаблона E затронуты. Например:

(a*?)*              against     aaaa
!\[(.*?)*\]         against     ![][][]
(.*?){1,4}          against     asdf
(|a)*               against     aaaa
(a|()|b){0,4}       against     aaabbb

Firefox и другие браузеры на основе Webkit, похоже, внимательно следуют спецификациям, в то время как IE отключен от спецификаций в случае, когда нет продолжения затронутого шаблона.

ответ

соответствующая часть спецификаций приведена ниже. Некоторая часть спецификаций опущена ([...]) сохранить дискуссию актуальной. Я объясню, конденсируя информация из спецификаций, сохраняя ее простой.

словарь

  • A государство является упорядоченной парой (endindex включительно, захват), где endindex включительно является целым числом и захват внутренний блок NcapturingParens значения. государства используются для представления частичных состояний соответствия в алгоритмах сопоставления регулярных выражений. Этот endindex включительно один плюс индекс последнего входного символа, до сих пор соответствует шаблону, в то время как захват содержит результаты захвата скобками. [...]. Из-за отступления, многие государства может использоваться в любое время в процессе сопоставления.
  • A MatchResult или государство или специальный маркер провал это означает, что матч не удался.

здесь не должно быть путаницы. государство отслеживает позицию, которая была обработана до сих пор (а также надписи, которые нас не интересуют в данный момент). MatchResult, ну, это результат матча (duh!).

  • A продолжение процедура-это внутреннее закрытие (т. е. внутренняя процедура с некоторыми аргументами, уже связанными со значениями), которое принимает один государство аргумент и возвращает MatchResult результат. Если внутреннее закрытие ссылается на переменные, связанные в функции, которая создает закрытие, закрытие использует значения, которые эти переменные имели в момент создания закрытия. The продолжение пытается сопоставить оставшуюся часть (заданную уже связанными аргументами закрытия) шаблона с входной строкой, начиная с промежуточного состояния, заданного его государствомин, затем бросить SyntaxError исключение.
  • пусть parenIndex число левых скобок в регулярном выражении, которые происходят слева от этого расширения производства термин. [...]
  • пусть parenCount быть числом левых скобок захвата в расширении этого производства Атом. [...]
  • возвращает внутреннее закрытие сопоставления, которое принимает два аргумента, состояние x и продолжение c, и выполняет следующие:
    1. Вызов RepeatMatcher (m, min, max, greedy, x, c, parenIndex, parenCount) и возвращает его результат.

обратите внимание, что m является Сопоставителем для Атом это повторяется, и продолжение c передается из кода, созданного из производственных правил более высокого уровня.

абстрактная операция RepeatMatcher принимает восемь параметров, сопоставление m целое число мин целое число (или ∞) Макс, булево жадина государство x, продолжение c целое число parenIndex, и целое число parenCount, и выполняет следующее:

  1. если Макс равно нулю, затем вызовите c (x) и вернуть его результат.
  2. создать внутреннее закрытие продолжения d для этого требуется один аргумент состояния y и выполняет следующие:
    1. если мин равна нулю и y ' s endindex включительно равна x ' s endindex включительно, а затем вернуться провал.
    2. если мин равно нулю, тогда пусть грн / мин2 равняться нулю; в противном случае пусть грн / мин2 be мин - 1.
    3. если Макс это ∞, то пусть max2 быть ∞; в противном случае пусть max2 be Макс - 1.
    4. Вызов RepeatMatcher (m, min2, max2, greedy, y, c, parenIndex, parenCount) и возвращает его результат.
  3. пусть cap будьте свежей копией xзахват внутренний массив.
  4. для каждого целого числа k, что соответствует parenIndex k и k ? parenIndex + parenCount, set cap[k] к неопределено.
  5. пусть e be x ' s endindex включительно.
  6. пусть xr государство (e, cap).
  7. если мин не ноль, тогда звоните m (xr, d) и вернуть его результат.
  8. если жадина is false, тогда
    1. вызов c (x) и пусть z его результат.
    2. если z не провал, вернуть z.
    3. вызов m (xr, d) и возвратить его результат.
  9. вызов m (xr, d) и пусть z его результат.
  10. если z не провал, вернуть z.
  11. вызов c (x) и вернуть его результат.

Шаг 2 определяет продолжение d где мы пытаемся сопоставить другой экземпляр повторяющегося атома, который позже вызывается на шаге 7 (мин > 0 case), шаг 8.3 (ленивый случай) и Шаг 9 (жадный случай) через Matcher m.

Шаг 5 и 6 создает копию текущего состояния для отслеживания и обнаружения совпадения пустой строки на Шаге 2.

шаги отсюда описывают 3 отдельных случая:

  • Шаг 7 охватывает случай, когда Квантор имеет ненулевой мин значение. Жадность не имеет значения, мы должны соответствовать, по крайней мере, мин экземпляры Atom, прежде чем мы сможем вызвать продолжение c.

  • из-за условия в шаге 7, мин С этого момента 0. Шаг 8 касается случая, когда Квантор ленив. Шаг 9, 10, 11 касается случая, когда Квантор жаден. Обратите внимание на обратный порядок вызова.

вызов m (xr, d) означает соответствие одному экземпляру атома, тогда вызываем продолжение d продолжить повторение.

Назвав Продолжение c (x) означает выход из повторения и соответствие тому, что будет дальше. Обратите внимание, как продолжение c передается в RepeatMatcher внутри продолжения d, так что он доступен для всех последующих итераций повторения.

объяснение причуды в JavaScript RegExp

шаг 2.1 является виновником, который вызывает расхождение в результате между PCRE и JavaScript.

  1. если мин равна нулю и y ' s endindex включительно равна x ' s endindex включительно, а затем вернуться провал.

условие мин = 0 достигается, когда Квантор первоначально имеет 0 как мин (* или {0,n}) или через Шаг 7, который должен вызываться до тех пор, пока мин > 0 (оригинальный квантификатор + или {n,} или {n,m}).

, когда мин = 0 и Квантор жаден, Matcher m будет вызван (в шаге 9), который пытается сопоставить экземпляр Atom и вызвать продолжение d, указанный в шаге 2. Продолжение d будет рекурсивно вызывать RepeatMatcher, который в свою очередь будет вызовите Matcher m (Шаг 9) снова.

без шага 2.1, если Matcher m имеет пустую строку в качестве первого возможного выбора для продвижения вперед во входных данных, итерация (теоретически) будет продолжаться вечно без продвижения вперед. Учитывая ограниченные возможности, которые поддерживает JavaScript RegExp (без прямого объявления обратной ссылки или других причудливых функций), нет необходимости идти вперед с другой итерацией, когда текущая итерация соответствует пустой строке-an пустая строка в любом случае будет сопоставлена снова. Шаг 2.1-это метод JavaScript для работы с повторением пустой строки.

как настроено в шаге 2.1, когда мин = 0, то JavaScript regex engine будет обрезать, когда пустая строка сопоставляется Matcher m (return провал). Это эффективно отклоняет "пустая строка, повторенная конечное число раз, является пустой строкой".

другая сторона эффект шаг 2.1 заключается в том, что с момента, когда мин = 0, пока есть один экземпляр, где Matcher m соответствует непустой строке, последнее повторение Атом гарантированно будет непустым.

напротив, это кажется PCRE (и другие движки) принимает "пустая строка, повторенная конечное число раз, является пустой строкой" и продолжается с остальной частью шаблона. Это объясняет поведение PCRE в случаи, перечисленные выше. Что касается алгоритма, PCRE останавливает повторение после сопоставления пустой строки два раза подряд.


Я сделал несколько тестов и обнаружил, что в Firefox и Chrome, Если в группе есть "жадный" квантификатор и прямо или косвенно содержит один или более ленивыми квантификаторами с нуля, как минимальное число итераций, то для итераций, дополняющий жадный Квантор, где минимальное число итераций уже удовлетворены, левый ленивый квантификатор, который может соответствовать одна или более итераций будет соответствовать одной итерации, если группа хотела найти нулевой длины матча, если ленивый квантификатор были совпадение нулевых итераций.

Е. Г. (.{0,2}?){5,8} соответствует " abc " в "abcdefghijklmnopqrstuvwxyz", потому что .{0,2}? соответствует одной итерации к итерации, 6, 7, и 8 {5,8}.

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

на той же строке, (.{0,3}?){5,6}[ad] соответствует "abcd" в то время как (.{0,3}?){5,6}a соответствует "a".

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

то же самое происходит в Internet Explorer, если и только если есть маркеры, которые не являются необязательными после группы с жадным квантором. Если после группы нет токенов или они все необязательны, то IE ведет себя так большинство других движков регулярных выражений.

объяснение поведения в Firefox и Chrome, по-видимому, представляет собой сочетание двух шагов в разделе 15.10.2.5 в стандарте JavaScript. Шаг 2.1 для RepeatMatcher заставляет механизм регулярных выражений отказывать итерации нулевой длины кванторов, которые уже соответствовали минимальному числу требуемых итераций, вместо того, чтобы просто останавливать продолжение итерации. Шаг 9 оценивает все, что приходит после ленивого квантора, прежде чем оценивать ленивый Квантор себя. В этих примерах это включает в себя продолжающееся повторение жадного квантора. Когда этот жадный Квантор терпит неудачу на шаге 2.1, ленивый Квантор вынужден повторить хотя бы один раз.

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

Opera ведет себя по-другому. (.{0,2}?){5,8} соответствует "abcd" в то время как (.{0,2}?){6,8} соответствует "abcde". Opera, похоже, заставляет ленивый квантификатор соответствовать хотя бы одной итерации для всех, кроме первой итерации жадного квантификатора, а затем прекратить итерацию, когда жадный квантификатор нашел требуемое минимальное количество итераций.

я рекомендую не использовать группы или альтернативы, в которых все необязательно. Убедитесь, что каждый альтернатива и каждая группа соответствует чему-то. Если группа должна быть необязательной, используйте ? чтобы сделать всю группу необязательной, вместо того, чтобы делать все внутри группы необязательным. Это уменьшит количество перестановок, которые должен попробовать движок регулярных выражений.


это не отвечает почему grediness ведет себя по-разному в Javascript, но это показывает, что это не ошибка, и она была протестирована, чтобы вести себя так. Я возьму в качестве примера двигатель V8 google. Я нашел аналогичный пример в их тестах.

/ test/mjsunit/third_party / regexp-pcre.js:

line 1085: res[1006] = /([a]*?)*/;
line 4822: assertToStringEquals("aaaa,a", res[1006].exec("aaaa "), 3176);

https://code.google.com/p/v8/source/browse/trunk/test/mjsunit/third_party/regexp-pcre.js#1085

этот код хорошо работает для Javascript http://regex101.com/r/nD0uG8 но он не имеет одинаковых выходных данных для PCRE php и python.

обновление: О вашем вопросе:

Кажется, что движок регулярных выражений JavaScript интерпретирует второй * для преобразования .*? от ленивого до жадного

https://code.google.com/p/v8/source/browse/trunk/src/parser.cc#5157

RegExpQuantifier::QuantifierType quantifier_type = RegExpQuantifier::GREEDY;
if (current() == '?') {
    quantifier_type = RegExpQuantifier::NON_GREEDY;
    Advance();
} else if (FLAG_regexp_possessive_quantifier && current() == '+') {
    // FLAG_regexp_possessive_quantifier is a debug-only flag.
    quantifier_type = RegExpQuantifier::POSSESSIVE;
    Advance();
}