Преобразование рекурсивного шаблона регулярных выражений PCRE в определение групп балансировки .NET - PullRequest
21 голосов
/ 28 июля 2010

PCRE имеет функцию, называемую рекурсивным шаблоном, которая может использоваться для сопоставления вложенных подгрупп.Например, рассмотрим «грамматику»

Q -> \w | '[' A ';' Q* ','? Q* ']' | '<' A '>'
A -> (Q | ',')*
// to match ^A$.

. Это можно сделать в PCRE с шаблоном

^((?:,|(\w|\[(?1);(?2)*,?(?2)*\]|<(?1)>))*)$

(Пример теста: http://www.ideone.com/L4lHE)

Следуетсовпадение:

abcdefg abc,def,ghi abc,,,def ,,,,,, [abc;] [a,bc;] sss[abc;d] as[abc;d,e] [abc;d,e][fgh;j,k] <abc> [<a>b;<c,d>,<e,f>] <a,b,c> <a,bb,c> <,,,><> <><> <>,<> a<<<<>>><a>> <<<<<>>>><><<<>>>> <z>[a;b] <z[a;b]> [[;];] [,;,] [;[;]] [<[;]>;<[;][;,<[;,]>]>]

Не должно совпадать:

<a bc> <abc<de> [a<b;c>;d,e] [a] <<<<<>>>><><<<>>>>> <<<<<>>>><><<<>>> [abc;def;] [[;],] [;,,] [abc;d,e,f] [<[;]>;<[;][;,<[;,]>]]> <z[a;b>]

Нет рекурсивного шаблона в.NET. Вместо этого он предоставляет балансировочных групп для стековых манипуляций для сопоставления простых вложенных шаблонов.

Возможно ли преобразовать вышеуказанный шаблон PCRE в стиль .NET Regex?

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

Ссылки

Ответы [ 2 ]

12 голосов
/ 18 декабря 2013

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

Немного другое обозначение для грамматик

  • Во-первых, нам нужны грамматические правила (например, A и Q в вопросе).
  • У нас есть один стек. Стек может содержать только правила.
  • На каждом шаге мы извлекаем текущее состояние из стека, сопоставляем с тем, что нам нужно, и помещаем дополнительные правила в стек. Когда мы закончим с состоянием, мы ничего не нажимаем и не возвращаемся в предыдущее состояние.

Эта нотация находится где-то между CFG и Автоматом Pushdown , где мы помещаем правила в стек.

* * Пример тысяча двадцать-один: * * 1 022

Начнем с простого примера: a n b n . Обычная грамматика для этого языка:

S -> aSb | ε

Мы можем перефразировать это в соответствии с обозначением:

# Start with <push S>
<pop S> -> "a" <push B> <push S> | ε
<pop B> -> "b"

На словах:

  • Мы начинаем с S в стеке.
  • Когда мы извлекаем S из стека, мы можем либо:
    • Ничего не соответствует или ...
    • соответствует «a», но затем мы должны поместить состояние B в стек. Это обещание, мы будем соответствовать "б". Затем мы также нажимаем S, чтобы мы могли продолжать сопоставлять «а», если хотим.
  • Когда мы подобрали достаточно «a», мы начинаем выталкивать B s из стека и сопоставлять «b» для каждого.
  • Когда это будет сделано, мы сопоставим четное число «a» и «b».

или, более свободно:

Когда мы находимся в случае S, сопоставьте «a» и нажмите B, а затем S или ничего не подходите.
Когда мы находимся в случае B, сопоставьте «b».

Во всех случаях мы извлекаем текущее состояние из стека.

Запись шаблона в регулярном выражении .Net

Нам нужно как-то представлять разные состояния. Мы не можем выбрать '1' '2' '3' или 'a' 'b' 'c' , потому что они могут быть недоступны в нашей входной строке - мы можем соответствует только тому, что присутствует в строке.
Одним из вариантов является нумерация наших состояний (в приведенном выше примере S будет указывать номер 0, а B - это состояние 1).
Для состояния S ? мы можем поместить ? символов в стек. Для удобства мы вставим первые ? символов в начале строки. Опять же, нас не волнует , что это символы, сколько их.

Состояние нажатия

В .Net, если мы хотим поместить первые 5 символов строки в стек, мы можем написать:

(?<=(?=(?<StateId>.{5}))\A.*)

Это немного запутанно:

  • (?<=…\A.*) - это взгляд назад, идущий до самого начала строки.
  • Когда мы на старте, впереди есть взгляд: (?=…). Это сделано для того, чтобы мы могли соответствовать текущей позиции - если мы находимся в позиции 2 , у нас нет 5 символов перед нами. Итак, мы смотрим назад и с нетерпением ждем.
  • (?<StateId>.{5}) поместите 5 символов в стек, указывая, что в какой-то момент нам нужно сопоставить состояние 5.

Pop state

Согласно нашим обозначениям, мы всегда извлекаем верхнее состояние из стека. Это легко: (?<-StateId>).
Но прежде чем мы это сделаем, мы хотим знать, в каком состоянии это было - или сколько символов оно захватило. Более конкретно, нам нужно явно проверять каждое состояние, например, блок switch / case. Итак, чтобы проверить, имеет ли стек текущее состояние 5:

(?<=(?=.{5}(?<=\A\k<StateId>))\A.*)
  • Опять же, (?<=…\A.*) идет до самого начала.
  • Теперь мы опережаем (?=.{5}…) на пять символов ...
  • И используйте другой вид сзади, (?<=\A\k<StateId>), чтобы проверить, действительно ли в стеке 5 символов.

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

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

Результат

Наш шаблон для n b n выглядит следующим образом:

\A
# Push State A, Index = 0
(?<StateId>)
(?:
    (?:
        (?:
            # When In State A, Index = 0
            (?<=(?=.{0}(?<=\A\k<StateId>))\A.*)
            (?<-StateId>)
            (?:
                # Push State B, Index = 1
                (?<=(?=(?<StateId>.{1}))\A.*)
                a
                # Push State A, Index = 0
                (?<StateId>)
                |

            )
        )
        |
        (?:
            # When In State B, Index = 1
            (?<=(?=.{1}(?<=\A\k<StateId>))\A.*)
            (?<-StateId>)
            b
        )
        |\Z
    ){2}
)+
\Z
# Assert state stack is empty
(?(StateId)(?!))

Рабочий пример для Regex Storm

Примечания:

  • Обратите внимание, что квантификатор вокруг состояний равен (?:(?:…){2})+, то есть (число состояний) × (длина входа).Я также добавил чередование для \Z.Давайте не будем вдаваться в подробности, но это обходной путь для раздражающей оптимизации в движке .Net.
  • То же самое можно записать как (?<A>a)+(?<-A>b)+(?(A)(?!)) - это всего лишь упражнение.

Чтобы ответить на вопрос

Грамматика вопроса может быть переписана как:

# Start with <push A>
<pop A> -> <push A> ( @"," | <push Q> ) | ε
<pop Q> -> \w
           | "<" <push Q2Close> <push A>
           | "[" <push Q1Close> <push QStar> <push Q1Comma> <push QStar> <push Q1Semicolon> <push A>
<pop Q2Close> -> ">"
<pop QStar> -> <push QStar> <push Q> | ε 
<pop Q1Comma> -> ","?
<pop Q1Semicolon> -> ";"
<pop Q1Close> -> "]"

Шаблон:

\A
# Push State A, Index = 0
(?<StateId>)
(?:
    (?:
        (?:
            # When In State A, Index = 0
            (?<=(?=.{0}(?<=\A\k<StateId>))\A.*)
            (?<-StateId>)
            (?:
                # Push State A, Index = 0
                (?<StateId>)
                (?:
                    ,
                    |
                    # Push State Q, Index = 1
                    (?<=(?=(?<StateId>.{1}))\A.*)
                )
                |

            )
        )
        |
        (?:
            # When In State Q, Index = 1
            (?<=(?=.{1}(?<=\A\k<StateId>))\A.*)
            (?<-StateId>)
            (?:
                \w
                |
                <
                # Push State Q2Close, Index = 2
                (?<=(?=(?<StateId>.{2}))\A.*)
                # Push State A, Index = 0
                (?<StateId>)
                |
                \[
                # Push State Q1Close, Index = 6
                (?<=(?=(?<StateId>.{6}))\A.*)
                # Push State QStar, Index = 3
                (?<=(?=(?<StateId>.{3}))\A.*)
                # Push State Q1Comma, Index = 4
                (?<=(?=(?<StateId>.{4}))\A.*)
                # Push State QStar, Index = 3
                (?<=(?=(?<StateId>.{3}))\A.*)
                # Push State Q1Semicolon, Index = 5
                (?<=(?=(?<StateId>.{5}))\A.*)
                # Push State A, Index = 0
                (?<StateId>)
            )
        )
        |
        (?:
            # When In State Q2Close, Index = 2
            (?<=(?=.{2}(?<=\A\k<StateId>))\A.*)
            (?<-StateId>)
            >
        )
        |
        (?:
            # When In State QStar, Index = 3
            (?<=(?=.{3}(?<=\A\k<StateId>))\A.*)
            (?<-StateId>)
            (?:
                # Push State QStar, Index = 3
                (?<=(?=(?<StateId>.{3}))\A.*)
                # Push State Q, Index = 1
                (?<=(?=(?<StateId>.{1}))\A.*)
                |

            )
        )
        |
        (?:
            # When In State Q1Comma, Index = 4
            (?<=(?=.{4}(?<=\A\k<StateId>))\A.*)
            (?<-StateId>)
            ,?
        )
        |
        (?:
            # When In State Q1Semicolon, Index = 5
            (?<=(?=.{5}(?<=\A\k<StateId>))\A.*)
            (?<-StateId>)
            ;
        )
        |
        (?:
            # When In State Q1Close, Index = 6
            (?<=(?=.{6}(?<=\A\k<StateId>))\A.*)
            (?<-StateId>)
            \]
        )
        |\Z
    ){7}
)+
\Z
# Assert state stack is empty
(?(StateId)(?!))

К сожалению, он слишком длинный, чтобы поместиться в URL, поэтому нет онлайн-примера.

Если бы мы использовали "двоичные" стеки с одним или нулевым символом, это выглядело бы так: https://gist.github.com/kobi/8012361

Вот скриншот паттерна, проходящего все тесты: http://i.stack.imgur.com/IW2xr.png

Bonus

Движок .Net может делать больше, чем просто балансировать - он также может захватывать каждый экземплярA или Q.Это требует небольшой модификации шаблона: https://gist.github.com/kobi/8156968.
Обратите внимание, что мы добавили группы Start, A и Q к шаблону, но они не влияют на поток, они используются исключительно для захвата.

Результат: например, для строки "[<a>b;<c,d>,<e,f>]" мы можем получить эти Capture s:

Group A
    (0-17) [<a>b;<c,d>,<e,f>]
    (1-4) <a>b
    (2-2) a
    (7-9) c,d
    (13-15) e,f
Group Q
    (0-17) [<a>b;<c,d>,<e,f>]
    (1-3) <a>
    (2-2) a
    (4-4) b
    (6-10) <c,d>
    (7-7) c
    (9-9) d
    (12-16) <e,f>
    (13-13) e
    (15-15) f

Открытые вопросы

  • Может ли каждая грамматика быть преобразована в нотацию стека состояний?
  • Is (счетчик состояний) × (входдлина) достаточно шагов, чтобы соответствовать всем словам?Какая другая формула может работать?

Исходный код

Код, использованный для генерации этих шаблонов и всех тестовых случаев, можно найти в https://github.com/kobi/RecreationalRegex

9 голосов
/ 29 июля 2010

Ответ: (возможно) Да.

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

^(?:
    (\w(?<Q>)) # Q1
    |
    (<(?<Angle>))  #Q2 - start <
    |
    (\>(?<-Angle>)(?<-A>)?(?<Q>))  #Q2 - end >, match Q
    |
    (\[(?<Block>))  # Q3 start - [
    |
    (;(?<Semi-Block>)(?<-A>)?)  #Q3 - ; after [
    |
    (\](?<-Semi>)(?<-Q>)*(?<Q>))  #Q3 ] after ;, match Q
    |
    ((,|(?<-Q>))*(?<A>))   #Match an A group
)*$
# Post Conditions
(?(Angle)(?!))
(?(Block)(?!))
(?(Semi)(?!))

Отсутствует часть разрешения запятых в Q->[A;Q*,?Q*], и по какой-то причине допускается [A;A], поэтому он соответствует [;,,] и [abc;d,e,f].Остальные строки совпадают с тестовыми примерами.
Еще одним незначительным моментом является проблема с переносом в стек с пустым захватом - это не так.A принимает Ø, поэтому мне пришлось использовать (?<-A>)?, чтобы проверить, захвачен ли он.

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

Почему это не работает?

Невозможно синхронизировать стеки: если я нажму (?<A>) и (?<B>), я могу вытолкнуть их в любом порядке.Вот почему этот шаблон не может отличить <z[a;b>] от <z[a;b]> ... нам нужен один стек для всех.
Этот можно решить для простых случаев, но здесь у нас есть кое-что гораздо более сложное -Целое Q или A, а не только "<" или "[". </p>

...