使用 net/html 解析 Go HTML
最后修改时间 2024 年 4 月 11 日
在本文中,我们将介绍如何使用 net/html 库在 Golang 中解析 HTML。net/html 是一个补充性的 Go 网络库。
Go net/html 库提供了两套基本的 API 来解析 HTML:分词器 API 和基于树的节点解析 API。
在分词器 API 中,一个 `Token` 由一个 `TokenType` 和一些 `Data`(开始和结束标签的标签名,文本、注释和 doctype 的内容)组成。标签 `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 文件。
Go 解析 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 循环中遍历 token。`Next` 函数扫描下一个 token 并返回其类型。
case tt == html.ErrorToken: return vals
我们在解析结束时终止 for 循环并返回数据。
case tt == html.StartTagToken: t := tkn.Token() isLi = t.Data == "li"
如果 token 是一个开始标签,我们就用 `Token` 函数获取当前 token。如果遇到 `li` 标签,我们就将 `isLi` 变量设置为 true。
case tt == html.TextToken: t := tkn.Token() if isLi { vals = append(vals, t.Data) } isLi = false
当 token 是文本数据时,只要 `isLi` 变量被设置(即我们正在解析 `li` 标签内的文本),我们就将其内容添加到 `vals` 切片中。
$ go run parse_list.go [red green blue yellow orange brown pink]
Go 解析 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 ...
Go 解析 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]
Go 按 ID 查找标签
在下面的示例中,我们将按其 `id` 查找一个标签。HTML 文档中应该只有一个具有特定 `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>
Go 并发解析标题
在下一个示例中,我们将并发地从各种网站解析 HTML 标题。该示例使用了分词器 API。
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://perl.net.cn", "https://php.ac.cn", "https://pythonlang.cn", "https://vscode.js.cn", "https://clojure.net.cn", } 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。