為什麼要學正則表達式?
因為利用正則表達式可以非常方便的匹配我們想要的任何字符串。比如,在一大堆字符串中,我們想找包含“Go語言”並且以“架構師”結尾的所有字符串,利用正則表達式就能非常方便快速的查找出來:
<code>re, _ := regexp.Compile(`Go語言.*架構師`) strs := "kfhewGo語言jiohjnfew.fewujj架構師" fmt.Println(re.FindString(strs)) // 輸出:Go語言jiohjnfew.fewujj架構師 fmt.Println(re.MatchString(strs)) // 輸出:true/<code>
一、編譯
Go語言的正則表達式在regexp包中。一般,在使用正則表達式之前,我們會把模式字符串編譯成正則表達式實例。
regexp.Compile(expr string) (*Regexp, error)
Compile函數會嘗試把模式字符串expr編譯成正則表達式。什麼是模式字符串?也就是本文最開始例子中的“Go語言.*架構師”。這個模式字符串表示先匹配“Go語言”這個字符串,緊接著匹配任意個字符串,最後匹配“架構師”這個字符串。
*Regexp就是返回的編譯後的正則表達式實例指針,然後我們可以利用這個正則表達式做很多字符串操作。那麼,我們可以寫出哪些模式字符串?哪些模式字符串才是合法的?先來看一下模式字符串的語法規則。
語法規則
字符
數量詞(用在字符或 (...) 之後)
邊界匹配
邏輯、分組
特殊構造(不作為分組)
瞭解了模式字符串規則,我們來看一下regexp的另一個編譯函數。
regexp.MustCompile(expr string) *Regexp
Compile函數編譯了正則表達式之後,如果正則表達式不合法,會返回error。MustCompile在編譯正則表達式過程中,如果正則表達式不合法,會拋出panic異常。MustCompile在一些正則表達式全局變量初始化的場景下很有用。
<code>re := regexp.MustCompile(`Go語言.*架構師`) strs := "kfhewGo語言jiohjnfew.fewujj架構師" fmt.Println(re.FindString(strs)) // 輸出:Go語言jiohjnfew.fewujj架構師 fmt.Println(re.MatchString(strs)) // 輸出:true/<code>
二、匹配
編譯得到正則表達式實例之後,就可以用來任意匹配字符串了,最典型的就是Match函數。
regexp包中有兩個Match函數:
- Regexp的Match方法;
- regexp包的Match函數。
如果正則表達式能匹配到傳入的字符串(或字節切片),則會返回true。
<code>func (re *Regexp) MatchString(s string) bool { return re.doMatch(nil, nil, s) } func (re *Regexp) Match(b []byte) bool { return re.doMatch(nil, b, "") } func MatchString(pattern string, s string) (matched bool, err error) { re, err := Compile(pattern) if err != nil { return false, err } return re.MatchString(s), nil } func Match(pattern string, b []byte) (matched bool, err error) { re, err := Compile(pattern) if err != nil { return false, err } return re.Match(b), nil }/<code>
可以看到,regexp包的Match函數內部先把模式字符串pattern編譯成了正則表達式實例,然後在調用Regexp的Match方法來實現的。模式字符串編譯成正則表達式實例的這個操作,如果是在大規模字符串處理中,是非常耗時的。因此我們可以先把編譯好的正則表達式實例緩存在全局變量中,要用的時候直接匹配就好了。
來看一下使用案例:
<code>re := regexp.MustCompile(`^Go語言.*架構師`) strs := "kfhewGo語言jiohjnfew.fewujj架構師" fmt.Println(re.MatchString(strs)) // 輸出:false/<code>
這個例子中,模式字符串希望匹配以“Go語言”這個字符串開頭,中間包含人一個字符串,然後包含“架構師”這個字符串,然而strs這個字符串並不是以“Go語言”開頭,因此匹配失敗,返回false。
三、查找
正則表達式的查找是非常有用的功能之一。在我們寫爬蟲的時候,拿到了網頁的源代碼,如果想從html中提取出下一頁的連接,我們就可以寫正則表達式查找出來。查找最典型的函數就是FindString和FindStringSubmatch。我們來看一下,主要有以下幾類:
<code>// 查找能匹配的字符串,查找所匹配的字符串的起止位置 func (re *Regexp) FindString(s string) string func (re *Regexp) FindStringIndex(s string) (loc []int) func (re *Regexp) FindReaderIndex(r io.RuneReader) (loc []int) // 查找能匹配的字符串和所有的匹配組 func (re *Regexp) FindStringSubmatch(s string) []string func (re *Regexp) FindStringSubmatchIndex(s string) []int func (re *Regexp) FindReaderSubmatchIndex(r io.RuneReader) []int // 查找所有能匹配的字符串(最多查找n次。如果n為負數,則查找所有能匹配到的字符串,以切片形式返回) func (re *Regexp) FindAllString(s string, n int) []string func (re *Regexp) FindAllStringIndex(s string, n int) [][]int // 查找所有能匹配的字符串(最多查找n次。如果n為負數,則查找所有能匹配到的字符串,以切片形式返回)和所有的匹配組 func (re *Regexp) FindAllStringSubmatch(s string, n int) [][]string func (re *Regexp) FindAllStringSubmatchIndex(s string, n int) [][]int/<code>
第一類:FindString和FindStringIndex
FindString函數會查找第一個能被正則表達式匹配到的字符串,並返回匹配到的字符串。FindStringIndex會返回匹配到的字符串的起止位置。
來看一下例子:
<code>re := regexp.MustCompile(`Go語言(.*?)架構師`) // 此處小括號中的問號表示勉強型匹配,見下文第五點 str := "前綴____Go語言_中間字符串111_架構師____中間字符串222____Go語言_中間字符串333_架構師" fmt.Println(re.FindString(str)) // 輸出:Go語言_中間字符串111_架構師 fmt.Println(re.FindStringIndex(str)) // 輸出:[7 44]/<code>
可以看到,FindString找到了字符串中第一個被正則表達式匹配到的字符串,FindStringIndex返回了它的起止位置(由此可見,FindStringIndex結果切片只會包含2個元素)。如果FindString找不到(結果返回空字符串,不是nil),則FindStringIndex會返回nil。
第二類:FindStringSubmatch和FindStringSubmatchIndex
FindStringSubmatch函數不僅會查找第一個能被正則表達式匹配到的字符串,還會找出其中匹配組所匹配到的字符串(即正則表達式中小括號裡的內容),會放在切片中一起返回。FindStringSubmatchIndex不僅會返回匹配到的字符串的起止位置,還會返回匹配組所匹配到的字符串起止位置。
這段文字比較繞,來看一下例子就明白了。
<code>re := regexp.MustCompile(`Go語言(.*?)架構師`) str := "前綴____Go語言_中間字符串111_架構師____中間字符串222____Go語言_中間字符串333_架構師" fmt.Println(re.FindStringSubmatch(str)) // 輸出:[Go語言_中間字符串111_架構師 _中間字符串111_] fmt.Println(re.FindStringSubmatchIndex(str)) // 輸出:[7 44 15 35]/<code>
FindStringSubmatch結果中的第一個元素“Go語言_中間字符串111_架構師”就是整個正則表達式所匹配到的第一個字符串;結果中的第二個元素就是匹配組(即正則表達式的小括號內的內容)所匹配到的第一個字符串。匹配組是幹啥用的?匹配組就是在整個正則表達式的匹配結果上,再進行的一次匹配。
理解了FindStringSubmatch,那麼FindStringSubmatchIndex就自然而然理解了。
第三類:FindAllString和FindAllStringIndex
和FindString不一樣,FindString會查找第一個能被正則表達式匹配到的字符串。FindAllString函數會查找n個(n是FindAllString的第二個參數)能被正則表達式匹配到的字符串,並返回所有匹配到的字符串所組成的切片。FindAllStringIndex會返回所有匹配到的字符串的起止位置,結果是個二維切片。如果n為整數,則最多匹配n次;如果n為負數,則會返回所有匹配結果。
來看一下例子:
<code>re := regexp.MustCompile(`Go語言(.*?)架構師`) str := "前綴____Go語言_中間字符串111_架構師____中間字符串222____Go語言_中間字符串333_架構師" fmt.Println(re.FindAllString(str, -1)) // 輸出:[Go語言_中間字符串111_架構師 Go語言_中間字符串333_架構師] fmt.Println(re.FindAllStringIndex(str, -1)) // 輸出:[[7 44] [64 101]]/<code>
第二個參數傳入-1,表示要返回所有匹配結果。可以看到,有匹配結果時,FindAllStringIndex的結果是個二維切片。
第四類:FindAllStringSubmatch和FindAllStringSubmatchIndex
和第二類一樣,都是在FindAllString的結果上,再返回匹配組所匹配到的內容。看一下例子:
<code>re := regexp.MustCompile(`Go語言(.*?)架構師`) str := "前綴____Go語言_中間字符串111_架構師____中間字符串222____Go語言_中間字符串333_架構師" fmt.Println(re.FindAllStringSubmatch(str, -1)) // 輸出:[[Go語言_中間字符串111_架構師 _中間字符串111_] [Go語言_中間字符串333_架構師 _中間字符串333_]] fmt.Println(re.FindAllStringSubmatchIndex(str, -1)) // 輸出:[[7 44 15 35] [64 101 72 92]]/<code>
對於字符串str,FindAllString能查找到兩個匹配結果,在這兩個匹配結果上,FindAllStringSubmatch再返回匹配組所匹配到的內容,那麼結果就是下面這個二維切片了:
<code>[[Go語言_中間字符串111_架構師 _中間字符串111_] [Go語言_中間字符串333_架構師 _中間字符串333_]]/<code>
這四類函數在解析爬蟲網頁的時候特別有用。
四、替換
替換是正則表達式中另一個非常有用的功能。替換主要有三個函數:
- func (re *Regexp) ReplaceAllString(src, repl string) string
- func (re *Regexp) ReplaceAllLiteralString(src, repl string) string
- func (re *Regexp) ReplaceAllStringFunc(src string, repl func(string) string) string
ReplaceAllString
ReplaceAllString會把第一個參數所表示的字符串中所有匹配到的內容用第二個參數代替。在第二個參數中,可以使用$符號來引用匹配組所匹配到的內容。$0表示第0個匹配組所匹配到的內容,即整個正則表達式所匹配到的內容。$1表示第一個匹配組所匹配到的內容,即正則表達式中第一個小括號內的正則表達式所匹配到的內容。
來看一下例子:
<code>re := regexp.MustCompile(`Go語言(.*?)架構師`) str := "前綴____Go語言_中間字符串111_架構師____中間字符串222____Go語言_中間字符串333_架構師" fmt.Println(re.ReplaceAllString(str, "$1")) // 輸出:前綴_____中間字符串111_____中間字符串222_____中間字符串333_/<code>
這個例子把str字符串中所有被正則表達式所匹配到的內容用第一個匹配組的內容進行替換。“Go語言(.*?)架構師”這個正則表達式能匹配到字符串“Go語言_中間字符串111_架構師”,而第一個匹配組(即.*?)所匹配到的內容是“_中間字符串111_”,所以最終結果就是“前綴_____中間字符串111_____中間字符串222_____中間字符串333_”。
ReplaceAllLiteralString
跟ReplaceAllString不一樣,ReplaceAllLiteralString中的第二個參數不能引用匹配組的內容,會把第二個參數當做字符串字面量去做替換。
看一下例子:
<code>re := regexp.MustCompile(`Go語言(.*?)架構師`) str := "前綴____Go語言_中間字符串111_架構師____中間字符串222____Go語言_中間字符串333_架構師" fmt.Println(re.ReplaceAllLiteralString(str, "$1")) // 輸出:前綴____$1____中間字符串222____$1/<code>
可以看到跟ReplaceAllString的結果不一樣。
ReplaceAllStringFunc
ReplaceAllStringFunc函數的第二個參數是個函數,表示我們可以自己編寫函數來決定如何替換掉匹配到的字符串。
一起來看一下三個函數的使用案例:
<code>re := regexp.MustCompile(`Go語言(.*?)架構師`) str := "前綴____Go語言_中間字符串111_架構師____中間字符串222____Go語言_中間字符串333_架構師" fmt.Println(re.ReplaceAllStringFunc(str, func(s string) string { return "Q" + s + "Q" }))/<code>
我們在匹配到的字符串前後都加了一個字母Q,可以看到,輸出符合預期:
<code>前綴____QGo語言_中間字符串111_架構師Q____中間字符串222____QGo語言_中間字符串333_架構師Q/<code>
五、三種模式
任何語言的正則表達式匹配都避免不了正則表達式的三種匹配模式:貪婪型、勉強型、佔有型。如果你是語言深耕者,一定要對這三種模式瞭如指掌。(如果對Java正則表達式匹配感興趣的同學可以看一下我發的這篇文章:
Java正則表達式中的貪婪型、勉強型和佔有性)貪婪型屬於正常的表示(平時寫的那些),勉強型則在後面加個“問號”,佔有型加個“加號”,都只作用於前面的問號、星號、加號、大括號,因為前面如果沒有這些,就變成普通的問號和加號了(也就是變成貪婪型了)。
- 貪婪型匹配模式表示儘可能多的去匹配字符。
- 勉強型匹配模式表示儘可能少的去匹配字符。
- 佔有型匹配模式表示儘可能做完全匹配。
貪婪型
貪婪型匹配模式的正則表達式形式為星號或者加號。我們知道,星號表示匹配0個或任意多個字符,加號表示匹配1個或者任意多個字符。
貪婪型匹配,先一直匹配到最後,發現最後的字符不匹配時,往前退一格再嘗試匹配,不匹配時再退一格。
看一下例子就很明白了:
<code>re := regexp.MustCompile(`我是.*字符串`) str := "我是第1個字符串_我是第2個字符串" fmt.Println(re.FindString(str)) // 輸出:我是第1個字符串_我是第2個字符串/<code>
這個例子的輸出是“我是第1個字符串_我是第2個字符串”,而不是“我是第1個字符串”。這個結果和勉強型匹配形成了強烈的對比。
勉強型
勉強型匹配模式的正則表達式形式為星號或者加號,後面再加個問號(注意與貪婪型的區別)。我們知道,星號表示匹配0個或任意多個字符,加號表示匹配1個或者任意多個字符。後面加個問號表示儘可能少的去匹配字符。
看一下例子就很明白了,還是上面那個例子,在正則表達式的星號後面加個問號:
<code>re := regexp.MustCompile(`我是.*?字符串`) str := "我是第1個字符串_我是第2個字符串" fmt.Println(re.FindString(str)) // 輸出:我是第1個字符串/<code>
這個例子的輸出是“我是第1個字符串”。和貪婪型匹配結果形成了強烈的對比。
佔有型
佔有型匹配模式的正則表達式形式為星號或者加號,後面再加個加號(注意與貪婪型、勉強型的區別)。我們知道,星號表示匹配0個或任意多個字符,加號表示匹配1個或者任意多個字符。後面加個加號表示正則表達式必須完全匹配整個字符串。
Go語言中正則表達式沒有“佔有型”,只有Java中有,感興趣的可以看一下這篇文章Java正則表達式中的貪婪型、勉強型和佔有性。
我們可以嘗試一下,還是上面那個例子,在正則表達式的星號後面加個加號:
<code>re := regexp.MustCompile(`我是.*+字符串`) str := "我是第1個字符串_我是第2個字符串" fmt.Println(re.FindString(str))/<code>
發現編譯時報錯。Go語言中如果想實現完全匹配,在正則表達式中使用“^”和“$”表示首尾就好了。