Лучшие идеи синтаксиса регулярных выражений
Мне нужна помощь, чтобы завершить мою идею о регулярных выражениях.
Вступление
Был вопрос о лучшем синтаксисе для регулярных выражений в SE, но я не думаю, что я бы использовал свободный синтаксис. Это, конечно, хорошо для новичков, но в случае сложного регулярного выражения вы заменяете строку тарабарщины на целую страницу немного лучшего тарабарщины. Мне нравится подход Мартина Фаулера, где регулярное выражение состоит из небольших частей. Его решение читабельно, но сделано вручную; он предлагает умный способ построить сложное регулярное выражение вместо класса, поддерживающего его.
Я пытаюсь сделать это в классе, используя что-то вроде (сначала посмотрите его пример)
final MyPattern pattern = MyPattern.builder()
.caseInsensitive()
.define("numberOfPoints", "\\d+")
.define("numberOfNights", "\\d+")
.define("hotelName", ".*")
.define(' ', "\\s+")
.build("score `numberOfPoints` for `numberOfNights` nights? at `hotelName`");
MyMatcher m = pattern.matcher("Score 400 FOR 2 nights at Minas Tirith Airport");
System.out.println(m.group("numberOfPoints")); // prints 400
где свободный синтаксис используется для объединения регулярных выражений, расширенных следующим образом:
- определить именованные шаблоны и использовать их, заключив в обратные знаки
`name`
создает именованную группу- мнемоника: оболочка захватывает результат команды, заключенной в backticks
`:name`
создает группу без захвата- мнемоника: похожа на
(?:
...)
- мнемоника: похожа на
`-name`
создает обратную ссылку- мнемоника: тире связывает его с предыдущим вхождением
- переопределить отдельные символы и использовать его везде, если не указано
- здесь только некоторые символы (например,
~ @#%
") разрешены- переопределение
+
или же(
было бы очень запутанным, так что это не разрешено - переопределение пространства для обозначения любого расстояния очень естественно в приведенном выше примере
- переопределение символа может сделать шаблон более компактным, что хорошо, если не использовать слишком много
- например, используя что-то вроде
define('#', "\\\\")
для сопоставления обратной косой черты может сделать шаблон более читабельным
- переопределение
- здесь только некоторые символы (например,
- переопределить некоторые цитируемые последовательности, такие как
\s
или же\w
- стандартные определения не соответствуют Unicode
- иногда вы можете иметь собственное представление о том, что такое слово или пробел
Названные шаблоны служат своего рода локальными переменными, помогающими разложить сложное выражение на маленькие и простые для понимания части. Правильный шаблон именования часто делает комментарии ненужными.
Вопросы
Вышесказанное не должно быть сложным для реализации (я уже сделал большую часть этого) и может быть действительно полезным, я надеюсь.Ты так думаешь?
Тем не менее, я не уверен, как он должен вести себя в скобках, иногда имеет смысл использовать определения, а иногда нет, например, в
.define(' ', "\\s") // a blank character
.define('~', "/\**[^*]+\*/") // an inline comment (simplified)
.define("something", "[ ~\\d]")
расширяя пространство для \s
имеет смысл, но расширение тильды - нет.Может быть, должен быть отдельный синтаксис, чтобы как-то определять собственные классы символов?
Можете ли вы привести примеры, когда названный шаблон очень полезен или не полезен вообще? Мне нужны некоторые пограничные случаи и некоторые идеи для улучшения.
Реакция на ответ Триста
Комментарии к его возражениям
- Отсутствие многострочных шаблонных строк.
- В Java нет многострочных строк, которые я бы хотел изменить, но не могу.
- Свобода от безумно обременительных и подверженных ошибкам двойных ударов...
- Это опять то, что я не могу сделать, я могу только предложить обходной путь, с. ниже.
- Отсутствие исключений времени компиляции для недопустимых литералов регулярных выражений и отсутствие кэширования во время компиляции правильно скомпилированных литералов регулярных выражений.
- Поскольку регулярные выражения являются лишь частью стандартной библиотеки, а не самого языка, здесь ничего нельзя сделать.
- Нет средств отладки или профилирования.
- Я ничего не могу сделать здесь.
- Несоблюдение UTS#18.
- Это может быть легко решено путем переопределения соответствующих шаблонов, как я предложил. Это не идеально, так как в отладчике вы увидите взорванные замены.
Похоже, вы не любите Java. Я был бы рад увидеть некоторые улучшения синтаксиса, но я ничего не могу с этим поделать. Я ищу что-то, работающее с текущей Java.
RFC 5322
Ваш пример может быть легко написан с использованием моего синтаксиса:
final MyPattern pattern = MyPattern.builder()
.define(" ", "") // ignore spaces
.useForBackslash('#') // (1): see (2)
.define("address", "`mailbox` | `group`")
.define("WSP", "[\u0020\u0009]")
.define("DQUOTE", "\"")
.define("CRLF", "\r\n")
.define("DIGIT", "[0-9]")
.define("ALPHA", "[A-Za-z]")
.define("NO_WS_CTL", "[\u0001-\u0008\u000b\u000c\u000e-\u001f\u007f]") // No whitespace control
...
.define("domain_literal", "`CFWS`? #[ (?: `FWS`? `dcontent`)* `FWS`? #] `CFWS1?") // (2): see (1)
...
.define("group", "`display_name` : (?:`mailbox_list` | `CFWS`)? ; `CFWS`?")
.define("angle_addr", "`CFWS`? < `addr_spec` `CFWS`?")
.define("name_addr", "`display_name`? `angle_addr`")
.define("mailbox", "`name_addr` | `addr_spec`")
.define("address", "`mailbox` | `group`")
.build("`address`");
Недостатки
Переписывая ваш пример, я столкнулся со следующими проблемами:
- Как нет
\xdd
escape-последовательности\udddd
должен быть использован - Использование другого символа вместо обратной косой черты немного странно
- Поскольку я предпочитаю писать это снизу вверх, мне пришлось принять ваши строки вспять
- Без особого представления, что он делает, я, кроме себя, сделал несколько ошибок
С другой стороны: - Игнорирование пробелов не проблема - Комментарии не проблема - Удобочитаемость хорошая
И самое главное: это простая Java и использует существующий regex-движок как есть.
2 ответа
Именованные Примеры захвата
Можете ли вы привести примеры, когда названный шаблон очень полезен или не полезен вообще?
В ответ на ваш вопрос, вот пример, где именованные шаблоны особенно полезны. Это шаблон Perl или PCRE для анализа почтового адреса RFC 5322. Во-первых, это в /x
режим в силу (?x)
, Во-вторых, он отделяет определения от вызова; названная группа address
это то, что делает полный анализ рекурсивного спуска. Его определение следует за неисполнением (?DEFINE)…)
блок.
(?x) # allow whitespace and comments
(?&address) # this is the capture we call as a "regex subroutine"
# the rest is all definitions, in a nicely BNF-style
(?(DEFINE)
(?<address> (?&mailbox) | (?&group))
(?<mailbox> (?&name_addr) | (?&addr_spec))
(?<name_addr> (?&display_name)? (?&angle_addr))
(?<angle_addr> (?&CFWS)? < (?&addr_spec) > (?&CFWS)?)
(?<group> (?&display_name) : (?:(?&mailbox_list) | (?&CFWS))? ; (?&CFWS)?)
(?<display_name> (?&phrase))
(?<mailbox_list> (?&mailbox) (?: , (?&mailbox))*)
(?<addr_spec> (?&local_part) \@ (?&domain))
(?<local_part> (?&dot_atom) | (?"ed_string))
(?<domain> (?&dot_atom) | (?&domain_literal))
(?<domain_literal> (?&CFWS)? \[ (?: (?&FWS)? (?&dcontent))* (?&FWS)?
\] (?&CFWS)?)
(?<dcontent> (?&dtext) | (?"ed_pair))
(?<dtext> (?&NO_WS_CTL) | [\x21-\x5a\x5e-\x7e])
(?<atext> (?&ALPHA) | (?&DIGIT) | [!#\$%&'*+-/=?^_`{|}~])
(?<atom> (?&CFWS)? (?&atext)+ (?&CFWS)?)
(?<dot_atom> (?&CFWS)? (?&dot_atom_text) (?&CFWS)?)
(?<dot_atom_text> (?&atext)+ (?: \. (?&atext)+)*)
(?<text> [\x01-\x09\x0b\x0c\x0e-\x7f])
(?<quoted_pair> \\ (?&text))
(?<qtext> (?&NO_WS_CTL) | [\x21\x23-\x5b\x5d-\x7e])
(?<qcontent> (?&qtext) | (?"ed_pair))
(?<quoted_string> (?&CFWS)? (?&DQUOTE) (?:(?&FWS)? (?&qcontent))*
(?&FWS)? (?&DQUOTE) (?&CFWS)?)
(?<word> (?&atom) | (?"ed_string))
(?<phrase> (?&word)+)
# Folding white space
(?<FWS> (?: (?&WSP)* (?&CRLF))? (?&WSP)+)
(?<ctext> (?&NO_WS_CTL) | [\x21-\x27\x2a-\x5b\x5d-\x7e])
(?<ccontent> (?&ctext) | (?"ed_pair) | (?&comment))
(?<comment> \( (?: (?&FWS)? (?&ccontent))* (?&FWS)? \) )
(?<CFWS> (?: (?&FWS)? (?&comment))*
(?: (?:(?&FWS)? (?&comment)) | (?&FWS)))
# No whitespace control
(?<NO_WS_CTL> [\x01-\x08\x0b\x0c\x0e-\x1f\x7f])
(?<ALPHA> [A-Za-z])
(?<DIGIT> [0-9])
(?<CRLF> \x0d \x0a)
(?<DQUOTE> ")
(?<WSP> [\x20\x09])
)
Я настоятельно рекомендую не изобретать совершенно хороший руль. Начните с того, чтобы стать PCRE-совместимым. Если вы хотите выйти за рамки базовых шаблонов Perl5, таких как описанный выше RFC5322-парсер, всегда есть шаблоны Perl6, на которые можно опираться.
Это действительно очень полезно проводить исследования существующей практики и литературы, прежде чем отправиться на открытую миссию НИОКР. Все эти проблемы давно решены, иногда довольно элегантно.
Улучшение синтаксиса Java Regex
Если вам действительно нужны лучшие идеи синтаксиса регулярных выражений для Java, вы должны сначала устранить эти недостатки в регулярных выражениях Java:
- Отсутствие многострочных шаблонных строк, как показано выше.
- Свобода от безумно обременительной и подверженной ошибкам двойной обратной косой черты, как также продемонстрировано выше.
- Отсутствие исключений времени компиляции для недопустимых литералов регулярных выражений и отсутствие кэширования во время компиляции правильно скомпилированных литералов регулярных выражений.
- Невозможно изменить что-то вроде
"foo".matches(pattern)
использовать лучшую библиотеку шаблонов, частично, но не исключительно из-заfinal
классы, которые не могут быть переопределены. - Нет средств отладки или профилирования.
- Несоответствие UTS#18: поддержка базовых регулярных выражений, самых элементарных шагов, необходимых для того, чтобы сделать регулярные выражения Java полезными для Unicode. Их в настоящее время нет. Они даже не поддерживают свойства Unicode 3.1 десятилетия назад, что означает, что вы не можете использовать шаблоны Java для Unicode любым разумным способом; основные строительные блоки отсутствуют.
Из них первые 3 были адресованы на нескольких языках JVM, включая Groovy и Scala; даже Clojure идет туда-сюда.
Второй набор из 3 шагов будет более жестким, но абсолютно обязательным. Последнее, отсутствие даже самой базовой поддержки Unicode в регулярных выражениях, просто убивает Java для работы с Unicode. Это совершенно непростительно в конце игры. В случае необходимости я могу привести множество примеров, но вы должны мне доверять, потому что я действительно знаю, о чем я говорю здесь.
Только после того, как вы выполнили все это, вы должны быть обеспокоены исправлением регулярных выражений Java, чтобы они могли догнать текущее состояние дел в сопоставлении с образцом. До тех пор, пока вы не позаботитесь об этих прошлых упущениях, вы не сможете начать смотреть в настоящее, не говоря уже о будущем.
Я думаю, что, возможно, Регулярное выражение на самом деле не то, что нужно, а скорее что-то вроде библиотеки Parser-Combinator (которая может работать с символами и / или включать регулярные выражения в свои конструкции).
То есть, шаг за пределы области регулярных выражений (настолько нерегулярно, насколько они могут быть реализованы - tchrist определенно наслаждается реализацией Perl;-) и в контекстно-свободные грамматики - или по крайней мере те, которые могут быть представлены в LL(n), желательно с минимальным возвратом.
Scala: Волшебство Begind Parse-Combinators Обратите внимание, как это выглядит очень похоже на BCNF. Имеет приятное вступление.
Хаскель: Парсек То же.
Некоторыми примерами на Java являются JParsec и JPC.
Java, как язык, однако, не так благоприятен для таких бесшовных расширений DSL, как некоторые конкуренты;-)