实际应用中,通过灵活组合正则表达式的各种用法,可以实现非常复杂和实用的功能,本节将简单介绍一些常见或典型的应用实例
* 注:注意先要导入包github.com/topxeq/goexamples/tools(方法是执行命令go get -v github.com/topxeq/goexamples/tools),如果不用该包,文中代码内的 t.Printfln相当于fmt.Printf加上一个“\n”。
-> 匹配中文
正则表达式中匹配中文可以根据中文字符的Unicode范围来进行匹配,中文在Unicode编码中的范围是十六进制的4E00至9FA5,在正则表达式中可以用“[\u4E00-\u9FA5]”的范围表达形式来表示。
需要特别注意的是,当使用类似“\u4E00”这种表达形式时,不可以用反引号“`”来括起正则表达式,会与“\uXXXX”转义符产生冲突而导致异常,因此只能用双引号来括起,内部如果有转义字符要双写反斜杠字符“\”,而“\u”本身因为只是在字符串中转义而不是在正则表达式中表示特殊含义,因此只需要写一个反斜杠(双写会导致运行异常)。
regexT :=regexp.MustCompile("[\\*\u4e00-\u9fa5]+")
t.Printfln("查找结果:%#v", regexT.FindAllString("有中文,Chinese,有英文,English,也有数字32.1*56.25=?",
-1))
代码 8‑23 正则表达式中匹配中文
代码8‑23的运行结果是:
查找结果:[]string{"有中文", "有英文", "也有数字", "*"}
注意代码中对“*”号使用了双写的反斜杠,而“\u”则是单个的反斜杠。
另一种匹配中文的方法是用Unicode转义符,即用类似8.2.1.5中介绍的“\p{Han}”这样的正则表达式来匹配中文字符。
匹配中文和外文混合的字符串时,一般也不需要考虑字节或rune的问题。
-> 判断是否合理身份证号
身份证号一般是由18位数字组成的,其中最后一位数字可能是字母“X”,代表数字10,如果用程序来判断任意一个字符串是否符合身份证号的规则,用正则表达式来匹配会比较方便。
s1 := "110101198008085356"
s2 := "15020320030616272X"
s3 := "356432648326483246872346"
s4 := "1101aB198008085356"
s5 := "110106248008083398"
regexT := regexp.MustCompile(`\A\d{17}[\dX]\z`)
fmt.Printf("检查结果1:%#v\n", regexT.MatchString(s1))
fmt.Printf("检查结果2:%#v\n", regexT.MatchString(s2))
fmt.Printf ("检查结果3:%#v\n", regexT.MatchString(s3))
fmt.Printf ("检查结果4:%#v\n", regexT.MatchString(s4))
fmt.Printf ("检查结果5:%#v\n", regexT.MatchString(s5))
代码 8‑24 用正则表达式判断符合规则的身份证号
代码8‑24运行的结果是:
检查结果1:true
检查结果2:true
检查结果3:false
检查结果4:false
检查结果5:true
结果为true表示是符合规则的身份证号,即字符串完全符合我们的正则表达式“\A\d{17}[\dX]\z”,该正则表达式中用到了位置指示符“\A”和“\z”来保证整个字符串完整匹配,然后指明有连续17个数字(用“\d{17}”表示),之后再接一个可以是数字或字母“X”的字符(用[\dX])表示。可以看出,该程序的判断基本是准确的,对于合理的身份证号都判断对了,对于超出长度或者前面数字中含有字母的情况都能分辨出来,唯一有问题的是最后一个身份证号s5,其中代表年份的第7、8位明显不合逻辑,年份应该是“19”或“20”才是合理的,因此我们可以修改代码为:
s1 := "110101198008085356"
s2 := "15020320030616272X"
s3 := "356432648326483246872346"
s4 := "1101aB198008085356"
s5 := "110106248008083398"
regexT :=regexp.MustCompile(`\A\d{6}(19|20)\d\d[01]\d[0123]\d\d{3}[\dX]\z`)
fmt.Printf("检查结果1:%#v\n", regexT.MatchString(s1))
fmt.Printf("检查结果2:%#v\n", regexT.MatchString(s2))
fmt.Printf("检查结果3:%#v\n", regexT.MatchString(s3))
fmt.Printf("检查结果4:%#v\n", regexT.MatchString(s4))
fmt.Printf("检查结果5:%#v\n", regexT.MatchString(s5))
代码 8‑25 改进后的身份证号判断正则表达式
代码8‑25中主要对用于身份证号判断的正则表达式做了修改,以便能够正确判断身份证号中第7位开始的出生日期格式。该正则表达式为:“\A\d{6}(19|20)\d\d[01]\d[0123]\d\d{3}[\dX]\z”,明显复杂了一些,主要区别在于第7位开始表示出生日期的8位,用了“(19|20)\d\d[01]\d[0123]\d”这样的方式表达。其中,一开始圆括号内是可选的“19”或“20“,表示年份只能以“19”或 “20”开头;之后紧跟任意两个数字代表年份的后两位;然后月份的第一位只能是“0”或“1”,因此用“[01]”这样的可选字符来表示,月份的第二位则是任意的数字;日子的头一位也是只能在0至3中选择,而最后一位则是任意数字。这样,整个日期的判断就更加合理一些,代码8‑25的运行结果是:
检查结果1:true
检查结果2:true
检查结果3:false
检查结果4:false
检查结果5:false
这样,该代码已经能够正确判断出最后s5是不合理的身份证号。如果我们想的更细一些,这个正则表达式还可以改进,例如月份的头一位是“1”的时候,后面的数字应该只能是“0”、“1”或“2”,这种情况可以用在正则表达式中用“(0[1-9]|1[0-2])”来判断月份的部分,其含义是:月份数字的组成有两种选择,第一种是以数字“0”开始,后面可以跟数字“1”至“9”中任意的一个(不可以跟数字“0”,因为不是合理的月份号),第二种是以数字“1”开始,后面只能跟数字“0”至“2”中任意的一个。下面的代码是对月份数字合理性的判断,为了避免其他代码过多引起干扰,仅对月份的两位数字来进行判断。
s1 := "01"
s2 := "09"
s3 := "10"
s4 := "11"
s5 := "12"
s6 := "16"
regexT := regexp.MustCompile(`(0[1-9]|1[0-2])`)
t.Printfln("检查结果1:%#v", regexT.MatchString(s1))
t.Printfln("检查结果2:%#v", regexT.MatchString(s2))
t.Printfln("检查结果3:%#v", regexT.MatchString(s3))
t.Printfln("检查结果4:%#v", regexT.MatchString(s4))
t.Printfln("检查结果5:%#v", regexT.MatchString(s5))
t.Printfln("检查结果6:%#v", regexT.MatchString(s6))
代码 8‑26 判断月份数字的合理性
代码8‑26的运行结果是:
检查结果1:true
检查结果2:true
检查结果3:true
检查结果4:true
检查结果5:true
检查结果6:false
推而广之,我们还可以对日子的合理性甚至是行政区划编码的合理性都做出进一步的细化,给出更加完善的正则表达式来更准确地判断整个身份证号的合理性,由于方法是类似的,在此我们就不进一步展开了。
-> 从大量文本中提取手机号和邮件地址
如同身份证号判断一样,我们也可以通过正则表达式来判断出合理的手机号和邮件地址,在此基础上我们可以进一步从大量文本信息(也就是一个大字符串)中设法提取出所有的手机号和邮件地址。
package main
import (
"regexp"
t "github.com/topxeq/goexamples/tools"
)
func main() {
s := `员工信息
姓名:张三
性别:男
手机:15918015888
邮箱:zhangsan@mycom.com
姓名:李四
性别:女
手机:13125937866
邮箱:lisi@study.org
`
regexT := regexp.MustCompile(`\b1\d{10}\b`)
t.Printfln("查找结果:%#v", regexT.FindAllString(s, -1))
regexT = regexp.MustCompile(`\b[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+\b`)
t.Printfln("查找结果:%#v", regexT.FindAllString(s, -1))
regexT = regexp.MustCompile(`\b(1\d{10}|[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+)\b`)
t.Printfln("查找结果:%#v", regexT.FindAllString(s, -1))
}
代码 8‑27 用正则表达式提取手机号与邮件地址
代码8‑27中,首先需要说明的是,字符串s用了反引号“`”来括起,这样会把回车符(代码内的,不是指“\n”转义符)也原样算做字符串内的正常字符。用这种方法我们可以方便地在程序中放入多行的文本字符串。
代码8‑27中第一个正则表达式“\b1\d{10}\b”用于提取手机号,用了比较简单的规则,首先用前后两个“\b”来截取单词边界,保证取到完整的无空格的子串,然后按手机号由11位数字组成并且第一位一定是1来判断。
第二个正则表达式“\b[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+\b”用于判断提取邮件地址,虽然看起来貌似复杂一些,但其实分解开来看也不难:邮件地址一般形如“zhangsan@mycom.com”,其中除了“@”、“.”字符外,一般只允许有大小写字母、数字和下划线“_”、连接符“-”等,因此我们用“[a-zA-Z0-9_-]”这种可选字符范围的方式来表示每个字符;而“@”后面的域名可以是多段的,例如“zhangsan@mycom.com.cn”,所以用“(\.[a-zA-Z0-9_-]+)+”来表示至少一个以“.”开始紧跟1个以上其他字符的子串。
前两个正则表达式用于分别从字符串s中提取手机号或邮件地址,那么如何同时从字符串中提取手机号和邮件地址呢?第三个正则表达式索性将前两个正则表达式用可选项的形式写在一起,这样就能一次性提取出所有的手机号和邮件地址了。
代码8‑27的运行结果是:
查找结果:[]string{"15918015888", "13125937866"}
查找结果:[]string{"zhangsan@mycom.com", "lisi@study.org"}
查找结果:[]string{"15918015888", "zhangsan@mycom.com", "13125937866", "lisi@study.org"}
* 注:大家有兴趣可以用Regexp.FindStringSubmatch函数来看看提取邮件地址的正则表达式和第三个大正则表达式中的圆括号各自是否是捕获组。
-> 批量文件名后缀的替换
用正则表达式也可以方便地批量为文件改名,例如:假设我们有一批视频文件和非视频文件,我们希望将所有的视频文件后缀(也常称作扩展名)都改为“.avi”,那么可以用类似下面的代码来实现。
package
main
import (
"regexp"
t "github.com/topxeq/goexamples/tools"
)
func main() {
files := []string{"video.mp4","last_day.mov", "popular.mpg", "2358.wmv","picture.jpg"}
regexT :=regexp.MustCompile(`(\w+\.)(mp4|mov|mpg|wmv)`)
renamedFiles := make([]string, 0)
for _, v := range files {
renamedFiles = append(renamedFiles,regexT.ReplaceAllString(v, "${1}avi"))
}
t.Printfln("改名结果:%#v", renamedFiles)
}
代码 8‑28 用正则表达式给视频文件改扩展名
代码8‑28中,在字符串切片变量files中定义了5个文件名,其中前4个都是不同格式不同后缀的视频文件,第5个则是一个jpg格式的图片文件。我们定义的正则表达式regexT“(\w+\.)(mp4|mov|mpg|wmv)”则用可选项的方式来将这几种可能出现的视频文件后缀都包括进去,但不包括其他的任何后缀,因此最后一个文件名将不会被匹配到。之后的代码用了一个循环来遍历整个files中的文件名,并一一进行正则表达式替换,符合规则的将把后缀统一替换为“avi”,因为我们的正则表达式中自己用圆括号括出了一个捕获组,包括文件名中除了后缀的部分(包括后缀前的“.”号),而替换时用“${1}”表示替换为第一个捕获组,再加上固定的“avi”就达到了将后缀统一替换为“avi”的效果。代码8‑28的运行结果是:
改名结果:[]string{"video.avi", "last_day.avi", "popular.avi", "2358.avi", "picture.jpg"}
-> 批量将文件名编号
8.3.4节中的例子是批量修改文件名中的后缀部分,如果想保留文件后缀而将前面的文件名部分改成顺序编号的形式,可以用类似下面的代码。
package main
import (
"regexp"
t "github.com/topxeq/goexamples/tools"
)
func main() {
files := []string{"video.mp4","last_day.mov", "popular.mpg", "2358.wmv","picture.jpg"}
regexT := regexp.MustCompile(`\A\w+\.(mp4|mov|mpg|wmv)\z`)
renamedFiles := make([]string, 0)
countT := 1
for _, v := range files {
if regexT.MatchString(v) {
renamedFiles = append(renamedFiles,regexT.ReplaceAllString(v, t.IntToString(countT)+".${1}"))
countT++
}
}
t.Printfln("改名结果:%#v", renamedFiles)
}
代码 8‑29 批量为文件编号
代码8‑29中,正则表达式略做了修改,仅有一个捕获组,也就是括起文件名后缀可选项内的部分,在循环遍历文件名时,增加了一个计数器变量countT用于表示第几次成功匹配到符合规则的文件名,同时也用作文件的编号。在替换文件名时,使用了我们自己定义的int转为字符串的函数tools.IntToString。代码8‑29的运行结果是:
改名结果:[]string{"1.mp4", "2.mov", "3.mpg", "4.wmv"}
可以看到,符合规则的前4个文件名被依次编号并保留了后缀。
-> Go语言正则表达式的特点
Go语言使用的正则表达式语法与Perl, Python和一些其他计算机语言大体相同,更准确地说是符合开源的正则表达式库RE2的标准(除不常用的字符类“\C”外)。我们可以用命令go doc regexp/syntax 来在命令行查看Go语言支持的语法参考,当然,查看网页形式的参考文档更方便。
Go语言中使用的正则表达式引擎,据官方说明可以保证其运行的速度与所查找的字符串长度成线性增长关系(正比),也就是说不会因为字符串的增长而速度越下降越快,这一点据说是很多其他引擎无法保证的,并且这一点据说可以一定程度上防御网站遭受到的ReDOS(正则表达式拒绝服务)攻击。当然,为了达到这一点,以及为了维持Go语言极简的风格理念,Go语言中正则表达式引擎能够支持的功能与某些引擎相比稍弱一些,例如用于在正则表达式匹配时排除某些字符串的负向先行断言(negative look behind assertion)和负向后行断言(negative look ahead assertion)等复杂功能在Go语言中不被支持。
一般来说,Go语言支持的正则表达式语法已经足够使用,如果仍希望使用一些复杂功能,可以选用第三方包来实现,例如dlclark/regexp2和glenn-brown/golang-pkg-pcre等都是可选的方案,在Github网站上搜索即可。
有疑问加站长微信联系(非本文作者)