Как стать автором
Обновить

Regex for lazy developers

Время на прочтение 9 мин
Количество просмотров 22K

Регулярные выражения – это система обработки текста, основанная на специальной системе записи образцов. Проще говоря представляет программистам возможность легко обрабатывать и валидировать строки. Представляет имплементацию принципа DRY (Don’t Repeat Yourself), почти во всех поддерживаемых языках паттерн регулярных выражений не будет изменятся от слова совсем. Код написанный на backend и frontend приложениях будет идентичным, тем самым позволяя экономить время командам на реализацию одинаковых фич. Также стоит акцентировать внимание на том, что данный модуль идеально подходит для работы с большими строками или сложными строками т.к. даёт возможность решать задачи связанные с ними просто и быстро. Бывает за чашечкой чая на кухне или в команде вы можете услышать, что регулярные выражения довольно сложные в изучение, написание и чтение и вообще их придумали ужасные люди 😈. Но так ли это? Давайте разбираться

Примечание:

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

Как это выглядит

Далее представлены примеры на 6-х языках программирования определения российского номера телефона.

Паттерн

^(?:8|\+7)[0-9]{10}$

C#

new Regex(“^(?:8|\+7)[0-9]{10}$”).Match(“testdata”)

PHP

preg_match(“/^(?:8|\+7)[0-9]{10}$/”, “testdata”, match[])

Python

re.match(r’^(?:8|\+7)[0-9]{10}$’,’testdata’)

JS

/^(?:8|\+7)[0-9]{10}$/.exec(“testdata”)

Java

“testdata”.matches(“^(?:8|\+7)[0-9]{10}$”)

Dart

RegExp(r"^(?:8|\+7)[0-9]{10}$").allMatches(“testdata”)

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

История появления

Впервые регулярные выражения возникли в научных работах по теории автоматов и теории формальных языков в середине 1950-х. Стефан Коул Клин признан человеком, который впервые ввёл понятие Регулярных Выражений.

Принципы и идеи, заложенные в его работах, были практически реализованы Кеном Томпсоном и с его лёгкой руки проникли в язык Perl.

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

Язык Регулярных Выражений не является полноценным языком программирования, хотя, как и другие языки, имеет свой синтаксис и команды.

Какие языки программирования поддерживают их?

Список довольно обширный вот лишь несколько из них:

Языки программирования поддерживающие regex
  • C

  • C#

  • C++

  • Cobol

  • Delphi

  • F#

  • Go

  • Groovy

  • Haskell

  • Java

  • JavaScript

  • Julia

  • Kotlin

  • MATLAB

  • Objective-C

  • PHP

  • Perl

  • Python

  • R

  • Ruby

  • Rust

  • Scala

  • Swift

  • Visual Basic

  • Visual Basic .NET

Возможности

  • Сопоставление входных данных по шаблону.

  • Поиск и изменение входных данных по шаблону.

  • Возвращение первого или всех результатов из входной строки.

  • Возвращение вместе с результатом общего поиска, именованные и не только подстроки при поиске. 

  • Замена символов, слов, фраз в входной строке после прохода.

  • И самое главное, пиши один раз и используй везде.

Где пригодится?

  • Поиск и замена кода по паттерну в IDE (VS Code, Rider, CLion, VS)

  • Валидация строк на соответствие шаблону (file extension).

  • Валидация полей на фронте (e-mail, phone number and other).

  • Валидация request и response данных.

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

Базовый синтаксис

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

  • $ – окончания строки  (означает что все условия до этого символа буду являться конечным результатом входной строки и после них ничего дальше нет. Не подходит если вы хотите вернуть несколько результатов из входной строки)

  • * – означает что предыдущее условие до данного символа, может встречаться один или несколько раз или вовсе не будет (соответственно может повторятся)

  • + – означает что предыдущее условие до данного символа должно встречаться один и более раз (соответственно, может повторятся)

  • [a-z] – перечисление допустимого символа в входной строке, то есть может быть любой буквой из латыни в нижнем регистре (a or b or c … or x or y or z)

  • [0-9] – перечисление допустимого символа в входной строке, то есть может быть любой буквой из латыни в нижнем регистре (1 or 2 or 3 … or 7 or 8 or 9)

  • . – один любой символ

  • \ – экранирование любого спец. символа (ПРИМЕР)

  • | – операция OR (означает, что должно выполнится условие слева или условия справа от этой операнды)

Упрощение синтаксиса

  • \d ≡ [0-9] – любой символ от 0 до 9

  • \D ≡ [^0-9] – любой символ кроме чисел

  • \w ≡ [a-zA-Z0-9_] – любой символ латыни, все числа и _

  • \W ≡ [^a-zA-Z0-9_] – любой символ кроме латинских символов, чисел и _

  • \s ≡ [ ] – исключительно пробел

  • \S ≡ [^ ] – любой символ, кроме пробела

Разбор базового синтаксиса

Регулярное выражение

Позитивный результат

Негативный результат

(Ч|ч)асы

“Часы”, “часы”

“39”, “Уаз”, “Час”, “чсы”, “асы”

(Ч|ч)?асы

“Часы”, “часы”, “асы”

“39”, “Уаз”, “Час”, “чсы”

.асы

“Часы”, “уасы”, “#асы”, “Zасы”, “4асы”

“39”, “Уаз”, “Час”, “чсы”, “асы”

[0-9]+

“62”, “1”, “5123”, “632”

“abs”

a+b

“ab”, “aab”, “aaaaaaab”

“abs”, “b”, “cb” 

[0-9]*

“62”, “1”, “5123”, “632”

“abs”

a*b

“ab”, “aab”, “aaaaaaab”, “b”

“abs”

\+7

“+7”

“+1”, “++7”, “-7”

^\+7\d{10}

“+79991112233”, “+712345678900”

“+712345678”

^\+7\d{10}$

“+79991112233” 

“+712345678900”, “+712345678”

Длина условий

Кроме валидации значений в строке мы также можем указывать сколько символов должно проходить одно и тоже условие. Есть всего три возможности работать длиной условий:

  • {3} – обязательное кол-во символов для условия 

  • {3,5} – мин. и макс. кол-во символов для условия

  • {3,} – обязательное мин. кол-во и неограниченное макс. кол-во

Регулярное выражение

Позитивный результат

Негативный результат

[0-9] {3}

“632”, “777”, “891”

“39”, “1”, “5123”, “abs”

[0-9] {3,5}

“632”, “12345”, “5123”

“39”, “7”, “125123”, “abs”

[0-9] {3,}

“632”, “123456”, “5123”

“39”, “0”, “abs”

Примечание: Условие “[0-9]” можно заменить на сокращение “\d”

Работа с группами (Advanced)

Дальше будет немного сложно, готовьтесь.

  • () – создание анонимной группы  (создание подстроки и выделение на неё памяти)

  • (?‘nameGroup’) - (?<nameGroup>) – создание именованной строки

  • (\k<nameGroup>) – служит для избавления паттерна от дубликат кода, то-есть если у вас есть именованная группа “nameGroup” с каким-то условием, вы можете не писать вторую группу в паттерне, а просто использовать данную директиву регулярных выражением указывая лишь название группы, которая была описана до. Тем самым, условие будет повторятся и вам не нужно описывать его заново.

  • (?:) – выделение в логические скобки условия, без именование и создания подстроки

  • (<=) – Исключает условия внутри скобок и не включает его в выборку.

  • (?!) – Проверяет условия внутри скобок и не включает его в выборку.

Реальный пример из жизни

Однажды по работе нужно было парсить данные с QR кода, которые печатались на чеках при покупке/возврате разных товаров, услуг и т.д. Первая версия парсера была написана на бекенд части (C#). Кодовая база парсера была объёмом в ~150 строк кода, она не учитывала некоторые особенности разных фискальных регистраторов (устройства, которые печатают чеки и отправляют данные в ФНС). Для изменения данной функции требовалось внимательно смотреть, проверять каждую строчку кода. Позже вариантов стало так много и появилась необходимость использовать её на фронте для валидации. Соответственно, было решено переписать её, используя регулярные выражения для упрощение парсера и возможности легкого и быстрого переноса его на другой язык программирования.

Цели: 

  1. Парсить входные значения для валидации по шаблону

  2. Брать необходимые поля дату и сумму покупки для дальнейшего использования в системе.

  3. Проверять что поле “n” равно всегда 1 (0 – возврат, 1 – покупка)

Входные данные:

t=20181125T142800&s=850.12&fn=8715000100011785&i=86841&fp=1440325305&n=1

Регулярное выражение на парсинг данных:

^t=(?<Date>[0-9-:T]+)&s=(?<Sum>[0-9]+(?:\.[0-9]{2})?)&fn=[0-9]+&i=[0-9]+&fp=[0-9]+&n=1$

Пример кода (C#):

private static (string date, string sum) parseQRCode(string data)
{
   var pattern = new Regex(@"^t=(?<Date>[0-9-:T]+)&s=(?<Sum>[0-9]+(?:\.[0-9]{2})?)&fn=[0-9]+&i=[0-9]+&fp=[0-9]+&n=1$", RegexOptions.ECMAScript);
   var matchResult = pattern.Match(data);
   if (!matchResult.Success)
       throw new ArgumentException("Invalid qrCode");
   var dateGroup = matchResult.Groups["Date"];
   if(!dateGroup.Success)
       throw new ArgumentException("Invalid qrCode, Date group not found");
   var sumGroup = matchResult.Groups["Sum"];
   if(!sumGroup.Success)
       throw new ArgumentException("Invalid qrCode, Sum group not found");

   return (dateGroup.Value, sumGroup.Value);
}

Пример кода на JS:

Это вариант сделан через Exception-ы, но можно сделать через return false или return null.

const parseQRCode = (data:string) : {date: string, sum: string} => {
  const pattern = new RegExp("^t=(?<Date>[0-9-:T]+)&s=(?<Sum>[0-9]+(?:\.[0-9]{2})?)&fn=[0-9]+&i=[0-9]+&fp=[0-9]+&n=1$");
  const matchResult = pattern.exec(data);
  if (!matchResult)
      throw "Invalid qrCode";
  const dateGroup = matchResult[1];
  if(!dateGroup)
      throw "Invalid qrCode, Date group not found";
  const sumGroup = matchResult[2];
  if(!sumGroup)
      throw "Invalid qrCode, Sum group not found";
  return {date: dateGroup, sum: sumGroup};
};

На выходе мы получаем получаем два значения:

  1. Date – поле обозначающее дату и время покупки (осталось только спарсить её и превратить в объект даты)

  2. Sum – сумма покупки

Теперь давайте разберём паттерн поподробнее:

  1. ^ – обозначающая начало строки

  2. t=(?<Date>[0-9-:T]+) – обязательные символы  t=(далее любые символы (от 0 до 9 или - или : или T) в одном или более экземпляре)

  3. &s=(?<Sum>[0-9]+(?:\.[0-9]{2})?) – обязательные символы 

    1. &s= – обязательная последовательность символов & и s и =

    2. [0-9]+(символы от 0 до 9 одном или более экземпляре)

    3. (?:\.[0-9]{2})?

  4. $ – обозначающая конец строки

  5. &fn=[0-9]+ – обязательные символы &fn= и далее [0-9]+ -> (любое число от 0 до 9 в одном или более экземпляре)

  6. &i=[0-9]+ – обязательные символы &i= и далее [0-9]+ -> (любое число от 0 до 9 в одном или более экземпляре)

  7. &fp=[0-9]+ – обязательные символы &fp= и далее [0-9]+ -> (любое число от 0 до 9 в одном или более экземпляре)

  8. &n=1 – обязательные символы &n=1

Проблема работы с не латынью

Когда вам необходимо работать со всем алфавитом из латыни, достаточно просто написать [a-zA-Z]. Многие подумают что при работе с кириллицей хватает написать [а-яА-Я]. Вроде всё логично и всё хорошо, но в какой-то момент вы поймете, что у вас иногда она работает некорректно. Проблема заключается в том, что диапазон [а-я] не включает в себя букву “ё”, соответственно, вам необходимо изменить ваш паттерн с [а-яА-Я] на [а-яёА-ЯË], чтобы код учитывал специфичную букву в алфавите. Такая проблема есть не только в кириллице, также эта проблема актуальна для греческого, турецкого и ряда других языков. Будьте внимательны при написание паттерна, который должен использовать эти языки.

Флаги regex в JS

  • global (g) - не прекращает поиск после нахождения первого соответствия.

  • multi line (m) - ищет по строке включая перенос строки (^ начало строки, $ конец строки).

  • insensitive (i) - производить поиск вне зависимости от регистра (a ≡ A)

  • sticky (y) - поиск возвращает, кроме совпадения индекс с начала совпадения подвыборки (не поддерживается в IE)

  • unicode (u) - поиск включает unicode символы (не поддерживается в IE)

  • single line (s) - в этом режиме символ “.” включает в себя также перенос на новую строку (поддерживается в Chrome, Opera, Safari)

Дополнительные настройки regex в C#

RegexOptions выставляется как дополнительный параметр в конструкторе Regex класса. Также его можно указывать в методах Match, Matches.

  • None - выставляется по дефолту.

  • IgnoreCase (\i) - проверяет без учёта регистра.

  • Multiline (\m) - работа со строкой где есть переносы \n.

  • ExplicitCapture (\n) - добавляет в результат только именованные группы.

  • Compiled (будет полезен лишь в static варианте, ускоряет регулярку, замедляет компиляцию).

  • Singleline (знак “.” будет обозначать любой символ кроме \n и игнорирует его при поиске)

  • IgnorePatternWhitespace (\x) . (вырезает все пробелы, исключения в конструкциях[],{})

  • RightToLeft - поиск справа налево.

  • ECMAScript (JS like версия, но стиль группировки как в .NET).

  • CultureInvariant (сравнивает игнорирую раскладку клавиатуры).

Хорошие практики и советы по оптимизации

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

  2. Используя сокращения (\d, \w и другие), будьте уверены что они полностью соответствуют вашим условиям поиска.

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

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

  5. Старайтесь уменьшить количество экранирования (\), данный функционал замедляет скорость выполнения во многих языках программирования.

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

Заключение

Регулярные выражения лишь хотят казаться сложными, но на деле возможности которые они предоставляют дают массу возможностей и позволяют упростить и ускорить работу всем от Junior-а до Senior/Lead-a. Пожалуйста, если у вас будут вопросы милости прошу в комментарии, где мы сможем с вами подискутировать.

P.S. Программируйте это всё ещё классно)

P.P.S. Привет всем кто пришёл сюда с mergeconf

P.P.P.S. Спасибо Serge78rus за небольшое исправление по описанию операции +.

Ссылки

Замеры на разных языках

→ Онлайн regex-помощник со словарём всех доступных команд и поддержкой нескольких языков программирования.

Теги:
Хабы:
+10
Комментарии 34
Комментарии Комментарии 34

Публикации

Истории

Работа

.NET разработчик
65 вакансий
Python разработчик
130 вакансий
Data Scientist
66 вакансий
PHP программист
171 вакансия
React разработчик
67 вакансий

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн