Статьи

Альтернатива регулярным выражениям: apg-exp

Эта статья была рецензирована Себастьяном Зейтцем и Альмиром Биджедиком . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!

Вряд ли кто-нибудь из программистов избежит необходимости время от времени использовать регулярные выражения в той или иной форме. Для многих синтаксис шаблона может показаться загадочным и запретным. В этом руководстве будет представлен новый механизм сопоставления с образцом, apg-exp — многофункциональная альтернатива RegExp с синтаксисом шаблонов ABNF, который немного проще для глаз.

Быстрое сравнение

Вам когда-нибудь нужно было проверять адрес электронной почты и сталкиваться с чем-то вроде этого?

^[\w!#$%&'*+/=?^_`{|}~-]+(?:\.[\w!#$%&'*+/=?^_`{|}~-]+)*@(?:[A-Z0-9-]+\.)+[AZ]{2,6}$ 

Механизм сопоставления с образцом является правильным инструментом для работы. Это хорошо разработанное, хорошо написанное регулярное выражение. Работает отлично. Так что не нравится?

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

  • Трудно читать
  • Еще сложнее написать
  • Трудно поддерживать

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

Однако существует альтернативный синтаксис, который существует почти столько же лет, очень популярен среди авторов и пользователей технических спецификаций Интернета, обладает всеми возможностями регулярных выражений, но редко используется в мире программирования на JavaScript. А именно, дополненная форма B ackus- N aur F , или ABNF, формально определенная IETF в RFC 5234 и RFC 7405 .

Давайте посмотрим, как этот же адрес электронной почты может выглядеть в ABNF.

 email-address = local "@" domain local = local-word *("." local-word) domain = 1*(sub-domain ".") top-domain local-word = 1*local-char sub-domain = 1*sub-domain-char top-domain = 2*6top-domain-char local-char = alpha / num / special sub-domain-char = alpha / num / "-" top-domain-char = alpha alpha = %d65-90 / %d97-122 num = %d48-57 special = %d33 / %d35 / %d36-39 / %d42-43 / %d45 / %d47 / %d61 / %d63 / %d94-96 / %d123-126 

Конечно, не такой компактный, но, как HTML и XML, он предназначен для чтения людьми и машинами. Я предполагаю, что, не имея ничего, кроме мимолетного знания шаблонов поиска по шаблону, вы можете просто прочитать, что здесь происходит, на «простом английском».

  • адрес электронной почты определяется как локальная часть и домен, разделенные @
  • локальная часть — одно слово, за которым следуют необязательные слова, разделенные точками
  • домен является одним или несколькими разделенными точками подобластями, за которыми следует один верхний домен
  • единственные вещи, которые вы можете не знать здесь, но, вероятно, можете догадаться, это:
    • так же, как подстановочный знак * означает «ноль или более», 1* означает «один или несколько», а 2*6 означает минимум 2 и максимум 6 повторений
    • / отделяет альтернативные варианты
    • %d определяет десятичные коды символов и диапазоны кодов символов
    • например, %d35 представляет # , десятичное ASCII 35
    • %d65-90 представляет любой символ в диапазоне AZ , десятичные дроби ASCII 65-90

RegExp и apg-exp сравниваются для этого адреса электронной почты в примере 1 .

apg-exp — это механизм сопоставления с образцом, разработанный для того, чтобы иметь внешний вид RegExp, но использовать синтаксис ABNF для определений шаблонов. В следующих нескольких разделах я проведу вас через:

  • Как добавить apg-exp в ваше приложение
  • Краткое руководство по синтаксису ABNF
  • Работа с apg-exp — несколько примеров
  • Куда идти дальше — больше подробностей, расширенные примеры

И работает — как его получить

НПМ

Если вы работаете в среде Node.js , из каталога вашего проекта запустите:

 npm install apg-exp --save 

Затем вы можете получить к нему доступ в своем коде с помощью require() .

Например:

 var ApgExp = require("apg-exp"); var exp = new ApgExp(pattern, flags); var result = exp.exec(stringToMatch); 

GitHub

Чтобы получить копию кода из GitHub, вы можете клонировать репозиторий в каталог вашего проекта:

 git clone https://github.com/ldthomas/apg-js2-exp.git apg-exp 

или загрузите это как файл почтового индекса .

Тогда в page.html :

 <!-- optional stylesheet used in tutorial examples --> <link rel="stylesheet" href="./apg-exp/apgexp.css"> <script src="./apg-exp/apgexp-min.js"></script> <script> var useApgExp = function(){ var exp = new ApgExp(pattern, flags); var result = exp.exec(stringToMatch); /* do something with the result */ } </script> 

CDN

Вы также можете создать версию CDN непосредственно из источника GitHub, используя RawGit . Тем не менее, обязательно прочитайте отсутствие времени безотказной работы или гарантий поддержки (на самом деле, обязательно прочитайте весь FAQ ).

Следующее используется во всех примерах в этом руководстве.

 <link rel="stylesheet" href="https://cdn.rawgit.com/ldthomas/apg-js2-exp/89c6681798ba9e47583b685c87b244406b18a26d/apgexp.css"> <script src="https://cdn.rawgit.com/ldthomas/apg-js2-exp/c0fc3adac954a6f6ad6f265fd2f8f06f68001e10/apgexp-min.js" charset="utf-8"></script> 

Эти файлы кэшируются на серверах MaxCDN, и вы можете использовать их для тестирования, пока они остаются доступными. Однако для производства вы должны разместить копии apgexp-min.js и apgexp.css на своих серверах для гарантированного доступа
и включите их в свои страницы как лучше всего подходящие для вашего приложения.

Краткое руководство по ABNF

ABNF — это синтаксис для описания фраз, фраза — любая строка. Как вы видели в примере электронной почты выше, он позволяет разбить сложные фразы на набор простых фраз. Определение фразы имеет вид:

 name = elements LF 

где LF — символ перевода строки (новая строка \n ).

В таблице ниже приведено краткое руководство по элементам (полное руководство см. В SABNF ).

Элемент Определение Комментарии / Примеры
имя название правила алфавит + дефис ( см. примечание 1 ниже )
% d32 односимвольный код десятичное значение кода символа
% d97.98.99 строка кодов символов азбука
% d48-57 диапазон кодов символов любая цифра ASCII 0-9
«А» регистронезависимая буквенная строка ABC или ABC и т. д.
% S»ABC» регистр буквенная строка только aBc (=% d97.66.99)
Космос объединяет два элемента % d97% d98 (= ab)
/ разделяет два альтернативных элемента % d97 /% d98 (= a или b)
*элемент ноль или более повторений элемента ( см. примечание 2 ниже )
(элементы) группировка, рассматриваемая как единый элемент ( см. примечание 3 ниже )
[элементы] необязательная группировка [% d97]% d98 (ab или b)
% ^ начало строки ввода соответствует позиции только как пустая фраза
% $ конец строки ввода соответствует позиции только как пустая фраза
&элемент смотреть вперед для элемента элемент должен следовать за текущей строкой
!элемент не смотри вперед элемент НЕ должен следовать за текущей строкой
&&элемент искать элемент элемент должен предшествовать текущей позиции строки
!!элемент не оглядываться элемент не должен предшествовать текущей позиции строки
\имя обратная ссылка на правило «имя» соответствует предыдущей фразе, найденной для «name»
;комментарий комментарий комментарий начинается с; до конца строки

Примечание 1. Имена правил должны начинаться с буквенного символа в первом столбце строки. Строки продолжения должны начинаться с пробела или табуляции. Первое правило определяет всю фразу или образец для сопоставления. Следующие правила определяют именованные подфразы (или именованные захваты) во всей фразе.

Примечание 2 : общая форма повторения n*m , определяющая минимум n и максимум m повторений. Сокращенное обозначение может быть *m от нуля до m , n* для n до бесконечности или просто n для n*n .

Примечание 3 : Группировка важна для поддержания чередования и сцепления в соответствии с ожиданиями. Конкатенация имеет более жесткую привязку, чем чередование. Если

 phrase1 = elem (foo / bar) blat LF phrase2 = (elem foo) / (bar blat) LF phrase3 = elem foo / bar blat LF 

затем phrase1 соответствует phrase1 elem foo blat или elem bar blat а обе phrase2 и phrase3 соответствуют phrase3 elem foo или bar blat . Будьте осторожны и используйте группы свободно.

Использование apg-exp — несколько примеров

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

Я поместил скучные детали построенного объекта в следующем разделе ниже. Вы можете обратиться к нему по мере необходимости, чтобы понять примеры. Я также пропущу обработку ошибок, но вы должны знать, что в случае ошибки шаблона конструктор сгенерирует ApgExpError исключения ApgExpError который имеет несколько удобных функций для форматированного отображения ошибок шаблона. Ваш блок try/catch может выглядеть примерно так:

 try { var exp = new ApgExp(pattern, flags); var result = exp.exec(stringToMatch); if (result) { // do something with results } else { // handle failure } } catch(e) { if (e.name === "ApgExpError") { // display pattern errors to console console.log(e.toText()); // display pattern errors to HTML page $("#errors").html(e.toHtml()); } else { // handle other exceptions } } 

Телефонные номера

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

Давайте просто потребуем, чтобы он начинался с круглых скобок ( или цифры) и имел блоки из 3, 3 и 4 цифр, разделенных от нуля до трех нецифров. Это достаточно общее, чтобы принять наиболее распространенные форматы, и достаточно конкретные, чтобы захватить область , офисные и абонентские коды в качестве групп захвата для любого переформатирования.

Вот как это выглядит в ABNF:

 phone-number = ["("] area-code sep office-code sep subscriber area-code = 3digit ; 3 digits office-code = 3digit ; 3 digits subscriber = 4digit ; 4 digits sep = *3(%d32-47 / %d58-126 / %d9) ; 0-3 ASCII non-digits digit = %d48-57 ; 0-9 

Пример 2 демонстрирует это и дает вам возможность варьировать форматы телефонных номеров:

Пример 3 позволяет вам поиграть с синтаксисом шаблона и взглянуть на сообщения об ошибках, которые вы, возможно, ожидаете увидеть:

Синтаксис RegExp,

 \(?(\d{3})\D{0,3}(\d{3})\D{0,3}(\d{4}) 

тоже не сложно, и пример 4 дает параллельное сравнение.

Даты

Теперь давайте немного увеличим его и посмотрим, как apg-exp и RegExp сравниваются при сопоставлении дат.

Наши требования к формату даты:

 mm/dd/yy or mm/dd/yyyy dd/mm/yy or dd/mm/yyyy mm, 1-12 or 01-12, ie with or without leading zero dd, 1-31 or 01-31, ie with or without leading zero yy, 00-99 yyyy, 1900-1999 or 2000-2099 

Сам по себе формат mm/dd/yyyy не так уж и сложен, но ограничение числовых диапазонов — вот что его усиливает. Как добраться с ABNF выглядит так:

 date = %^ (mm-first / dd-first) %$ mm-first = mm "/" dd "/" yyyy ; month before day dd-first = dd "/" mm "/" yyyy ; day before month dd = "0" digit1 ; 01-09 / ("1"/"2") digit ; or 10-29 / "3" %d48-49 ; or 30-31 / digit1 ; or 1-9 mm = "0" digit1 ; 01-09 / "1" %d48-50 ; or 10-12 / digit1 ; or 1-9 yyyy = ("19" / "20") 2digit ; 1900-1999 or 2000-2099 / 2digit ; or 00-99 digit = %d48-57 ; 0-9 digit1 = %d49-57 ; 1-9 

Комментарии делают это довольно очевидным. Обратите внимание, что dd , mm и yyyy имеют самую короткую альтернативу последней. Это очень важно, так как apg-exp всегда использует подход «первый матч выигрывает» к альтернативам. Альтернативы пробуются слева направо, и как только совпадение найдено, все оставшиеся альтернативы игнорируются. Здесь это означает, что когда шаблон может соответствовать одной или двум цифрам, шаблон из двух цифр должен быть первым.

Следуя тому же подходу, что и выше, разбиение даты на альтернативные шаблоны dd , mm и [yy]yy затем объединение их для полной даты приводит к следующему синтаксису RegExp:

 ^(?:((?:0[1-9])|(?:1[0-2])|(?:[1-9]))/((?:0[1-9])|(?:(?:1|2)[0-9])|(?:3[0-1])|(?:[1-9]))|((?:0[1-9])|(?:(?:1|2)[0-9])|(?:3[0-1])|(?:[1-9]))/((?:0[1-9])|(?:1[0-2])|(?:[1-9])))/((?:19|20)?[0-9][0-9])$ 

Я не эксперт по RegExp, поэтому могут быть способы сократить это, но этот работает. Вы можете сравнить для себя в примере 5 . Перепрыгни туда и попробуй.

Сопоставление вложенных пар (()) с рекурсией

Наконец, я хотел бы показать, как сопоставлять вложенные пары скобок, скобок и тому подобное. Хотя это очень важная проблема сопоставления с образцом, вы не можете сделать это с помощью RegExp. Рассмотрим следующую ABNF для совпавших пар скобок:

 P = LPR / LR L = "(" R = ")" 

Обратите внимание, что правило P появляется в своем собственном определении. Это называется рекурсия. И хотя некоторые разновидности движков регулярных выражений поддерживают рекурсию, а некоторые инструменты RegExp предоставляют меру возможностей рекурсии, RegExp в JavaScript вообще не поддерживает ее. L и R выше были выбраны для соответствия скобкам, но они могут быть самыми разными, если L не может совпадать с R

Перейдите к примеру 6, и мы повеселимся с рекурсией.

Прежде чем оставить тему соответствия, вложенных пар, я хотел бы продемонстрировать пару реальных примеров, с которыми apg-exp может вам помочь.

В примере 6 вы увидели, как сопоставлять пары внутри пар и включать текст в квадратные скобки. Предположим, что вы получили задание написать программу, в которой: если курсор находится на открывающей фигурной скобке, { , выделите соответствующую закрывающую скобку, } .

Пример 7 показывает вам решение этой проблемы. Вам нужно будет понять режим sticky .

Один последний пример вложенной пары. Вы когда-нибудь хотели закомментировать большой блок HTML только для того, чтобы обнаружить, что в блоке уже есть комментарии? Разочарование? Просто поищите в Интернете «проблемы с вложенными комментариями в HTML» и почувствуйте разочарование. Пример 8 показывает вам одно возможное решение этой проблемы. Предупреждение — это может вас немного растянуть. Вам нужно будет понять объект result.rules и global режим.

Собираем все вместе

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

Обычно при создании новой учетной записи у вас запрашивают имя пользователя, адрес электронной почты, пароль и подтверждение пароля. Давайте потребуем, чтобы имена пользователей состояли из 3-32 букв ASCII, дефисов и точек. Пароль должен состоять из 8-16 букв верхнего регистра, букв нижнего регистра или цифр и должен иметь хотя бы одну из них.

Форма будет отображать описательное сообщение об ошибке над любой неверной записью, прежде чем продолжить. Пример 9 объединяет все это.

API библиотеки

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

Входные аргументы

Конструктор apg-exp , здесь и ApgExp , принимает до четырех аргументов.

 var exp = new ApgExp(pattern[, flags[, nodeHits[, treeDepth]]]); 
  • шаблон
    • строка: синтаксис шаблона SABNF *, как описано в предыдущем разделе.
    • объект: экземпляр объекта анализатора APG . (Расширенный вариант.)
  • флаги: строка: любой из символов "gyud"
    • g — глобальный режим: перебор всех совпадений с образцом (см. пример 8 )
    • y — режим exp.lastIndex совпадение к exp.lastIndex (см. пример 7 )
    • u — режим Unicode: возвращает результаты в виде массивов целочисленных кодов символов вместо строк
    • d — режим отладки: расширенный параметр — предоставляет объект трассировки APG
  • nodeHits: integer> 0: default = Infinity : Ограничивает алгоритм "nodeHits" шагами "nodeHits" . Защищает от катастрофического возврата.
  • treeDepth: integer> 0: default = Infinity : Ограничивает глубину дерева соответствующего анализатора.

* Например, для сопоставления буквенно-цифрового имени вы можете использовать строку ввода:

 var pattern = "alphanum = alpha *(alpha / num)\n" + "alpha = %d65-90 / %d97-122\n" + "num = %d48-57\n"; 

Свойства и методы

Сам построенный объект exp имеет 16 свойств и 14 методов.

Свойство Тип Описание
аст объект (Дополнительно: объект абстрактного синтаксиса APG )
отлаживать логический true, если был установлен флаг отладки d , в противном случае — false
флаги строка переформатированная копия аргумента flags
Глобальный логический true, если установлен глобальный флаг g , в противном случае — false
вход строка / массив (*) копия входной строки
LastIndex целое число индекс символа для начала попытки совпадения (**)
leftContext строка / массив (*) префикс совпавшего шаблона
lastMatch строка / массив (*) сопоставленный шаблон (= result[0] )
nodeHits целое число входное значение (см. result.nodeHits для фактического значения)
rightContext строка / массив (*) суффикс сопоставленного шаблона
правила объект Смотрите result.rules . Здесь сохраняется только последняя найденная фраза.
источник строка строка синтаксиса входного шаблона SABNF
липкий логический true, если был установлен липкий флаг y , в противном случае — false
след объект (Дополнительно: см. Объект трассировки APG .) undefined если флаг отладки имеет значение false.
treeDepth целое число входное значение (см. result.treeDepth для фактического значения)
юникода логический true, если был установлен флаг Unicode u , в противном случае — false

Примечание 1. Если флаг unicode имеет значение true это будет массив кодов целочисленных символов, в противном случае — строка.

Примечание 2 : пользователь может установить любое значение. Сопоставление с шаблоном всегда начинается с этого значения индекса. Его использование и значение после попытки сопоставления зависят от глобального и липкого режима. Смотрите примеры 7 и 8 .

метод аргументы Описание
defineUdt (name, func) строка, функция Дополнительно: используется для функции UDT SABNF.
исключить ([строка]) массив имен правил список имен правил, исключаемых из объекта result.rules
Exec (ул) строка пытается найти образец в str
включают в себя ([строка]) массив имен правил список имен правил, включаемых исключительно в объект result.rules
maxCallStackDepth () никто возвращает верхнюю границу глубины стека вызовов
заменить (ул, репл) строка, строка или функция сопоставить паттен в str , заменить на repl
sourceToHtml () никто возвращает шаблон ввода в формате HTML
sourceToHtmlPage () никто возвращает шаблон ввода в виде полной HTML-страницы
sourceToText () никто возвращает шаблон ввода в текстовом формате ASCII
разделить (строка [, предел]) строка, целое число разбивает входную строку при совпадении с образцом
тест (ул) строка возвращает true если найдено соответствие шаблону, иначе false (см. пример 9 ).
toHtml () никто возвращает свойства этого объекта в формате HTML
toHtmlPage () никто возвращает свойства этого объекта как полную страницу HTML
печатать() никто возвращает свойства этого объекта в текстовом формате ASCII

Объект результата

Успешное сопоставление с шаблоном возвращает результаты в объекте result с 7 свойствами и 3 методами:

 var result = exp.exec(str); 
Свойство Тип Описание
[0] строка / массив (*) согласованный образец
вход строка / массив (*) копия входной строки
показатель целое число индекс первого символа совпадающего шаблона
длина целое число количество символов в сопоставленном шаблоне
nodeHits целое число фактическое количество шагов разбора, необходимых для совпадения
правила объект (именованный захват) Массив всех подходящих фраз для каждого именованного правила (***)
treeDepth целое число фактическая максимальная глубина дерева разбора, достигнутая во время матча

Примечание 3 : Например, result.rules["name"][i] = {phrase: string/array, index: integer} См. Пример 8 .

метод аргументы Описание
toHtml () никто возвращает свойства в формате HTML
toHtmlPage () никто возвращает свойства в виде полной HTML-страницы
печатать() никто возвращает свойства в текстовом формате ASCII

Куда идти дальше

Я постарался свести это к минимуму, просто чтобы дать вам представление о том, как выглядят библиотека apg-exp и ABNF и как они складываются с RegExp. Но вы можете сделать гораздо больше. Если вы хотите улучшить свои навыки сопоставления с шаблоном или просто хотите приключений, взгляните на эти более сложные примеры и обратитесь к полному руководству пользователя .

Я убедил вас попробовать apg-exp в вашем следующем проекте? Как вы думаете, с ABNF легче работать, чем с регулярными выражениями? Я хотел бы услышать от вас в комментариях ниже!