ZetCode

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

我们需要安装这些库。

index.html
<!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 列表。

parse_list.go
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 列表。

parse_table.go
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 列表。

parsing.go
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` 属性获取标签的属性。

find_by_id.go
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。

parse_titles.go
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/http 包 - 参考

在本文中,我们使用 Go 的 net/html 库解析了 HTML。

作者

我叫 Jan Bodnar,我是一名充满热情的程序员,拥有丰富的编程经验。我从 2007 年开始撰写编程文章。迄今为止,我已撰写了 1400 多篇文章和 8 本电子书。我在编程教学方面拥有十多年的经验。

列出所有 Go 教程