Некоторые из них для наглядности будут показаны на примере языков программирования PHP или JavaScript, но в целом они работают независимо от ЯП.
Из названия понятно, что статья ориентирована на самый начальный уровень — тех, кто еще ни разу не использовал регулярные выражения в своих программах или делал это без должного понимания.
В конце статьи я в двух словах расскажу, какие задачи нельзя решить регулярными выражениями и какие инструменты для этого стоит использовать.
Поехали!
Вступление
Регулярные выражения — язык поиска подстроки или подстрок в тексте. Для поиска используется паттерн (шаблон, маска), состоящий из символов и метасимволов (символы, которые обозначают не сами себя, а набор символов).
Это довольно мощный инструмент, который может пригодиться во многих случая — поиск, проверка на корректность строки и т.д. Спектр его возможностей трудно уместить в одну статью.
В PHP работа с регулярными выражениями заключается в наборе функций, из которых я чаще всего использую следующие:
- preg_match (http://php.net/manual/en/function.preg-match.php)
- preg_match_all (http://php.net/manual/en/function.preg-match-all.php)
- preg_replace (http://php.net/manual/en/function.preg-replace.php)
Для работы с ними нужен текст, в котором мы будем искать или заменять подстроки, а также само регулярное выражение, описывающее правило поиска.
Функции на match возвращают число найденных подстрок или false в случае ошибок. Функция на replace возвращает измененную строку/массив или null в случае ошибки. Результат можно привести к bool (false, если не было найдено значений и true, если было) и использовать вместе с if или assertTrue для обработки результата работы.
В JS чаще всего мне приходится использовать:
- match (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/match)
- test (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test)
- replace (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace)
Все дальнейшие примеры предлагаю смотреть в https://regex101.com/. Это удобный и наглядный интерфейс для работы с регулярными выражениями.
Пример использования функций
В PHP регулярное выражение — это строка, которая начинается и заканчивается символом-разделителем. Все, что находится между разделителями и есть регулярное выражение.
Часто используемыми разделителями являются косые черты “/”, знаки решетки “#” и тильды “~”. Ниже представлены примеры шаблонов с корректными разделителями:
- /foo bar/
- #^[^0-9]$#
- %[a-zA-Z0-9_-]%
Если необходимо использовать разделитель внутри шаблона, его нужно проэкранировать с помощью обратной косой черты. Если разделитель часто используется в шаблоне, в целях удобочитаемости, лучше выбрать другой разделитель для этого шаблона.
- /http:\/\//
- #http://#
В JavaScript регулярные выражения реализованы отдельным объектом RegExp и интегрированы в методы строк.
Создать регулярное выражение можно так:
let regexp = new RegExp("шаблон", "флаги");
Или более короткий вариант:let regexp = /шаблон/; // без флаговlet regexp = /шаблон/gmi; // с флагами gmi (изучим их дальше)
Пример самого простого регулярного выражения для поиска:RegExp: /o/Text: hello world
В этом примере мы просто ищем все символы “o”.
В PHP разница между preg_match и preg_match_all в том, что первая функция найдет только первый match и закончит поиск, в то время как вторая функция вернет все вхождения.
Пример кода на PHP:<?php$text = ‘hello world’;$regexp = ‘/o/’;$result = preg_match($regexp, $text, $match);var_dump($result,$match);int(1) // нам вернулось одно вхождение, т.к. после функция заканчивает работуarray(1) {[0]=>string(1) "o" // нам вернулось вхождение, аналогичное запросу, так как метасимволов мы пока не использовали}
Пробуем то же самое для второй функции:<?php$text = ‘hello world’;$regexp = ‘/o/’;$result = preg_match_all($regexp, $text, $match);var_dump($result,$match);int(2)array(1) {[0]=>array(2) {[0]=>string(1) "o"[1]=>string(1) "o"}}
В последнем случае функция вернула все вхождения, которые есть в нашем тексте.
Тот же пример на JavaScript:let str = 'Hello world';let result = str.match(/o/);console.log(result);["o", index: 4, input: "Hello world"]Модификаторы шаблонов
Для регулярных выражений существует набор модификаторов, которые меняют работу поиска. Они обозначаются одиночной буквой латинского алфавита и ставятся в конце регулярного выражения, после закрывающего “/”.
- i — символы в шаблоне соответствуют символам как верхнего, так и нижнего регистра.
- m — по умолчанию текст обрабатывается, как однострочная символьная строка. Метасимвол начала строки '^' соответствует только началу обрабатываемого текста, в то время как метасимвол конца строки '$' соответствует концу текста. Если этот модификатор используется, метасимволы «начало строки» и «конец строки» также соответствуют позициям перед произвольным символом перевода и строки и, соответственно, после, как и в самом начале, и в самом конце строки.
Об остальных модификаторах, используемых в PHP, можно почитать тут.
В JavaScript — тут.
О том, какие вообще бывают модификаторы, можно почитать тут.
Пример предыдущего регулярного выражения с модификатором на JavaScript:let str = "hello world \How is it going?"let result = str.match(/o/g);console.log(result);["o", "o", "o", "o"]Метасимволы в регулярных выражениях
Примеры по началу будут довольно примитивные, потому что мы знакомимся с самыми основами. Чем больше мы узнаем, тем ближе к реалиям будут примеры.
Чаще всего мы заранее не знаем, какой текст нам придется парсить. Заранее известен только примерный набор правил. Будь то пинкод в смс, email в письме и т.п.
Первый пример, нам надо получить все числа из текста:Текст: “Привет, твой номер 1528. Запомни его.”
Чтобы выбрать любое число, надо собрать все числа, указав “[0123456789]”. Более коротко можно задать вот так: “[0-9]”. Для всех цифр существует метасимвол “\d”. Он работает идентично.
Но если мы укажем регулярное выражение “/\d/”, то нам вернётся только первая цифра. Мы, конечно, можем использовать модификатор “g”, но в таком случае каждая цифра вернется отдельным элементом массива, поскольку будет считаться новым вхождением.
Для того, чтобы вывести подстроку единым вхождением, существуют символы плюс “+” и звездочка “*”. Первый указывает, что нам подойдет подстрока, где есть как минимум один подходящий под набор символ. Второй — что данный набор символов может быть, а может и не быть, и это нормально. Помимо этого мы можем указать точное значение подходящих символов вот так: “{N}”, где N — нужное количество. Или задать “от” и “до”, указав вот так: “{N, M}”.
Сейчас будет пара примеров, чтобы это уложилось в голове:Текст: “Я хочу ходить на работу 2 раза в неделю.”Надо получить цифру из тексте.RegExp: “/\d/”Текст: “Ваш пинкод: 24356” или “У вас нет пинкода.”Надо получить пинкод или ничего, если его нет.RegExp: “/\d*/”Текст: “Номер телефона 89091534357”Надо получить первые 11 символов, или FALSE, если их меньше.RegExp: “/\d{11}/”
Примерно так же мы работает с буквами, не забывая, что у них бывает регистр. Вот так можно задавать буквы:
- [a-z]
- [a-zA-Z]
- [а-яА-Я]
C кириллицей указанный диапазон работает по-разному для разных кодировок. В юникоде, например, в этот диапазон не входит буква “ё”. Подробнее об этом тут.
Пара примеров:Текст: “Вот бежит олень” или “Вот ваш индюк”Надо выбрать либо слово “олень”, либо слово “индюк”.RegExp: “/[а-яА-Я]+/”
Такое выражение выберет все слова, которые есть в предложении и написаны кириллицей. Нам нужно третье слово.
Помимо букв и цифр у нас могут быть еще важные символы, такие как:
- \s — пробел
- ^ — начало строки
- $ — конец строки
- | — “или”
Предыдущий пример стал проще:Текст: “Вот бежит олень” или “Вот бежит индюк”Надо выбрать либо “олень”, либо “индюк”.RegExp: “/[а-яА-Я]+$/”
Если мы точно знаем, что искомое слово последнее, мы ставим “$” и результатом работы будет только тот набор символов, после которого идет конец строки.
То же самое с началом строки:Текст: “Олень вкусный” или “Индюк вкусный”Надо выбрать либо “олень”, либо “индюк”.RegExp: “/^[а-яА-Я]+/”
Прежде, чем знакомиться с метасимволами дальше, надо отдельно обсудить символ “^”, потому что он у нас ходит на две работы сразу (это чтобы было интереснее). В некоторых случаях он обозначает начало строки, но в некоторых — отрицание.
Это нужно для тех случаев, когда проще указать символы, которые нас не устраивают, чем те, которые устраивают.
Допустим, мы собрали набор символов, которые нам подходят: “[a-z0-9]” (нас устроит любая маленькая латинская буква или цифра). А теперь предположим, что нас устроит любой символ, кроме этого. Это будет обозначаться вот так: “[^a-z0-9]”.
Пример:Текст: “Я люблю кушать суп”Надо выбрать все слова.RegExp: “[^\s]+”
Выбираем все “не пробелы”.
Итак, вот список основных метасимволов:
- \d — соответствует любой цифре; эквивалент [0-9]
- \D — соответствует любому не числовому символу; эквивалент [^0-9]
- \s — соответствует любому символу whitespace; эквивалент [ \t\n\r\f\v]
- \S — соответствует любому не-whitespace символу; эквивалент [^ \t\n\r\f\v]
- \w — соответствует любой букве или цифре; эквивалент [a-zA-Z0-9_]
- \W — наоборот; эквивалент [^a-zA-Z0-9_]
- . — (просто точка) любой символ, кроме перевода “каретки”
Операторы [] и ()
По описанному выше можно было догадаться, что [] используется для группировки нескольких символов вместе. Так мы говорим, что нас устроит любой символ из набора.
Пример:Текст: “Не могу перевести I dont know, помогите!”Надо получить весь английский текст.RegExp: “/[A-Za-z\s]{2,}/”
Тут мы собрали в группу (между символами []) все латинские буквы и пробел. При помощи {} указали, что нас интересуют вхождения, где минимум 2 символа, чтобы исключить вхождения из пустых пробелов.
Аналогично мы могли бы получить все русские слова, сделав инверсию: “[^A-Za-z\s]{2,}”.
В отличие от [], символы () собирают отмеченные выражения. Их иногда называют “захватом”.
Они нужны для того, чтобы передать выбранный кусок (который, возможно, состоит из нескольких вхождений [] в результат выдачи).
Пример:Текст: ‘Email you sent was ololo@example.com Is it correct?’Нам надо выбрать email.
Существует много решений. Пример ниже — это приближенный вариант, который просто покажет возможности регулярных выражений. На самом деле есть RFC, который определяет правильность email. И есть “регулярки” по RFC — вот примеры.
Мы выбираем все, что не пробел (потому что первая часть email может содержать любой набор символов), далее должен идти символ @, далее что угодно, кроме точки и пробела, далее точка, далее любой символ латиницы в нижнем регистре…
Итак, поехали:
- мы выбираем все, что не пробел: “[^\s]+”
- мы выбираем знак @: “@”
- мы выбираем что угодно, кроме точки и пробела: “[^\s\.]+”
- мы выбираем точку: “\.” (обратный слеш нужен для экранирования метасимвола, так как знак точки описывает любой символ — см. выше)
- мы выбираем любой символ латиницы в нижнем регистре: “[a-z]+”
Оказалось не так сложно. Теперь у нас есть email, собранный по частям. Рассмотрим на примере результата работы preg_match в PHP:<?php$text = ‘Email you sent was ololo@example.com. Is it correct?’;$regexp = ‘/[^\s]+@[^\s\.]+\.[a-z]+/’;$result = preg_match_all($regexp, $text, $match);var_dump($result,$match);int(1)array(1) {[0]=>array(1) {[0]=>string(13) "ololo@example.com"}}
Получилось! Но что, если теперь нам надо по отдельности получить домен и имя по email? И как-то использовать дальше в коде? Вот тут нам поможет “захват”. Мы просто выбираем, что нам нужно, и оборачиваем знаками (), как в примере:
Было:/[^\s]+@[^\s\.]+\.[a-z]+/
Стало:/([^\s]+)@([^\s\.]+\.[a-z]+)/
Пробуем:<?php$text = ‘Email you sent was ololo@example.com. Is it correct?’;$regexp = ‘/([^\s]+)@([^\s\.]+\.[a-z]+)/’;$result = preg_match_all($regexp, $text, $match);var_dump($result,$match);int(1)array(3) {[0]=>array(1) {[0]=>string(13) "ololo@example.com"}[1]=>array(1) {[0]=>string(5) "ololo"}[2]=>array(1) {[0]=>string(7) "example.com"}}
В массиве match нулевым элементом всегда идет полное вхождение регулярного выражения. А дальше по очереди идут “захваты”.
В PHP можно именовать “захваты”, используя следующий синтаксис:/(?<mail>[^\s]+)@(?<domain>[^\s\.]+\.[a-z]+)/
Тогда массив матча станет ассоциативным:<?php$text = ‘Email you sent was ololo@example.com. Is it correct?’;$regexp = ‘/(?<mail>[^\s]+)@(?<domain>[^\s\.]+\.[a-z]+)/’;$result = preg_match_all($regexp, $text, $match);var_dump(