39·标准库进阶

正则表达式 regexp

regexpmatchreplacecapture

第 39 课:正则表达式 regexp

所属模块: 标准库 难度: Intermediate 标签: regexp, match, replace, capture 上一课: 排序与搜索 sort 下一课: HTTP 客户端


学习目标

完成本课后,你将能够:

  1. 理解 Go 标准库中 regexp 包的基本工作原理和最佳实践。
  2. 使用正则表达式在字符串中进行精确或模糊的模式匹配。
  3. 掌握 FindStringFindAllString 等方法进行文本查找。
  4. 运用 ReplaceAllString 方法对文本进行动态替换。
  5. 利用正则表达式的分组捕获功能提取复杂结构中的特定信息。

核心概念

正则表达式(Regular Expression,简称 regex 或 regexp)是一套用于匹配字符串模式的强大规则。你可以把它想象成字符串世界的“通配符”和“查找替换”功能的超级加强版。

Go 语言通过 regexp 包实现了对正则表达式的支持。Go 的正则表达式语法基于 RE2,它保证了线性时间的匹配复杂度,性能优秀且安全,但与某些语言(如 Perl, Python)相比,它不支持一些回溯特性(如 (?=) lookahead 等)。

关键特性:

  • 预编译: 对于需要重复使用的正则表达式,建议先使用 regexp.Compileregexp.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-DDYYYY/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."

常见错误

  1. 忽略预编译: 在循环或频繁调用的函数内部直接使用 regexp.Compileregexp.MatchString 等函数。这会导致每次调用都重新解析正则表达式,性能低下。解决方案: 在包级别或 init 函数中预编译正则表达式。

    // 错误示例
    for _, s := range myStrings {
        matched, _ := regexp.MatchString(`\d+`, s) // 每次循环都编译
    }
    
    // 正确示例
    var numRe = regexp.MustCompile(`\d+`)
    for _, s := range myStrings {
        matched := numRe.MatchString(s)
    }
    
  2. 错误处理遗漏: 使用 regexp.Compile 返回的 error 被忽略,或者使用 MustCompile 但正则表达式来自不可信的外部输入,可能导致程序 panic。解决方案: 对用户输入或动态构建的正则表达式,使用 Compile 并妥善处理错误。

  3. 贪婪匹配陷阱: 默认的量词(如 *, +, {n,m})是贪婪的,会尽可能多地匹配字符。例如,<.*> 会匹配从第一个 < 到最后一个 > 的所有内容。解决方案: 使用非贪婪量词 *?, +? 等,或者更精确地定义字符集(如 <[^>]*>)。

  4. 特殊字符未转义: 当需要匹配 ., *, +, ? 等在正则中有特殊含义的字符时,忘记使用 \ 进行转义。例如,要匹配 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](字符集取反)和 .*?(非贪婪)来编写更精确的模式。

正则表达式功能强大但也容易写出难以维护的代码。建议先从简单的模式开始,逐步构建,并充分测试,确保它按预期工作。

练习编辑器

go
Loading...

继续学习

完成本课后,建议继续学习下一课「HTTP 客户端」 以巩固所学知识。