Files
build-web-application-with-…/zh-tw/07.3.md
2019-02-26 01:40:54 +08:00

252 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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("\\<style[\\S\\s]+?\\</style\\>")
src = re.ReplaceAllString(src, "")
//去除SCRIPT
re, _ = regexp.Compile("\\<script[\\S\\s]+?\\</script\\>")
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<cmd>\w+)\s+(?P<arg>.+)\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
* [目錄](<preface.md>)
* 上一節: [Json處理](<07.2.md>)
* 下一節: [範本處理](<07.4.md>)