Написание анализатора для регулярных выражений

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

Итак, чтобы правильно научить себя и правильно понимать регулярные выражения, я решил делать то, что я всегда делаю, пытаясь чему-то научиться; т.е. попытайтесь написать что-то амбициозное, что я, вероятно, покину, как только почувствую, что я достаточно научился.

С этой целью я хочу написать парсер регулярного выражения в Python. В этом случае "узнать достаточно" означает, что я хочу реализовать парсер, который полностью может понять синтаксис расширенного регулярного выражения Perl. Однако он не должен быть самым эффективным парсером или даже обязательно использоваться в реальном мире. Он просто должен правильно совпадать или не соответствовать шаблону в строке.

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

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

Ответ 1

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

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

Регулярное соответствие выражений может быть простым и быстрым (но медленный в Java, Perl, PHP, Python, Ruby,...)

В нем объясняется, как многие из популярных языков программирования реализуют регулярные выражения таким образом, который может быть очень медленным для некоторых регулярных выражений, и объясняет немного другой метод, который выполняется быстрее. Статья содержит некоторые сведения о том, как работает предлагаемая реализация, включая некоторые исходные тексты на C. Это может быть немного тяжелое чтение, если вы только начинаете изучать регулярные выражения, но я думаю, что стоит знать о различии между двумя подходы.

Ответ 2

Я уже дал +1 марку Байерсу, но, насколько я помню, в этой статье действительно не говорится о том, как работает регулярное выражение, не объясняя, почему один алгоритм плох, а другой намного лучше. Может быть, что-то в ссылках?

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

То, что я (очень быстро) описал, - это подход, сделанный в Современный дизайн компилятора.

Представьте, что у вас есть следующее регулярное выражение...

a (b c)* d

Буквы представляют буквенные символы для соответствия. * - обычное совпадение с нулем или большим числом повторений. Основная идея - вывести состояния, основанные на пунктирных правилах. State zero мы возьмем за состояние, в котором ничего не было сопоставлено, поэтому точка идет впереди...

0 : .a (b c)* d

Единственное возможное совпадение - 'a', поэтому следующее состояние, которое мы получаем, является...

1 : a.(b c)* d

Теперь у нас есть две возможности - соответствовать "b" (если есть хотя бы один повтор "b c" ) или соответствовать "d" в противном случае. Примечание. В основном мы делаем поиск по орграфу здесь (сначала по глубине или по ширине, или по другому), но мы обнаруживаем орграф, когда мы его ищем. Предполагая, что стратегия в ширину, нам нужно будет поставить очередь на один из наших случаев для последующего рассмотрения, но я проигнорирую этот вопрос здесь. Во всяком случае, мы обнаружили два новых состояния...

2 : a (b.c)* d
3 : a (b c)* d.

Состояние 3 является конечным состоянием (их может быть несколько). Для состояния 2 мы можем сопоставлять только "c", но после этого мы должны быть осторожны с точкой. Мы получаем "a. (B c) * d" - то же, что и состояние 1, поэтому нам не нужно новое состояние.

IIRC, подход в Modern Compiler Design заключается в том, чтобы перевести правило, когда вы нажмете на оператора, чтобы упростить обработку точки. Состояние 1 будет преобразовано в...

1 : a.b c (b c)* d
    a.d

Таким образом, ваш следующий параметр должен либо соответствовать первому повторению, либо пропустить повторение. Следующие состояния из этого эквивалентны состояниям 2 и 3. Преимущество такого подхода состоит в том, что вы можете отбросить все свои прошлые матчи (все до "." ), Поскольку вы заботитесь только о будущих матчах. Обычно это дает меньшую модель состояния (но не обязательно минимальную).

EDIT Если вы отбрасываете уже согласованные данные, ваше описание состояния представляет собой представление набора строк, которое может произойти с этой точки.

В терминах абстрактной алгебры это является своеобразным замыканием множества. Алгебра в основном представляет собой набор с одним (или более) оператором. Наш набор - описания состояний, а наши операторы - наши переходы (совпадения персонажей). Закрытое множество - это приложение, в котором применение любого оператора к любому члену в наборе всегда создает другой элемент, который находится в наборе. Закрытие множества - это конечное множество, которое является замкнутым. Таким образом, в основном, начиная с очевидного состояния старта, мы строим минимальное множество состояний, которое замкнуто относительно нашего набора операторов перехода - минимального множества достижимых состояний.

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

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

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

Алгоритм Hopcrofts - эффективный способ справиться с этой основной идеей.

Особенно интересно, что минимизация состоит в том, что каждый детерминированный конечный автомат имеет ровно одну минимальную форму. Кроме того, алгоритм Hopcrofts будет давать то же представление этой минимальной формы, независимо от того, какое представление о том, в каком большем случае это началось. То есть это "каноническое" представление, которое может быть использовано для получения хэша или для произвольно-непротиворечивых порядков. Это означает, что вы можете использовать минимальные автоматы как ключи в контейнерах.

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

BTW - взгляните на остальную часть сайт Дика Грунеса - у него есть бесплатная книга PDF по методам разбора. Первое издание Modern Compiler Design довольно неплохое ИМО, но, как вы увидите, появляется второе издание.

Ответ 4

Там интересная (если немного короткая) глава в Beautiful Code Брайана Кернигана, соответственно называемая "Регулятор регулярного выражения". В нем он обсуждает простой совпадение, которое может соответствовать буквальным символам и символам .^$*.

Ответ 5

Я согласен с тем, что создание механизма регулярного выражения улучшит понимание, но вы взглянули на ANTLR??. Он автоматически генерирует парсеры для любого языка. Поэтому, возможно, вы можете попробовать свои силы, взяв одну из языковых грамматик, перечисленных в "Примеры грамматики" , и пройдите через созданный им AST и парсер. Он генерирует действительно сложный код, но у вас будет хорошее понимание того, как работает парсер.