Fork me on GitHub

Go语言开发-字符串-regexp包

3.6.5. regexp包

本小节给出了一些表,这些表列出了regexp包里的函数和支持的正则表达式语法,同时也给出了几个例子。在本小节或本书的其他地方也会使用到正则表达式,我们假设读者已经具有了一些正则表达式基础。

regexp包是Russ Cox的RE2正则表达式引擎的Go语言实现,该引擎速度非常快且是线程安全的。RE2引擎并不使用回溯法,所以能够保证线性的执行时间O(n),其中n是匹配的字符串的长度,而回溯引擎可以很容易地达到指数执行时间 O(2^n)(参阅大O表示法)。具有如此优秀的性能的代价是在搜索时不支持反向引用,但是,通常我们可以通过正确的使用正则表达式API来直接绕过这个限制。

表3.12列出了regexp包中的函数,包括四个用于创建*regexp.Regexp的函数,*regexp.Regexp提供的方法在表3.18和表3.19中给出。RE2引擎语法支持表3.13列出的转义、表3.14列出的字符类、表3.15列出的零宽断言、表3.16列出的数量限定符和表3.17列出的标识。

regexp.Regexp.ReplaceAll() 和regexp.Regexp.ReplaceAllString()方法都支持按编号或名字进行替换。对于第一个括号括起来的匹配,按编号替换从$1开始,按名字替换则按照已命名的捕获组。虽然可以按照数字或名字(如$2或$filename)引用来进行替换,但是使用括号作为分隔符(如${2},或${filename})是最安全的方式。如果要替换的字符串中含有$字符常量,则要使用$$来替换。

表3.12 regexp包中的函数

变量p和s都是string类型,p是正则表达式模式

regexp.Match(p, b) 如果p匹配[]byte类型的b,则返回true和nil
regexp.MatchReader(p, r) 如果p匹配从io.RuneReader类型的r中读取的数据,则返回true和nil
regexp.MatchString(p, s) 如果p匹配s,则返回true和nil
regexp.QuoteMeta(s) 返回与引号引起来的正则表达式元字符匹配的字符串
regexp.Compile(p) 如果p编译成功,则返回*regexp.Regexp和nil,参见表3.18和表3.19
regexp.CompilePOSIX(p) 如果p编译成功,则返回*regexp.Regexp和nil,参见表3.18和表3.19
regexp.MustCompile(p) 如果p编译成功,则返回*regexp.Regexp,否则抛出异常,参见表3.18和表3.19
regexp.MustCompilePOSIX(p) 如果p编译成功,则返回*regexp.Regexp,否则抛出异常,参见表3.18和表3.19

表3.13 regexp包中的转义字符

\c 字符常量c,如\*是一个常量而不是一个修饰符
\000 八进制码点表示的字符
\xHH 两位十六进制码点表示的字符
\x{HHHH} 1至6位十六进制码点表示的字符
\a ASCII 响铃符(BEL),等于\007
\f ASCII 换页符(FF),等于\014
\n ASCII 换行符(LF),等于\012
\r ASCII 回车符 (CR),等于\015
\t ASCII 制表符(TAB),等于\011
\v ASCII 垂直制表符 (VT),等于\013
\Q…\E 匹配…中的文本常量,即使其含有字符*

表3.14 regexp包中的字符类

[chars] chars中的任何字符
[^chars] 任何不在chars中的字符
[:name:] name字符类中的任何ASCII字符

[[:alnum:]] ≡ [0-9A-Za-z]

[[:lower:]] ≡ [a-z]

[[:alpha:]] ≡ [A-Za-z]

[[:print:]] ≡ [ -~]

[[:ascii:]] ≡ [\x00-\x7F]

[[:punct:]] ≡ [!-/:-@[-`{-~]

[[:blank:]] ≡ [ \t]

[[:space:]] ≡ [ \t\n\v\f\r]

[[:cntrl:]] ≡ [\x00-\x1F\x7F]

[[:upper:]] ≡ [A-Z]

[[:digit:]] ≡ [0-9]

[[:word:]] ≡ [0-9A-Za-z_]

[[:graph:]] ≡ [!-~]

[[:xdigit:]] ≡ [0-9A-Fa-z]

[:^name:] 任何不在name字符类中的ASCII字符
. 任何字符(如果设置了标识s,则包括换行符)
\d 任何ASCII数字:[0-9]
\D 任何非数字的ASCII字符:[^0-9]
\s 任何ASCII空白字符:[ \t\n\f\r]
\S 任何非空白字符的ASCII字符:[^ \t\n\f\r]
\w 任何ASCII “单词” 字符:[0-9A-Za-z_]
\W 任何非“单词”的ASCII字符:[^0-9A-Za-z_]
\pN 只有一个字母的字符类N中的任何Unicode字符,例如,\pL匹配一个Unicode字母
\PN 任何不在只有一个字母的字符类N中的Unicode字符,例如,\pL匹配一个非字母的Unicode字符
\p{Name} 字符类Name中的任何Unicode字符,例如,\p{Ll}匹配小写字母,\p{Lu}匹配大写字母,\p{Greek}匹配希腊字符
\P{Name} 任何不在字符类Name中的Unicode字符

表3.15 regexp包中的零宽断言

^ 文本的开头(如果设置了标识m,则表示行的开头)
$ 文本的末尾(如果设置了标识m,则表示行的末尾)
\A 文本的开头
\z 文本的末尾
\b 单词边界(\w和\W之间的字符,或\A和\z之间的字符,反之亦然)
\B 非单词边界

表3.16 regexp包中的数量限定符

e? or e{0,1} 贪婪匹配,表达式e出现0次或1次
e+ or e{1,} 贪婪匹配,表达式e出现1次或多次
e* or e{0,} 贪婪匹配,表达式e出现0次或多次
e{m,} 贪婪匹配,表达式e至少出现m次
e{,n} 贪婪匹配,表达式e最多出现n次
e{m,n} 贪婪匹配,表达式e至少出现m次且最多出现n次
e{m} or e{m}? 完全匹配,表达式e出现m次
e?? or e{0,1}? 非贪婪匹配,表达式e出现0次或1次
e+? or e{1,}? 非贪婪匹配,表达式e出现1次或多次
e*? or e{0,}? 非贪婪匹配,表达式e出现0次或多次
e{m,}? 非贪婪匹配,表达式e至少出现m次
e{,n}? 非贪婪匹配,表达式e最多出现n次
e{m,n}? 非贪婪匹配,表达式e至少出现m次且最多出现n次

表3.17 regexp包中的标识和分组

i 大小写不敏感的匹配(默认大小写敏感)
m 多行模式,^和$匹配每一行的开头和末尾
s 使.能够匹配任何字符包括换行符(默认不匹配换行符)
U 使贪婪匹配匹配非贪婪模式,反之亦然;即反转数量限定符后?的含义
(?flags) 从这一点开始使用给定的flags
(?flags:e) 将给定的flags应用于表达式e
(e) 表达式e的组和捕获匹配
(?P<name>e) 使用名字为name的捕获的表达式e的组和捕获匹配
(?:e) 表达式e的组但不包括捕获匹配

表3.18 *regexp.Regexp类型的方法 #1

变量rx是*regexp.Regexp类型;s是要匹配的字符串;b是要匹配的[]byte切片;r是要匹配的io.RuneReader;n是要匹配的最大数量(-1表示匹配所有)。返回nil表示没有匹配

rx.Expand(…) 通过ReplaceAll()方法执行$替换,很少直接使用(高级用法)
rx.ExpandString(…) 通过ReplaceAllString()方法执行$替换,很少直接使用(高级用法)
rx.Find(b) 最左匹配,返回一个[]byte切片或nil
rx.FindAll(b, n) 所有非重叠匹配,返回一个[][]byte切片或nil
rx.FindAllIndex(b, n) 标识匹配,返回一个[][]int切片(二维切片)或nil;例如b[pos[0]:pos[1]] ,其中pos是二维切片的其中一维
rx.FindAllString(s, n) 所有非重叠匹配,返回一个[]string切片或nil
rx.FindAllStringIndex(s, n) 标识匹配,返回一个[][]int切片(二维切片)或nil;例如s[pos[0]:pos[1]] ,其中pos是二维切片的其中一维
rx.FindAllStringSubmatch(s, n) 返回一个[][]string切片(字符串切片的切片,每个元素对应一个捕获)或nil
rx.FindAllStringSubmatchIndex(s, n) 返回一个[][]int切片(二维切片,每个元素对应一个捕获)或nil
rx.FindAllSubmatch(b, n) 返回一个[][][]byte切片(三维切片,每个[]byte切片对应一个捕获)或nil
rx.FindAllSubmatchIndex(b, n) 返回一个[][]int切片(包含两项的int切片的切片,每个元素对应一个捕获)或nil
rx.FindIndex(b) 最左匹配,返回一个包含两项的切片或nil;例如b[pos[0]:pos[1]],其中pos是一个包含两项的切片
rx.FindReaderIndex(r) 最左匹配,返回一个包含两项的切片或nil
rx.FindReaderSubmatchIndex(r) 最左匹配和捕获,返回一个[]string切片或nil
rx.FindString(s) 最左匹配或空字符串
rx.FindStringIndex(s) 最左匹配,返回一个两项的[]int切片或nil
rx.FindStringSubmatch(s) 最左匹配和捕获,返回一个[]string切片或nil
rx.FindStringSubmatchIndex(s) 最左匹配和捕获,返回一个[]int切片或nil

表3.19  *regexp.Regexp类型的方法 #2

变量rx是*regexp.Regexp类型;s是要匹配的字符串;b是要匹配的[]byte切片

rx.FindSubmatch(b) 最左匹配和捕获,返回一个[][]byte切片或nil
rx.FindSubmatchIndex(b) 最左匹配和捕获,返回一个[][]byte切片或nil
rx.LiteralPrefix() 返回一个可能为空的string类型的前缀和一个指明整个个表达式是否与该前缀匹配的布尔值
rx.Match(b) 如果正则表达式匹配b,则返回true
rx.MatchReader(r) 如果正则表达式匹配io.RuneReader类型的r,则返回true
rx.MatchString(s) 如果正则表达式匹配s,则返回true
rx.NumSubexp() 返回正则表达式有多少圆括号括起来的组
rx.ReplaceAll(b, br) 返回一个[]byte切片,其中每一个匹配都使用[]byte类型的br进行$替换
rx.ReplaceAllFunc(b, f) 返回一个[]byte切片,其中每一个匹配都使用func([]byte) []byte类型的函数f的返回值进行替换,函数f的参数是一个匹配项
rx.ReplaceAllLiteral(b, br) 返回一个[]byte切片,其中每一个匹配都使用[]byte类型的br进行替换
rx.ReplaceAllLiteralString(s, sr) 返回一个字符串,其中每一个匹配都使用[string类型的sr进行替换
rx.ReplaceAllString(s, sr) 返回一个字符串,其中每一个匹配都使用string类型的sr进行$替换
rx.ReplaceAllStringFunc(s, f) 返回一个字符串,其中每一个匹配都使用func(string) string类型的函数f的返回值进行替换,函数f的参数是一个匹配项
rx.String() 返回一个包含正则表达式模式的字符串
rx.SubexpNames() 返回一个[]string切片(不可修改),其包含所有已命名的子表达式的名字

一个典型的涉及到使用替换的例子是,我们有一个具有forename1…forenameN surname形式的名字列表,我们希望将其转换成surname, forename1 … forenameN的形式。现在我们将介绍如何使用regexp包来实现这个功能,并正确处理重音符号和其他的非英文字符。

变量names是一个[]string类型的切片,最初保存了原始名字列表。一旦循环结束,names变量保存的是修改后的名字列表。

这个正则表达式匹配一个或多个用空白符分隔的名字,每个名字由一个或者多个Unicode字母组成,名字后面可以有一个句点,然后是空白符和由一个或多个Unicode字符组成的姓。

使用编号来替换可能将导致维护问题,例如,如果我们在中间插入一个新的捕获组,则至少有一个编号是错的,解决方案就是使用按名字替换的方式,这是因为它们都不依赖于特定的顺序。

这里我们给两个捕获组取了有意义的名字,这有助于我们更容易的理解正则表达式和替换字符串。

一个简单的匹配重复”单词”的正则表达式依赖于反向引用,对于Python 或 Perl,可以写作\b(\w+)\s+\1\b。但是regexp包并不支持反向引用,所以我们必须结合regexp并加入几行代码来达到同样的效果。

这个正则表达式贪婪匹配一个或者多个“单词”字符,regexp.Regexp.FindAllString()函数返回所有不重叠的匹配项,并存储到[]string切片中。如果至少存在一个匹配项(即matches不为nil),我们就遍历这个字符串切片并比较当前匹配的单词和前一个单词,最后打印出所有重复的单词。

另一个常用的正则表达式是用来匹配配置文件中的“键:值”行。下面的例子就是基于匹配这样的行并将其填充到map中。

这个正则表达式的作用是跳过所有前导空白符,并匹配一个必须以英文字母开头后面跟着0个或多个字母、数字、下划线的键,接着是一个可选的空白,一个冒号,一个可选的空白,最后是值,其中值可以是任何字符但不包括换行符和字符串结束符。顺便提一下,我们可以使用稍短一点的[A-Za-z] 而不是[[:alpha:]],或者如果我们希望支持Unicode编码的键的话,我们可以使用(\pL[\pL\p{Nd}_]*),其含义是Unicode字母后面跟着0个或多个Unicode字母、十进制数字或下划线。因为.+表达式不会匹配换行符,所以该正则表达式可以作用于包含多个“键:值”行的字符串。

归功于贪婪匹配(默认)的使用,正则表达式可以去除值开头处的任何空白符。但是为了去除值结尾处的空白符,我们必须要使用trim函数,因为.+表达式的贪婪模式意味着在其后使用\s*是无效的。但我们也不能使用非贪婪匹配(例如.+?),因为这样只会匹配包含两个或多个空格分隔的单词的值的第一个单词。

通过使用regexp.Regexp.FindAllStringSubmatch()函数我们可以得到一个字符串切片的切片(或nil);-1表示尽可能多的匹配(没有重叠)。在本例中,每个匹配都将产生包含3个字符串的切片,第一个字符串包含整个匹配,第二个字符串包含键,第三个字符串包含值。键和值都至少有一个字符,这是因为它们的最小数量限定为1。

虽然使用Go语言的xml.Decoder来解析XML是最好的方法,但是有时候我们可能只是简单地想得到XML格式的文件里的属性值,形如name=”value” 或name=’value’。但是在这种情况下,一个简单的正则表达式就足够了。

attrValueRx正则表达式匹配一个己经被安全转义了的属性名且其后跟着一个等号和一个单引号或双引号引起来的字符串。用于替换(|)的圆括号也会捕获该表达式,但在这种情况下,我们不希望这样做,因为我们不希望捕引号,所以我们使圆括号处于非捕获组((?:))。为了演示它是如何做到的,我们通过检索索引位置而不是检索实际匹配的字符串。在本例中,有3对([start:end])索引,第一对用于整个匹配,第二对用于双引号引起来的值,第三对用于单引号引起来的值。当然,实际上只有一个值会被匹配,其他两个值都是-1。

就像之前的例子中那样,我们要求匹配字符串中每个非重叠的匹配,这样我们就可以得到一个[][]int类型的索引位置(或nil)。对于切片中每个索引位置切片,整个匹配则是切片attribs[positions[0]:positions[1]]。引号引起来的字符串attribs[positions[2]:positions[3]] 或attribs[positions[4]:positions[5]],这取决于使用的引号类型。上面这段代码中假设使用的是双引号,但如果不是(即start==-1),那它就会使用单引号。

之前我们介绍了如何编写SimplifyWhitespace()函数,下面我们将演示如何使用正则表达式和strings.TrimSpace()函数来完成同样的功能。

这个正则表达式作用于传递的字符串,strings.TrimSpace()函数仅可以处理字符串结尾处的空白符,所以这两者的结合并没有做太多的工作。regexp.Regexp.ReplaceAllLiteralString()函数接受一个要处理字符串和一个用于替换的文本,并完成替换。(regexp.Regexp.ReplaceAllString() 和regexp.Regexp. ReplaceAllLiteralString()函数之间的不同之处在于前者使用$替换,而后者不是。)所以,在这种情况下,任何一个或多个连续空格符(ASCII空白符和Unicode行及段落分隔符)都会被单个空格替换掉。

对于最后一个正则表达式,我们将会介绍如何使用函数来执行替换操作。

这个正则表达式只是简单的匹配一个或多个重音Latin-1字母。regexp.Regexp.ReplaceAllStringFunc()函数会在每次有匹配的时候调用作为第二个参数(具有func(string)string)签名的)传入的函数,该函数接受给定的已匹配的文本作为其参数,该文本会被函数返回的文本(可以是空字符串)替换掉。

作为其第二个参数 (与签名 func(string)string) 每一次比赛。函数作为其参数匹配的文本,这个文本替换的文本,该函数返回 (它可以是空字符串)。

这个简单的函数将每个重音Latin-1字符替换为其非重音字符,同时它还会将æ(这是某些语言中是全字符)替换为字符a和e。当然,这个例子并不会在实际中出现,因为这里我们完全可以简单的使用unaccented := UnaccentedLatin1(latin1)来执行转换。

现在我们完成了对正则表达式的介绍,注意在表3.18和表3.19中,对于每个“字符串”正则表达式函数,都存在相应的处理对象为[]byte切片的函数。此外,本书中的一些其它例子中也用到了regexp包。

现在我们已经介绍完了Go语言的strings包和其它字符串相关的包,接下来我们将演示一个使用Go语言字符串函数的例子来完美的结束本章,和往常一样,后面会有一些练习。


目录


作者:Johnson
原创文章,版权所有,转载请保留原文链接。

发表评论

电子邮件地址不会被公开。 必填项已用*标注