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

Можно тренироваться в придумывании и использовании регулярных выражений на сайте regexr.com.

Регулярное выражение — это описание множества слов, где слова это последовательности символов. Т.е. фактически регулярное выражение описывает подмножество значений типа String.

Примеры множеств слов:

  1. Все строки длины 2. L = {“ab”, “xy”, …}, “abc” — не подходит.
  2. Все строки, начинающиеся и заканчивающиеся на букву ‘a’. L = {“asdfga”, “auuuua”, …}, “abc” - нет
  3. Все строки, которые похожи на email: L = {“abc@yandex.ru”, “iposov@gmail.com”, “ilya@usa.gov”, …} “asdfasd@@@@fkasdfas” - нет.
  4. Строки похожие на даты
  5. … на имена
  6. … на числа
  7. Строки, в которых сначала несколько букв “a”, потом несколько букв “b”: L = {“aabbbbb”, “ab”, “aabb”, “abbb”}. “abc”, “abba” — не подходят.

Зачем. Можно

  1. Проверить, соответствует ли строка регулярному выражению. Например, вы можете проверить email, введенный пользователем, похож ли он вообще на email.
  2. Искать вхождения внутри текста. Например, искать внутри длинного текста строки, похожие на email.
  3. Искать и заменять. Например, находить имена внутри текста, заменять их на ***. Это может быть нужно для анонимизации.

Примеры. Вы можете вводить их на указанном выше сайте.

  1. ss. Это одно слово. Под это регулярное выражение подходит только одно слово “ss”.
  2. as|es Это два слова: “as” и “es”.
  3. (a|e)s Это те же два слова. Скобки группируют варианты, получается, что сначала буква “a” или “e”, потом “s”.
  4. (a|e)(s|t) слова as, es, at, et. Два варианта первой буквы и два варианта второй.
  5. 0|1|2|3|4|5|6|7|8|9 10 вариантов слов. Любая цифра.
  6. \d аналог предыдущего, любая цифра. Есть много подобных последовательностей, начинающихся на обратный слеш и обозначающих какое-то множество символов.
  7. s* несколько s подряд (возможно 0). Т.е. это слова “”, “s”, “ss”, “sss” и т.д. Это первый из всех примеров, в котором мы описали бесконечное множество слов.
  8. (a|e|i|o|u|y)* несколько гласных подряд. Т.е. одна из гласных букв, потом снова одна из гласных букв и т.д. Буквы могут быть разные, т.е. слова “”, “aeii”, “aaa”, “auiaiu” подходят.
  9. [aeiouy]* аналогично предыдущему, гласные подряд. [] — сопоставляется с одним символом, одним из тех, что перечислены внутри.
  10. [a-zA-Z] т.е. одна английская буква.
  11. [a-zA-Z]* несколько английских букв подряд.
  12. [a-z.-]*@[a-z.-]*.[a-z][a-z]([a-z]|) Это попытка изобразить что-то, похожее на email. В конце после точки может быть две или три буквы. Посмотрите на последнюю скобку, там написано, что это или буква, или ничего.
  13. [a-z.-]+@[a-z.-]+\.[a-z]{2,3}. Аналогично предыдущему.

    + означает повторение хотя бы один раз, в отличие от *, которая означает, что может повториться 0 раз.

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

    {a,b} это значит повторить от a до b раз.

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

  14. \s пробельный символ. Т.е. пробел, табуляция, перевод строки. Один из этих символв. Довольно часто используется \s+, т.е. несколько пробельных символов подряд.

Группы

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

  1. ([a-z])(\d) буква потом цифра. Скобки задают группы. Первая группа - буква, вторая группа — цифра.
  2. ([a-zA-Z]+)-([a-zA-Z]+). Слова через дефис. Первая группа это первая половина слова. Вторая группа - вторая половина.

Флаги

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

  1. CASE_INSENSITIVE — игнорировать ли регистр при поиске, при включенном режиме регистр игнорируется.
  2. UNICODE_CASE — поддержка unicode при игнорировании регистра. Нам обычно захочется включать этот режим, особенно если мы будем использовать русские буквы в регулярных выражениях и хотеть, чтобы регистр при поиске не имел значения.
  3. DOTALL — без этого режима . означает любой символ кроме перевода строки, Если режим DOTALL включен,. означает вообще любой символ, включая перевод строки,
  4. MULTILINE — без этого режима ^ означает начало всего ввода, это даже не символ, а позиция внутри текста, сразу после начала текста. $ означает конец текста. Если режим MULTILINE включен, ^ — дополнительно означает начало строки, т.е. позицию сразу после символа перевода строки. $ — дополнительно означает конец строки, точнее, позицию перед символом конца строки).

Использование в Java

Допустим, мы хотим записать регулярное выражение \d\.\d\d в Java. Это цифра точка и две цифры.

String s = "\d\.\d\d"; - такую строку нельзя написать в Java. Потому что \d- нет такого символа в Java. Вспомните, что есть \n, но символа \d — нет.

Надо писать так, с экранированием: String s = "\\d\\.\\d\\d", здесь Java воспримет \\ как один символ \. Поэтому внутри переменной s будет хранится нужное нам выражение. К счастью, IDEA подсказывает, если вы неправильно запишете регулярное выражение.

В Python специально есть отдельный вид “сырых” (raw) строк s = r"\d\.\d\d", в которых можно не писать лишний раз экранирование. В Java эту возможность еще обсуждают, но она обязательно появится, если наша цивилизация продержится еще несколько лет.

## Методы, принимающие регулярные выражения

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

Вот так можно проверить, соответствует ли строка регулярному выражению:

"abc".matches("[a-z]+"); // true

Этот метод проверят соответствие целиком, т.е., например,

"abc42".matches("[a-z]+"); // false

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

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

"abc3iii6".replaceAll("\\d", "++") // "abc++iii++"

Здесь цифры заменились на двойные плюсы. Обратите внимание, что регулярное выражение \d пришлось экранировать, чтобы записать его в Java.

Замену можно делать с группами:

"abc3iii6".replaceAll("([a-z])(\\d)", "$2!$1") // "ab3!сii6!i"

Здесь $1 и $2 это ссылки на значения первой и второй группы. Команда находит буквы, после которых сразу следует цифра, в нашем примере это c3 и i6, и заменяет эти вхождения на цифру (вторая группа), символ восклицательного знака и букву (первую группу). В нашем примере на 3!c и 6!i.

Метод replaceFirst заменит только первое вхождение.

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

"abc    asdf 234".split("\\s+") // получим массив String[]
                                // "abc", "asdf", "234"

Более тонкие возможности работы с регулярными выражениями.

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


public class A extends Application {

    public static void main(String[] args) {
        // Создадим регулярное выражение для поиска нескольких
        // цифр, после которых идет слово "кот", "кота" или "котов"
        // Число заключено в группу.
        Pattern p = Pattern.compile(
                "(\\d+) кот(|а|ов)",
                Pattern.CASE_INSENSITIVE + Pattern.UNICODE_CHARACTER_CLASS
        );

        // Matcher связывает регулярное выражение с конкретной строкой, в которой
        // дальше с этим регулярным выражением произойдет сопоставление:
        Matcher m1 = p.matcher("42 кота");
        if (m1.matches()) {
            System.out.println("строка сопоставилась с выражением");
            System.out.println("значение первой группы " + m1.group(1));
        }

        // Теперь поищем выражение в строке
        Matcher m2 = p.matcher("1 кот, 2 кота, еще 10 котов");
        while (m2.find()) {
            // find() ищет очередное вхождение регулярного выражения после
            // уже найденного и возвращает, было ли что-то найдено.
            System.out.println("Найдены очередные коты:");
            System.out.println("Целиком сопоставилось: " + m2.group());
            System.out.println("Количество: " + m2.group(1));
        }

        // Ну и поиск с заменой. Этот хитрый код взят из документации по
        // методу appendReplacement, поэтому его надо использовать как есть,
        // не придумывая каждый раз заново:

        Matcher m3 = p.matcher("1 кот, 2 кота, еще 10 котов");
        StringBuilder result = new StringBuilder();
        while (m3.find())
            m3.appendReplacement(result, "несколько котов");
        m3.appendTail(result);
        System.out.println("Результат замены: " + result.toString());
        
        // Этот способ имеет больше возможностей, чем метод replaceAll, потому что
        // вы можете проделать произвольные вычисления перед тем как понять,
        // на что заменить. Например, увеличить количество котов на 1.
    }
}