正则指引——Golang

Golang

Google发明的Go语言(严格来说并没有"Go语言"​,所以下文简称Golang)在近些年发展迅猛。Golang号称"互联网时代的C"​,既保证了高效率,又提供了高级的编程范式和丰富的库,因此深受不少服务端开发人员的喜爱。与C和C++不同的是,Golang内置了正则表达式的库regexp,使用正则表达式尤其方便。所以,主要讲解regexp的使用。

要说明的是,Golang到目前(2018年1月)来说还算"年轻"​,所以并没有提供某些"业界通用"的特性,有些特性显得比较古怪(比如用某个模式修饰符交换匹配优先量词和忽略优先量词的意义)​。本章的讲解基于Golang1.9.2,写作时我会确认所有代码都经过实际运行。如果你读到时发现同样代码的结果与描述的不符,有可能是语 言特性发生了变化。

不过,Golang的正则表达式不支持回溯的特性估计很难改变。Golang的正则表达式基于RE2规范,而RE2的正则引擎采用的是DFA。之前讲过,DFA"天生"不支持回溯。

同样需要注意的是,Golang的文档虽然很详细,但是其中关于正则表达式的许多术语与业界流行的并不相同,比如多选分支不叫alternative而叫composite,量词不叫quantifier而叫repetitions,忽略优先量词不叫lazy/reluctant quantifier而叫prefer fewer。所以如果用常见名词去搜索未必能找到答案,倘若你已经熟悉正则表达式的通行规则,阅读Golang的文档应当没有问题。

Golang中的字符串直接使用Unicode字符,所以,Golang的正则表达式对Unicode的兼容也非常好。

1、预备知识

Golang中与正则表达式相关的、最重要的package是regexp。​如果要在代码中使用正则表达式,一定要在程序头部导入它(同样要注意的是,因为Golang对导入有严格限制,如果有package导入了但没有使用,编译时就会出错)​。

go 复制代码
import (
	"fmt"
	"regexp"
)

func main() {
	matchString, _ := regexp.MatchString("\\d+", "123456")
	fmt.Println(matchString) //true
}

Golang中也提供了"原生字符串"​,也就是"特殊字符不会经过转义"的字符串,具体方法是用两个后引号(backquote,不是单引号,键盘上字符1左边的那个键)​,比如\d。用到正则表达式时,采用原生字符串会方便很多,如果采用普通字符串,正则表达式中的不少表示法就需要额外转义,否则会出现编译错误。

额外的转义必然会增加出错的几率。与Java不同,Golang中的正则表达式并不会处理\转义序列,所以如果要输出\,只需写为\,如果写为\\则会输出\。

为节省篇幅,后文中关于正则表达式的讲解我尽量使用类似下面的表格加以讲解。表格中的文本和正则表达式均以源代码中的格式为准,实践时可以照原样抄入源代码中,不必考虑转义问题。

要注意的是,虽然Golang的字符串中可以直接包含Unicode字符,但它其实还是被当作UTF-8的字节数组来对待的,所以在计算字符串位置(比如截取字符串、计算表达式匹配结果的偏移值)时,得到的结果仍然是按照字节来计数的。虽然Golang也提供了专门针对Unicode字符串处理的rune​(其实是uint32数组)​,不幸的是,正则表达式相关的函数都不能接收rune类型的参数。所以,如果你看到正则表达式34+在字符串"012345"中匹配结果的偏移值是3~4,而在"零壹贰34伍"中匹配结果的偏移值是9~11,千万不要感到奇怪。

2、正则功能详解

2.1、列表

下表列出了Golang中的正则功能。

2.2、字符组

Golang中的字符串原生支持UTF-8,所以字符组中可以直接使用中文,不会出现多字节字符错误匹配的问题,点号.可以匹配中文字符。

Golang中的字符组不支持集合运算,所以不要使用类似[[a-z]&&[^aeiou]​][​[0-4]||[6-9]​]的字符组。如果出现了这样的字符组,编译时不会报错,但程序运行时的行为无法预测。

Golang中可以用\uhex指定Unicode码值,其中hex为4位十六进制数,所以可以用字符组[\u4E00-\u9FFF]匹配所有的中文字符。要注意的是,如果使用这种表示法,就不应当使用原生字符串,否则\uhex的转义序列无法被正确识别。

如果要指定BMP之外的Unicode字符,因为其码值不能用4位十六进制数表示,就必须使用\Uhex指定,其中hex为8位十六进制数,比如\U0000672C。

Golang中可以使用POSIX字符组[​[:name:]​]。它们都采用ASCII匹配规则,请注意,必须使用两个方括号。

2.3、Unicode属性

Golang对Unicode字符组的支持比较好,它支持UnicodeProperty(Golang的官方文档称为Unicode Characterclass)​。这样,用\p{N}就可以匹配所有的数字字符,包括中文的全角数字字符,比如0、1、2;\p{P}可以匹配各种标点符号,包括中文的冒号:等。

虽然官方文档只说明支持Unicode Property,但实际代码测试表明,Golang也支持Unicode Script。

2.4、字符组简记法

在Golang的正则表达式中,\d、\D、\w、\W、\s、\S全部使用ASCII匹配规则。也就是说,\d等价于0-9, \w等价于0-9a-zA-Z_,\s不能匹配ASCII编码之外的空白字符。这些都需要在使用中注意。

2.5、单词边界

在Golang中,默认情况下,正则表达式的单词边界\b的匹配保持了和\w同样的规则,都遵循ASCII匹配规则。所以处理中英文混排文本时可以放心用\b来进行英文单词边界的判断。

2.6、行起始/结束位置

^匹配的是"行开始的位置"​,在默认情况下它只能匹配"整个字符串的开始位置"​,如果指定使用多行模式(Multiline Mode)​,^可以匹配字符串内部的文本行的开始位置;而\A无论在什么情况下都匹配"整个字符串的开始位置"​。

在默认情况下,Golang中的$\z匹配的是整个字符串的结束位置(与其他语言不同的是,Golang中没有\Z,而且,如果结束位置有换行符,则$并不会匹配这个换行符之前的位置)​,多行模式只会影响到$的匹配,这时候它可以匹配文本内部的行结束位置,所以进行准确验证时应当使用\z

2.7、环视

很遗憾,虽然环视已经得到广泛使用,但是到目前(1.9.2)为止,Golang中的regexp并没有提供对环视的支持。如果一定要使用环视,可以采用第三方的库,或者等待Golang的后续版本。

2.8、匹配模式

下表列出了Golang中的正则表达式支持的常见匹配模式,所有模式对应的常量都包含在一个uint16的变量Flags中。

匹配模式可以在表达式中以(?modifier)指定,原理上说应当可以通过预定义常量指定,但我在网络上没有搜索到任何示范。考虑到使用修饰符本来就是业界通行的做法,我推荐大家使用模式修饰符。

Golang也支持用(?-modifier)停用某个匹配模式。

2.9、纯文本模式

在\Q和\E之内,所有的元字符和特殊结构都失去特殊意义,只能匹配它们对应的字符本身。不过在字符串中,\Q和\E中的反斜线也需要经过字符串转义,应该写成\Q和\E。

2.10、捕获分组的引用

在Golang中,如果仅使用内置的regexp,无法在正则表达式内部引用捕获分组。如果你确实需要用到这个特性,可以考虑第三方的库。

替换时可以引用捕获分组,应当使用$num记法。

要在replacement字符串中表示 字符,可以直接写 ' 字符,可以直接写` 字符,可以直接写'`,不必转义。

2.11、命名分组

在Golang中,如果要在正则表达式内部使用捕获分组,应当使用(?P<name>...)记法,其中name为对应捕获分组的名字。

如果要在替换中使用命名分组,可以使用${name}记法。

截至本章写作时(2018年1月)​,尚未发现在正则表达式内部引用命名分组的文档,尝试通用的表示法均不可行,所以暂时可以认为正则表达式内部无法引用命名分组。

3、正则API简介

前面说过,Golang的正则表达式采用"面向对象"的处理方法。也就是说,在使用正则表达式的相关功能之前,必须先创建正则表达式对象。然后可以调用其他正则表达式相关的函数。比如最常见的,检查一个字符串中是否存在能由正则表达式匹配的文本。

Golang的正则表达式相关函数与其他语言中的有一个显著的区别,即它往往既提供了针对(接收参数为)string类型的函数,也提供了针对(接收参数为)byte类型的函数,相应的,返回的参数类型也分别为string或者byte​。两种函数通常成对出现,有相同的处理逻辑、类似的命名。在本书中,一方面因为篇幅所限,一方面参考网络上的相关文档和教程,主要采用接收、返回参数类型均为string的函数,如果要用到接收、返回参数类型均为byte的函数,也可以直接参考。

3.1、Compile和MustCompile

Compile和MustCompile都可以用来生成正则表达式对象,两者唯一的区别是:如果正则表达式本身存在错误,MustCompile会立刻抛出异常,所以无须考虑错误码的第二个返回值。所以,如果希望使用"创建正则表达式-调用正则表达式"的链式编程方法,MustCompile会更加方便。

除了Compile和MustCompile,Golang中还提供了CompilePOSIX和MustCompilePOSIX。它们对应的是另一台正则引擎,即POSIX ERE。考虑到实际情况,除非你有比较特殊的需求,否则一般不会用到POSIX标准的正则表达式相关特性,所以这两个函数在此不做讲解。

如果要指定匹配模式,目前只能在表达式中使用(?modifier)修饰符指定。

Golang也提供了一些静态函数,​"看起来"不用编译生成正则表达式对象。但如果你仔细阅读过之前的章节就会知道,这些函数无非是一些语法糖,使用起来比较趁手,运行速度却没有什么差别。而且,如果你需要反复用到某些正则表达式,最好还是老老实实地先编译,再使用。

3.2、MatchString

这个函数可以检验字符串中是否存在能由正则表达式匹配的文本。要注意的是,它检验的是"字符串中(的子串)能否由表达式匹配"​,所以如果你要用它来进行验证,最好在首尾加上匹配字符串起始和结束位置的锚点\A和\z。我们可以先调用Compile和MustCompile创建正则表达式对象,也可以使用静态函数直接检验。要注意的是,如果用静态函数检验,必须同时接收返回的两个参数,第一个参数是bool类型,表示能否找到。

3.4、FindAllString

这个函数可以视作上一个函数的完整版本,它会返回一个string数组,包含了目标字符串中能由正则表达式匹配的所有文本。调用这个函数时,还需要传入一个int参数,它指定返回数组的大小:如果n>0,则返回的数组最多包含n个元素(更多匹配结果会被忽略)​;如果n=0,则返回空数组;如果n<0,则返回所有的匹配。

3.5、FindStringIndex

如果在目标字符串里找到了正则表达式能匹配的文本,有时希望知道这段文本在目标字符串中的位置,此时可以使用这个函数。它返回一个数组,其中包含两个int,分别是匹配文本在目标字符串中的起始、结束偏移值。如果没有找到,返回的是空数组。

要补充说明的是,虽然Golang中的字符串直接支持Unicode字符,但是计算偏移值时,仍然是按照字节而不是字符来算的。如果采用UTF-8编码格式,则每个中文字符需要3字节。所以,有些时候程序的结果可能出乎意料。

3.6、FindAllStringIndex

这个函数也可以视作上一个函数的完整版本,不同的是它返回一个二维数组,其中的每个元素也是一个包含两个int的数组,分别对应每段匹配文本在目标字符串中的起始、结束偏移值。它同样需要传入一个int参数,指定返回数组的大小:如果n>0,则返回的数组最多包含n个元素(更多匹配结果会被忽略)​;如果n=0,则返回空数组;如果n<0,则返回所有的匹配。

3.7、FindStringSubmatch

如果指定的正则表达式里存在捕获分组,这个函数可以找到(目标字符串中最左边的匹配结果中)各个捕获分组匹配的文本。在返回的数组中,第0个元素为整个正则表达式匹配的文本,第1个元素为编号为1的捕获分组匹配的文本......如果没有匹配成功,则返回空数组。

如果对应编号的捕获分组匹配的是空文本,则返回数组中对应元素为空字符串。

3.8、FindAllStringSubmatch

这个函数也可以视作上一个函数的完整版本,不同的是,它返回一个二维数组,其中的每个元素可以视为"当次匹配时调用FindStringSubmatch的结果"​。它同样需要传入一个int参数,指定返回数组的大小:如果n>0,则返回的数组中最多包含n个元素(更多匹配结果会被忽略)​;如果n=0,则返回空数组;如果n<0,则返回所有的匹配。

3.9、SubexpNames

前面说过,Golang的正则表达式支持命名分组。如果用到了命名分组,多半要用到SubexpNames这个函数,因为即便正则表达式中包含命名分组,匹配时还是需要调用FindStringSubmatch或者FindAllStringSubmatch。然而此时的返回结果中并不包含命名分组的信息,所以需要手工"拼装"​,将命名分组信息和FindStringSubmatch或者FindAllStringSubmatch的返回结果对应起来。

3.10、Split

Split可以用来切分字符串,它接收两个参数,第一个是希望切分的字符串---注意,是string,没有byte的版本;第二个是int参数n,它影响切分的结果:如果n>0,返回的数组最多包含n个元素(最后一个元素对应的字符串不再做切分)​;如果n=0,则返回的是空数组;如果n<0,则做尽可能多的切分。

要注意的是,Split的结果不会自动忽略空字符串,所以如果你觉得结果有点怪异,很可能是空字符串的原因。

3.11、ReplaceAllString

这个函数是进行替换操作的,它接收两个参数,第一个是需要进行替换处理的字符串,第二个是要替换成的字符串replacement。

也可以在replacement中用 n u m 的方式指定编号为 n u m 的捕获分组对应的文本。 ! 在这里插入图片描述 ( h t t p s : / / i − b l o g . c s d n i m g . c n / d i r e c t / 776 d 7821 a 7 f 943 e a 9145 e e e f 8 d a e a 312. p n g ) 为了避免 ' num的方式指定编号为num的捕获分组对应的文本。 !在这里插入图片描述(https://i-blog.csdnimg.cn/direct/776d7821a7f943ea9145eeef8daea312.png) 为了避免` num的方式指定编号为num的捕获分组对应的文本。!在这里插入图片描述(https://i−blog.csdnimg.cn/direct/776d7821a7f943ea9145eeef8daea312.png)为了避免'的二义性,更好的做法是给$num加上花括号,变成${num}`。

如果要在replacement中使用$字符,必须写作$$

3.12、ReplaceAllLiteralString

这个函数和上面的ReplaceAllString非常类似,是进行替换操作的,它接收两个参数,第一个是需要进行替换处理的字符串,第二个是要替换成的字符串replacement。不同之处在于,replacement中没有任何字符有特殊含义,全部被当成普通文字来处理。

如果在replacement中使用 n u m 和 num和 num和$等表示法,并不会产生特别的后果。

4、常用操作示例

4.1、验证

简单验证

4.2、提取

4.3、替换

简单替换

在replacement中使用$

4.4、切分

简单切分

相关推荐
坐吃山猪1 小时前
WebFlux_学习Subscriber总结
java·开发语言·学习·webflux
白露与泡影1 小时前
Java面试题及答案整理(2026年金九银十最新版,持续更新)
java·开发语言
少控科技1 小时前
农场设备管理代码(1)
开发语言·c#
❀搜不到1 小时前
isat-sam分割导出掩码图
开发语言·python
geovindu1 小时前
java: Memento Pattern
java·开发语言·后端·备忘录模式·行为模式
曾阿伦2 小时前
Trae CN Python环境调试debug指南
开发语言·python
PPPPickup2 小时前
基础知识差缺补漏-面试版持续更新
java·开发语言
兔兔兔兔12 小时前
记录C++ 3
开发语言·c++
黄鸭部落格ShiYuYuanFang2 小时前
【分享】软考每日一练与解析-计组 8/12
java·开发语言