# 7.3 正則處理 正則表示式是一種進行模式匹配和文字操縱的複雜而又強大的工具。雖然正則表示式比純粹的文字匹配效率低,但是它卻更靈活。按照它的語法規則,隨需構造出的匹配模式就能夠從原始文字中篩選出幾乎任何你想要得到的字元組合。如果你在 Web 開發中需要從一些文字資料來源中取得資料,那麼你只需要按照它的語法規則,隨需構造出正確的模式字串就能夠從原資料來源提取出有意義的文字資訊。 Go 語言透過 `regexp` 標準套件為正則表示式提供了官方支援,如果你已經使用過其他程式語言提供的正則相關功能,那麼你應該對 Go 語言版本的不會太陌生,但是它們之間也有一些小的差異,因為 Go 實現的是 RE2 標準,除了\C,詳細的語法描述參考:`http://code.google.com/p/re2/wiki/Syntax` 其實字串處理我們可以使用 `strings` 套件來進行搜尋(Contains、Index)、替換(Replace)和解析(Split、Join)等操作,但是這些都是簡單的字串操作,他們的搜尋都是區分大小寫的,而且固定的字串,如果我們需要匹配可變的那種就沒辦法實現了,當然如果 `strings` 套件能解決你的問題,那麼就儘量使用它來解決。因為他們足夠簡單、而且效能和可讀性都會比正則好。 如果你還記得,在前面表單驗證的小節裡,我們已經接觸過正則處理,在那裡我們利用了它來驗證輸入的資訊是否滿足某些預設的條件。在使用中需要注意的一點就是:所有的字元都是 UTF-8 編碼的。接下來讓我們更加深入的來學習 Go 語言的 `regexp` 套件相關知識吧。 ## 透過正則判斷是否匹配 `regexp`套件中含有三個函式用來判斷是否匹配,如果匹配回傳 true,否則回傳 false ```Go func Match(pattern string, b []byte) (matched bool, error error) func MatchReader(pattern string, r io.RuneReader) (matched bool, error error) func MatchString(pattern string, s string) (matched bool, error error) ``` 上面的三個函式實現了同一個功能,就是判斷 `pattern` 是否和輸入源匹配,匹配的話就回傳 true,如果解析正則出錯則回傳 error。三個函式的輸入源分別是 byte slice、RuneReader 和 string。 如果要驗證一個輸入是不是 IP 地址,那麼如何來判斷呢?請看如下實現 ```Go func IsIP(ip string) (b bool) { if m, _ := regexp.MatchString("^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$", ip); !m { return false } return true } ``` 可以看到,`regexp`的 pattern 和我們平常使用的正則一模一樣。再來看一個例子:當用戶輸入一個字串,我們想知道是不是一次合法的輸入: ```Go func main() { if len(os.Args) == 1 { fmt.Println("Usage: regexp [string]") os.Exit(1) } else if m, _ := regexp.MatchString("^[0-9]+$", os.Args[1]); m { fmt.Println("數字") } else { fmt.Println("不是數字") } } ``` 在上面的兩個小例子中,我們採用了 Match(Reader|String)來判斷一些字串是否符合我們的描述需求,它們使用起來非常方便。 ## 透過正則取得內容 Match 模式只能用來對字串的判斷,而無法擷取字串的一部分、過濾字串、或者提取出符合條件的一批字串。如果想要滿足這些需求,那就需要使用正則表示式的複雜模式。 我們經常需要一些爬蟲程式,下面就以爬蟲為例來說明如何使用正則來過濾或擷取抓取到的資料: ```Go package main import ( "fmt" "io/ioutil" "net/http" "regexp" "strings" ) func main() { resp, err := http.Get("http://www.baidu.com") if err != nil { fmt.Println("http get error.") } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { fmt.Println("http read error") return } src := string(body) //將 HTML 標籤全轉換成小寫 re, _ := regexp.Compile("\\<[\\S\\s]+?\\>") src = re.ReplaceAllStringFunc(src, strings.ToLower) //去除 STYLE re, _ = regexp.Compile("\\") src = re.ReplaceAllString(src, "") //去除 SCRIPT re, _ = regexp.Compile("\\") src = re.ReplaceAllString(src, "") //去除所有尖括號內的 HTML 程式碼,並換成換行符 re, _ = regexp.Compile("\\<[\\S\\s]+?\\>") src = re.ReplaceAllString(src, "\n") //去除連續的換行符 re, _ = regexp.Compile("\\s{2,}") src = re.ReplaceAllString(src, "\n") fmt.Println(strings.TrimSpace(src)) } ``` 從這個範例可以看出,使用複雜的正則首先是 Compile,它會解析正則表示式是否合法,如果正確,那麼就會回傳一個 Regexp,然後就可以利用回傳的 Regexp 在任意的字串上面執行需要的操作。 解析正則表示式的有如下幾個方法: ```Go func Compile(expr string) (*Regexp, error) func CompilePOSIX(expr string) (*Regexp, error) func MustCompile(str string) *Regexp func MustCompilePOSIX(str string) *Regexp ``` CompilePOSIX 和 Compile 的不同點在於 POSIX 必須使用 POSIX 語法,它使用最左最長方式搜尋,而 Compile 是採用的則只採用最左方式搜尋(例如[a-z]{2,4}這樣一個正則表示式,應用於"aa09aaa88aaaa"這個文字串時,CompilePOSIX 回傳了 aaaa,而 Compile 的回傳的是 aa)。字首有 Must 的函式表示,在解析正則語法的時候,如果匹配模式串不滿足正確的語法則直接 panic,而不加 Must 的則只是回傳錯誤。 在了解了如何建立一個 Regexp 之後,我們再來看一下這個 struct 提供了哪些方法來輔助我們操作字串,首先我們來看下面這些用來搜尋的函式: ```Go func (re *Regexp) Find(b []byte) []byte func (re *Regexp) FindAll(b []byte, n int) [][]byte func (re *Regexp) FindAllIndex(b []byte, n int) [][]int func (re *Regexp) FindAllString(s string, n int) []string func (re *Regexp) FindAllStringIndex(s string, n int) [][]int func (re *Regexp) FindAllStringSubmatch(s string, n int) [][]string func (re *Regexp) FindAllStringSubmatchIndex(s string, n int) [][]int func (re *Regexp) FindAllSubmatch(b []byte, n int) [][][]byte func (re *Regexp) FindAllSubmatchIndex(b []byte, n int) [][]int func (re *Regexp) FindIndex(b []byte) (loc []int) func (re *Regexp) FindReaderIndex(r io.RuneReader) (loc []int) func (re *Regexp) FindReaderSubmatchIndex(r io.RuneReader) []int func (re *Regexp) FindString(s string) string func (re *Regexp) FindStringIndex(s string) (loc []int) func (re *Regexp) FindStringSubmatch(s string) []string func (re *Regexp) FindStringSubmatchIndex(s string) []int func (re *Regexp) FindSubmatch(b []byte) [][]byte func (re *Regexp) FindSubmatchIndex(b []byte) []int ``` 上面這 18 個函式我們根據輸入源(byte slice、string 和 io.RuneReader)不同還可以繼續簡化成如下幾個,其他的只是輸入源不一樣,其他功能基本是一樣的: ```Go func (re *Regexp) Find(b []byte) []byte func (re *Regexp) FindAll(b []byte, n int) [][]byte func (re *Regexp) FindAllIndex(b []byte, n int) [][]int func (re *Regexp) FindAllSubmatch(b []byte, n int) [][][]byte func (re *Regexp) FindAllSubmatchIndex(b []byte, n int) [][]int func (re *Regexp) FindIndex(b []byte) (loc []int) func (re *Regexp) FindSubmatch(b []byte) [][]byte func (re *Regexp) FindSubmatchIndex(b []byte) []int ``` 對於這些函式的使用我們來看下面這個例子 ```Go package main import ( "fmt" "regexp" ) func main() { a := "I am learning Go language" re, _ := regexp.Compile("[a-z]{2,4}") //查詢符合正則的第一個 one := re.Find([]byte(a)) fmt.Println("Find:", string(one)) //查詢符合正則的所有 slice,n 小於 0 表示回傳全部符合的字串,不然就是回傳指定的長度 all := re.FindAll([]byte(a), -1) fmt.Println("FindAll", all) //查詢符合條件的 index 位置,開始位置和結束位置 index := re.FindIndex([]byte(a)) fmt.Println("FindIndex", index) //查詢符合條件的所有的 index 位置,n 同上 allindex := re.FindAllIndex([]byte(a), -1) fmt.Println("FindAllIndex", allindex) re2, _ := regexp.Compile("am(.*)lang(.*)") //查詢 Submatch,回傳陣列,第一個元素是匹配的全部元素,第二個元素是第一個()裡面的,第三個是第二個()裡面的 //下面的輸出第一個元素是"am learning Go language" //第二個元素是" learning Go ",注意包含空格的輸出 //第三個元素是"uage" submatch := re2.FindSubmatch([]byte(a)) fmt.Println("FindSubmatch", submatch) for _, v := range submatch { fmt.Println(string(v)) } //定義和上面的 FindIndex 一樣 submatchindex := re2.FindSubmatchIndex([]byte(a)) fmt.Println(submatchindex) //FindAllSubmatch,查詢所有符合條件的子匹配 submatchall := re2.FindAllSubmatch([]byte(a), -1) fmt.Println(submatchall) //FindAllSubmatchIndex,查詢所有字匹配的 index submatchallindex := re2.FindAllSubmatchIndex([]byte(a), -1) fmt.Println(submatchallindex) } ``` 前面介紹過匹配函式,Regexp 也定義了三個函式,它們和同名的外部函式功能一模一樣,其實外部函式就是呼叫了這 Regexp 的三個函式來實現的: ```Go func (re *Regexp) Match(b []byte) bool func (re *Regexp) MatchReader(r io.RuneReader) bool func (re *Regexp) MatchString(s string) bool ``` 接下來讓我們來了解替換函式是怎麼操作的? ```Go func (re *Regexp) ReplaceAll(src, repl []byte) []byte func (re *Regexp) ReplaceAllFunc(src []byte, repl func([]byte) []byte) []byte func (re *Regexp) ReplaceAllLiteral(src, repl []byte) []byte func (re *Regexp) ReplaceAllLiteralString(src, repl string) string func (re *Regexp) ReplaceAllString(src, repl string) string func (re *Regexp) ReplaceAllStringFunc(src string, repl func(string) string) string ``` 這些替換函式我們在上面的抓網頁的例子有詳細應用範例, 接下來我們看一下 Expand 的解釋: ```Go func (re *Regexp) Expand(dst []byte, template []byte, src []byte, match []int) []byte func (re *Regexp) ExpandString(dst []byte, template string, src string, match []int) []byte ``` 那麼這個 Expand 到底用來幹嘛的呢?請看下面的例子: ```Go func main() { src := []byte(` call hello alice hello bob call hello eve `) pat := regexp.MustCompile(`(?m)(call)\s+(?P\w+)\s+(?P.+)\s*$`) res := []byte{} for _, s := range pat.FindAllSubmatchIndex(src, -1) { res = pat.Expand(res, []byte("$cmd('$arg')\n"), src, s) } fmt.Println(string(res)) } ``` 至此我們已經全部介紹完 Go 語言的 `regexp` 套件,透過對它的主要函式介紹及示範,相信大家應該能夠透過 Go 語言的正則套件進行一些基本的正則的操作了。 ## links * [目錄]() * 上一節:[Json 處理](<07.2.md>) * 下一節:[範本處理](<07.4.md>)