首页 > 其他分享 >golang标准库---regexp — 正则表达式

golang标准库---regexp — 正则表达式

时间:2022-12-18 11:32:33浏览次数:70  
标签:--- 匹配 err fmt golang Printf Compile regexp

基础知识

简单匹配

你想知道一个字符串和一个正则表达式是否匹配。如果字符串参数与用 Compile 函数编译好的正则匹配的话,MatchString 函数就会返回 'true'.

package main

import (
"fmt"
"regexp"
)

func main() {
r, err := regexp.Compile(`Hello`)

if err != nil {
fmt.Printf("There is a problem with your regexp.\n")
return
}

// Will print 'Match'
if r.MatchString("Hello Regular Expression.") == true {
fmt.Printf("Match ")
} else {
fmt.Printf("No match ")
}
}

Compile 函数是 regexp 包的核心所在。 每一个正则必由 Compile 或其姊妹函数 MustCompile 编译后方可使用。MustCompile 除了正则在不能正确被编译时会抛出异常外,使用方法和 Compile 几乎相同。因为 MustCompile 的任何错误都会导致一个异常,所以它无需返回表示错误码的第二个返回值。这就使得把 MustCompile 和匹配函数链在一起调用更加容易。像下面这样: (但考虑性能因素,要避免在一个循环里重复编译正则表达式的用法)

package main

import (
"fmt"
"regexp"
)

func main() {
if regexp.MustCompile(`Hello`).MatchString("Hello Regular Expression.") == true {
fmt.Printf("Match ") // 会再次打印 'Match'
} else {
fmt.Printf("No match ")
}
}

这句不合法的正则

var myre = regexp.MustCompile(`\d(+`)

会导致错误:

panic: regexp: Compile(`\d(+`): error parsing regexp: missing argument to repetition operator: `+`

goroutine 1 [running]:
regexp.MustCompile(0x4de620, 0x4, 0x4148e8)
go/src/pkg/regexp/regexp.go:207 +0x13f

Compile 函数的第二个参数会返回一个错误值。 在本教程中我通常都忽略这第二个参数,因为我写的所有正则都棒棒哒 ;-)。如果你写的正则也是字面量当然也可能没有问题,但是如果是在运行时从输入获取的值作为正则表达式,那你最好还是检查一下返回的这个错误值。

本教程接下来为了简洁会略过所有错误返回值的检查。

下面这个正则会匹配失败:

r, err := regexp.Compile(`Hxllo`)
// Will print 'false'
fmt.Printf("%v", r.MatchString("Hello Regular Expression."))

CompilePOSIX/MustCompilePOSIX

CompilePOSIX 和 MustCompilePOSIX 方法运行着的是一个略为不同的引擎。这两个里面采用的是 POSIX ERE (extended regular expression) 引擎。从 Go 语言的视角看它们采用了严格的规则集合,也就是 egrep 所支持的标准。因此 Go 的标准 re2 引擎支持的某些细节在 POSIX 版本中是没有的,比如 \A.

s := "ABCDEEEEE"
rr := regexp.MustCompile(`\AABCDE{2}|ABCDE{4}`)
rp := regexp.MustCompilePOSIX(`\AABCDE{2}|ABCDE{4}`)
fmt.Println(rr.FindAllString(s, 2))
fmt.Println(rp.FindAllString(s, 2))

这里只有 MustCompilePOSIX 函数会解析失败,因为 POSIX ERE 中不支持 \A

还有,POSIX 引擎更趋向最左最长(leftmost-longest)的匹配。在初次匹配到时并不会返回,而是会检查匹配到的是不是最长的匹配。 比如:

s := "ABCDEEEEE"
rr := regexp.MustCompile(`ABCDE{2}|ABCDE{4}`)
rp := regexp.MustCompilePOSIX(`ABCDE{2}|ABCDE{4}`)
fmt.Println(rr.FindAllString(s, 2))
fmt.Println(rp.FindAllString(s, 2))

将打印:

[ABCDEE]    <- 第一个可接受的匹配
[ABCDEEEE] <- 但是 POSIX 想要更长的匹配

只有当你有一些特殊需求时,POSIX 函数也许才会是你的不二之选。

字符分类

字符类别 '\w' 代表所有 [A-Za-z0-9_] 包含在内的字符。 助记法:'word'。

r, err := regexp.Compile(`H\wllo`)
// Will print 'true'.
fmt.Printf("%v", r.MatchString("Hello Regular Expression."))

字符类别 '\d' 代表所有数字字符。

r, err := regexp.Compile(`\d`)
// Will print 'true':
fmt.Printf("%v", r.MatchString("Seven times seven is 49."))
// Will print 'false':
fmt.Printf("%v", r.MatchString("Seven times seven is forty-nine."))

字符类别 '\s' 代表以下任何空白:TAB, SPACE, CR, LF。或者更确切的说是 [\t\n\f\r ]。

r, err := regexp.Compile(`\s`)
// Will print 'true':
fmt.Printf("%v", r.MatchString("/home/bill/My Documents"))

使用字符类别表示方法的大写形式表示相反的类别。所以 '\D' 代表任何不属于 '\d' 类别的字符。

r, err := regexp.Compile(`\S`) // Not a whitespace
// Will print 'true', obviously there are non-whitespaces here:
fmt.Printf("%v", r.MatchString("/home/bill/My Documents"))

检查一个字符串是不是包含单词字符以外的字符:

r, err := regexp.Compile(`\W`) // Not a \w character.

fmt.Printf("%v", r.MatchString("555-shoe")) // true: has a non-word char: The hyphen
fmt.Printf("%v", r.MatchString("555shoe")) // false: has no non-word char.

匹配的内容中有什么?

FindString 函数会查找一个字符串。当你使用一个字面量的字符串作为正则时,结果自然就是该字符串本身。只有当你使用模式以及分类时,结果才会更加有趣。

r, err := regexp.Compile(`Hello`)
// 会打印 'Hello'
fmt.Printf(r.FindString("Hello Regular Expression. Hullo again."))

当 FindString 找不到和正则表达式匹配的字符串时,它会返回空白字符串。要知道空白字符串也算是一次有效匹配的结果。

r, err := regexp.Compile(`Hxllo`)
// 什么都不打印 (也就是空字符串)
fmt.Printf(r.FindString("Hello Regular Expression."))

FindString 会在首次匹配后即返回。如果你想尽可能多地匹配你就需要 FindAllString() 函数,这个后面会讲到。

特殊字符

句点 '.' 匹配任意字符。

// 会打印出 'cat'
r, err := regexp.Compile(`.at`)
fmt.Printf(r.FindString("The cat sat on the mat."))

'cat' 是第一个匹配。

// 更多的点号
s:= "Nobody expects the Spanish inquisition."
// -- -- --
r, err := regexp.Compile(`e.`)
res := r.FindAllString(s, -1) // negative: all matches
// 打印 [ex ec e ]。最后一个元素是 'e' 和一个空白字符
fmt.Printf("%v", res)
res = r.FindAllString(s, 2) // find 2 or less matches
// 打印 [ex ec]
fmt.Printf("%v", res)

特殊字符的字面量

查找 '':在字符串里 '' 需要跳脱一次,而在正则里就要跳脱两次。

r, err := regexp.Compile(`C:\\\\`)
if r.MatchString("Working on drive C:\\") == true {
fmt.Printf("Matches.") // <---
} else {
fmt.Printf("No match.")
}

查找一个字面量的句点:

r, err := regexp.Compile(`\.`)
if r.MatchString("Short.") == true {
fmt.Printf("Has a dot.") // <---
} else {
fmt.Printf("Has no dot.")
}

其它用来组成正则表达式的特殊字符也基本这样用: .+*?()|[]{}^$

如查找一个字面量的美元符号:

r, err := regexp.Compile(`\$`)
if len(r.FindString("He paid $150 for that software.")) != 0 {
fmt.Printf("Found $-symbol.") // <-
} else {
fmt.Printf("No $$$.")
}

简单的重复模式

FindAllString 函数返回匹配到的所有字符串的一个数组。FindAllString 需要两个参数,一个字符串正则以及需要返回的匹配内容的最大数量,如果你确定需要所有的匹配内容时就传 '-1' 给它。

查找字词。一个词就是字符类型 \w 的一个序列。加号 '+' 可以表示重复:

s := "Eenie meenie miny moe."
r, err := regexp.Compile(`\w+`)
res := r.FindAllString(s, -1)
// 打印 [Eenie meenie miny moe]
fmt.Printf("%v", res)

和在命令行下作为文件名字通配符不同,'*' 并不表示“任意字符”,而是表示它前面的一个字符(或分组)的重复次数。'+' 需要它前面的字符至少出现一次,'*' 在零次时也是满足的。这个可能会导致匪夷所思的结果。

s := "Firstname Lastname"
r, err := regexp.Compile(`\w+\s\w+`)
res := r.FindString(s)
// Prints Firstname Lastname
fmt.Printf("%v", res)

但是如果是有些用户输入的内容可能会有两个空格:

s := "Firstname  Lastname"
r, err := regexp.Compile(`\w+\s\w+`)
res := r.FindString(s)
// 打印为空 (空字符串说明没有匹配到)
fmt.Printf("%v", res)

使用 '\s+' 我们可以允许任意数量(但至少一个)的空白字符:

s := "Firstname  Lastname"
r, err := regexp.Compile(`\w+\s+\w+`)
res := r.FindString(s)
// Prints Firstname Lastname
fmt.Printf("%v", res)

如果你读取一个 INI 配置格式的文本文件,你也许会宽松地对待等号两侧的空白字符。

s := "Key=Value"
r, err := regexp.Compile(`\w+=\w+`)
res := r.FindAllString(s, -1)
// OK, prints Key=Value
fmt.Printf("%v", res)

现在让我们在等号两边加上空格。

s := "Key = Value"
r, err := regexp.Compile(`\w+=\w+`)
res := r.FindAllString(s, -1)
// 失败了,什么都没有打印出来,因为 \w 不匹配空格
fmt.Printf("%v", res)

于是我们用 '\s*' 来允许一些空格(包括没有空格的情况):

s := "Key = Value"
r, err := regexp.Compile(`\w+\s*=\s*\w+`)
res := r.FindAllString(s, -1)
fmt.Printf("%v", res)

Go 的正则模式支持更多的和 '?' 结合使用的模式。

锚点和边界

插入符号 ^ 标记“行的开始”。

s := "Never say never."
r, err1 := regexp.Compile(`^N`) // Do we have an 'N' at the beginning?
fmt.Printf("%v ", r.MatchString(s)) // true
t, err2 := regexp.Compile(`^n`) // Do we have an 'n' at the beginning?
fmt.Printf("%v ", t.MatchString(s)) // false

美元符号 $ 标记“行的结束”。

s := "All is well that ends well"
r, err := regexp.Compile(`well$`)
fmt.Printf("%v ", r.MatchString(s)) // true

r, err = regexp.Compile(`well`)
fmt.Printf("%v ", r.MatchString(s)) // true, but matches with first
// occurrence of 'well'
r, err = regexp.Compile(`ends$`)
fmt.Printf("%v ", r.MatchString(s)) // false, not at end of line.

我们看到 'well' 匹配到了。为了找到正则确切匹配到的位置,我们可以看一下索引。FindStringIndex 函数返回带有两个元素。第一个元素是正则表达式开始匹配到的位置的索引(当然是从0开始的)。第二个元素是正则匹配结束的下一个位置的索引。

s := "All is well that ends well"
// 012345678901234567890123456
// 1 2
r, err := regexp.Compile(`well$`)
fmt.Printf("%v", r.FindStringIndex(s)) // 打印 [22 26]

r, err = regexp.Compile(`well`)
fmt.Printf("%v ", r.MatchString(s)) // true, 但是这回匹配第一次出现的 'well'
fmt.Printf("%v", r.FindStringIndex(s)) // Prints [7 11], the match starts at 7 and end before 11.

r, err = regexp.Compile(`ends$`)
fmt.Printf("%v ", r.MatchString(s)) // false, 'ends' 并不是在结尾处

你可以使用 '\b' 查找一个单词的边界。FindAllStringIndex 函数会捕获一个正则中所有命中的位置,以一个数组容器的形式返回。

s := "How much wood would a woodchuck chuck in Hollywood?"
// 012345678901234567890123456789012345678901234567890
// 10 20 30 40 50
// -1-- -2-- -3--
// 查找以 wood 开头的词
r, err := regexp.Compile(`\bwood`) // 1 2
fmt.Printf("%v", r.FindAllStringIndex(s, -1)) // [[9 13] [22 26]]

// 查找以 wood 结尾的词
r, err = regexp.Compile(`wood\b`) // 1 3
fmt.Printf("%v", r.FindAllStringIndex(s, -1)) // [[9 13] [46 50]]

// 查找以 wood 开头并以其结尾的词
r, err = regexp.Compile(`\bwood\b`) // 1
fmt.Printf("%v", r.FindAllStringIndex(s, -1)) // [[9 13]]

字符分类

你可以在任何位置获取一组(或类)字符串,而不是一个单个的字面量字符。在本例中[uio] 就是一个“字符串分类”。在方括号中的任意字符都满足该正则表达式。所以,这个正则会匹配到 'Hullo','Hillo',以及 'Hollo'。

r, err := regexp.Compile(`H[uio]llo`)
// Will print 'Hullo'.
fmt.Printf(r.FindString("Hello Regular Expression. Hullo again."))

一个排除在外的字符分类会对分类的匹配取反。这时该正则就会匹配所有 'H.llo' 中的点号  是 'o', 'i' 或者 'u'的字符串。它不会匹配 "Hullo", "Hillo", "Hollo",但是会匹配 "Hallo" 甚至是 "H9llo"。

r, err := regexp.Compile(`H[^uio]llo`)
fmt.Printf("%v ", r.MatchString("Hillo")) // false
fmt.Printf("%v ", r.MatchString("Hallo")) // true
fmt.Printf("%v ", r.MatchString("H9llo")) // true

POSIX 字符分类

Golang regexp 库实现了 POSIX 字符分类。这不过就是给常用的类别取个可读性更好的别名。这些分类有: (​​https://github.com/google/re2/blob/master/doc/syntax.txt​​)

[:alnum:] 字母和数字(alphanumeric) (≡ [0-9A-Za-z])
[:alpha:] 字母(alphabetic) (≡ [A-Za-z])
[:ascii:] ASCII (≡ [\x00-\x7F])
[:blank:] 空字符(blank) (≡ [\t ])
[:cntrl:] 控制字符(control) (≡ [\x00-\x1F\x7F])
[:digit:] 数字字符(digits) (≡ [0-9])
[:graph:] 图形符号(graphical) (≡ [!-~] == [A-Za-z0-9!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~])
[:lower:] 小写字母(lower case) (≡ [a-z])
[:print:] 可打印字符(printable) (≡ [ -~] == [ [:graph:]])
[:punct:] 标点符号(punctuation) (≡ [!-/:-@[-`{-~])
[:space:] 空格字符(whitespace) (≡ [\t\n\v\f\r ])
[:upper:] 大写字母(upper case) (≡ [A-Z])
[:word:] 文字字符(word characters) (≡ [0-9A-Za-z_])
[:xdigit:] 十六进制(hex digit) (≡ [0-9A-Fa-f])

注意你必须把一个 ASCII 字符用 [] 包起来。而且还要注意无论何时我们说到字母的时候我们仅仅是在指 ASCII 从65-90范围内的26个字母, 不包括那些带有变音符的字母。

例子:查找一个包含一个小写字母、一个标点符号、一个空格(空白字符)以及一个数字的序列:

r, err := regexp.Compile(`[[:lower:]][[:punct:]][[:blank:]][[:digit:]]`)
if r.MatchString("Fred: 12345769") == true {
----
fmt.Printf("Match ") //
} else {
fmt.Printf("No match ")
}

我从来不用这些,因为它们需要打更多的字。但是在一些很多程序员一起工作的项目中,而且并不是每个人都像你一样 对正则表达式游刃有余的话,使用 POSIX 的写法也许也不失是一个好主意。

Unicode 字符分类

Unicode 是以区块(block)来组织的,典型地以主题或者语言进行分组。在本章我给出一些例子,因为完全覆盖到全部那是 不可能的(况且也无甚用处)。参见 ​​re2 引擎完整 unicode 字符列表​​.

示例:希腊语

我们以一个希腊语代码块的简单例子开始。

r, err := regexp.Compile(`\p{Greek}`)

if r.MatchString("This is all Γςεεκ to me.") == true {
fmt.Printf("Match ") // 会打印出 'Match'
} else {
fmt.Printf("No match ")
}

在 Windows-1252 代码页有个 mu,但是没有被认定为希腊语。因为 \p{Greek} 仅仅覆盖 U+0370 到 U+03FF 的部分 ​​http://en.wikipedia.org/wiki/Greek_and_Coptic​​ 。

if r.MatchString("the µ is right before ¶") == true {
fmt.Printf("Match ")
} else {
fmt.Printf("No match ") // 会打印出 'No match'
}

有些来自希腊语和科普特语(Coptic)代码页的特别酷的字母被认定为希腊语,而实际上 可能是科普特语,要注意。

if r.MatchString("ϵ϶ϓϔϕϖϗϘϙϚϛϜ") == true {
fmt.Printf("Match ") // Will print 'Match'
} else {
fmt.Printf("No match ")
}

示例:布莱叶盲文(Braille)###

你必须使用一种支持布莱叶盲文的字体。 ​​布莱叶盲文​

我怀疑这得配合一个支持布莱叶盲文的打印机才会有用,但这个就随你了。

r2, err := regexp.Compile(`\p{Braille}`)
if r2.MatchString("This is all ⢓⢔⢕⢖⢗⢘⢙⢚⢛ to me.") == true {
fmt.Printf("Match ") // 会打印出 'Match'
} else {
fmt.Printf("No match ")
}

示例:彻罗基语(Cherokee)###

你必须使用一种支持彻罗基语的字体(比如 Code2000)。 彻罗基语言的故事绝对值得一读。​​去读​​.

r3, err := regexp.Compile(`\p{Cherokee}`)
if r3.MatchString("This is all ᏯᏰᏱᏲᏳᏴ to me.") == true {
fmt.Printf("Match ") // 会打印出 'Match'
} else {
fmt.Printf("No match ")
}

择一匹配

你可以使用管道符号 '|' 允许两个或多个不同的可能来提供可选择性的匹配。如果你只是想对正则表达式中的某些部分进行可选择性的匹配,你可以使用括号来进行分组。

r, err1 := regexp.Compile(`Jim|Tim`)
fmt.Printf("%v", r.MatchString("Dickie, Tom and Tim")) // true
fmt.Printf("%v", r.MatchString("Jimmy, John and Jim")) // true

t, err2 := regexp.Compile(`Santa Clara|Santa Barbara`)
s := "Clara was from Santa Barbara and Barbara was from Santa Clara"
// ------------- -----------
fmt.Printf("%v", t.FindAllStringIndex(s, -1))
// [[15 28] [50 61]]

u, err3 := regexp.Compile(`Santa (Clara|Barbara)`) // Equivalent
v := "Clara was from Santa Barbara and Barbara was from Santa Clara"
// ------------- -----------
fmt.Printf("%v", u.FindAllStringIndex(v, -1))
// [[15 28] [50 61]]


高级用法

捕获分组

有时你用正则匹配一个字符串,但其实只是想留意之中的某一小段内容。而在前一章我们一直都停留在匹配到的整个的字符串上。

//[[cat] [sat] [mat]]
re, err := regexp.Compile(`.at`)
res := re.FindAllStringSubmatch("The cat sat on the mat.", -1)
fmt.Printf("%v", res)

你可以使用括号来捕捉你真正需要的那部分,而不是整个正则匹配到的全部内容。

//[[cat c] [sat s] [mat m]]
re, err := regexp.Compile(`(.)at`) // want to know what is in front of 'at'
res := re.FindAllStringSubmatch("The cat sat on the mat.", -1)
fmt.Printf("%v", res)

你可以有多个捕获分组。

// 结果是 [[ex e x] [ec e c] [e  e  ]]
s := "Nobody expects the Spanish inquisition."
re1, err := regexp.Compile(`(e)(.)`) // Prepare our regex
result_slice := re1.FindAllStringSubmatch(s, -1)
fmt.Printf("%v", result_slice)

FindAllStringSubmatch 这个方法对每一个捕获都返回一个数组,其中第一个元素是整个的匹配结果,接下来的元素是每个匹配到的分组的结果。最后每一个这样的数组再全部包进一个外层的数组里。

如果你有一个可选的捕获分组在一个字符串中没有出现,结果数组里会包含一个空的字符串的壳儿。换句话说,结果数组的元素数量总是分组数量再加上一。

s := "Mr. Leonard Spock"
re1, err := regexp.Compile(`(Mr)(s)?\. (\w+) (\w+)`)
result:= re1.FindStringSubmatch(s)

for k, v := range result {
fmt.Printf("%d. %s\n", k, v)
}
// Prints
// 0. Mr. Leonard Spock
// 1. Mr
// 2.
// 3. Leonard
// 4. Spock

你不能把捕获分组进行部分叠加。比如下面的例子我们想让第一个正则匹配 'expects the',另外一个匹配 'the Spanish',这里括号要分开用才行。 助记法:最后开始的,最先闭合。这里的在 'the' 之前开启的括号要在其之后是闭合的。

s := "Nobody expects the Spanish inquisition."
re1, err := regexp.Compile(`(expects (...) Spanish)`)
// Wanted regex1 --------------
// Wanted regex2 --------------
result:= re1.FindStringSubmatch(s)

for k, v := range result {
fmt.Printf("%d. %s\n", k, v)
}
// 0. expects the Spanish
// 1. expects the Spanish
// 2. the

FindStringSubmatchIndex 函数...

命名捕获

仅仅把匹配到的内容存入数组中的序列里会略显不便,会出现两个问题。

首先,当你在正则的某处插入一个新的分组时,在其后的分组在结果数组中的索引值肯定会增加。这是件麻烦事儿。

其次,正则本身也许是在运行时拼成的,这可能会包含很多超出我们控制的括号。也就是说我们不知道我们精心拼成的括号匹配到的内容的索引是多少。

为了解决这个问题,named matches 应运而生。允许给匹配的内容取一个符号化的名称用来到匹配的结果中进行查询。

re := regexp.MustCompile("(?P<first_char>.)(?P<middle_part>.*)(?P<last_char>.)")
n1 := re.SubexpNames()
r2 := re.FindAllStringSubmatch("Super", -1)[0]

md := map[string]string{}
for i, n := range r2 {
fmt.Printf("%d. match='%s'\tname='%s'\n", i, n, n1[i])
md[n1[i]] = n
}
fmt.Printf("The names are : %v\n", n1)
fmt.Printf("The matches are: %v\n", r2)
fmt.Printf("The first character is %s\n", md["first_char"])
fmt.Printf("The last character is %s\n", md["last_char"])

在该例中字符串 'Super' 使用一个由三部分组成的正则进行匹配:

一个单字符(.),命名为 'first_char'。

一个中间由一串字符组成的部分,命名为 'middle_part'。

一个结尾的字符(.),因此命名为 'last_char'。

为了简化匹配结果的使用,我们把所有的捕获命名都存在 n1 中,然后和匹配的结果 r2 一一对应后存储到一个新的叫 md 的 map,其中匹配结果是作为捕获命名的值。

注意整个字符串 'Super' 这个值用的是空字符这样一个伪键。

该例子会打印出:

0. match='Super'    name=''
1. match='S' name='first_char'
2. match='upe' name='middle_part'
3. match='r' name='last_char'
The names are : [ first_char middle_part last_char]
The matches are: [Super S upe r]
The first character is S
The last character is r

重复:高级篇

非匹配捕获/分组重复

如果一个复杂的正则表达式有多个分组,你可能会碰到使用括号进行分组但是对捕获到的内容并不需要关心的情况。这时你可以使用 (?:regex) 这样一个“非捕获分组”的方式丢弃一组匹配到的内容。问号加上冒号会告诉编译器用这个模式匹配但是不要作保存。

不包括非捕获分组:

s := "Mrs. Leonora Spock"
re1, err := regexp.Compile(`Mr(s)?\. (\w+) (\w+)`)
result:= re1.FindStringSubmatch(s)
for k, v := range result {
fmt.Printf("%d. %s\n", k, v)
}
// 0. Mrs. Leonora Spock
// 1. s
// 2. Leonora
// 3. Spock

带有一个非捕获分组:

s := "Mrs. Leonora Spock"
re1, err := regexp.Compile(`Mr(?:s)?\. (\w+) (\w+)`)
result:= re1.FindStringSubmatch(s)
for k, v := range result {
fmt.Printf("%d. %s\n", k, v)
}
// 0. Mrs. Leonora Spock
// 1. Leonora
// 2. Spock

到底是多少?

你可能非常清楚需要重复的具体次数。当你知道一个正则中你需要的部分有具体多少个实例的时候我们就需要 {}。

s := "11110010101111100101001001110101"
re1, err := regexp.Compile(`1{4}`)
res := re1.FindAllStringSubmatch(s,-1)
fmt.Printf("<%v>", res)
// <[[1111] [1111]]>

res2 := re1.FindAllStringIndex(s,-1)
fmt.Printf("<%v>", res2)
// <[[0 4] [10 14]]>

{} 的语法并不是很常用。其中一个原因是很多或是也许所有的情形下你都会通过简单地重复写出这些重复的部分来修改正则表达式。[但是假如重复的数量是120次的话,我觉得你应该就不愿意这么写了吧] 仅当你有非常明确的需求(比如 {123,130})时你才会想使用 {}。

(ab){3} == (ababab)
(ab){3,4} == (ababab(ab)??)

注:?? 表示 “零个或是一个,更倾向零个”。

{} 的通用模式是 x{n,m}。这里 'n' 是 x 出现的最小数量,'m' 是出现的最大数量。

Go-regexp 包支持 {} 家族中略多一些的模式。

标志项

regexp 包有如下的标志项可用 [引自文档]:

  • i 不区分大小写 (默认区分)
  • m 多行模式: ^ 和 $ 匹配整个文本的开头/结尾的同时也匹配每行的开头和结尾(默认不匹配)
  • s 让 . 匹配 \n (默认不匹配)
  • U 非贪婪:对 x* 和 x*?, x+ 和 x+? 等模式进行切换(默认是关闭的)

标志项的语法是 xyz(设置)或 -xyz(清除)或是 xy-z(设置 xy,清除 z)。

区分大小写

也许你已经知道有些字符存在两种形式:大写和小写。[ 你也许会说:“我当然知道这个,大家都知道!” 好吧,如果你觉得这问题有点儿吹毛求疵那你看下这些特例的大小写问题:a, $, 本, ß, Ω。好了,我们别把问题复杂化了,还是先只考虑英语的情况吧。]

如果你明确地想忽略大小写的情况,或者说你想在一个正则或是其中的一部分允许大小写,那就使用 'i' 标志符。

s := "Never say never."
r, err := regexp.Compile(`(?i)^n`) // 是否是以 'N' 或 'n' 开头?
fmt.Printf("%v", r.MatchString(s)) // true, 不区分大小写

在现实世界中我们很少会去匹配一个不区分大小写的正则。通常我们都倾向于先把整个字符串转换成大写或者小写,然后再去只匹配这一种情形:

sMixed := "Never say never."
sLower := strings.ToLower(sMixed) // 不要忘记 import "strings" 包
r, err := regexp.Compile(`^n`)
fmt.Printf("%v ", r.MatchString(sMixed)) // false, N != n
fmt.Printf("%v ", r.MatchString(sLower)) // true, n == n

贪婪匹配 vs 非贪婪匹配

如前所见,正则表达式可能包含重复的部分。在大多情况下,对于给定的字符串会有不止一种可行方案的正则。

比如,使用正则 '.*' (包括单引号部分),对下面的字符串匹配的结果是怎样的?

'abc','def','ghi'

你可能只是想取到 'abc' 部分,但是却非如此。正则表达式默认情况下是 贪婪的。它们在能匹配的情况下会尽可能多的去取字符。所以这里答案是 'abc','def','ghi',因为中间部分的引号也是和 "." 匹配的!如下:

r, err := regexp.Compile(`'.*'`)
res := r.FindString(" 'abc','def','ghi' ")
fmt.Printf("<%v>", res)
// Will print: <'abc','def','ghi'>

如果想确认进行最短可能匹配(即非贪婪),你要在正则表达式后面加上特殊符合 '?'。

r, err := regexp.Compile(`'.*?'`)
res := r.FindString(" 'abc','def','ghi' ")
fmt.Printf("<%v>", res)
// Will print: <'abc'>

没有捷径可以让你写一条匹配 'abc','def' 的这样的正则。

你可以使用 U 这个标志项把正则表达式的行为恢复到默认非贪婪的模式。

r, err := regexp.Compile(`(?U)'.*'`)
res := r.FindString(" 'abc','def','ghi' ")
fmt.Printf("<%v>", res)
// Will print: <'abc'>

在你的正则里你可以前后相继地在这两个行为之间进行切换。

点号是否匹配换行符?

当我们有一个多行字符串(也就是包含换行符 '\n' 的字符串)你可以使用 (?s) 标志符控制是否让 '.' 匹配 换行符。默认是不匹配。哪位能贡献一个更合理的用例吗?

r, err := regexp.Compile(`a.`)
s := "atlanta\narkansas\nalabama\narachnophobia"
res := r.FindAllString(s, -1)
fmt.Printf("<%v>", res)
// <[at an ar an as al ab am ar ac]>

这时如果使用 (?s) 标志符,换行符就会在结果中保留。

r, err := regexp.Compile(`(?s)a.`)
s := "atlanta\narkansas\nalabama\narachnophobia"
res := r.FindAllString(s, -1)
fmt.Printf("<%v>", res)
// Prints
// <[at an a
// ar an as al ab am a
// ar ac]>

要不要 ^/$ 匹配换行符?

对于多行文本,你可以通过'(?m)' 这个标志符来控制 '^' 或者 '$' 是否匹配换行符。默认是不匹配。('^' 表示行的起始符 BOL=Begin-of-line, '$' 表示行的结尾符 EOL=End-of-line)

r, err1 := regexp.Compile(`a$`) // without flag
s := "atlanta\narkansas\nalabama\narachnophobia"
// 01234567 890123456 78901234 5678901234567
// -
res := r.FindAllStringIndex(s,-1)
fmt.Printf("<%v>\n", res)
// 1 match
// <[[37 38]]>

t, err2 := regexp.Compile(`(?m)a$`) // with flag
u := "atlanta\narkansas\nalabama\narachnophobia"
// 01234567 890123456 78901234 5678901234567
// -- -- -
res2 := t.FindAllStringIndex(u,-1)
fmt.Printf("<%v>", res2)
// 3 matches
// <[[6 7] [23 24] [37 38]]>

示例 Cookbook

grep

这个 grep 工具用来在文本文件中搜索匹配一个正则表达式。读取到的每行文本都会和命令行中给定的正则进行匹配,匹配到的行会被打印出来。

package main

import (
"flag"
"regexp"
"bufio"
"fmt"
"os"
)

func grep(re, filename string) {
regex, err := regexp.Compile(re)
if err != nil {
return // there was a problem with the regular expression.
}

fh, err := os.Open(filename)
f := bufio.NewReader(fh)

if err != nil {
return // there was a problem opening the file.
}
defer fh.Close()

buf := make([]byte, 1024)
for {
buf, _ , err = f.ReadLine()
if err != nil {
return
}

s := string(buf)
if regex.MatchString(s) {
fmt.Printf("%s\n", string(buf))
}
}
}

func main() {
flag.Parse()
if flag.NArg() == 2 {
grep(flag.Arg(0), flag.Arg(1))
} else {
fmt.Printf("Wrong number of arguments.\n")
}
}

如果你不知道 grep 为何物,可以在命令行里运行 'man grep' 一下。

搜索替换

这个工具是上面 grep 工具的升级版。它在搜索匹配一个模式的同时会用其它内容替换掉匹配到的内容。显然我们是在对上面已有的 grep 版本基础上进行一些二次加工。

用法: ./replacer old new filename

package main

import (
"flag"
"regexp"
"bufio"
"fmt"
"os"
)

func replace(re, repl, filename string) {
regex, err := regexp.Compile(re)
if err != nil {
return // there was a problem with the regular expression.
}

fh, err := os.Open(filename)
f := bufio.NewReader(fh)

if err != nil {
return // there was a problem opening the file.
}
defer fh.Close()

buf := make([]byte, 1024)
for {
buf, _ , err = f.ReadLine()
if err != nil {
return
}

s := string(buf)
result := regex.ReplaceAllString(s, repl)
fmt.Print(result + "\n")
}
}

func main() {
flag.Parse()
if flag.NArg() == 3 {
repl(flag.Arg(0), flag.Arg(1), flag.Arg(2))
} else {
fmt.Printf("Wrong number of arguments.\n")
}
}

验证电子邮件地址

RFC2822 对于电子邮件的格式定义的过于宽松,以至于很难用简单的正则表达式验证一个邮件地址是否合规。很有趣啊。大多数情况下尽管你的程序会对邮件地址做一些预设,但是我发现下面这条正则对所有的情况都是实地有效的:

(\w[-._\w]*\w@\w[-._\w]*\w\.\w{2,3})

邮件地址必须以一个字符 \w 开头,接下来是任何数量的包含了破折号、英文句点以及下划线在内的字符。同时,在 @ 之前的最后一个字符必须又是一个“正常的”字符才行。对于域名部分我们也是同样的规则,但域名的后缀部分必须只由2到3个字符组成。这个规则基本可以覆盖大多数的情况。如果你碰到一个和这个正则不匹配的邮件地址,那很可能是故意拼凑起来逗你玩儿的,忽略即可。



分词

如果输入部分字面量是字符串,你则不必使用正则。

s := "abc,def,ghi"
r, err := regexp.Compile(`[^,]+`) // everything that is not a comma
res := r.FindAllString(s, -1)
// Prints [abc def ghi]
fmt.Printf("%v", res)

strings 包里面的 Split 函数就是用来做这个的,而且语法更可读。

s := "abc,def,ghi"
res:= strings.Split(s, ",")
// Prints [abc def ghi]
fmt.Printf("%v", res)

验证在一个字符串里是否存在一个指定的子字符串

使用 MatchString 函数可以在一个字符串里查找另一个字面量的字符串。

s := "OttoFritzHermanWaldoKarlSiegfried"
r, err := regexp.Compile(`Waldo`)
res := r.MatchString(s)
// Prints true
fmt.Printf("%v", res)

但是使用 strings.Index 函数可以在字串中获取匹配到子串的索引。当不匹配时则返回的索引为-1。

s := "OttoFritzHermanWaldoKarlSiegfried"
res:= strings.Index(s, "Waldo")
// Prints true
fmt.Printf("%v", res != -1)

删除空格

每当你读一些来自文件或是用户的文本时,你可能都想忽略那些句子开头和末尾的空格。

你可以用正则来搞定:

s := "  Institute of Experimental Computer Science  "
r, err := regexp.Compile(`\s*(.*)\s*`)
res:= r.FindStringSubmatch(s)
// <Institute of Experimental Computer Science >
fmt.Printf("<%v>", res[1])

首次移除空格大作战以失败告终。只有字符串开头前面的空格被删除了,接下来的 .* 这个片段是贪婪匹配,所以它会捕获余下的全部内容。但是对于这样的任务我不想继续折腾正则了,因为我知道还有 strings.TrimSpace 这个东东。

s := "  Institute of Experimental Computer Science  "
// <Institute of Experimental Computer Science>
fmt.Printf("<%v>", strings.TrimSpace(s))

TrimSpace 删除了开头和结尾的空格。翻阅 strings 包的文档会发现 Trim 家族还有其它一些函数。



文档来自:​​https://github.com/StefanSchroeder/Golang-Regex-Tutorial​

标签:---,匹配,err,fmt,golang,Printf,Compile,regexp
From: https://blog.51cto.com/wyf1226/5950827

相关文章