Golang 正则表达式入门指南 – wiki基地


Golang 正则表达式入门指南:从基础到实战

正则表达式(Regular Expression),简称 regex 或 regexp,是一种强大而灵活的文本处理工具。它使用一种特殊的字符序列来定义一个搜索模式,常用于在字符串中查找、匹配、替换或分割符合特定模式的文本。无论你是需要验证用户输入的格式、从日志文件中提取特定信息、还是进行复杂的文本替换,正则表达式都能派上大用场。

作为一门现代化的编程语言,Golang (Go) 在标准库中提供了功能完备的 regexp 包,使得在 Go 中使用正则表达式变得非常方便和高效。本文将带领你从零开始,逐步掌握 Golang 中正则表达式的使用。

1. 什么是正则表达式?为什么在 Go 中使用它?

简单来说,正则表达式就是一套描述字符串模式的“迷你语言”。它由普通字符(如字母、数字)和特殊字符(元字符)组成。例如,模式 a+ 可以匹配一个或多个连续的字母 ‘a’。

在 Go 中使用正则表达式的主要原因包括:

  1. 强大的模式匹配能力: 能够处理比简单字符串匹配复杂得多的模式。
  2. 简洁性: 用少量字符表达复杂的匹配规则。
  3. 效率: Go 的 regexp 包经过优化,性能良好(尤其在编译后)。
  4. 标准库支持: 无需安装第三方库,开箱即用。

2. 正则表达式基础概念速览

虽然本文主要关注 Go 的 regexp 包,但了解正则表达式本身的基础概念至关重要。以下是一些最常见的正则表达式元素:

2.1 普通字符 (Literals)

大多数字符都代表其本身,例如 a 匹配字符 ‘a’,1 匹配字符 ‘1’。

2.2 元字符 (Metacharacters)

元字符是正则表达式中具有特殊含义的字符。最常见的包括:

  • .: 匹配除换行符外的任何单个字符。
  • *: 匹配前一个元素零次或多次。
  • +: 匹配前一个元素一次或多次。
  • ?: 匹配前一个元素零次或一次。
  • ^: 匹配行的开头(或字符串的开头)。
  • $: 匹配行的结尾(或字符串的结尾)。
  • []: 字符集,匹配方括号内的任意一个字符。例如 [abc] 匹配 ‘a’、’b’ 或 ‘c’。
  • [^]: 否定字符集,匹配不在方括号内的任意一个字符。例如 [^0-9] 匹配任意非数字字符。
  • |: 或操作,匹配 | 符号前或后的模式。例如 cat|dog 匹配 “cat” 或 “dog”。
  • (): 分组,将多个元素视为一个整体,也可用于捕获匹配的子串。
  • \: 转义字符,用于取消元字符的特殊含义,或表示特殊序列。例如 \. 匹配字面意义的点,\\ 匹配字面意义的反斜杠。

2.3 特殊序列 (Special Sequences)

使用反斜杠 \ 后面跟特定字符表示预定义字符集或位置:

  • \d: 匹配任意数字 (0-9),等价于 [0-9]
  • \D: 匹配任意非数字字符,等价于 [^0-9]
  • \w: 匹配任意字母、数字或下划线(通常指单词字符),等价于 [a-zA-Z0-9_]
  • \W: 匹配任意非单词字符,等价于 [^a-zA-Z0-9_]
  • \s: 匹配任意空白字符(空格、制表符、换行符等)。
  • \S: 匹配任意非空白字符。
  • \b: 匹配单词边界。
  • \B: 匹配非单词边界。

2.4 量词 (Quantifiers)

控制前一个元素出现的次数:

  • {n}: 恰好匹配 n 次。
  • {n,}: 至少匹配 n 次。
  • {n,m}: 匹配 n 到 m 次(包含 n 和 m)。

量词的贪婪与非贪婪:

默认情况下,量词是“贪婪”的,它们会尽可能多地匹配字符。在量词后加上 ? 可以使其变为“非贪婪”或“惰性”,它会尽可能少地匹配字符。例如:

  • 对于字符串 <tag>content</tag> 和模式 <.*>,贪婪匹配会得到 <tag>content</tag>
  • 对于字符串 <tag>content</tag> 和模式 <.*?>,非贪婪匹配会得到 <tag>

3. Golang 的 regexp 包入门

Golang 标准库的 regexp 包提供了对正则表达式的支持。使用它通常分为两步:

  1. 编译正则表达式: 将字符串形式的正则表达式模式编译成一个可用的 regexp.Regexp 对象。这个步骤会检查模式的语法是否正确,并进行优化,以便后续匹配操作更高效。
  2. 使用编译后的对象进行匹配、查找、替换等操作。

3.1 导入包

首先,你需要导入 regexp 包:

go
import "regexp"

3.2 编译正则表达式

有两种主要方法来编译正则表达式:

  • regexp.Compile(pattern string): 这个函数返回一个 *regexp.Regexp 对象和一个 error。如果模式语法错误,错误会被返回。你需要在代码中检查这个错误。

“`go
package main

import (
“fmt”
“regexp”
)

func main() {
// 编译正则表达式,匹配一个或多个数字
pattern := \d+
re, err := regexp.Compile(pattern)
if err != nil {
fmt.Println(“正则表达式编译错误:”, err)
return
}

fmt.Printf("正则表达式编译成功: %v\n", re)

}
“`

  • regexp.MustCompile(pattern string): 这个函数也返回一个 *regexp.Regexp 对象,但它不返回错误。如果模式语法错误,它会 panic。这通常用于包或全局变量的初始化,当你知道模式字符串是静态的且一定是正确的时候。

“`go
package main

import (
“fmt”
“regexp”
)

// 在包初始化时使用 MustCompile,确保模式正确
var emailRegex = regexp.MustCompile(^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$)

func main() {
fmt.Printf(“编译后的邮件正则对象: %v\n”, emailRegex)

// 后续可以直接使用 emailRegex 进行匹配
fmt.Println("[email protected] 是否是有效邮件:", emailRegex.MatchString("[email protected]"))
fmt.Println("invalid-email 是否是有效邮件:", emailRegex.MatchString("invalid-email"))

}
“`

为什么需要编译?

编译过程会将字符串模式转换成内部高效的状态机表示。对于一个需要重复使用的正则表达式,只编译一次,然后多次使用编译后的 *regexp.Regexp 对象进行操作,会比每次都使用接受字符串模式的便捷函数(例如 regexp.MatchString)效率更高,因为后者在内部每次调用时都需要重新编译模式。

4. 常用的 regexp.Regexp 对象方法

编译获得 *regexp.Regexp 对象后,就可以调用它的各种方法来执行匹配、查找、替换等任务。

4.1 匹配 (Match)

  • Match(b []byte) bool: 检查字节切片 b 中是否存在任何子串与正则表达式匹配。
  • MatchString(s string) bool: 检查字符串 s 中是否存在任何子串与正则表达式匹配。

注意:这两个方法只判断是否存在匹配,不返回匹配的具体位置或内容。如果需要检查整个字符串是否完全匹配模式,你需要在模式的开头和结尾分别加上 ^$

“`go
package main

import (
“fmt”
“regexp”
)

func main() {
re := regexp.MustCompile([a-z]+) // 匹配一个或多个小写字母

fmt.Println("匹配 'hello':", re.MatchString("hello"))       // true
fmt.Println("匹配 'Hello':", re.MatchString("Hello"))       // true (因为存在 'ello')
fmt.Println("匹配 '12345':", re.MatchString("12345"))       // false
fmt.Println("匹配 'abc123def':", re.MatchString("abc123def")) // true (存在 'abc' 和 'def')

// 检查整个字符串是否完全由数字组成
reFullDigits := regexp.MustCompile(`^\d+$`)
fmt.Println("'12345' 是纯数字吗:", reFullDigits.MatchString("12345"))   // true
fmt.Println("'12a345' 是纯数字吗:", reFullDigits.MatchString("12a345")) // false

}
“`

4.2 查找 (Find)

查找第一个匹配的子串。

  • Find(b []byte) []byte: 在字节切片 b 中查找第一个匹配项,返回匹配的字节切片。如果没有找到,返回 nil
  • FindString(s string) string: 在字符串 s 中查找第一个匹配项,返回匹配的字符串。如果没有找到,返回空字符串 ""
  • FindIndex(b []byte) []int: 在字节切片 b 中查找第一个匹配项,返回匹配项的起始和结束索引([start, end])。如果没有找到,返回 nil
  • FindStringIndex(s string) []int: 在字符串 s 中查找第一个匹配项,返回匹配项的起始和结束索引([start, end])。如果没有找到,返回 nil

“`go
package main

import (
“fmt”
“regexp”
)

func main() {
re := regexp.MustCompile(\d+) // 匹配一个或多个数字
text := “abc 123 def 456 ghi”

// 查找第一个匹配的字符串
firstMatch := re.FindString(text)
fmt.Println("第一个数字串:", firstMatch) // 输出: 123

// 查找第一个匹配的字节切片
firstMatchBytes := re.Find([]byte(text))
fmt.Println("第一个数字串 (bytes):", string(firstMatchBytes)) // 输出: 123

// 查找第一个匹配的索引
firstIndex := re.FindStringIndex(text)
fmt.Println("第一个数字串索引:", firstIndex) // 输出: [4 7] (索引4到7,不包含7)

}
“`

4.3 查找所有 (FindAll)

查找所有非重叠的匹配子串。

  • FindAll(b []byte, n int) [][]byte: 在字节切片 b 中查找最多 n 个非重叠匹配项。如果 n < 0,则查找所有匹配项。返回一个二维字节切片。
  • FindAllString(s string, n int) []string: 在字符串 s 中查找最多 n 个非重叠匹配项。如果 n < 0,则查找所有匹配项。返回一个字符串切片。
  • FindAllIndex(b []byte, n int) [][]int: 在字节切片 b 中查找最多 n 个非重叠匹配项的索引。如果 n < 0,则查找所有匹配项。返回一个二维整数切片。
  • FindAllStringIndex(s string, n int) [][]int: 在字符串 s 中查找最多 n 个非重叠匹配项的索引。如果 n < 0,则查找所有匹配项。返回一个二维整数切片。

“`go
package main

import (
“fmt”
“regexp”
)

func main() {
re := regexp.MustCompile(\w+) // 匹配一个或多个单词字符
text := “Go is a powerful language”

// 查找所有单词
allWords := re.FindAllString(text, -1) // -1 表示查找所有
fmt.Println("所有单词:", allWords) // 输出: [Go is a powerful language]

// 查找前两个单词
twoWords := re.FindAllString(text, 2)
fmt.Println("前两个单词:", twoWords) // 输出: [Go is]

// 查找所有单词的索引
allIndexes := re.FindAllStringIndex(text, -1)
fmt.Println("所有单词索引:", allIndexes) // 输出: [[0 2] [3 5] [6 7] [8 16] [17 25]]

}
“`

4.4 查找子匹配 (FindSubmatch)

当正则表达式包含分组 () 时,除了整个匹配项,你可能还需要获取分组内部匹配的内容(称为子匹配或捕获)。

  • FindSubmatch(b []byte) [][]byte: 查找第一个匹配项及其所有子匹配项。返回一个二维字节切片。结果切片的第一个元素是整个匹配项,随后的元素是每个分组的匹配项。
  • FindStringSubmatch(s string) []string: 查找第一个匹配项及其所有子匹配项。返回一个字符串切片。结果切片的第一个元素是整个匹配项,随后的元素是每个分组的匹配项。
  • FindAllSubmatch(b []byte, n int) [][][]byte: 查找所有匹配项及其子匹配项。
  • FindAllStringSubmatch(s string, n int) [][]string: 查找所有匹配项及其子匹配项。

“`go
package main

import (
“fmt”
“regexp”
)

func main() {
// 匹配 HTML 标签及其内容
// (.*?)是非贪婪匹配任意字符,直到遇到第一个
re := regexp.MustCompile(<([^>]+)>(.*?)</\1>)
text := “

Hello

World

// 查找第一个匹配及其子匹配
// 结果: [整个匹配 子匹配1 子匹配2]
firstMatch := re.FindStringSubmatch(text)
fmt.Println("第一个匹配及子匹配:", firstMatch)
// 输出: [<div>Hello</div> div Hello]

// 查找所有匹配及其子匹配
allMatches := re.FindAllStringSubmatch(text, -1)
fmt.Println("所有匹配及子匹配:")
for _, match := range allMatches {
    fmt.Println(match)
}
// 输出:
// [<div>Hello</div> div Hello]
// [<p>World</p> p World]

}
“`

FindStringSubmatch 的结果中:

  • 索引 0:是整个匹配的字符串 (<div>Hello</div>)。
  • 索引 1:是第一个分组 ([^>]+) 的匹配结果 (div)。
  • 索引 2:是第二个分组 (.*?) 的匹配结果 (Hello)。

4.5 替换 (Replace)

替换匹配正则表达式的子串。

  • ReplaceAll(src, repl []byte) []byte: 将 src 中所有匹配项替换为 repl
  • ReplaceAllString(src, repl string) string: 将 src 中所有匹配项替换为 repl
  • ReplaceAllLiteral(src, repl []byte) []byte: 类似 ReplaceAll,但 repl 被视为字面值,其中的 $1, $2 等不会被解释为分组引用。
  • ReplaceAllLiteralString(src, repl string) string: 类似 ReplaceAllString,但 repl 被视为字面值。
  • ReplaceAllFunc(src []byte, repl func([]byte) []byte) []byte: 使用一个函数来确定替换内容。函数接收匹配的字节切片作为参数,返回用于替换的字节切片。
  • ReplaceAllStringFunc(src string, repl func(string) string) string: 使用一个函数来确定替换内容。函数接收匹配的字符串作为参数,返回用于替换的字符串。

“`go
package main

import (
“fmt”
“regexp”
“strings”
)

func main() {
re := regexp.MustCompile(\s+) // 匹配一个或多个空白字符
text := “Hello World Go Language”

// 将所有连续空白替换为单个空格
replacedText := re.ReplaceAllString(text, " ")
fmt.Println("替换空白:", replacedText) // 输出: Hello World Go Language

// 使用分组引用进行替换
reEmail := regexp.MustCompile(`(\w+)@(\w+)\.(\w+)`) // 匹配简单邮箱格式
email := "[email protected]"
// 将邮箱格式改为 user [at] domain [dot] com
// $1 引用第一个分组(\w+),$2 引用第二个分组(\w+),$3 引用第三个分组(\w+)
formattedEmail := reEmail.ReplaceAllString(email, `$1 [at] $2 [dot] $3`)
fmt.Println("格式化邮箱:", formattedEmail) // 输出: test [at] example [dot] com

// 使用函数进行动态替换
reUppercase := regexp.MustCompile(`[a-z]+`) // 匹配小写字母串
sentence := "This is a Test Sentence."
// 将所有匹配到的小写字母串转换为大写
upperSentence := reUppercase.ReplaceAllStringFunc(sentence, strings.ToUpper)
fmt.Println("转换大写:", upperSentence) // 输出: THIS IS A TEST SENTENCE.

}
“`

ReplaceAllStringrepl 参数中,可以使用 $n 来引用正则表达式中第 n 个捕获分组匹配到的内容($0 引用整个匹配项)。

4.6 分割 (Split)

使用正则表达式作为分隔符来分割字符串。

  • Split(s string, n int) []string: 根据正则表达式在字符串 s 中分割。n 参数控制返回的子串数量:
    • n < 0: 返回所有可能的子串。
    • n == 0: 返回 nil
    • n > 0: 返回最多 n 个子串(最后一个子串是未分割的剩余部分)。

“`go
package main

import (
“fmt”
“regexp”
)

func main() {
// 使用逗号或分号或空格作为分隔符
reSeparator := regexp.MustCompile([,;\s]+)
data := “apple,banana;cherry grape”

// 分割所有部分
parts := reSeparator.Split(data, -1)
fmt.Println("分割结果:", parts) // 输出: [apple banana cherry grape]

// 分割前两部分
twoParts := reSeparator.Split(data, 2)
fmt.Println("分割前两部分:", twoParts) // 输出: [apple banana;cherry grape]

}
“`

5. 一些进阶概念(在 Go 中的体现)

5.1 正则表达式旗标 (Flags)

Go 的 regexp 包支持一些常用的旗标,可以在模式字符串的开头使用 (?flags) 的形式指定,或者通过 regexp.Compile 的变种函数 regexp.CompilePOSIXregexp.Compile 后的 CompileFlags 方法(虽然不常用,直接在模式里加旗标更常见)指定。

常见的旗标包括:

  • i: 忽略大小写匹配 (Case-insensitive)。
  • m: 多行模式 (Multiline),使 ^$ 匹配行的开头和结尾,而不仅仅是整个字符串的开头和结尾。
  • s: Dotall 模式 (让 . 匹配包括换行符在内的所有字符)。

例如,要进行忽略大小写的匹配:

“`go
package main

import (
“fmt”
“regexp”
)

func main() {
// 使用 (?i) 旗标忽略大小写
re := regexp.MustCompile((?i)apple)
text1 := “Apple pie”
text2 := “apple juice”
text3 := “Pineapple”

fmt.Println("匹配 'Apple':", re.MatchString(text1)) // true
fmt.Println("匹配 'apple':", re.MatchString(text2)) // true
fmt.Println("匹配 'Pineapple':", re.MatchString(text3)) // true (因为存在 'apple')

}
“`

5.2 POSIX 模式

regexp 包提供了 CompilePOSIX 和相关方法。POSIX 标准的正则表达式有一些细微的差异,尤其在处理字符类(如 [[:alnum:]])和锚点 (^, $) 的多行行为上。通常情况下,使用默认的非 POSIX 模式就足够了。

5.3 原子分组和占有型量词

Go 的 regexp 包支持原子分组 (?>...)。原子分组一旦匹配成功,其内部的匹配就不会因为后面的匹配失败而回溯。这可以防止某些情况下导致性能问题的“回溯失控”(Catastrophic Backtracking)。占有型量词(如 *+, ++)是原子分组的语法糖。

对于初学者来说,了解有这个概念即可,不必一开始就深入掌握。在遇到特定性能问题时,可以考虑使用原子分组。

5.4 Lookarounds (环视)

Go 的 regexp不完全支持所有的 Lookaround 特性(零宽断言),例如后向肯定/否定断言 ((?<=...), (?<!...))。它主要支持前向肯定断言 ((?=...)) 和前向否定断言 ((?!...))。如果你需要复杂的 Lookaround,可能需要考虑使用第三方库或通过代码逻辑辅助正则表达式来实现。

6. 错误处理

当你使用 regexp.Compile 时,必须检查返回的 error。一个模式字符串可能因为语法错误而无法编译。

“`go
package main

import (
“fmt”
“regexp”
)

func main() {
// 这是一个错误的正则表达式模式 (缺少右括号)
pattern := (\d+
re, err := regexp.Compile(pattern)
if err != nil {
// 编译失败,打印错误信息
fmt.Println(“正则表达式编译错误:”, err)
return
}

// 如果编译成功,re 不会是 nil,可以继续使用
fmt.Println("编译成功:", re) // 这行代码不会被执行

}
“`

对于用户输入的正则表达式模式,务必使用 Compile 并进行错误检查,避免程序因为 MustCompile 失败而崩溃。

7. 最佳实践

  • 编译一次,多次使用: 如果同一个正则表达式需要使用多次,强烈建议先用 CompileMustCompile 编译,然后重复使用编译后的 *regexp.Regexp 对象。这能显著提高性能。
  • 使用 Raw String Literals: 正则表达式中包含大量反斜杠 \。在 Go 中,字符串字面值也使用反斜杠进行转义(如 \n, \t, \\)。为了避免双重转义带来的混淆,推荐使用反引号 ` 包围的 Raw String Literals 来定义正则表达式模式字符串。例如:

    “`go
    // 普通字符串字面值,需要双重转义
    pattern1 := “\d+\.\d+” // 匹配数字.数字

    // Raw String Literal,无需双重转义
    pattern2 := \d+\.\d+ // 更清晰

    fmt.Println(pattern1 == pattern2) // true
    ``
    * **保持简单和可读性:** 复杂的正则表达式很难理解和维护。如果模式变得异常复杂,考虑是否可以通过多次简单的正则表达式操作或结合 Go 代码逻辑来实现相同的功能。
    * **彻底测试:** 使用各种输入情况(包括边界情况和无效输入)来测试你的正则表达式,确保它按照预期工作。可以利用在线正则表达式测试工具(如 regex101.com, regexr.com)来构建和测试模式。
    * **注意性能:** 大多数情况下,
    regexp` 包性能良好。但在处理超大文本或使用非常复杂的、可能导致回溯失控的模式时,性能可能会成为问题。对关键路径上的正则表达式进行性能测试,并在必要时优化模式或采用其他文本处理方法。

8. 总结

Golang 的 regexp 包提供了全面且高效的正则表达式功能。通过本文的学习,你应该已经掌握了:

  • 正则表达式的基本概念:元字符、量词、字符集、锚点等。
  • 在 Go 中导入 regexp 包。
  • 使用 CompileMustCompile 编译正则表达式。
  • 使用 *regexp.Regexp 对象进行各种操作:
    • Match/MatchString:判断是否存在匹配。
    • Find/FindString:查找第一个匹配。
    • FindAll/FindAllString:查找所有匹配。
    • FindSubmatch/FindStringSubmatch:查找匹配和捕获分组。
    • ReplaceAll/ReplaceAllString:替换匹配项。
    • Split:根据模式分割字符串。
  • 理解错误处理的重要性。
  • 一些基本的正则表达式最佳实践。

正则表达式是一个强大的工具,但掌握它需要时间和实践。最好的学习方法是多动手编写和测试不同的正则表达式模式,解决实际问题。结合 Go 语言的强大能力,你将能够高效地处理各种复杂的文本处理任务。

祝你在 Go 的正则表达式世界中探索愉快!


发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部