Обезболиваем RegEx

image-loader.svg

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

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

https://www.commitstrip.com/wp-content/uploads/2016/04/Strip-Notice-a-vie-650-finalenglish-1.jpghttps://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. Если лучший выход — все-таки использовать регулярные выражения, напишите документацию и тесты. В документации укажите, чего вы в целом хотите добиться, а в тестах сделайте проверку всех возможных кейсов. Это поможет новым разработчикам и даже вам в будущем разобраться с этим регулярным выражением.

© Habrahabr.ru