使用 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.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。