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教程。

