☰
Go 语言 regexp 正则处理实战指南:从匹配判断到爬虫文本提取(build-web-application-with-golang 第 7.3 节)
2026/10/7 2:01:43 网站建设 项目流程
  • 文档
  • 教程

【免费下载链接】build-web-application-with-golang

A golang ebook intro how to build a web with golang

项目地址:https://gitcode.com/gh_mirrors/bu/build-web-application-with-golang
点击查看免费下载

正则表达式(Regular Expression)是 Go Web 开发中处理文本校验与提取的核心工具:在服务端验证用户输入是否合法、从抓取的 HTML 页面中过滤标签、抽取结构化信息,都需要依靠regexp标准库。本篇以本仓库《build-web-application-with-golang》第 7.3 节为基础,系统讲解 Go 语言regexp包的匹配判断、编译解析、查找、替换与 Expand 展开等全部核心 API,并结合仓库中的表单验证源码给出可直接落地的实战示例。读完本文,你将能熟练使用 Go 正则完成「判断是否匹配」与「提取/过滤内容」两大类任务。

1. 正则与 Go 的 regexp 包

正则表达式是一种进行模式匹配和文本操作的复杂而强大的工具。虽然正则表达式比纯粹的文字匹配效率低,但是它却更灵活——按照语法规则随时构造出的匹配模式,就能从原始文本中筛选出几乎任何你想要的字符组合。在 Web 开发中,当你需要从文本数据源中取数据时,只需按照语法规则构造出正确的模式字符串,就能从原始数据源提取出有意义的文本信息。

Go 语言通过regexp标准包为正则表达式提供了官方支持。如果你已经使用过其他语言提供的正则功能,那么对 Go 版本不会太陌生,但它们之间也有一些小的差异:Go 实现的是 RE2 标准(除\C外),RE2 采用线性时间算法,避免了回溯导致的最坏情况灾难性性能问题,这也是 Go 正则在处理不可信输入时更安全的原因。关于 RE2 的详细语法描述可参考官方 Syntax 文档。

1.1 strings 包能解决的,优先用 strings

其实字符串处理我们还可以使用strings包进行搜索(Contains、Index)、替换(Replace)和解析(Split、Join)等操作,但是这些都属于简单的字符串操作——它们的搜索区分大小写,而且匹配的是固定字符串。如果我们需要匹配可变内容(如任意数字、任意长度的字符段),strings包就无法实现了。

当然,如果strings包能解决你的问题,就尽量使用它:它们足够简单,而且性能和可读性都会优于正则。

1.2 与表单验证小节的衔接

如果你还记得,在前面表单验证小节中我们已经接触过正则处理——在那里利用它验证输入信息是否满足预设条件(如年龄是否为数字、姓名是否全为中文、邮箱格式是否正确)。使用中需要注意的一点是:所有字符都是 UTF-8 编码的。仓库中对应源码 validator/main.go 正是这一应用的完整实现,例如校验中文姓名与邮箱:

// 校验中文名:只允许 \x{4e00}-\x{9fa5} 区间内的汉字 if m, _ := regexp.MatchString("^[\\x{4e00}-\\x{9fa5}]+$", strings.Trim(str, " ")); !m { return errors.New("Please make sure that the chinese name only contains chinese characters.") } // 校验邮箱:非空字符 @ 非空字符 if m, err := regexp.MatchString(`^[^@]+@[^@]+$`, str); !m { return errors.New("Please enter a valid email address.") }

可以看到,regexp.MatchString正是表单服务端校验中最常用、最直接的入口。接下来让我们更深入地学习 Go 语言regexp包的相关知识。

2. 通过正则判断是否匹配

regexp包中含有三个函数用来判断是否匹配,如果匹配则返回 true,否则返回 false:

func Match(pattern string, b []byte) (matched bool, error error) func MatchReader(pattern string, r io.RuneReader) (matched bool, error error) func MatchString(pattern string, s string) (matched bool, error error)

上面的三个函数实现了同一个功能:判断pattern是否与输入源匹配,匹配就返回 true;如果解析正则出错则返回 error。三个函数的输入源分别是byte slice、RuneReader和string。

2.1 实战:判断 IP 地址

如果要验证一个输入是不是 IP 地址,该如何判断?请看如下实现:

func IsIP(ip string) (b bool) { if m, _ := regexp.MatchString("^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$", ip); !m { return false } return true }

可以看到,regexp的 pattern 和我们平常使用的正则一模一样:^与$锚定整个字符串,[0-9]{1,3}匹配 1 到 3 位数字,\\.转义匹配字面点号。

2.2 实战:判断用户输入是否为数字

再来看一个例子:当用户输入一个字符串,我们想知道它是不是一次合法的数字输入:

func main() { if len(os.Args) == 1 { fmt.Println("Usage: regexp [string]") os.Exit(1) } else if m, _ := regexp.MatchString("^[0-9]+$", os.Args[1]); m { fmt.Println("數字") } else { fmt.Println("不是數字") } }

该程序从命令行参数os.Args[1]取输入,用^[0-9]+$判断是否全部由数字组成。在上面的两个小例子中,我们采用了Match(String)来判断一些字符串是否符合我们的描述需求,它们使用起来非常方便。在仓库的表单验证场景中,判断年龄是否为数字也采用了完全相同的模式(见 validator/main.go 中的checkAge,配合strconv.Atoi做类型转换与范围判断)。

3. 通过正则取得内容

Match模式只能用于对字符串的判定,而无法截取字符串的一部分、过滤字符串、或者提取出符合条件的一批字符串。如果想要满足这些需求,那就需要使用正则表达式的复杂模式。

我们经常需要一些爬虫程序,下面就以爬虫为例来说明如何使用正则来过滤或截取抓取到的数据:

package main import ( "fmt" "io/ioutil" "net/http" "regexp" "strings" ) func main() { resp, err := http.Get("http://www.baidu.com") if err != nil { fmt.Println("http get error.") } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { fmt.Println("http read error") return } src := string(body) //將 HTML 標籤全轉換成小寫 re, _ := regexp.Compile("\\<[\\S\\s]+?\\>") src = re.ReplaceAllStringFunc(src, strings.ToLower) //去除 STYLE re, _ = regexp.Compile("\\<style[\\S\\s]+?\\</style\\>") src = re.ReplaceAllString(src, "") //去除 SCRIPT re, _ = regexp.Compile("\\<script[\\S\\s]+?\\</script\\>") src = re.ReplaceAllString(src, "") //去除所有尖括號內的 HTML 程式碼,並換成換行符 re, _ = regexp.Compile("\\<[\\S\\s]+?\\>") src = re.ReplaceAllString(src, "\n") //去除連續的換行符 re, _ = regexp.Compile("\\s{2,}") src = re.ReplaceAllString(src, "\n") fmt.Println(strings.TrimSpace(src)) }

这个例子的处理管线清晰展示了正则的典型用法:

  1. http.Get抓取页面 →ioutil.ReadAll读取响应体;
  2. \\<[\\S\\s]+?\\>匹配所有尖括号 HTML 标签,用ReplaceAllStringFunc(src, strings.ToLower)将标签统一转为小写(便于后续精确匹配);
  3. 用\\<style[\\S\\s]+?\\</style\\>与\\<script[\\S\\s]+?\\</script\\>分别移除<style>、<script>块;
  4. 再次用\\<[\\S\\s]+?\\>把剩余标签替换为换行符;
  5. 用\\s{2,}将连续的空白字符压缩为单个换行符;
  6. 最后strings.TrimSpace去掉首尾空白,输出纯净的页面文本。

注意其中的[\\S\\s]+?是非贪婪匹配任意字符(包括换行)的惯用写法。从这个例子可以看出,使用复杂正则的第一步是Compile:它会解析正则表达式是否合法,如果正确,就返回一个*Regexp,然后就可以利用返回的Regexp在任意字符串上执行需要的操作。

4. 解析正则表达式的方法

解析正则表达式有如下几个方法:

func Compile(expr string) (*Regexp, error) func CompilePOSIX(expr string) (*Regexp, error) func MustCompile(str string) *Regexp func MustCompilePOSIX(str string) *Regexp

这四者的区别要点如下:

函数语法约束搜索策略错误处理
Compile通用 RE2 语法最左方式(leftmost-first)返回error
CompilePOSIX必须使用 POSIX 语法最左最长方式(leftmost-longest)返回error
MustCompile通用 RE2 语法最左方式解析失败直接panic
MustCompilePOSIX必须使用 POSIX 语法最左最长方式解析失败直接panic

CompilePOSIX 与 Compile 的不同点在于:POSIX 必须使用 POSIX 语法,它使用最左最长方式搜索;而 Compile 只采用最左方式搜索。例如对[a-z]{2,4}这样一个正则表达式,应用于"aa09aaa88aaaa"这个文本串时,CompilePOSIX返回aaaa,而Compile返回的是aa。

前缀带 Must 的函数表示:在解析正则语法的时候,如果匹配模式串不满足正确的语法则直接 panic;而不带 Must 的版本则只返回错误,由调用方决定如何处理。在编写爬虫、日志解析等「模式是硬编码常量」的场景中,MustCompile更常用——它可以在程序启动时立即暴露正则有误的问题,如文档的 Expand 示例中所用的regexp.MustCompile(...)。

5. Regexp 的查找方法

在了解如何创建一个Regexp之后,我们再来看一下这个 struct 提供了哪些方法来辅助我们操作字符串。首先来看这些用于搜索的函数:

func (re *Regexp) Find(b []byte) []byte func (re *Regexp) FindAll(b []byte, n int) [][]byte func (re *Regexp) FindAllIndex(b []byte, n int) [][]int func (re *Regexp) FindAllString(s string, n int) []string func (re *Regexp) FindAllStringIndex(s string, n int) [][]int func (re *Regexp) FindAllStringSubmatch(s string, n int) [][]string func (re *Regexp) FindAllStringSubmatchIndex(s string, n int) [][]int func (re *Regexp) FindAllSubmatch(b []byte, n int) [][][]byte func (re *Regexp) FindAllSubmatchIndex(b []byte, n int) [][]int func (re *Regexp) FindIndex(b []byte) (loc []int) func (re *Regexp) FindReaderIndex(r io.RuneReader) (loc []int) func (re *Regexp) FindReaderSubmatchIndex(r io.RuneReader) []int func (re *Regexp) FindString(s string) string func (re *Regexp) FindStringIndex(s string) (loc []int) func (re *Regexp) FindStringSubmatch(s string) []string func (re *Regexp) FindStringSubmatchIndex(s string) []int func (re *Regexp) FindSubmatch(b []byte) [][]byte func (re *Regexp) FindSubmatchIndex(b []byte) []int

上面这 18 个函数根据输入源(byte slice、string 和 io.RuneReader)不同,还可以简化成如下几个——其余只是输入源不一样,功能基本是一样的:

func (re *Regexp) Find(b []byte) []byte func (re *Regexp) FindAll(b []byte, n int) [][]byte func (re *Regexp) FindAllIndex(b []byte, n int) [][]int func (re *Regexp) FindAllSubmatch(b []byte, n int) [][][]byte func (re *Regexp) FindAllSubmatchIndex(b []byte, n int) [][]int func (re *Regexp) FindIndex(b []byte) (loc []int) func (re *Regexp) FindSubmatch(b []byte) [][]byte func (re *Regexp) FindSubmatchIndex(b []byte) []int

使用时的关键语义:

  • Find:返回第一个匹配到的内容(byte slice);
  • FindAll:返回全部匹配内容,第二个参数n控制返回数量,n 小于 0 表示返回全部,否则返回指定长度;
  • FindIndex/FindAllIndex:返回匹配内容的起始与结束位置([]int,长度为 2 或 2n);
  • FindSubmatch/FindAllSubmatch:返回包含分组捕获的匹配结果,[][]byte的第一个元素是整体匹配,后续元素依次是第 1、第 2 个()分组的内容;
  • FindSubmatchIndex/FindAllSubmatchIndex:返回分组的起止位置索引。

5.1 完整示例:Find 系列函数

对于这些函数的使用,我们来看下面这个例子:

package main import ( "fmt" "regexp" ) func main() { a := "I am learning Go language" re, _ := regexp.Compile("[a-z]{2,4}") //查詢符合正則的第一個 one := re.Find([]byte(a)) fmt.Println("Find:", string(one)) //查詢符合正則的所有 slice,n 小於 0 表示回傳全部符合的字串,不然就是回傳指定的長度 all := re.FindAll([]byte(a), -1) fmt.Println("FindAll", all) //查詢符合條件的 index 位置,開始位置和結束位置 index := re.FindIndex([]byte(a)) fmt.Println("FindIndex", index) //查詢符合條件的所有的 index 位置,n 同上 allindex := re.FindAllIndex([]byte(a), -1) fmt.Println("FindAllIndex", allindex) re2, _ := regexp.Compile("am(.*)lang(.*)") //查詢 Submatch,回傳陣列,第一個元素是匹配的全部元素,第二個元素是第一個()裡面的,第三個是第二個()裡面的 //下面的輸出第一個元素是"am learning Go language" //第二個元素是" learning Go ",注意包含空格的輸出 //第三個元素是"uage" submatch := re2.FindSubmatch([]byte(a)) fmt.Println("FindSubmatch", submatch) for _, v := range submatch { fmt.Println(string(v)) } //定義和上面的 FindIndex 一樣 submatchindex := re2.FindSubmatchIndex([]byte(a)) fmt.Println(submatchindex) //FindAllSubmatch,查詢所有符合條件的子匹配 submatchall := re2.FindAllSubmatch([]byte(a), -1) fmt.Println(submatchall) //FindAllSubmatchIndex,查詢所有字匹配的 index submatchallindex := re2.FindAllSubmatchIndex([]byte(a), -1) fmt.Println(submatchallindex) }

运行后可以直观看到:Find得到首个匹配am;FindAll得到所有[a-z]{2,4}片段(am、learning、Go、language等);FindSubmatch中re2的正则am(.*)lang(.*)会返回三个元素——整体"am learning Go language"、第一个分组" learning Go "(注意包含空格)和第二个分组"uage"。这正是从文本中按分组抽取字段的核心手段。

6. Regexp 的匹配方法

前面介绍过匹配函数,Regexp也定义了三个方法,它们和同名的包级函数功能一模一样——其实包级函数(Match、MatchReader、MatchString)内部就是调用这三个方法来实现的:

func (re *Regexp) Match(b []byte) bool func (re *Regexp) MatchReader(r io.RuneReader) bool func (re *Regexp) MatchString(s string) bool

也就是说,当你已经通过Compile持有*Regexp时,无需再传入 pattern 字符串,直接调用re.MatchString(s)即可完成判断,同时避免了对同一模式反复编译的开销。

7. 替换函数

接下来了解替换函数是如何操作的:

func (re *Regexp) ReplaceAll(src, repl []byte) []byte func (re *Regexp) ReplaceAllFunc(src []byte, repl func([]byte) []byte) []byte func (re *Regexp) ReplaceAllLiteral(src, repl []byte) []byte func (re *Regexp) ReplaceAllLiteralString(src, repl string) string func (re *Regexp) ReplaceAllString(src, repl string) string func (re *Regexp) ReplaceAllStringFunc(src string, repl func(string) string) string

使用要点:

  • ReplaceAll/ReplaceAllString:将所有匹配到的内容替换为repl,且repl中支持$1、${name}形式的反向引用;
  • ReplaceAllFunc/ReplaceAllStringFunc:第二个参数是回调函数,对每个匹配到的内容调用该函数,用返回值替换(前述爬虫示例中用strings.ToLower做函数式替换即属此类);
  • ReplaceAllLiteral/ReplaceAllLiteralString:字面替换,repl中的$不会做反向引用展开,按原样输出。

这些替换函数我们在上面的抓网页例子中有详细应用示范(去除<style>、<script>、压缩连续空白等)。

8. Expand:基于模板与分组展开文本

接下来看一下 Expand 的说明:

func (re *Regexp) Expand(dst []byte, template []byte, src []byte, match []int) []byte func (re *Regexp) ExpandString(dst []byte, template string, src string, match []int) []byte

Expand的用途是:在已经通过FindSubmatchIndex拿到分组位置信息的基础上,按照template模板把匹配到的分组内容展开追加到dst中。它通常与FindAllSubmatchIndex配合使用,用于批量重构文本。请看下面的例子:

func main() { src := []byte(` call hello alice hello bob call hello eve `) pat := regexp.MustCompile(`(?m)(call)\s+(?P<cmd>\w+)\s+(?P<arg>.+)\s*$`) res := []byte{} for _, s := range pat.FindAllSubmatchIndex(src, -1) { res = pat.Expand(res, []byte("$cmd('$arg')\n"), src, s) } fmt.Println(string(res)) }

关键点解读:

  • 正则(?m)(call)\s+(?P<cmd>\w+)\s+(?P<arg>.+)\s*$中,(?m)开启多行模式($匹配行尾),(?P<cmd>...)、(?P<arg>...)是命名分组;
  • FindAllSubmatchIndex(src, -1)一次性找出所有匹配行的分组位置[]int;
  • 对每个匹配位置,pat.Expand(res, []byte("$cmd('$arg')\n"), src, s)按模板$cmd('$arg')展开——$cmd、$arg引用命名分组的内容,展开结果追加进res;
  • 最终输出为:
hello('alice') hello('eve')

注意hello bob一行因不满足(call)前缀条件未被匹配,这正是用正则做「条件化文本重构」的典型场景。

9. 小结

至此我们已完整介绍 Go 语言的regexp包。回顾全篇,核心能力可归纳为四类:

  1. 判断匹配:regexp.Match/MatchReader/MatchString,以及*Regexp上的同名方法,适合表单校验、输入合法性检查;
  2. 编译解析:Compile/CompilePOSIX/MustCompile/MustCompilePOSIX,注意 POSIX 语法的最左最长与普通模式最左搜索的差异,以及 Must 前缀的 panic 行为;
  3. 查找提取:Find、FindAll、FindIndex、FindSubmatch及其 Index 变体,配合分组捕获从文本中抽取结构化字段;
  4. 替换与展开:ReplaceAll系列(含函数式与字面替换)用于清洗/改写文本,Expand/ExpandString基于命名分组与模板批量重构文本。

在实际的 Go Web 开发中,正则的典型落地路径是:表单数据 →regexp.MatchString校验(如仓库 validator/main.go 所示);爬虫抓取 →Compile+ReplaceAll清洗 HTML;日志与文本解析 →FindAllSubmatch+Expand抽取字段。掌握了regexp包,你就拥有了对任意文本进行模式匹配与内容操纵的完整能力。

本文为《build-web-application-with-golang》第 7.3 节的深度展开,相关章节可继续阅读:目录、上一节 JSON 处理、下一节 模板处理,以及本节正则的前置应用场景表单验证。

  • 文档
  • 教程

【免费下载链接】build-web-application-with-golang

A golang ebook intro how to build a web with golang

项目地址:https://gitcode.com/gh_mirrors/bu/build-web-application-with-golang
点击查看免费下载
上一篇:requests-html招聘网站爬虫:职位信息与薪资水平分析
下一篇:告别"面条代码"的终极指南:YAPF Python自动格式化解决方案

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询