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