Статьи

Регулярные выражения с Go: часть 1

Регулярные выражения (AKA regex) – это формальный язык, который определяет последовательность символов с некоторым шаблоном. В реальном мире их можно использовать для решения множества проблем с полуструктурированным текстом. Вы можете извлечь важные фрагменты из текста с большим количеством украшений или несвязанного содержимого. Go имеет в своей стандартной библиотеке надежный пакет регулярных выражений, который позволяет вырезать и вырезать текст с помощью регулярных выражений.

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

Давайте начнем с быстрого примера. У вас есть текст, и вы хотите проверить, содержит ли он адрес электронной почты. Адрес электронной почты строго указан в RFC 822 . Короче говоря, он имеет локальную часть, за которой следует символ @, за которым следует домен. Почтовый адрес будет отделен от остального текста пробелом.

Чтобы выяснить, содержит ли он адрес электронной почты, подойдет следующее регулярное выражение: ^\w+@\w+\.\w+$ . Обратите внимание, что это регулярное выражение немного допустимо и пропустит некоторые недействительные адреса электронной почты. Но это достаточно хорошо, чтобы продемонстрировать концепцию. Давайте попробуем это на паре потенциальных адресов электронной почты, прежде чем объяснить, как это работает:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package main
 
import (
    “os”
    “regexp”
    “fmt”
)
 
 
func check(err error) {
    if err != nil {
        fmt.Println(err.Error())
        os.Exit(1)
    }
}
 
func main() {
    emails := []string{
        “brown@fox”,
        “brown@fox.”,
        “brown@fox.com”,
        “br@own@fox.com”,
    }
 
    pattern := `^\w+@\w+\.\w+$`
    for _, email := range emails {
        matched, err := regexp.Match(pattern, []byte(email))
        check(err)
        if matched {
            fmt.Printf(“√ ‘%s’ is a valid email\n”, email)
        } else {
            fmt.Printf(“X ‘%s’ is not a valid email\n”, email)
        }
    }
}
 
Output:
 
X ‘brown@fox’ is not a valid email
X ‘brown@fox.’
√ ‘brown@fox.com’ is a valid email
X ‘br@own@fox.com’ is not a valid email

Наше регулярное выражение работает на этом маленьком образце. Первые два адреса были отклонены, поскольку в домене не было точки или не было символов после точки. Третье письмо было отформатировано правильно. У последнего кандидата было два символа @.

Давайте разберем это регулярное выражение: ^\w+@\w+\.\w+$

Символ / Symbol Смысл
^ Начало целевого текста
\ ш Любые символы слова [0-9A-Za-z_]
+ Хотя бы один из предыдущих персонажей
@ Буквально символ @
\. Буквенный точечный символ. Нужно сбежать с \
$ Конец целевого текста

В целом, это регулярное выражение будет соответствовать фрагментам текста, которые начинаются с одного или нескольких символов слова, после которых следует символ “@”, затем снова один или несколько символов слова, затем точка и затем снова один или несколько символов слова.

Следующие символы имеют специальные значения в регулярных выражениях:. .+*?()|[]{}^$\ . Мы уже видели многих из них в примере электронной почты. Если мы хотим сопоставить их буквально, нам нужно избежать обратной косой черты. Давайте представим небольшую вспомогательную функцию под названием match() , которая сэкономит нам много печатания. Он берет шаблон и некоторый текст, использует метод regexp.Match() для сопоставления шаблона с текстом (после преобразования текста в байтовый массив) и печатает результаты:

1
2
3
4
5
6
7
8
func match(pattern string, text string) {
    matched, _ := regexp.Match(pattern, []byte(text))
    if matched {
        fmt.Println(“√”, pattern, “:”, text)
    } else {
        fmt.Println(“X”, pattern, “:”, text)
    }
}

Вот пример соответствия обычного символа, такого как z и соответствия специального символа, такого как ? :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
func main() {
    text := “Can I haz cheezburger?”
    pattern := “z”
    match(pattern, text)
 
    pattern = “\\?”
    match(pattern, text)
 
    pattern = `\?`
    match(pattern, text)
}
 
Output:
 
√ z : Can I haz cheezburger?
√ \?
√ \?

Шаблон регулярного выражения \? содержит обратную косую черту, которая должна быть экранирована другой обратной косой чертой, когда она представлена ​​в виде обычной строки Go. Причина в том, что обратная косая черта также используется для экранирования специальных символов в строках Go, таких как символ новой строки ( \n ). Если вы хотите сопоставить сам символ обратной косой черты, вам понадобится четыре слеша!

Решение состоит в том, чтобы использовать строки Go raw с обратным кавычком ( ` ) вместо двойных кавычек. Конечно, если вы хотите сопоставить символ новой строки, вы должны вернуться к обычным строкам и иметь дело с множественными символами обратной косой черты.

В большинстве случаев вы не пытаетесь буквально сопоставить последовательность определенных символов, таких как «abc», но последовательность неизвестной длины с, возможно, некоторыми известными символами, вставленными где-то. Регулярные выражения поддерживают этот вариант использования с точкой . специальный символ, который обозначает любого персонажа. Специальный символ * повторяет предыдущий символ (или группу) ноль или более раз. Если вы объединяете их, как в .* , То вы сопоставляете что угодно, потому что это просто означает ноль или более символов Знак + очень похож на * , но соответствует одному или нескольким предыдущим символам или группам. Итак .+ Будет соответствовать любому непустому тексту.

Существует три типа границ: начало текста, обозначенного ^ , конец текста, обозначенного $ , и граница слова, обозначенная \b . Например, рассмотрим текст из классического фильма «Принцесса-невеста» : «Меня зовут Иниго Монтойя. Ты убил моего отца. Приготовься умереть». Если вы сопоставляете просто «папа», вы получаете совпадение, но если вы ищете «папа» в конце текста, вам нужно добавить символ $ , и тогда совпадения не будет. С другой стороны, соответствие «Hello» в начале работает хорошо.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
func main() {
    text := “Hello, my name is Inigo Montoya, you killed my father, prepare to die.”
    pattern := “father”
    match(pattern, text)
 
    pattern = “father$”
    match(pattern, text)
 
    pattern = “^Hello”
    match(pattern, text)
}
 
Output:
 
√ father : Hello, my name is Inigo Montoya,
            you killed my father, prepare to die.
X father$ : Hello, my name is Inigo Montoya,
            you killed my father, prepare to die.
√ ^Hello : Hello, my name is Inigo Montoya,
            you killed my father, prepare to die.

Границы слова смотрят на каждое слово. Вы можете начать и / или закончить шаблон с помощью \b . Обратите внимание, что знаки препинания, такие как запятые, считаются границей, а не частью слова. Вот несколько примеров:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
func main() {
    text := `Hello, my name is Inigo Montoya,
             you killed my father, prepare to die.`
    pattern := `kill`
    match(pattern, text)
 
    pattern = `\bkill`
    match(pattern, text)
 
    pattern = `kill\b`
    match(pattern, text)
 
    pattern = `\bkill\b`
    match(pattern, text)
 
    pattern = `\bkilled\b`
    match(pattern, text)
 
    pattern = `\bMontoya,\b`
    match(pattern, text)
}
 
Output:
 
√ kill : Hello, my name is Inigo Montoya,
                 you killed my father, prepare to die.
√ \bkill : Hello, my name is Inigo Montoya,
                 you killed my father, prepare to die.
X kill\b : Hello, my name is Inigo Montoya,
                 you killed my father, prepare to die.
X \bkill\b : Hello, my name is Inigo Montoya,
                 you killed my father, prepare to die.
√ \bkilled\b : Hello, my name is Inigo Montoya,
                 you killed my father, prepare to die.
X \bMontoya,\b : Hello, my name is Inigo Montoya,
                 you killed my father, prepare to die.

Часто полезно рассматривать все группы символов вместе как все цифры, пробельные символы или все буквенно-цифровые символы. Golang поддерживает классы POSIX, которые:

Класс персонажа Смысл
[Цифра, буква] буквенно-цифровой (≡ [0-9A-Za-z])
[:альфа:] буквенный (≡ [A-Za-z])
[: ASCII:] ASCII (≡ [\ x00- \ x7F])
[: Пусто:] пусто (≡ [\ t])
[: CNTRL:] управление (≡ [\ x00- \ x1F \ x7F])
[: Цифры:] цифры (≡ [0-9])
[: График:] графический (≡ [! – ~] == [A-Za-z0-9! “# $% & ‘() * +, \ -. / :; <=>? @ [\\\] ^ _` { |} ~])
[: Опустить:] строчные буквы (az [аз])
[:Распечатать:] для печати (≡ [- ~] == [[: graph:]])
[Пунктуатор] пунктуация (! [! – /: – @ [- `{- ~])
[:Космос:] пробел (≡ [\ t \ n \ v \ f \ r])
[: Верхняя:] верхний регистр (AZ [AZ])
[:слово:] символы слова (≡ [0-9A-Za-z_])
[: Xdigit:] шестнадцатеричная цифра (≡ [0-9A-Fa-f])

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
func main() {
    text := `The answer to life, universe and
             everything is 42 .”
    pattern := “[[:digit:]]{3}”
    match(pattern, text)
 
    pattern = “[[:digit:]]{2}”
    match(pattern, text)
}
 
Output:
 
X [[:digit:]]{3} : The answer to life, universe and
                   everything is 42.
√ [[:digit:]]{2} : The answer to life, universe and
                   everything is 42.

Вы также можете определять свои собственные классы, помещая символы в квадратные скобки. Например, если вы хотите проверить, является ли какой-либо текст допустимой последовательностью ДНК, содержащей только символы ACGT используйте регулярное выражение ^[ACGT]*$ :

01
02
03
04
05
06
07
08
09
10
11
12
13
func main() {
    text := “AGGCGTTGGGAACGTT”
    pattern := “^[ACGT]*$”
    match(pattern, text)
 
    text = “Not exactly a DNA sequence”
    match(pattern, text)
}
 
Output:
 
√ ^[ACGT]*$ : AGGCGTTGGGAACGTT
X ^[ACGT]*$ : Not exactly a DNA sequence

В некоторых случаях существует несколько жизнеспособных альтернатив. Соответствующие URL-адреса HTTP могут характеризоваться схемой протокола, которая является либо http:// либо https:// . Символ трубы | позволяет выбирать между альтернативами. Вот регулярное выражение, которое отсортирует их: (http)|(https)://\w+\.\w{2,} . Он переводится в строку, которая начинается с http:// или https:// за которым следует хотя бы один символ слова, за которым следует точка, за которой следуют как минимум два слова.

01
02
03
04
05
06
07
08
09
10
11
12
func main() {
    pattern := `(http)|(https)://\w+\.\w{2,}`
    match(pattern, “http://tutsplus.com”)
    match(pattern, “https://tutsplus.com”)
    match(pattern, “htt://tutsplus.com”)
}
 
Output:
 
√ (http)|(https)://\w+\.\w{2,} : http://tutsplus.com
√ (http)|(https)://\w+\.\w{2,} : https://tutsplus.com
X (http)|(https)://\w+\.\w{2,} : htt://tutsplus.com

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

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