cpp

Синтаксис регулярных выражений

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

re := regexp.MustCompile(`строка`)
fmt.Println(re.MatchString("строка")) // true
fmt.Println(re.MatchString("СТРОКА")) // false

Проверить, состоит ли шаблон только из обычных символов, позволяет метод LiteralPrefix(). Через первое возвращаемое значение доступен префикс, состоящий только из обычных символов. Через второе возвращаемое значение доступно true, если шаблон состоит только из обычных символов, и false — в противном случае. Формат метода:

(*regexp.Regexp).LiteralPrefix() (prefix string, complete bool)

Пример:

re := regexp.MustCompile(`строка`)
fmt.Println(re.LiteralPrefix()) // строка true
re = regexp.MustCompile(`строка[0-9]+`)
fmt.Println(re.LiteralPrefix()) // строка false

Экранирование специальных символов

Внутри регулярного выражения символы ., ^, $, *, +, ?, {, }, [, ], \, |, ( и ) имеют специальное значение. Если эти символы должны трактоваться как есть, то их следует экранировать с помощью слэша. Некоторые специальные символы теряют свое специальное значение, если их разместить внутри квадратных скобок. В этом случае экранировать их не нужно. Например, метасимвол точка соответствует любому символу, кроме символа перевода строки. Если необходимо найти именно точку, то перед точкой необходимо указать символ \ или поместить точку внутри квадратных скобок ([.]). Продемонстрируем это на примере проверки правильности введенной даты (листинг 8.1).

Листинг 8.1. Проверка правильности ввода даты

package main

import (
   "fmt"
   "regexp"
)

func main() {
   d := "29,07.2022" // Вместо точки указана запятая
   // Символ "\" не указан перед точкой
   re := regexp.MustCompile(`[0-3][0-9].[01][0-9].[12][09][0-9][0-9]`)
   if re.MatchString(d) {
      fmt.Println("Дата введена правильно")
   } else {
      fmt.Println("Дата введена неправильно")
   }
   // Т. к. точка означает любой символ, выведет:
   // Дата введена правильно
   
   // Символ "\" указан перед точкой
   re = regexp.MustCompile(`[0-3][0-9]\.[01][0-9]\.[12][09][0-9][0-9]`)
   if re.MatchString(d) {
      fmt.Println("Дата введена правильно")
   } else {
      fmt.Println("Дата введена неправильно")
   }
   // Т. к. перед точкой указан символ "\", выведет:
   // Дата введена неправильно
   
   // Точка внутри квадратных скобок
   re = regexp.MustCompile(`[0-3][0-9][.][01][0-9][.][12][09][0-9][0-9]`)
   if re.MatchString(d) {
      fmt.Println("Дата введена правильно")
   } else {
      fmt.Println("Дата введена неправильно")
   }
   // Выведет: Дата введена неправильно
}

Следует учитывать, что символ обратного слеша является специальным не только в шаблоне регулярного выражения, но и в строке. Поэтому его нужно экранировать. Например, проверим равна ли строка значению "\s". Внутри шаблона регулярного выражения данная комбинация символов является специальной и обозначает любые пробельные символы. Поэтому внутри шаблона мы должны добавить еще три слеша, чтобы экранировать специальную комбинацию:

re := regexp.MustCompile("^\\\\s$")
fmt.Println(re.MatchString("\\s")) // true

Посмотрите сколько слешей! Чтобы не путаться, следует выводить строку с шаблоном в консоль и смотреть, что получилось в итоге. Помните, что экранирование внутри шаблона должно быть видно при выводе:

re := regexp.MustCompile("^\\\\s$")
fmt.Println(re.String())           // ^\\s$

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

re := regexp.MustCompile(`^\\s$`)
fmt.Println(re.String())           // ^\\s$
fmt.Println(re.MatchString("\\s")) // true

Метасимвол точка теряет свое специальное значение, если его заключить в квадратные скобки. Кроме того, внутри квадратных скобок могут встретиться символы, которые имеют специальное значение (например, ^ и ). Символ ^ теряет свое специальное значение, если он не расположен сразу после открывающей квадратной скобки:

re := regexp.MustCompile(`[09^]`)                // 0, 9 или ^
fmt.Println(re.FindAllString("0123456789^", -1)) // [0 9 ^]

Чтобы отменить специальное значение символа , его необходимо указать после перечисления всех символов, перед закрывающей квадратной скобкой:

re := regexp.MustCompile(`[09-]`)                // 0, 9 или -
fmt.Println(re.FindAllString("0123456789-", -1)) // [0 9 -]

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

re := regexp.MustCompile(`[0\-9]`)               // 0, - или 9
fmt.Println(re.FindAllString("0123456789-", -1)) // [0 9 -]

Для экранирования специальных символов можно использовать функцию QuoteMeta(). Формат функции:

regexp.QuoteMeta(s string) string

Пример:

fmt.Println(regexp.QuoteMeta(`.^$*+?{}[]\|()`))
// \.\^\$\*\+\?\{\}\[\]\\\|\(\)

Модификаторы

Перед регулярным выражением можно указать дополнительные параметры поиска в форматах:

(?<Модификаторы>)
(?<Модификаторы>:<Шаблон>)

Параметр <Модификатор> может содержать следующие буквы или их комбинацию (для отключения перед буквой следует указать символ -):

  • i — поиск без учета регистра:
re := regexp.MustCompile(`строка`)
fmt.Println(re.MatchString("строка")) // true
fmt.Println(re.MatchString("СТРОКА")) // false
re = regexp.MustCompile(`(?i)строка`)
fmt.Println(re.MatchString("строка")) // true
fmt.Println(re.MatchString("СТРОКА")) // true
re = regexp.MustCompile(`(?i:строка)`)
fmt.Println(re.MatchString("строка")) // true
fmt.Println(re.MatchString("СТРОКА")) // true
  • m — многострочный режим. Символ ^ соответствует началу каждой подстроки, а $ — концу каждой подстроки:
re := regexp.MustCompile(`^[0-9]$`)
fmt.Println(re.FindAllString("1\n2\n3\nстрока\n4", -1))
// []
re = regexp.MustCompile(`(?m)^[0-9]$`)
fmt.Println(re.FindAllString("1\n2\n3\nстрока\n4", -1))
// [1 2 3 4]
  • s — если флаг указан, то метасимвол точка будет соответствовать любому символу, включая символ перевода строки (\n):
re := regexp.MustCompile(`.`)
fmt.Println(re.MatchString("\n")) // false
re = regexp.MustCompile(`(?s).`)
fmt.Println(re.MatchString("\n")) // true
  • U — ограничивает "жадность" квантификаторов:
re := regexp.MustCompile(`(?i)<b>.*</b>`)
s := "<b>Text1</b>Text2<b>Text3</b>"
fmt.Println(re.FindAllString(s, -1))
// [<b>Text1</b>Text2<b>Text3</b>]
re = regexp.MustCompile(`(?iU)<b>.*</b>`)
fmt.Println(re.FindAllString(s, -1))
// [<b>Text1</b> <b>Text3</b>]

Метасимволы

Перечислим метасимволы, применяемые в регулярных выражениях:

  • ^ — привязка к началу строки. Если указан модификатор m, то соответствует началу каждой подстроки;
  • $ — привязка к концу строки. Если указан модификатор m, то соответствует концу каждой подстроки;
  • \A — привязка к началу строки (не зависит от модификатора);
  • \z — привязка к концу строки (не зависит от модификатора);
  • [] — позволяет указать символы, которые могут встречаться на этом месте в строке. Можно перечислять символы подряд или указать диапазон через тире;
  • [^] — позволяет указать символы, которые не могут встречаться на этом месте в строке. Можно перечислять символы подряд или указать диапазон через тире;
  • n|m — соответствует одному из фрагментов n или m:
re := regexp.MustCompile(`красн((ая)|(ое))`)
s := "красная или красное, но не красный"
fmt.Println(re.FindAllString(s, -1)) // [красная красное]
  • . (точка) — любой символ, кроме символа перевода строки (\n). Если указан модификатор s, то точка соответствует любом символу, включая символ перевода строки. Внутри квадратных скобок точка не имеет специального значения:
re := regexp.MustCompile(`.+`)
s := "10\n20"
fmt.Println(re.FindAllString(s, -1))        // [10 20]
re = regexp.MustCompile(`(?s).+`)
fmt.Printf("%q\n", re.FindAllString(s, -1)) // ["10\n20"]
re = regexp.MustCompile(`(?s)[.]+`)
fmt.Println(re.FindAllString(s, -1))        // []

Если нужно найти соответствие любому символу, включая символ перевода строки, то можно воспользоваться следующим кодом: [\s\S]. Класс \s означает любой пробельный символ, а класс \S — любой не пробельный символ. Пример:

re := regexp.MustCompile(`.+`)
s := "10\n20"
fmt.Println(re.FindAllString(s, -1))        // [10 20]
re = regexp.MustCompile(`[\s\S]+`)
fmt.Printf("%q\n", re.FindAllString(s, -1)) // ["10\n20"]

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

re := regexp.MustCompile(`^[0-9]+$`)
// Строка может содержать только числа
fmt.Println(re.MatchString("2"))     // true
fmt.Println(re.MatchString("n2"))    // false

Не указав привязку к началу и концу, мы получим соответствие шаблону любого числа внутри строки:

re := regexp.MustCompile(`[0-9]+`)
fmt.Println(re.MatchString("Строка2")) // true

Можно указать привязку только к началу или только к концу строки:

re := regexp.MustCompile(`[0-9]+$`)
if re.MatchString("Строка2") {
   fmt.Println("Есть число в конце строки")
} else {
   fmt.Println("Нет числа в конце строки")
}
// Выведет: Есть число в конце строки
re = regexp.MustCompile(`^[0-9]+`)
if re.MatchString("Строка2") {
   fmt.Println("Есть число в начале строки")
} else {
   fmt.Println("Нет числа в начале строки")
}
// Выведет: Нет числа в начале строки

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

  • [09] — соответствует цифре 0 или 9;
  • [0-9] — соответствует любой цифре от 0 до 9;
  • [абв] — соответствует буквам а, б и в;
  • [а-г] — соответствует буквам а, б, в и г;
  • [а-яё] — соответствует любой букве от а до я;
  • [АБВ] — соответствует буквам А, Б и В;
  • [А-ЯЁ] — соответствует любой букве от А до Я;
  • [а-яА-ЯёЁ] — соответствует любой русской букве в любом регистре;
  • [0-9а-яА-ЯёЁa-zA-Z] — любая цифра и любая русская или английская буква независимо от регистра.
Обратите внимание!

Значение можно инвертировать, если после первой скобки указать символ ^. Таким образом можно указать символы, которых не должно быть на этом месте в строке:

  • [^09] — не цифра 0 или 9;
  • [^0-9] — не цифра от 0 до 9;
  • [^а-яА-ЯёЁa-zA-Z] — не русская или английская буква в любом регистре.

Стандартные классы

В регулярных выражениях допустимы следующие основные стандартные классы (полный список смотрите в документации):

  • \d — соответствует любой цифре;
  • \w — соответствует любой латинской букве, цифре и знаку подчеркивания ([a-zA-Z0-9_]);
  • \s — любой пробельный символ (пробел, табуляция, перевод страницы, новая строка или перевод каретки);
  • \D — не цифра. Эквивалентно: [^\d];
  • \W — не латинская буква, не цифра и не знак подчеркивания. Эквивалентно: [^\w];
  • \S — не пробельный символ. Эквивалентно: [^\s].
Обратите внимание

Следует учитывать, что стандартные классы могут зависеть от различных флагов и трактоваться гораздо шире, чем мы указали. В итоге можно получить результат, который совсем не ожидался. Советую на практике использовать только классы \s и \S, а остальные заменять явным указанием диапазона символов внутри квадратных скобок (при этом не забывайте про букву ё):

re := regexp.MustCompile(`[0-9]`)               // Вместо \d
re = regexp.MustCompile(`[a-zA-Zа-яА-ЯёЁ0-9_]`) // Вместо \w

Получим все цифры из строки:

re := regexp.MustCompile(`[0-9]`)
fmt.Println(re.FindAllString("текст123 456", -1)) // [1 2 3 4 5 6]

Если нужно получить не цифры по отдельности, а числа, то после закрывающей квадратной скобки нужно указать символ +, означающий одно или большее число вхождений символа в строку:

re := regexp.MustCompile(`[0-9]+`)
fmt.Println(re.FindAllString("текст123 456", -1)) // [123 456]

Внутри квадратных скобок можно также использовать стандартные классы регулярных выражений формата POSIX:

  • [:alnum:] — алфавитно-цифровые символы ([a-zA-Z0-9]);
  • [:word:] — аналог класса \w ([a-zA-Z0-9_]);
  • [:alpha:] — буквенные латинские символы ([a-zA-Z]);
  • [:lower:] — строчные латинские буквы ([a-z]);
  • [:upper:] — прописные латинские буквы ([A-Z]);
  • [:ascii:] — символы кодировки ASCII;
  • [:digit:] — десятичные цифры;
  • [:xdigit:] — шестнадцатеричные цифры;
  • [:punct:] — знаки пунктуации;
  • [:blank:] — символы табуляции и пробелов;
  • [:space:] — пробельные символы;
  • [:cntrl:] — управляющие символы;
  • [:print:] — печатные символы (русские буквы сюда не входят);
  • [:graph:] — печатные символы, за исключением пробела (русские буквы сюда не входят).

Получим все числа из строки:

re := regexp.MustCompile(`[[:digit:]]+`)
fmt.Println(re.FindAllString("текст123 456", -1)) // [123 456]

Получим все символы, кроме цифр:

re := regexp.MustCompile(`[^[:digit:]]+`)
fmt.Println(re.FindAllString("текст123 456", -1)) // [текст  ]

Поддерживаются также Unicode классы в формате \p{<Класс>}. Полный список классов можно найти в документации к пакету unicode. Получим все буквы в любом регистре:

re := regexp.MustCompile(`[\p{L}]+`)
fmt.Println(re.FindAllString("текСТ123 456", -1)) // [текСТ]

Получим все символы, кроме букв:

re := regexp.MustCompile(`[^\p{L}]+`)
fmt.Println(re.FindAllString("текст123+456", -1)) // [123+456]

Вместо символа ^ для отрицания можно использовать формат \P{<Класс>}. Получим все символы, кроме букв:

re := regexp.MustCompile(`[\P{L}]+`)
fmt.Println(re.FindAllString("текст123+456", -1)) // [123+456]

Если название класса состоит из одной буквы, то фигурные скобки можно не указывать. Получим все числа:

re := regexp.MustCompile(`[\pN]+`)
fmt.Println(re.FindAllString("текСТ123 456", -1)) // [123 456]

Квантификаторы

Количество вхождений символа (или выражения) в строку задается с помощью квантификаторов:

  • {n}n вхождений символа в строку (шаблон [0-9]{2} соответствует двум вхождениям любой цифры);
  • {n,}n или более вхождений символа в строку (шаблон [0-9]{2,} соответствует двум и более вхождениям любой цифры);
  • {n,m} — не менее n и не более m вхождений символа в строку. Числа указываются через запятую без пробела. Например, шаблон [0-9]{2,4} соответствует от двух до четырех вхождениям любой цифры;
  • * — ноль или большее число вхождений символа в строку. Эквивалентно комбинации {0,};
  • + — одно или большее число вхождений символа в строку. Эквивалентно комбинации {1,};
  • ? — ни одного или одно вхождение символа в строку. Эквивалентно комбинации {0,1}.

«Жадность» квантификаторов

Все квантификаторы являются «жадными». При поиске соответствия ищется самая длинная подстрока, соответствующая шаблону, и не учитываются более короткие соответствия. Рассмотрим это на примере. Получим содержимое всех тегов <b>, вместе с тегами:

re := regexp.MustCompile(`(?i)<b>.*</b>`)
s := "<b>Text1</b>Text2<b>Text3</b>"
fmt.Println(re.FindAllString(s, -1))
// [<b>Text1</b>Text2<b>Text3</b>]

Вместо желаемого результата мы получили полностью строку. Чтобы ограничить «жадность» квантификатора необходимо после него указать символ ?:

re := regexp.MustCompile(`(?i)<b>.*?</b>`)
s := "<b>Text1</b>Text2<b>Text3</b>"
fmt.Println(re.FindAllString(s, -1))
// [<b>Text1</b> <b>Text3</b>]

Этот код выведет то, что мы искали.

Для ограничения "жадности" всех квантификаторов можно использовать модификатор U:

re := regexp.MustCompile(`(?iU)<b>.*</b>`)
s := "<b>Text1</b>Text2<b>Text3</b>"
fmt.Println(re.FindAllString(s, -1))
// [<b>Text1</b> <b>Text3</b>]

Группы

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

re := regexp.MustCompile(`(?i)<b>(.*?)</b>`)
s := "<b>Text1</b>Text2<b>Text3</b>"
fmt.Println(re.FindAllStringSubmatch(s, -1))
// [[<b>Text1</b> Text1] [<b>Text3</b> Text3]]

Метод FindAllStringSubmatch() возвращает двумерный строковый слайс. Первый элемент каждого слайса содержит полное совпадение с шаблоном, а последующие элементы — это фрагменты, заключенные внутри шаблона в круглые скобки.

Круглые скобки часто используются для группировки фрагментов внутри шаблона. В этих случаях не требуется, чтобы фрагмент запоминался и был доступен в результатах поиска. Пример:

re := regexp.MustCompile(`[a-z]+((st)|(xt))`)
s := "test text"
fmt.Println(re.FindAllStringSubmatch(s, -1))
// [[test st st ] [text xt  xt]]

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

re := regexp.MustCompile(`[a-z]+(?:(?:st)|(?:xt))`)
s := "test text"
fmt.Println(re.FindAllStringSubmatch(s, -1))
// [[test] [text]]

Получить количество групп внутри шаблона позволяет метод NumSubexp(). Формат метода:

(*regexp.Regexp).NumSubexp() int

Пример:

re := regexp.MustCompile(`([0-9]+)\-([a-z]+)`)
fmt.Println(re.NumSubexp()) // 2
re = regexp.MustCompile(`[a-z]+(?:(?:st)|(?:xt))`)
fmt.Println(re.NumSubexp()) // 0

В качестве примера использования групп разберем E-mail (листинг 8.2) и URL-адрес (листинг 8.3) на составные части.

Листинг 8.2. Разбор E-mail на составные части

package main

import (
   "fmt"
   "regexp"
)

func main() {
   p := `(?i)^([a-z0-9_.-]+)@((?:[a-z0-9-]+\.)+[a-z]{2,6})$`
   re := regexp.MustCompile(p)
   s := "user@mail.ru"
   arr := re.FindAllStringSubmatch(s, -1)
   if len(arr) > 0 {
      fmt.Println(arr)
      fmt.Println("Имя ящика -", arr[0][1])
      fmt.Println("Имя сайта -", arr[0][2])
      fmt.Println("Полный E-mail -", arr[0][0])
   } else {
      fmt.Println("E-mail не соответствует шаблону")
   }
}

В итоге получим следующий результат:

[[user@mail.ru user mail.ru]]
Имя ящика - user
Имя сайта - mail.ru
Полный E-mail - user@mail.ru

Листинг 8.3. Разбор URL-адреса на составные части

package main

import (
   "fmt"
   "regexp"
)

func main() {
   p := `(?i)^([a-z]+://)` +
        `((?:[a-z0-9-]+\.)+[a-z]{2,6})` +
        `([a-z0-9/-]*/)*` +
        `([a-z0-9-]+\.[a-z]+)$`;
   re := regexp.MustCompile(p)
   s := "http://www.mysite.ru/folder1/folder2/forder3/file.html"
   arr := re.FindAllStringSubmatch(s, -1)
   if len(arr) > 0 {
      fmt.Println("Протокол -", arr[0][1])
      fmt.Println("Сайт -", arr[0][2])
      fmt.Println("Путь -", arr[0][3])
      fmt.Println("Имя файла -", arr[0][4])
   } else {
      fmt.Println("URL не соответствует шаблону")
   }
}

В итоге получим следующий результат:

Протокол - http://
Сайт - www.mysite.ru
Путь - /folder1/folder2/forder3/
Имя файла - file.html

Фрагментам внутри круглых скобок можно дать имена. Для этого после открывающей круглой скобки следует указать комбинацию символов ?P<name>. Получить список всех именованных фрагментов позволяет метод SubexpNames(). Позиция фрагмента внутри шаблона совпадает с индексом внутри слайса. Формат метода:

(*regexp.Regexp).SubexpNames() []string

Получить индекс именованного фрагмента по его названию можно с помощью метода SubexpIndex(). Если фрагмент не найден, то возвращается значение -1. Формат метода:

(*regexp.Regexp).SubexpIndex(name string) int

В качестве примера найдем и разберем все E-mail на составные части (листинг 8.4).

Листинг 8.4. Именованные фрагменты

package main

import (
   "fmt"
   "regexp"
)

func main() {
   p := `(?i)(?P<name>[a-z0-9_.-]+)@` +
        `(?P<host>(?:[a-z0-9-]+\.)+[a-z]{2,6})`
   re := regexp.MustCompile(p)
   s := "user1@mail.ru user2@yandex.ru"
   names := re.SubexpNames()
   fmt.Println(names)
   for _, arr := range re.FindAllStringSubmatch(s, -1) {
      fmt.Println("Имя ящика -", arr[re.SubexpIndex("name")])
      fmt.Println("Имя сайта -", arr[re.SubexpIndex("host")])
      fmt.Println("Полный E-mail -", arr[0])
   }
}

В итоге получим следующий результат:

[ name host]
Имя ящика - user1
Имя сайта - mail.ru
Полный E-mail - user1@mail.ru
Имя ящика - user2
Имя сайта - yandex.ru
Полный E-mail - user2@yandex.ru
На заметку

Учебник Go (Golang)
Учебник Go (Golang) в формате PDF

Помощь сайту

ЮMoney (Yandex-деньги): 410011140483022

ПАО Сбербанк:
Счет: 40817810855006152256
Реквизиты банка:
Наименование: СЕВЕРО-ЗАПАДНЫЙ БАНК ПАО СБЕРБАНК
Корреспондентский счет: 30101810500000000653
БИК: 044030653
КПП: 784243001
ОКПО: 09171401
ОКОНХ: 96130
Скриншот реквизитов

cpp