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

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

https://www.commitstrip.com/wp-content/uploads/2016/04/Strip-Notice-a-vie-650-finalenglish-1.jpg
https://www.commitstrip.com/wp-content/uploads/2016/04/Strip-Notice-a-vie-650-finalenglish-1.jpg

Можно ли вообще не использовать регулярные выражения? А в каких случаях нельзя? Что делать, если использовать все-таки приходится? Предлагаю разобраться с этим. Определим ситуации, когда регулярные выражения можно не использовать, когда нужно использовать и как сделать так, чтобы не было мучительно больно к ним возвращаться.

А может, не надо?

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

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

Всего существует два типа движков:

  1. Текстовые движки.

  2. Движки, ориентированные на регулярные выражения.

Первые в программировании уже практически не встречаются из-за медленной работы. Поэтому говорить будем только о движках второго типа. Алгоритм работы вторых довольно прост. Вот переведенное мной разъяснение с сайта regular-expressions.info:

«Когда регулярное выражение применяется к строке, движок начинает работу с первого символа. Он применяет все возможные перестановки регулярного выражения к этому символу. Только когда все возможности исчерпаны и оказались безуспешными, движок продолжает работу со вторым символом в строке. Снова пытается применить все перестановки в том же порядке. Результат работы движка — самое первое совпадение».

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

Допустим, у нас есть строка, для которой мы должны найти индекс вхождения. Я попробовал посчитать время выполнения скрипта. Все, кто читал Илью Кантора, знают, что замерять время выполнения JS-кода — это дело неблагодарное и неточное, но я попробую, потому что результат вас удивит.

Итак, вариант с обычным поиском по строке:

const { performance } = require('perf_hooks');

const results = [];
const string = 'I love JavaScript';

for (let i = 0; i < 10; i++) {
  const start = performance.now();

  for (let j = 0; j < 1000000; j++) {
    string.indexOf('Script');
  }

  const end = performance.now();
  results.push(end - start);
}

const average = results.reduce((calc, val) => calc + val, 0) / results.length;

console.log(`Average: ${average}`); // В среднем 1.4

А теперь попробуем то же самое, но уже через RegExp:

const { performance } = require('perf_hooks');

const regExp = /Script/;
const results = [];
const string = 'I love JavaScript';

for (let i = 0; i < 10; i++) {
  const start = performance.now();

  for (let j = 0; j < 1000000; j++) {
    string.search(regExp);
  }

  const end = performance.now();
  results.push(end - start);
}

const average = results.reduce((calc, val) => calc + val, 0) / results.length;

console.log(`Average: ${average}`); // В среднем 28.24

Даже в таком простом случае мы видим, что RegExp работает более чем в 20 раз медленнее. Это значит, что если нужно реализовать какую-то простую проверку, нам не просто можно, а нужно использовать встроенные методы работы со строками.

Без RegEx не обойтись

Есть ситуации, когда любые другие способы проверки будут слишком большими и сложными. Например, нужно провалидировать цвет, введенный пользователем в формате HEX, достать URL из полученного текста или проверить дату.

Вот буквально пара примеров, попробуйте записать их через методы строки:

// 1. Строка начинается только с заглавной латинской буквы или цифры
// 2. За ним может быть разрешенный спецсимвол или единичный пробел
// 3. Нельзя использовать кириллицу и другие спецсимволы
const someEngPattern = `^[A-Z0-9]+([a-zA-Z0-9!#%]|\\s(?!\\s))*$`;

// Захватывает теги с атрибутами
const htmlTag = `<\/?[\w\s]*>|<.+[\W]>`;

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

Справляемся с RegEx

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

Что мы можем сделать, чтобы упростить жизнь новому разработчику, а может быть, и самим себе?

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

/**
1. Латинское слово, которое может начинаться только на заглавную букву или цифру.
2. Может содержать !, #, и %.
3. Может содержать только 1 пробел между словами.
4. Не может заканчиваться пробелом.

Пример: A23 New word
*/
const someEngPattern = /^[A-Z0-9]+([a-zA-Z0-9\\!\\#\\%]|\\s(?!\\s))*$/;

Написать тесты. Важно написать тесты на все критичные для вас случаи. В примере я написал совсем немного, но цель — передать, как должны выглядеть тесты и что содержать:

describe('Eng Pattern', () => {
    describe('Валидная строка', () => {
        it('Если строка начинается на латинскую заглавную букву, то она валидна', () => {
            expect(someEngPattern.test('Jessy')).toBe(true);
        });

        it('Если строка начинается на цифру, то она валидна', () => {
            expect(someEngPattern.test('1pattern')).toBe(true);
        });

				// ...
				// И еще несколько кейсов
    });

    describe('Невалидная строка', () => {
        it('Если строка начинается на латинскую строчную букву, то она не валидна', () => {
            expect(someEngPattern.test('jessy')).toBe(false);
        });

        it('Если строка начинается на кириллическую букву, то она не валидна', () => {
            expect(someEngPattern.test('Жessy')).toBe(false);
        });
				
				// ...
				// И здесь тоже
    });
});

У этого подхода есть много плюсов:

  1. Можно работать над регулярным выражением, не опасаясь, что сломается какая-то логика. Это позволяет проводить рефакторинг и вносить любые другие правки.

  2. Тесты также служат своеобразной документацией. Тем не менее они не могут заменить описания «Чего вы хотите добиться этим регулярным выражением». Тесты проверяют вашу конкретную реализацию и крайние кейсы.

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

Регулярные выражения работают не везде одинаково, поэтому нельзя просто так взять и скопировать регулярное выражение из JavaScript и надеяться, что оно будет так же работать на Kotlin. Особенности работы регулярных выражений в JavaScript описаны на learn.javascript.ru/regexp-specials.

Выводы

Регулярные выражения — это полезный и мощный инструмент для работы со строками. В статье я не рассказывал, как писать регулярные выражения, поэтому рекомендую прочитать хотя бы одну из множества статей на Хабре и на других ресурсах. А пока могу дать пару полезных советов:

  1. Если можете не пользоваться регулярными выражениями — не пользуйтесь. Попробуйте решить задачу с помощью встроенных методов работы со строками. Это быстрее и удобнее.

  2. Если лучший выход — все-таки использовать регулярные выражения, напишите документацию и тесты. В документации укажите, чего вы в целом хотите добиться, а в тестах сделайте проверку всех возможных кейсов. Это поможет новым разработчикам и даже вам в будущем разобраться с этим регулярным выражением.

Комментарии (32)


  1. webdevium
    02.08.2021 17:10
    +2

    Коротко: не используйте RegEx там, где достаточно String.prototype.indexOf()


    1. anton-nikulin Автор
      03.08.2021 08:09

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


  1. anonymous
    00.00.0000 00:00


    1. Eugeny1987
      03.08.2021 05:26
      +2

      \\!\\#\\%
      так тут еще экранируется экранирование


    1. anton-nikulin Автор
      03.08.2021 08:14

      Согласен. Спасибо за уточнение)


  1. exadmin
    02.08.2021 17:43
    +5

    Вот один из отличных on-line reg-exp редакторов: https://regex101.com/, в нем отлично реализована как подсветка групп, так и в целом cook-book имеется. + Поддерживаются различные популярные реализации.


    1. extempl
      02.08.2021 20:04
      +2

      А так же имеется счётчик сложности (шагов, необходимых движку на парсинг).


    1. anton-nikulin Автор
      03.08.2021 08:15

      Круто, спасибо). Буду использовать.


  1. gdt
    02.08.2021 18:10
    +2

    1. v1000
      03.08.2021 00:00
      +1

      Мило. Бегло пробежал по тексту и подумал, что у меня опять ГПУ ускорение в бразузере глючит.


    1. anton-nikulin Автор
      03.08.2021 08:18

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


      1. gdt
        03.08.2021 08:56
        +1

        Каждому инструменту должно быть свое применение. Если мне не изменяет память — HTML не является регулярным языком. Конечно, с использованием расширенных возможностей, вроде бэктрекинга, вы наверное сможете разобрать какое-то подмножество HTML. Но в общем случае, если взять любой движок и любой HTML — это невозможно.


        1. anton-nikulin Автор
          03.08.2021 14:53
          +1

          Здесь возразить нечего, соглашусь.


      1. Alexandroppolus
        03.08.2021 10:32

        В HTML возможна закрывающая треугольная скобка внутри значения атрибута. То есть простой вариант поиска тега уже разваливается. Ещё там возможны комментарии и вставки js и css (где могут быть любые расклады по скобкам и кавычкам). Вероятно, это всё можно запихнуть в регекс, но обезболить такой регекс уже не получится.


        1. anton-nikulin Автор
          03.08.2021 14:54

          Да, согласен. Видимо, мы просто не сталкивались с такими проблемами во время работы, поэтому RegEx нам подходил.


  1. nin-jin
    02.08.2021 19:48

    1. anton-nikulin Автор
      03.08.2021 08:21

      Я запускал на NodeJS один миллион раз. Вероятно, здесь V8 постарался и оптимизировал алгоритм.


      1. nin-jin
        03.08.2021 12:04

        У вас результат никак не используется, компилятор вообще может выкинуть всё вычисление.


        1. anton-nikulin Автор
          03.08.2021 14:55

          Возможно, но в одном случае выкидывает, а во втором - нет? Предлагаю просто оставить это как результат моего эксперимента.


          1. nin-jin
            03.08.2021 16:29

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


  1. nin-jin
    02.08.2021 19:51
    -1

    3 - использовать билдер регулярок: Да хватит уже писать эти регулярки


    1. anton-nikulin Автор
      03.08.2021 08:24

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


      1. nin-jin
        03.08.2021 12:07

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


        1. anton-nikulin Автор
          03.08.2021 14:56

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


  1. dimaaannn
    02.08.2021 20:55
    +3

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

    Если нужно найти одно вхождение - да, будет медленнее.
    Когда задача найти несколько разных строк, или более сложных условий - всё становится гораздо лучше.
    Это как использовать StringBuilder вместо конкатенации 2х строк.

    И да, вы же не забыли что у регулярок конечные автоматы
    под капотом?


    1. anton-nikulin Автор
      03.08.2021 08:29

      Да, я согласен, что в сложных случаях нужно использовать регулярки. Об этом я написал в пункте "Без RegEx не обойтись".


  1. Gummilion
    03.08.2021 12:38

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


    1. anton-nikulin Автор
      03.08.2021 14:59

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


  1. MihailGrin
    09.08.2021 08:08
    +1

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


  1. Vollger
    09.08.2021 08:08
    +1

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


    1. anton-nikulin Автор
      09.08.2021 08:08

      Согласен, при сложных кейсах реально без них не обойтись.


  1. SynCap
    26.08.2021 17:36
    +1

    • RegExp -- Regular Expressions -- правильно переводить -- "УПРАВЛЯЮЩИЕ Выражения".

    • RegExp -- это микропрограмма, которая компилируется в высокоскоростной конечный автомат, заточенный под обработку строк. Изначально предназначен на работу исключительно со строками из символов диапазона ASCII-7. Цель -- выполнять несколько операций поиска дополнительно используя ещё и математические выборки за один вызов.

    • в настоящее время в движке V8 (Chromium, Node), в редакторах SublimeText, VisualStudio Code и много где ещё используется расширенная реализация основанная на Си-шной библиотеке Boost, которую модифицируют под нужды приложений (того же V8, например), и в качестве расширений добавляют модификаторы якорей, поддержку Unicode/UTF8, дополнительные проверки контекста. Хотя, на мой взгляд, она сама хорошо оптимизирована и имеет арсенал богаче, чем изначальный ставший популярным вариант на Perl, который кое-где называют PCRE - Perl Compatible Regular Expressions.

    На сегодняшний день, лично я рекомендую использовать одиночные операции со строками (IndexOf, например) исключительно для читаемости кода.

    Но если важна производительность или краткость исходника (при минификации и/или оптимизации обработанный код получается компактнее, меньше внутренних переходов и вызовов), то РегЕксп предпочтительней даже для простейших операций.

    Дополнительно это может дать единый формат ответа функции обработки, что крайне важно при конвеерной обработке, где данные меняются от шага к шагу (про микроконвеер самих РегЕкспов уже всё все поняли)

    Если много операций со строками, то разница в размере бандла и скорости работы:

    • Вас приятно удивит на JavaScript и Go,

    • ничего не скажет на Python2, будет заметна на Python3,

    • и разочарует на чистом C.

    Причина разочарования -- С компилируется в процессорный, а не промежуточный байт-код или собирается в бандл.

    Дело в том, что реализация одиночных вызовов (о, чудо!) в современных реализациях ВСЕХ языков программирования использует тот же подход, который был в основе РегЕксп-ов

    Ну, а чтобы легче понимать Регулярки, точнее их основу, не поленитесь, найдите документацию по JScript5 от Microsoft, которая шла в поставе Office"95, Office"97 и Office"2003, шла в комплекте с WScript, была в составе официальной поставки Windows XP и Windows Vista. Название файла JSCRIPT5.CHM

    На мой взгляд -- лучшая документация по регуляркам. Книжка Фидла толщиной в 400 страниц курит в сторонке по сравнению с 3 страницами из доки от MS.

    Всем успеха в понимании и изучении!