第 39 课:正则表达式 regexp
所属模块: 标准库 难度: Intermediate 标签: regexp, match, replace, capture 上一课: 排序与搜索 sort 下一课: HTTP 客户端
学习目标
完成本课后,你将能够:
- 理解 Go 标准库中
regexp包的基本工作原理和最佳实践。 - 使用正则表达式在字符串中进行精确或模糊的模式匹配。
- 掌握
FindString、FindAllString等方法进行文本查找。 - 运用
ReplaceAllString方法对文本进行动态替换。 - 利用正则表达式的分组捕获功能提取复杂结构中的特定信息。
核心概念
正则表达式(Regular Expression,简称 regex 或 regexp)是一套用于匹配字符串模式的强大规则。你可以把它想象成字符串世界的“通配符”和“查找替换”功能的超级加强版。
Go 语言通过 regexp 包实现了对正则表达式的支持。Go 的正则表达式语法基于 RE2,它保证了线性时间的匹配复杂度,性能优秀且安全,但与某些语言(如 Perl, Python)相比,它不支持一些回溯特性(如 (?=) lookahead 等)。
关键特性:
- 预编译: 对于需要重复使用的正则表达式,建议先使用
regexp.Compile或regexp.MustCompile进行预编译,生成一个*Regexp对象。这样可以显著提升性能,避免重复解析正则表达式字符串。 - 匹配: 检查字符串是否整体或部分符合模式。
- 查找: 在字符串中搜索符合模式的一个或多个子串。
- 替换: 将匹配到的内容替换为新的字符串。
- 捕获: 使用
()将模式中的某些部分“括起来”,这些部分被称为“子匹配”或“捕获组”,可以在匹配成功后单独提取出来。
代码示例
下面是一个综合示例,演示了 regexp 包的常见用法。
package main
import (
"fmt"
"regexp"
)
func main() {
// 1. 预编译正则表达式(最佳实践)
// 匹配一个或多个数字,后面跟着一个或多个字母
pattern := `(\d+)([a-zA-Z]+)`
re, err := regexp.Compile(pattern)
if err != nil {
fmt.Printf("编译正则表达式失败: %v\n", err)
return
}
testString := "abc123def456ghi"
// 2. 匹配检查:测试整个字符串是否匹配
matchFull := re.MatchString(testString)
fmt.Printf("整个字符串\"%s\"完全匹配模式\"%s\"吗? %v\n", testString, pattern, matchFull)
// 输出: false (因为字符串以字母开头)
// 3. 查找匹配
// FindString: 返回第一个匹配的子串
firstMatch := re.FindString(testString)
fmt.Printf("第一个匹配的子串: \"%s\"\n", firstMatch)
// 输出: "123def"
// FindAllString: 返回所有匹配的子串的切片
allMatches := re.FindAllString(testString, -1) // -1 表示返回所有匹配
fmt.Printf("所有匹配的子串: %v\n", allMatches)
// 输出: ["123def" "456ghi"]
// 4. 替换匹配
// ReplaceAllString: 将所有匹配的子串替换为新字符串
replaced := re.ReplaceAllString(testString, "[$1-$2]") // 使用 $1, $2 引用捕获组
fmt.Printf("替换后的字符串: \"%s\"\n", replaced)
// 输出: "abc[123-def][456-ghi]"
// 5. 分组捕获
// FindStringSubmatch: 返回第一个匹配及其所有子匹配(捕获组)
firstSubmatch := re.FindStringSubmatch(testString)
fmt.Printf("第一个匹配及其子匹配: %v\n", firstSubmatch)
// 输出: ["123def" "123" "def"] 第一个元素是整个匹配,后续是各捕获组
// FindAllStringSubmatch: 返回所有匹配及其子匹配
allSubmatches := re.FindAllStringSubmatch(testString, -1)
fmt.Printf("所有匹配及其子匹配:\n")
for i, match := range allSubmatches {
fmt.Printf(" 匹配 %d: 整体=%s, 数字=%s, 字母=%s\n", i, match[0], match[1], match[2])
}
// 输出:
// 匹配 0: 整体=123def, 数字=123, 字母=def
// 匹配 1: 整体=456ghi, 数字=456, 字母=ghi
// 实用示例:提取邮箱地址
emailPattern := `([a-zA-Z0-9._%+-]+)@([a-zA-Z0-9.-]+)\.([a-zA-Z]{2,})`
emailRe := regexp.MustCompile(emailPattern) // 使用 MustCompile 简化,编译失败会 panic
emailText := "请联系我:[email protected] 或者 [email protected]"
emails := emailRe.FindAllStringSubmatch(emailText, -1)
fmt.Printf("\n提取到的邮箱信息:\n")
for _, e := range emails {
fmt.Printf(" 完整邮箱: %-25s 用户名: %-15s 域: %-15s 顶级域: %s\n", e[0], e[1], e[2], e[3])
}
}
输出:
整个字符串"abc123def456ghi"完全匹配模式"(\d+)([a-zA-Z]+)"吗? false
第一个匹配的子串: "123def"
所有匹配的子串: [123def 456ghi]
替换后的字符串: "abc[123-def][456-ghi]"
第一个匹配及其子匹配: [123def 123 def]
所有匹配及其子匹配:
匹配 0: 整体=123def, 数字=123, 字母=def
匹配 1: 整体=456ghi, 数字=456, 字母=ghi
提取到的邮箱信息:
完整邮箱: [email protected] 用户名: alice 域: example 顶级域: com
完整邮箱: [email protected] 用户名: bob.smith 域: sub.domain 顶级域: org
实践练习
练习 1:基础匹配与查找
编写程序,检查给定的字符串是否是有效的 Go 变量名(以字母或下划线开头,后续可以是字母、数字或下划线),并从一段代码文本中找出所有符合条件的变量名。
- 输入字符串:
"var myVar_1 = 100; anotherVar := \"hello\"; 1badName invalid-name" - 预期输出:
"myVar_1" 是有效的变量名。 "anotherVar" 是有效的变量名。 从代码中找到的变量名: [myVar_1 anotherVar]
练习 2:提取与格式化
给定一段包含日期的文本(日期格式可能为 YYYY-MM-DD 或 YYYY/MM/DD),使用正则表达式将它们全部提取出来,并统一格式化为 DD/MM/YYYY。
- 输入文本:
"会议定在2023-10-05,报告提交日是2023/11/22,最后期限是 2023-12-31。" - 预期输出:
提取到的日期: [2023-10-05 2023/11/22 2023-12-31] 格式化后: 05/10/2023, 22/11/2023, 31/12/2023
练习 3:高级替换(贪婪 vs 非贪婪)
编写一个函数,移除 HTML 文本中所有的标签,但保留标签之间的文本内容。
- 提示: 注意
<.*>是贪婪匹配,可能会错误匹配<b>bold</b> text。你需要使用非贪婪匹配.*?。 - 输入文本:
"<p>This is <b>bold</b> and <i>italic</i> text.</p>" - 预期输出:
"This is bold and italic text."
常见错误
-
忽略预编译: 在循环或频繁调用的函数内部直接使用
regexp.Compile或regexp.MatchString等函数。这会导致每次调用都重新解析正则表达式,性能低下。解决方案: 在包级别或init函数中预编译正则表达式。// 错误示例 for _, s := range myStrings { matched, _ := regexp.MatchString(`\d+`, s) // 每次循环都编译 } // 正确示例 var numRe = regexp.MustCompile(`\d+`) for _, s := range myStrings { matched := numRe.MatchString(s) } -
错误处理遗漏: 使用
regexp.Compile返回的error被忽略,或者使用MustCompile但正则表达式来自不可信的外部输入,可能导致程序 panic。解决方案: 对用户输入或动态构建的正则表达式,使用Compile并妥善处理错误。 -
贪婪匹配陷阱: 默认的量词(如
*,+,{n,m})是贪婪的,会尽可能多地匹配字符。例如,<.*>会匹配从第一个<到最后一个>的所有内容。解决方案: 使用非贪婪量词*?,+?等,或者更精确地定义字符集(如<[^>]*>)。 -
特殊字符未转义: 当需要匹配
.,*,+,?等在正则中有特殊含义的字符时,忘记使用\进行转义。例如,要匹配192.168.1.1,正则应该是192\.168\.1\.1,而不是192.168.1.1。解决方案: 使用regexp.QuoteMeta函数来转义字符串中的正则特殊字符。
小结
regexp包 是 Go 处理文本模式匹配的核心工具。- 性能关键: 预编译正则表达式是提升性能的最佳实践。优先使用
MustCompile处理固定的正则,用Compile处理可能出错的动态正则。 - 核心操作:
- 匹配:
MatchString检查是否存在匹配。 - 查找:
FindString/FindAllString获取匹配的文本。 - 替换:
ReplaceAllString结合$1, $2等引用捕获组进行复杂替换。 - 捕获:
FindStringSubmatch/FindAllStringSubmatch提取子模式匹配的内容。
- 匹配:
- 正则语法注意: Go 使用 RE2 语法,注意其与 Perl 系正则的细微差别(如不支持环视)。善用
[^abc](字符集取反)和.*?(非贪婪)来编写更精确的模式。
正则表达式功能强大但也容易写出难以维护的代码。建议先从简单的模式开始,逐步构建,并充分测试,确保它按预期工作。