RegEx не так сложны, как вам кажется. Часть 2

0
131
views

Перевод второй части статьи «Regular Expressions Demystified: RegEx isn’t as hard as it looks». Первую часть читайте здесь.

Регулярные выражения не так сложны, как кажется

3. Повторное совпадение для поиска дублирующихся символов

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

Представьте, что перед вами стоит следующая задача. У вас есть строка. Определите, есть ли в ней повторяющиеся символы.

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

let e=/(\w)\1/; 
e.test("abc"); //false 
e.test("abb"); //true

Шаблон в этом выражении не находит совпадений в строке «abc», поскольку в ней нет последовательно расположенных повторяющихся символов. Поэтому здесь test возвращает false.

Но в строке «abb» есть совпадение с шаблоном — «bb» — поэтому возвращается true.

Давайте, испробуйте это в своей консоли в DevTool!

Разобьем это выражение на понятные кусочки.

Обратный слэш

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

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

Что означает сочетание символов \n, встречающееся в строке? Да, это новая строка. Здесь у нас нечто похожее.

Фактически, \n это то, что вы используете в качестве шаблона при поиске новой строки. Обратный слэш экранирует обычное значение символа «n» и придает ему новый смысл — теперь это символ новой строки.

  • \d — условное обозначение цифр. Совпадает с любой одной цифрой.
  • \D — условное обозначение нецифровых символов. Совпадает с любым символом, кроме тех, которые совпадают с \d.
  • \s — условное обозначение пробелов (а также новой строки или таба).
  • \S — антоним \s, обозначает все, кроме пробелов.
  • \w — условное обозначение букв и цифр. Совпадает с a-z, A-Z, 0–9 и символом подчеркивания.
  • \W — антоним \w.

Запоминание и повторный поиск совпадений

Мы начали этот раздел с решения для поиска повторяющихся символов. Шаблон в выражении /(\w)\1/ находит совпадение в строке «abb». Это демонстрирует использование памяти и повторного вызова внутри регулярных выражений.

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

\1 запоминает и использует совпадение с первым выражением в скобках. Аналогично, \2 запоминает и использует совпадение с выражением во втором наборе скобок. И так далее.

Опишем наше выражение (\w)\1 простыми словами:

«Найди любой буквенно-цифровой символ в данной строке. Запомни его как \1. Проверь, появляется ли этот символ еще раз, справа от первого».

Приложение 1 — пары букв, идущих в обратном порядке

Допустим, нам нужно найти пары символов, находящихся рядом друг с другом и расположенных в обратном порядке. Как в «abba»: символы в «ab» идут в алфавитном порядке, а в «ba» – в обратном алфавитном порядке.

Вот наше выражение:

let e=/(\w)(\w)\2\1/; 
e.test("aabb"); //false 
e.test("abba"); //true 
e.test("abab"); //false

Первая часть в скобках — (\w) — совпадает с «а» и запоминается как \1 (первая переменная). Вторая часть в скобках совпадает с «b» и запоминается как вторая переменная, \2. Далее в нашем выражении идет переменная \2, а за ней — переменная \1. Из всех проверяемых строк только строка «abba» совпадает с указанным шаблоном.

Приложение 2 — отсутствие дубликатов

А сейчас давайте поищем последовательность символов, в которой не окажется двойных букв. Т.е., после любого символа НЕ должен идти такой же символ.

Вот наше решение:

let e=/^(\w)(?!\1)$/; 
e.test("a"); //true 
e.test("ab"); //false 
e.test("aa"); //false

Не совсем то, чего нам бы хотелось, но близко. Средний кейс не должен возвращать false. Но здесь мы добавили еще несколько символов, которые нужно объяснить. Нас ждет еще одна встреча с самым сильным мушкетером.

Возвращение вопросительного знака

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

Вопросительный и восклицательный знак, заключенные в скобки, называются look ahead, опережающей проверкой. Точнее, негативной опережающей проверкой. Шаблон a(?!b) совпадает с «а», только если после «а» НЕ идет «b».

А вот (?=) это позитивная опережающая проверка (positive look ahead). Шаблон a(?=b) совпадает с «а» только если после «а» следует «b».

В решении, которое у нас было, шаблон (\w)(?!\1) предполагал поиск символа, за которым НЕ идет такой же символ. Но, таким образом, мы искали только один символ. Нам нужно применить группирование, чтобы вести поиск 1 или более вхождений неповторяющихся символов. Здесь нам поможет знак плюс (+).

let e=/^((\w)(?!\1))+$/; 
e.test("madam"); //false 
e.test("maam"); //false

Кажется, не сработало. Тут дело в следующем. Если мы группируем шаблон при помощи второй пары круглых скобок, переменная \1 больше не представляет (\w), теперь она представляет выражение в более высокоуровневой паре скобок, которая группирует шаблон. Поэтому проверки провалены.

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

let e=/^(?:(\w)(?!\1))+$/; 
e.test("madam"); //true 
e.test("maam"); //false

В этот раз первая скобочная пара не запоминается (благодаря ?:), поэтому \1 помнит совпадение, возвращаемое \w.

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

«Ищи символ. Проверь, не следует ли за ним еще один такой же символ. Проделай это для всех символов в строке, от начала до конца».

Повторение

  • \w – представляет все буквенно-цифровые символы. То же самое, но в верхнем регистре (\W) означает любой символ, кроме букв и цифр.
  • () – выражение в круглых скобках запоминается для дальнейшего использования.
  • \1 – запоминает и использует совпадение с первой скобочной группой.
  • \2 – то же самое для второй скобочной группы. И так далее.
  • a(?!b) – комбинация скобок, вопросительного и восклицательного знаков называется негативной опережающей проверкой (look ahead). Этот шаблон даст совпадение с «а», только если после «а» НЕ идет «b».
  • a(?=b) – обратная сторона медали. Этот шаблон даст совпадение с «а», только если после «а» ИДЕТ «b».
  • (?:a) – «забывчивое» группирование. Ищет «а», но не запоминает его. Вы не сможете повторно использовать найденное совпадение при помощи \1.

4. Чередующаяся последовательность

Наш новый usecase прост. Наш шаблон должен совпадать со строкой, в которой используются только два символа. Эти два символа должны сменять друг друга по всей длине строки, как, например, в строках «abab» и «xyxyx».

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

Вот решение:

let e=/^(\S)(?!\1)(\S)(\1\2)*$/; 
e.test("abab"); //true 
e.test("$#$#"); //true 
e.test("#$%"); //false 
e.test("$ $ "); //false 
e.test("xyxyx"); //false

Здесь вы можете подумать: «Ну все, с меня хватит!» Но погодите, дождитесь «ага!»-момента! Вы в трех шагах от золотой жилы, сейчас не время прекращать копать.

Прежде чем приступить к разбору, давайте посмотрим на результаты тестов. «abab» совпадает, «$#$#» тоже совпадает.

«#$%» не совпадает, здесь добавлен третий символ.

«$ $ » не совпадает, потому что хоть чередование и соблюдено, здесь использован символ пробела, а \S исключает пробелы из шаблона.

В общем, все хорошо, за исключением того, что по строке «xyxyx» мы получили false. Наш шаблон не знает, что делать с последним «х». Но мы с этим разберемся.

Давайте посмотрим, какие инструменты мы применили, и вскоре нам все станет понятно.

По кусочку за раз

Большая часть кусочков вам уже знакомы. \S это антоним \s, используется для поиска любого символа кроме пробела.

Теперь давайте пошагово опишем шаблон /^(\S)(?!\1)(\S)(\1\2)*$/ простыми словами.

  • Начать с начала строки /^.
  • Искать любой символ, не являющийся пробелом (\S).
  • Запомнить его как \1.
  • Проверить, не следует ли за первым символом еще один такой же символ (?!\1). Вы помните, это негативная опережающая проверка.
  • Если все в порядке, искать следующий символ (\S).
  • Запомнить его как \2.
  • Далее искать 0 или больше пар первых двух совпадений (\1\2)*.
  • Искать этот паттерн до конца строки $/.

Применяем это к нашим тестовым случаям. «abab» и «$#$#» совпадают с описанным шаблоном.

Хвостовая часть

Давайте исправим выражение, чтобы шаблон охватывал и «xyxyx». Как мы знаем, проблема в последнем «х». Для «xyxy» у нас уже есть решение. Все, что нам нужно, это шаблон, говорящий «Ищи возможное вхождение первого символа».

Как обычно, начнем с готового решения.

let e=/^(\S)(?!\1)(\S)(\1\2)*\1?$/; 
e.test("xyxyx"); //true 
e.test("$#$#$"); //true

Вопросительный знак после шаблона (символа или группы символов) означает 0 или 1 вхождение этого шаблона. Этот знак не жадный.

В нашем случае \1? означает 0 или 1 вхождение первого символа, запомненного по первой скобочной группе.

Все просто.

Повторение

  • \S — представляет любой символ за исключением пробела.
  • а* — астериск (звездочка) указывает на 0 или более вхождений предыдущего символа. В данном случае — 0 или больше букв «а».
  • a(?!b) — негативная опережающая проверка. Дает совпадение с «а», только если за «а» НЕ идет «b». Например, совпадает с «а» в «аа», «ах», «а$», но не в «ab».
  • \s — одинарный пробел.
  • a(?=b) — позитивная опережающая проверка. Дает совпадение с «а», если после «а» идет « b».
  • ^ab*$ — возможно, вы подумали, что этот шаблон совпадает с 0 или большим числом вхождений «ab», но на самом деле это буква «а», за которой следует 0 или больше букв «b». Например, этот шаблон найдет совпадение в «abbb», «а» и «ab», но не в «abab».
  • ^(ab)*$ — а вот здесь речь идет о 0 или большем числе вхождений пары «ab». Этот шаблон совпадет с пустой строкой «»»», «ab» и «abab», но не с «abb».
  • а? — ? означает 0 или 1 вхождение предыдущего символа или шаблона.
  • \1? — означает 0 или 1 вхождение первого запомненного совпадения.

5. Проверка email-адреса

Регулярные выражения сами по себе не могут справиться с проверкой email-адресов. Кто-то может даже сказать, что в этом случае регулярные выражения не стоит использовать, поскольку они никогда не смогут охватить 100% адресов.

Только представьте все многообразие доменных имен. А также подумайте о включении различных символов внутри адресов, например, точек и знаков «плюс».

Email-адреса следует проверять дважды. Один раз на стороне клиента, чтобы помочь пользователям избежать опечаток в указании адреса. Начните с указания типа семантического тега input:

<input type='email'>.

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

А потом адрес нужно проверить еще раз на сервере, путем отправки подтверждающего email-а.

RegEx для Email

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

Практическое применение регулярных выражений

6. Проверка надежности пароля

Если любите кофе, самое время налить себе чашечку, да покрепче. Мы подобрались к последней части нашей статьи, причем самой длинной.

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

Перейдем к нашей задаче. Помните случай, когда вам лишь с нескольких попыток удалось подобрать пароль, соответствующий всем требованиям? «Слабый», «хороший», «сильный», «очень сильный». Вот такую проверку мы и создадим.

Наш пароль должен:

  • состоять как минимум из 4 символов,
  • содержать как минимум одну букву в нижнем регистре,
  • содержать как минимум одну букву в верхнем регистре,
  • содержать как минимум одну цифру,
  • содержать как минимум один символ.

Длина строки

Давайте сначала проверим, есть ли в пароле как минимум 4 символа. Это довольно просто, можно же использовать .length, да и все. Но нееет, мы не ищем легких путей.

//выражение, содержащее только lookahead
//не охватывает ни одного символа
e1=/^(?=.{4,})$/; 
e1.test("abc") //false
e1.test("abcd") //false  

//после lookahead 
//идет шаблон для охвата нужных символов.
e2=/^(?=.{4,}).*$/; 
e2.test("abc") //false 
e2.test("abcd") //true
  • (?=) вы помните по разделу с поиском дубликатов букв. Это использование опережающей проверки. Само по себе оно не охватывает никаких символов.
  • Точка — интересный знак. Она обозначает любой символ.
  • {4,} — означает как минимум 4 предшествующих символа, без верхнего лимита.
  • \d{4} — ищет точно 4 цифры.
  • \w{4,20} — ищет от 4 до 20 буквенно-цифровых символов.

Давайте переведем выражение /^(?=.{4,})$/ на человеческий язык.

«Начни с начала строки. Посмотри, есть ли впереди как минимум 4 символа. Не запоминай совпадение. Вернись в начало и проверь, кончается ли там строка».

Звучит неправильно, верно? По крайней мере последняя часть.

Вот почему мы сделали вариант /^(?=.{4,}).*$/. То есть, добавили к предыдущему дополнительную точку и звездочку. Это читается следующим образом:

«Начни с начала строки. Посмотри, есть ли впереди как минимум 4 символа. Не запоминай совпадение. Вернись к началу. Охвати все символы при помощи .* и посмотри, достигнут ли конец строки».

Вот теперь выражение приобрело смысл, не так ли?

Вот почему строка «abc» не проходит проверку, а «abcd» проходит.

Как минимум одна цифра

Это будет легко.

e=/^(?=.*\d+).*$/ 
e.test(""); //false 
e.test("a"); //false 
e.test("8"); //true 
e.test("a8b"); //true 
e.test("ab890"); //true
  • «Начни с начала строки – /^.
  • Посмотри, есть ли впереди 0 или больше любых символов – ?=.*.
  • Проверь, следует ли за ними 1 или больше цифр – \d+.
  • Если есть совпадение, вернись к началу (потому что это была опережающая проверка). Охвати все символы в строке до конца строки – .*$/».

Как минимум одна буква в нижнем регистре

e=/^(?=.*[a-z]+).*$/; 
e.test(""); //false 
e.test("A"); //false 
e.test("a"); //true

Здесь используется тот же шаблон, что и выше. Только вместо \d+ используется [a-z]+, то есть набор буквенных символов от «a» до «z».

Как минимум одна буква в верхнем регистре

Заменяем [a-z] на [A-Z] и используем то же самое выражение.

Как минимум один символ

Здесь будет посложнее. Можно поместить весь список возможных символов в набор.

/^(?=.*[-+=_)(\*&\^%\$#@!~”’:;|\}]{[/?.>,<]+).*$/.test(“$”)

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

Так что давайте перейдем к более простому варианту.

//считает пробел символом 
let e1; 
e1=/^(?=.*[^a-zA-Z0-9])[ -~]+$/ 
e1.test("_"); //true 
e1.test(" "); //true  

//не принимает пробелы 
let e2; 
e2=/^(?=.*[^a-zA-Z0-9])[!-~]+$/ 
e2.test(" "); //false 
e2.test("_"); //true  

//исключение подчеркивания 
let e3; 
e3=/^(?=.*[\W])[!-~]+$/ 
e3.test("_"); //false

Погодите, что это за ^ появляется снова в середине непонятно чего? Здесь мы подобрались к разбору того, почему невинный (на первый взгляд) знак ^ является двойным агентом (об этом упоминалось в первой части).

Внутри набора символов ^ служит для отрицания этого набора. То есть, шаблон [^a-z] означает любой символ, не входящий в набор a-z.

Следовательно, [^a-zA-Z0-9] означает любой символ, не являющийся буквой в нижнем или верхнем регистре и не являющийся цифрой.

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

Диапазон набора символов

Любопытно использование диапазона [!-~]. Восклицательный знак и тильда на клавиатуре находятся рядом, но их значения в ASCII диагонально противоположны.

Помните диапазоны a-z? A-Z? 0–9? Это не константы. Они, собственно, базируются на диапазонах их значений в ASCII.

Таблица ASCII содержит 125 символов. Символы по 31-й нас не интересуют. Диапазон нужных нам символов открывает восклицательный знак (33), а закрывает тильда (~).

Таким образом шаблон [!-~] включает в себя все символы, буквы и цифры, которые нам нужны.

Собираем войска

Собрав все это вместе, мы получаем отличное регулярное выражение:

/^(?=.{5,})(?=.*[a-z]+)(?=.*\d+)(?=.*[A-Z]+)(?=.*[^\w])[ -~]+$/

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

//начнем с префикса 
let p = "^"; 

//look ahead  
// min 4 символа 
p += "(?=.{4,})"; 
// нижний регистр 
p += "(?=.*[a-z]+)"; 
// верхний регистр 
p += "(?=.*[A-Z]+)"; 
// цифры 
p += "(?=.*\\d+)"; 
// символы 
p += "(?=.*[^ a-zA-Z0-9]+)"; 
//конец lookaheads  

//финальный охват 
p += "[ -~]+";  
//суффикс 
p += "$"; 

//Construct RegEx 
let e = new RegEx(p); 
// tests 
e.test("aB0#"); //true  
e.test(""); //false 
e.test("aB0"); //false 
e.test("ab0#"); //false 
e.test("AB0#"); //false 
e.test("aB00"); //false 
e.test("aB!!"); //false  

// space is in our control 
e.test("aB 0"); //false 
e.test("aB 0!"); //true

Если ваши глаза еще не устали, вы могли заметить две странности в этом коде.

  • Во-первых, мы не использовали /^, а вместо этого использовали просто ^. Также мы не использовали $/ для конца строки, заменив это на просто $. Это потому, что конструктор RegEx автоматически добавляет начальные и завершающие слэши.
  • Во-вторых, для цифр мы использовали \\d вместо обычного \d. Это потому, что переменная «р» это просто обычная строка внутри парных кавычек. Чтобы вставить обратный слэш, его самого нужно экранировать обратным слэшем. В конструкторе RegEx \\d резолвится в \d.

7. Заключение

Вот мы и подобрались к концу. Но это лишь начало пути.

Мы только прикоснулись к поиску совпадений RegEx при помощи метода test. Метод exec на основе всего этого возвращает совпадающие с шаблоном подстроки.

Строчные объекты имеют такие методы как match, search, replace и split, которые широко используются в регулярных выражениях.

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

ОСТАВЬТЕ ОТВЕТ

Please enter your comment!
Please enter your name here