Статьи

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

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

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

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

Есть два метода для компиляции регулярных выражений: Compile() и MustCompile() . Compile() вернет ошибку, если предоставленный шаблон недействителен. MustCompile() будет паниковать. Компиляция рекомендуется, если вы заботитесь о производительности и планируете использовать одно и то же регулярное выражение несколько раз. Давайте изменим нашу вспомогательную функцию match() на скомпилированное регулярное выражение. Обратите внимание, что нет необходимости проверять наличие ошибок, поскольку скомпилированное регулярное выражение должно быть допустимым.

1
2
3
4
5
6
7
8
func match(r *regexp.Regexp, text string) {
    matched := r.MatchString(text)
    if matched {
        fmt.Println(«√», r.String(), «:», text)
    } else {
        fmt.Println(«X», r.String(), «:», text)
    }
}

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
func main() {
    es := `(\bcats?\b)|(\bdogs?\b)|(\brats?\b)`
    e := regexp.MustCompile(es)
    match(e, «It’s raining dogs and cats»)
    match(e, «The catalog is ready. It’s hotdog time!»)
    match(e, «It’s a dog eat dog world.»)
}
 
Output:
 
√ (\bcats?\b)|(\bdogs?\b)|(\brats?\b) :
  It’s raining dogs and cats
X (\bcats?\b)|(\bdogs?\b)|(\brats?\b) :
  The catalog is ready.
√ (\bcats?\b)|(\bdogs?\b)|(\brats?\b) :
  It’s a dog eat dog world.

Объект Regexp имеет много методов FindXXX() . Некоторые из них возвращают первое совпадение, другие возвращают все совпадения, а третьи возвращают индекс или индексы. Интересно, что имена всех 16 методов функций соответствуют следующему регулярному выражению: Find(All)?(String)?(Submatch)?(Index)?

Если присутствует «Все», то возвращаются все совпадения против самого левого. Если присутствует «String», то целевой текст и возвращаемые значения являются строками против байтовых массивов. Если присутствует ‘Submatch’, то возвращаются submatches (группы) против простых совпадений. Если присутствует «Индекс», то возвращаются индексы в целевом тексте в сравнении с фактическими совпадениями.

Давайте возьмем одну из более сложных функций и используем метод FindAllStringSubmatch() . Требуется строка и число n . Если n равно -1, он вернет все совпадающие индексы. Если n — неотрицательное целое число, то оно вернет n самых левых совпадений. В результате получается фрагмент строки.

Результатом каждого совпадения является полное совпадение, за которым следует захваченная группа. Например, рассмотрим список имен, где некоторые из них имеют названия, такие как «Мистер», «Миссис» или «Доктор». Вот регулярное выражение, которое захватывает заголовок как подстроку, а затем оставшуюся часть имени после пробела: \b(Mr\.|Mrs\.|Dr\.) .* .

01
02
03
04
05
06
07
08
09
10
11
func main() {
    re := regexp.MustCompile(`\b(Mr\.|Mrs\.|Dr\.) .*`)
    fmt.Println(re.FindAllStringSubmatch(«Dr. Dolittle», -1))
    fmt.Println(re.FindAllStringSubmatch(`Mrs. Doubtfire
                                          Mr. Anderson`, -1))
}
 
Output:
 
[[Dr.
[[Mrs.

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

Поиск совпадений — это здорово, но часто вам может понадобиться заменить матч чем-то другим. У объекта ReplaceXXX() как обычно, есть несколько ReplaceXXX() для работы со строками, байтовыми массивами и литеральными заменами и расширениями. В великой книге 1984 года Джорджа Оруэлла лозунги партии записаны на белой пирамиде министерства правды:

  • Война есть мир
  • Свобода рабство
  • Невежество это сила

Я нашел небольшое эссе о цене свободы, в котором используются некоторые из этих терминов. Давайте исправим его фрагмент в соответствии с двойным выражением группы, используя регулярные выражения Go. Обратите внимание, что некоторые из целевых слов для замены используют различную заглавную букву. Решение состоит в том, чтобы добавить нечувствительный к регистру флаг (i?) В начале регулярного выражения.

Так как перевод отличается в зависимости от случая, нам нужен более сложный подход, чем буквальная замена. К счастью (или умышленно), объект Regexp имеет метод replace, который принимает функцию, которую он использует для выполнения фактической замены. Давайте определим нашу функцию заменителя, которая возвращает перевод с правильным регистром.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
func replacer(s string) string {
    d := map[string]string{
        «war»: «peace»,
        «WAR»: «PEACE»,
        «War»: «Peace»,
        «freedom»: «slavery»,
        «FREEDOM»: «SLAVERY»,
        «Freedom»: «Slavery»,
        «ignorance»: «strength»,
        «IGNORANCE»: «STRENGTH»,
        «Ignorance»: «Strength»,
    }
 
    r, ok := d[s]
    if ok {
        return r
    } else {
        return s
    }
}

Теперь мы можем выполнить фактическую замену:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
func main() {
    text := `THE PRICE OF FREEDOM: Americans at War
             Americans have gone to war to win their
             independence, expand their national
             boundaries, define their freedoms, and defend
             their interests around the globe.`
 
    expr := `(?i)(war|freedom|ignorance)`
    r := regexp.MustCompile(expr)
 
    result := r.ReplaceAllStringFunc(text, replacer)
    fmt.Println(result)
}
 
Output:
 
THE PRICE OF SLAVERY: Americans at Peace
    Americans have gone to peace to win their
    independence, expand their national
    boundaries, define their slaverys, and defend
    their interests around the globe.

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

Ранее мы видели, как использовать группировку с подсовпадениями. Но иногда бывает трудно обрабатывать несколько суб-соответствий. Именованные группы могут очень помочь здесь. Вот как называть ваши группы подсовпадений и заполнять словарь для быстрого доступа по имени:

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
func main() {
    e := `(?P<first>\w+) (?P<middle>.+ )?(?P<last>\w+)`
    r := regexp.MustCompile(e)
    names := r.SubexpNames()
    fullNames := []string{
        `John F. Kennedy`,
        `Michael Jordan`}
    for _, fullName := range fullNames {
        result := r.FindAllStringSubmatch(fullName, -1)
        m := map[string]string{}
        for i, n := range result[0] {
            m[names[i]] = n
        }
        fmt.Println(«first name:», m[«first»])
        fmt.Println(«middle_name:», m[«middle»])
        fmt.Println(«last name:», m[«last»])
        fmt.Println()
    }
}
 
Output:
 
first name: John
middle_name: F.
last name: Kennedy
 
first name: Michael
middle_name:
last name: Jordan

Если вы помните, я сказал, что специальный символ точки соответствует любому символу. Ну, я солгал. Он не соответствует символу новой строки ( \n ) по умолчанию. Это означает, что ваши совпадения не будут пересекать строки, если вы не укажете это явно с помощью специального флага (?s) который вы можете добавить в начало вашего регулярного выражения. Вот пример с и без флага.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
func main() {
    text := «1111\n2222»
 
    expr := []string{«.*», «(?s).*»}
    for _, e := range expr {
        r := regexp.MustCompile(e)
        result := r.FindString(text)
        result = strings.Replace(result, «\n», `\n`, -1)
        fmt.Println(e, «:», result)
        fmt.Println()
    }
}
 
Output:
 
.* : 1111
 
(?s).* : 1111\n2222

Другое соображение — рассматривать ли специальные символы ^ и $ как начало и конец всего текста (по умолчанию) или как начало и конец каждой строки с флагом (?m) .

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

Go предоставляет библиотеку с простым в использовании интерфейсом, который состоит из объекта Regexp со многими методами. Попробуйте, но остерегайтесь ловушек.