Go语言中的正则表达式
正则表达式在字符串的处理中占有重要的地位,Go语言中的regexp包提供了对正则表达式的支持。
正则表达式基础
正则表达式(Regular Expression)是一个特殊的字符串,它定义了一种文本模式。通过正则表达式,你可以从所有文本中匹配到满足特定模式的文本(字符串),然后可以:
- 测试字符串是否满足这种特定模式,例如:是不是IP地址、电话号码,银行卡号等
- 替换文本,将文本中部分或者所有满足这种特定模式的字符串替换、删除掉
- 提取满足这种特定模式的子字符串
元字符
元字符在正则表达式中有特殊的意义,要匹配这些元字符本身的话,需要用反斜杆转义。元字符有12个,如下所示:
1 | $()*+.?[\^{| |
]
和}
不属于元字符是因为它们是否需要被转义依赖于前面是否有[
和{
量词
以下几个字符我们称之为量词,它们表示匹配字符重复的次数。
量词 | 含义 |
---|---|
? | 重复0次或者1次 |
* | 重复0次或者多次 |
+ | 重复1次或者多次 |
{m, n} | m,n为数字,至少重复m次,最多重复n次, m和n中的一个可以不指定 |
惰性匹配和贪婪匹配
在量词后面加?
表示惰性匹配,如*?
,+?
,??
,{m,n}?
等。惰性匹配每匹配到一次就会停下来然后接着匹配表达式后面的部分,如果后面的部分匹配成功,则停止匹配。
贪婪匹配则相反,量词匹配成功后不会停下来,它会接着匹配直到失败,然后回溯之后再接着匹配表达式后面的部分。
例如下面的HTML文本,
1 | <p> |
如果用正则表达式<p>.*?</p>
来惰性匹配,它只匹配到第一个段,而如果用正则表达式<p>.*</p>
来进行贪婪匹配,它会匹配到整个文本。
匹配多个备选中的一个
1 | Mary|Jane|Sue |
匹配 Mary, 或者 Jane 或者 Sue.
零宽断言(Zero-Length Assertions) 或 环视 (lookaround)
零宽断言用来匹配某个字符串之前或者之后的文本,但匹配到的结果不包含该字符串本身。因为不包含该字符串,所以该断言匹配到的文本长度为0,所以称之为零宽断言。
零宽断言分为两种:
- 回望(lookbehind),即从匹配位置往后(左)查询。表达式为
(?<=exp)
,意思是如果文本的左边满足正则表达式exp,则匹配该文本,不包含exp本身匹配到的字符串。 - 前瞻(lookahead),即从匹配位置往前(右)查询。表达式为
(?=exp)
,意思是如果文本的右边满足正则表达式exp,则匹配该文本,不包含exp本身匹配到的字符串。
负向零宽断言(Negative lookaround)则是匹配不满足exp的情形,对应到回望和前瞻则是 (?<!=exp)
和 (?!=exp)
。例如(?<!=exp)
意思是如果文本的左边不满足正则表达式exp,则匹配该文本,同样不包含exp本身匹配不成功的字符串。
下面的例子可以说明零宽断言的作用。
如果需要匹配HTML中加粗的文本,但不包含HTML标签本身则可以用下面的正则表达式:
1 | (?<=<b>)\w+(?=</b>) |
如果用它来匹配文本 My <b>cat</b> is furry
的话,只会匹配到cat
,<b>
和</b>
不会被匹配到。
字符类
正则表达式内置了一些字符类,通过下表所列的字符类的写法可以匹配到多个属于这一类字符。
字符类 | 含义 |
---|---|
[chars] | 匹配chars中的任一字符 |
[^chars] | 匹配任一不在chars中的字符 |
[:name:] | 字符类中的所有ASCII字符,name为分类名,正则表达式支持的类名及含义如下表所示 |
[^:name:] | 不属于字符类中的所有ASCII字符 |
[[:name:]] | 字符类中的任一ASCII字符 |
[[^:name:]] | 不在字符类中的任一ASCII字符 |
. | 匹配任一字符,如果不指定s修饰符的话,不包括换行符 |
\d | 匹配任一ASCII数字,相当于[0-9] |
\D | 匹配任一非数字ASCII字符,相当于[^0-9] |
\s | 空白字符,[ \t\v\n\f\r] |
\S | 非空白字符 |
\w | 任一ASCII单词字符,相当于[0-9A-Za-z_] |
\W | 任一ASCII非单词字符,[^0-9A-Za-z_] |
\p{name} | 匹配unicode某一类中的字符 |
\P{name} | 匹配不在unicode某一类中的字符 |
分组与捕获
可以用 ()
对正则表达式进行分组,例如:
1 | \bMary|Jane|Sue\b |
表示 \bMary
,Jane
,Sue\b
中的一个,这显然不是我们想要的。这时候就需要用括号进行分组。
1 | \b(Mary|Jane|Sue)\b |
上面的正则表达式虽然达到了我们想要的结果,但是也捕获了一个分组。()中匹配到的内容会被记录到分组1里面。假如后面还有多个(),则分组名依次加1。但显然我们这里并没有用到捕获的内容,所以可以用下面的表达式告诉引擎不要捕获。
1 | \b(?:Mary|Jane|Sue)\b |
那如果我们想要使用捕获到的结果该怎么办呢?可以用 \1 来引用它。 1位分组名,如果要引用第二个分组捕获到的内容则是 \2 (Go语言不支持)
1 | \b\d\d(\d\d)-\1-\1\b |
上面的表达式可以匹配 2008-08-08
, 2009-09-09
这样的日期。
如果不想使用默认的数字捕获组名字,可以用下面的方法来给捕获组命名。
1 | \b\d\d(?P<m>\d\d)-\k<m>-\k<m>\b |
正则表达式的选项
选项 | 含义 |
---|---|
i | 大小写不敏感 |
m | 多行模式 |
s | . 可以匹配新行 |
Go语言中的regexp包
regexp中的函数
函数 | 作用 |
---|---|
regexp.Compile(exp) | 用exp编译一个正则表达式,如果成功则返回 *regexp.Regexp和nil |
regexp.MustCompile(exp) | 同上,但编译失败则会触发panic |
regexp.Match(exp, b) | 如果b []byte 满足exp,则返回true, nil |
regexp.MatchReader(exp, r) | 同上, r为 io.RuneReader类型 |
regexp.MatchString(exp, s) | 同上,s为字符串类型 |
Regexp类型(*regexp.Regexp)的方法
方法 | 作用 |
---|---|
rx.Match(b) | 如果类型为[]byte的b满足正则表达式,则返回true |
rx.MatchReader(r) | 如果类型为io.RuneReader的r满足正则表达式,则返回true |
rx.MatchString(s) | 如果字符串s满足正则表达式,则返回true |
rx.ReplaceAllString(s, sr) | 用sr替换s中匹配的部分 |
rx.FindString(s) | 返回匹配到的字符串 |
rx.FindAllString(s, n) | 返回所有匹配到的字符串 []string类型 |
regexp包的用法示例
1 | package main |