Gonet/html教程展示了如何使用net/html库在Golang中解析HTML。net/html是一个补充的Go网络库。
$ go version go version go1.18.1 linux/amd64
我们使用Go版本1.18。
Gonet/html库有两组基本的API来解析HTML:tokenizerAPI和基于树的节点解析API。
在tokenizerAPI中,一个Token
由一个TokenType
和一些Data
(开始和结束标签的标签名称,文本的内容,评论和文档类型)。标签Token
也可能包含一个属性片段。标记化是通过为io.Reader
创建一个Tokenizer
来完成的。
解析是通过使用io.Reader
调用Parse
完成的,它返回解析树的根(文档元素)作为*Node。节点由
NodeType
和一些Data
(元素节点的标签名称,文本的内容)组成,并且是Nodes
树的一部分。
$ go get -u golang.org/x/net
我们需要安装库。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Colour</title> </head> <body> <p> A list of colours </p> <ul> <li>red</li> <li>green</li> <li>blue</li> <li>yellow</li> <li>orange</li> <li>brown</li> <li>pink</li> </ul> <footer> A footer </footer> </body> </html>
部分示例使用此HTML文件。
去解析HTML列表
在下一个示例中,我们使用标记器API解析HTML列表。
package main import ( "fmt" "golang.org/x/net/html" "io/ioutil" "log" "strings" ) func readHtmlFromFile(fileName string) (string, error) { bs, err := ioutil.ReadFile(fileName) if err != nil { return "", err } return string(bs), nil } func parse(text string) (data []string) { tkn := html.NewTokenizer(strings.NewReader(text)) var vals []string var isLi bool for { tt := tkn.Next() switch { case tt == html.ErrorToken: return vals case tt == html.StartTagToken: t := tkn.Token() isLi = t.Data == "li" case tt == html.TextToken: t := tkn.Token() if isLi { vals = append(vals, t.Data) } isLi = false } } } func main() { fileName := "index.html" text, err := readHtmlFromFile(fileName) if err != nil { log.Fatal(err) } data := parse(text) fmt.Println(data) }
该示例打印列表中颜色的名称。
tkn := html.NewTokenizer(strings.NewReader(text))
分词器是使用html.NewTokenizer
创建的。
for { tt := tkn.Next() ...
我们在for循环中遍历标记。Next
函数扫描下一个标记并返回其类型。
case tt == html.ErrorToken: return vals
我们在解析结束时终止for循环并返回数据。
case tt == html.StartTagToken: t := tkn.Token() isLi = t.Data == "li"
如果令牌是起始标记,我们使用Token
函数获取当前令牌。如果遇到li
标记,我们将isLi
变量设置为true。
case tt == html.TextToken: t := tkn.Token() if isLi { vals = append(vals, t.Data) } isLi = false
当标记是文本数据时,我们将其内容添加到vals
切片中,前提是设置了isLi
变量;即我们正在解析li
标签内的文本。
$ go run parse_list.go [red green blue yellow orange brown pink]
去解析HTML表格
在下一个例子中,我们解析一个HTML列表。
package main import ( "fmt" "golang.org/x/net/html" "io/ioutil" "log" "net/http" "strings" ) func getHtmlPage(webPage string) (string, error) { resp, err := http.Get(webPage) if err != nil { return "", err } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { return "", err } return string(body), nil } func parseAndShow(text string) { tkn := html.NewTokenizer(strings.NewReader(text)) var isTd bool var n int for { tt := tkn.Next() switch { case tt == html.ErrorToken: return case tt == html.StartTagToken: t := tkn.Token() isTd = t.Data == "td" case tt == html.TextToken: t := tkn.Token() if isTd { fmt.Printf("%s ", t.Data) n++ } if isTd && n % 3 == 0 { fmt.Println() } isTd = false } } } func main() { webPage := "http://webcode.me/countries.html" data, err := getHtmlPage(webPage) if err != nil { log.Fatal(err) } parseAndShow(data) }
我们检索网页并解析其HTML表。我们从td
标签中获取数据。
$ go run parse_table.go Id Name Population 1 China 1382050000 2 India 1313210000 3 USA 324666000 4 Indonesia 260581000 5 Brazil 207221000 6 Pakistan 196626000 ...
去解析HTML列表II
在下一个示例中,我们使用解析API解析HTML列表。
package main import ( "fmt" "golang.org/x/net/html" "io/ioutil" "log" "strings" ) func main() { fileName := "index.html" bs, err := ioutil.ReadFile(fileName) if err != nil { log.Fatal(err) } text := string(bs) doc, err := html.Parse(strings.NewReader(text)) if err != nil { log.Fatal(err) } var data []string doTraverse(doc, &data, "li") fmt.Println(data) } func doTraverse(doc *html.Node, data *[]string, tag string) { var traverse func(n *html.Node, tag string) *html.Node traverse = func(n *html.Node, tag string) *html.Node { for c := n.FirstChild; c != nil; c = c.NextSibling { if c.Type == html.TextNode && c.Parent.Data == tag { *data = append(*data, c.Data) } res := traverse(c, tag) if res != nil { return res } } return nil } traverse(doc, tag) }
我们递归遍历文档以定位所有li
标签。
doc, err := html.Parse(strings.NewReader(text))
我们使用html.Parse
从字符串中获取树状文档。
traverse = func(n *html.Node, tag string) *html.Node { for c := n.FirstChild; c != nil; c = c.NextSibling { if c.Type == html.TextNode && c.Parent.Data == tag { *data = append(*data, c.Data) } res := traverse(c, tag) if res != nil { return res } } return nil }
我们通过递归算法遍历文档的标签。如果我们处理li
标签的文本节点,我们会将其内容附加到data
切片。
$ go run parsing.go [red green blue yellow orange brown pink]
通过id查找标签
在下面的例子中,我们通过id
找到一个标签。HTML文档中应该只有一个具有特定id
的唯一标记。id
。我们可以通过Attr
属性获取标签的属性。
package main import ( "bytes" "fmt" "golang.org/x/net/html" "io" "log" "strings" ) func getAttribute(n *html.Node, key string) (string, bool) { for _, attr := range n.Attr { if attr.Key == key { return attr.Val, true } } return "", false } func renderNode(n *html.Node) string { var buf bytes.Buffer w := io.Writer(&buf) err := html.Render(w, n) if err != nil { return "" } return buf.String() } func checkId(n *html.Node, id string) bool { if n.Type == html.ElementNode { s, ok := getAttribute(n, "id") if ok && s == id { return true } } return false } func traverse(n *html.Node, id string) *html.Node { if checkId(n, id) { return n } for c := n.FirstChild; c != nil; c = c.NextSibling { res := traverse(c, id) if res != nil { return res } } return nil } func getElementById(n *html.Node, id string) *html.Node { return traverse(n, id) } func main() { doc, err := html.Parse(strings.NewReader(data)) if err != nil { log.Fatal(err) } tag := getElementById(doc, "yellow") output := renderNode(tag) fmt.Println(output) } var data = `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Colour</title> </head> <body> <p> A list of colours: </p> <ul> <li>red</li> <li>green</li> <li>blue</li> <li id="yellow">yellow</li> <li>orange</li> <li>brown</li> <li>pink</li> </ul> </body> </html>`
我们找到一个特定的标签并呈现它的HTML。我们从多行字符串加载HTML数据。
func getAttribute(n *html.Node, key string) (string, bool) { for _, attr := range n.Attr { if attr.Key == key { return attr.Val, true } } return "", false }
我们从标签的Attr
属性中获取属性。
func renderNode(n *html.Node) string { var buf bytes.Buffer w := io.Writer(&buf) err := html.Render(w, n) if err != nil { return "" } return buf.String() }
html.Render
方法呈现标签。
$ go run find_by_id.go <li id="yellow">yellow</li>
并发解析标题
在下一个示例中,我们同时解析来自各个网站的HTML标题。该示例使用了tokenizerAPI。
package main import ( "fmt" "golang.org/x/net/html" "net/http" "sync" ) var wg sync.WaitGroup func main() { urls := []string{ "http://webcode.me", "https://example.com", "http://httpbin.org", "https://www.perl.org", "https://www.php.net", "https://www.python.org", "https://code.visualstudio.com", "https://clojure.org", } showTitles(urls) } func showTitles(urls []string) { c := getTitleTags(urls) for msg := range c { fmt.Println(msg) } } func getTitleTags(urls []string) chan string { c := make(chan string) for _, url := range urls { wg.Add(1) go getTitle(url, c) } go func() { wg.Wait() close(c) }() return c } func getTitle(url string, c chan string) { defer wg.Done() resp, err := http.Get(url) if err != nil { c <- "failed to fetch data" return } defer resp.Body.Close() tkn := html.NewTokenizer(resp.Body) var isTitle bool for { tt := tkn.Next() switch { case tt == html.ErrorToken: return case tt == html.StartTagToken: t := tkn.Token() isTitle = t.Data == "title" case tt == html.TextToken: t := tkn.Token() if isTitle { c <- t.Data isTitle = false } } } }
我们使用goroutines来同时启动我们的任务。解析的标题通过通道发送给调用者。sync.WaitGroup
用于在所有任务完成后完成程序。
$ go run parse_titles.go My html page Welcome to Python.org The Perl Programming Language - www.perl.org Clojure PHP: Hypertext Preprocessor Visual Studio Code - Code Editing. Redefined httpbin.org Example Domain
在本教程中,我们使用Go的net/html库解析了HTML。
列出所有Go教程。