ZetCode

Go goquery

最后修改时间 2024 年 4 月 11 日

在本文中,我们将介绍如何使用 goquery 在 Golang 中进行网页抓取/HTML 解析。goquery 的 API 类似于 jQuery。

goquery 基于 net/html 包和 CSS 选择器库 cascadia。

$ go get github.com/PuerkitoBio/goquery

我们为我们的项目获取 goquery 包。

Go goquery 获取标题

在下面的示例中,我们将获取网页的标题。

get_title.go
package main

import (
    "fmt"
    "github.com/PuerkitoBio/goquery"
    "log"
    "net/http"
)

func main() {

    webPage := "http://webcode.me"
    resp, err := http.Get(webPage)

    if err != nil {
        log.Fatal(err)
    }

    defer resp.Body.Close()

    if resp.StatusCode != 200 {
        log.Fatalf("failed to fetch data: %d %s", resp.StatusCode, resp.Status)
    }

    doc, err := goquery.NewDocumentFromReader(resp.Body)

    if err != nil {
        log.Fatal(err)
    }

    title := doc.Find("title").Text()
    fmt.Println(title)
}

我们向指定的网页发出 GET 请求并检索其内容。从响应体中,我们生成一个 goquery 文档。从该文档中,我们检索标题。

title := doc.Find("title").Text()

Find 方法返回一组匹配的元素。在我们的例子中,它是一个 title 标签。使用 Text,我们获取标签的文本内容。

$ go run get_title.go
My html page

Go goquery 读取本地文件

下面的示例读取本地 HTML 文件。

index.html
<!DOCTYPE html>
<html lang="en">

<body>
<main>
    <h1>My website</h1>

    <p>
        I am a Go programmer.
    </p>

    <p>
        My hobbies are:
    </p>

    <ul>
        <li>Swimming</li>
        <li>Tai Chi</li>
        <li>Running</li>
        <li>Web development</li>
        <li>Reading</li>
        <li>Music</li>
    </ul>
</main>
</body>

</html>

这是一个简单的 HTML 文件。

read_local.go
package main

import (
    "fmt"
    "io/ioutil"
    "log"
    "regexp"
    "strings"

    "github.com/PuerkitoBio/goquery"
)


func main() {

    data, err := ioutil.ReadFile("index.html")

    if err != nil {
        log.Fatal(err)
    }

    doc, err := goquery.NewDocumentFromReader(strings.NewReader(string(data)))

    if err != nil {
        log.Fatal(err)
    }

    text := doc.Find("h1,p").Text()

    re := regexp.MustCompile("\\s{2,}")
    fmt.Println(re.ReplaceAllString(text, "\n"))
}

我们获取两个标签的文本内容。

data, err := ioutil.ReadFile("index.html")

我们读取文件。

doc, err := goquery.NewDocumentFromReader(strings.NewReader(string(data)))

我们使用 NewDocumentFromReader 生成一个新的 goquery 文档。

text := doc.Find("h1,p").Text()

我们获取两个标签的文本内容:h1 和 p。

re := regexp.MustCompile("\\s{2,}")
fmt.Println(re.ReplaceAllString(text, "\n"))

使用正则表达式,我们删除多余的空格。

$ go run read_local.go
My website
I am a Go programmer.
My hobbies are:

Go goquery 从 HTML 字符串读取

在下一个示例中,我们处理一个内置的 HTML 字符串。

get_words.go
package main

import (
    "fmt"
    "log"
    "strings"

    "github.com/PuerkitoBio/goquery"
)

func main() {

    data := `
<html lang="en">
<body>
<p>List of words</p>
<ul>
    <li>dark</li>
    <li>smart</li>
    <li>war</li>
    <li>cloud</li>
    <li>park</li>
    <li>cup</li>
    <li>worm</li>
    <li>water</li>
    <li>rock</li>
    <li>warm</li>
</ul>
<footer>footer for words</footer>
</body>
</html>
`

    doc, err := goquery.NewDocumentFromReader(strings.NewReader(data))

    if err != nil {
    log.Fatal(err)
    }

    words := doc.Find("li").Map(func(i int, sel *goquery.Selection) string {
        return fmt.Sprintf("%d: %s", i+1, sel.Text())
    })

    fmt.Println(words)
}

我们从 HTML 列表中获取单词。

words := doc.Find("li").Map(func(i int, sel *goquery.Selection) string {
    return fmt.Sprintf("%d: %s", i+1, sel.Text())
})

使用 Find,我们获取所有 li 元素。Map 方法用于构建一个包含单词及其在列表中的索引的字符串。

$ go run get_words.go
[1: dark 2: smart 3: war 4: cloud 5: park 6: cup 7: worm 8: water 9: rock 10: warm]

Go goquery 过滤单词

下面的示例过滤单词。

filter_words.go
package main

import (
    "fmt"
    "log"
    "strings"

    "github.com/PuerkitoBio/goquery"
)

func main() {

    data := `
<html lang="en">
<body>
<p>List of words</p>
<ul>
    <li>dark</li>
    <li>smart</li>
    <li>war</li>
    <li>cloud</li>
    <li>park</li>
    <li>cup</li>
    <li>worm</li>
    <li>water</li>
    <li>rock</li>
    <li>warm</li>
</ul>
<footer>footer for words</footer>
</body>
</html>
`

    doc, err := goquery.NewDocumentFromReader(strings.NewReader(data))

    if err != nil {
        log.Fatal(err)
    }

    f := func(i int, sel *goquery.Selection) bool {
        return strings.HasPrefix(sel.Text(), "w")
    }

    var words []string

    doc.Find("li").FilterFunction(f).Each(func(_ int, sel *goquery.Selection) {
        words = append(words, sel.Text())
    })

    fmt.Println(words)
}

我们检索所有以“w”开头的单词。

f := func(i int, sel *goquery.Selection) bool {
    return strings.HasPrefix(sel.Text(), "w")
}

这是一个谓词函数,它对所有以“w”开头的单词返回布尔值 true。

doc.Find("li").FilterFunction(f).Each(func(_ int, sel *goquery.Selection) {
    words = append(words, sel.Text())
})

我们使用 Find 定位匹配标签的集合。我们使用 FilterFunction 过滤集合,并使用 Each 遍历过滤后的结果。我们将每个过滤后的单词添加到 words 切片中。

fmt.Println(words)

最后,我们打印切片。

$ go run filter_words.go
[war worm water warm]

Go goquery 合并单词

使用 Union,我们可以组合选择。

union_words.go
package main

import (
    "fmt"
    "log"
    "strings"

    "github.com/PuerkitoBio/goquery"
)

func main() {

    data := `
<html lang="en">
<body>
<p>List of words</p>
<ul>
    <li>dark</li>
    <li>smart</li>
    <li>war</li>
    <li>cloud</li>
    <li>park</li>
    <li>cup</li>
    <li>worm</li>
    <li>water</li>
    <li>rock</li>
    <li>warm</li>
</ul>
<footer>footer for words</footer>
</body>
</html>
`
    doc, err := goquery.NewDocumentFromReader(strings.NewReader(data))

    if err != nil {
        log.Fatal(err)
    }

    var words []string

    sel1 := doc.Find("li:first-child, li:last-child")
    sel2 := doc.Find("li:nth-child(3), li:nth-child(7)")

    sel1.Union(sel2).Each(func(_ int, sel *goquery.Selection) {
        words = append(words, sel.Text())
    })

    fmt.Println(words)
}

该示例组合了两个选择。

sel1 := doc.Find("li:first-child, li:last-child")

第一个选择包含第一个和最后一个元素。

sel2 := doc.Find("li:nth-child(3), li:nth-child(7)")

第二个选择包含第三个和第七个元素。

sel1.Union(sel2).Each(func(_ int, sel *goquery.Selection) {
    words = append(words, sel.Text())
})

我们使用 Union 组合这两个选择。

$ go run union_words.go
[dark warm war worm]

Go goquery 获取链接

下面的示例从网页中检索链接。

get_links.go
package main

import (
    "fmt"
    "log"
    "net/http"
    "strings"

    "github.com/PuerkitoBio/goquery"
)

func getLinks() {

    webPage := "https://golang.ac.cn"

    resp, err := http.Get(webPage)

    if err != nil {
        log.Fatal(err)
    }

    defer resp.Body.Close()

    if resp.StatusCode != 200 {
        log.Fatalf("status code error: %d %s", resp.StatusCode, resp.Status)
    }

    doc, err := goquery.NewDocumentFromReader(resp.Body)

    if err != nil {
        log.Fatal(err)
    }

    f := func(i int, s *goquery.Selection) bool {

        link, _ := s.Attr("href")
        return strings.HasPrefix(link, "https")
    }

    doc.Find("body a").FilterFunction(f).Each(func(_ int, tag *goquery.Selection) {

        link, _ := tag.Attr("href")
        linkText := tag.Text()
        fmt.Printf("%s %s\n", linkText, link)
    })
}

func main() {
    getLinks()
}

该示例检索指向安全网页的外部链接。

f := func(i int, s *goquery.Selection) bool {

    link, _ := s.Attr("href")
    return strings.HasPrefix(link, "https")
}

在谓词函数中,我们确保链接具有 https 前缀。

doc.Find("body a").FilterFunction(f).Each(func(_ int, tag *goquery.Selection) {

    link, _ := tag.Attr("href")
    linkText := tag.Text()
    fmt.Printf("%s %s\n", linkText, link)
})

我们查找所有锚点标签,过滤它们,然后将过滤后的链接打印到控制台。

Go goquery StackOverflow 问题

我们将获取 Raku 标签的最新 StackOverflow 问题。

get_qs.go
package main

import (
    "fmt"
    "log"
    "net/http"

    "github.com/PuerkitoBio/goquery"
)

func main() {

    webPage := "https://stackoverflow.com/questions/tagged/raku"

    resp, err := http.Get(webPage)

    if err != nil {
        log.Fatal(err)
    }

    defer resp.Body.Close()

    if resp.StatusCode != 200 {
        log.Fatalf("failed to fetch data: %d %s", resp.StatusCode, resp.Status)
    }

    doc, err := goquery.NewDocumentFromReader(resp.Body)

    if err != nil {
        log.Fatal(err)
    }

    doc.Find(".question-summary .summary").Each(func(i int, s *goquery.Selection) {

        title := s.Find("h3").Text()
        fmt.Println(i+1, title)
    })
}

在代码示例中,我们打印了关于 Raku 编程语言的 StackOverflow 问题的最后五十个标题。

doc.Find(".question-summary .summary").Each(func(i int, s *goquery.Selection) {

    title := s.Find("h3").Text()
    fmt.Println(i+1, title)
})

我们定位问题并打印它们的标题;标题在 h3 标签中。

$ go run get_qs.go
1 Raku pop() order of execution
2 Does the `do` keyword run a block or treat it as an expression?
3 Junction ~~ Junction behavior
4 Is there a way to detect whether something is immutable?
5 Optimize without sacrificing usual workflow: arguments, POD etc
6 Find out external command runability
...

Go goquery 获取地震

在下一个示例中,我们获取有关地震的数据。

$ go get github.com/olekukonko/tablewriter

我们使用 tablewriter 包以表格格式显示数据。

earthquakes.go
package main

import (
    "fmt"
    "github.com/PuerkitoBio/goquery"
    "github.com/olekukonko/tablewriter"
    "log"
    "net/http"
    "os"
    "strings"
)

type Earthquake struct {
    Date      string
    Latitude  string
    Longitude string
    Magnitude string
    Depth     string
    Location  string
    IrisId    string
}

var quakes []Earthquake

func fetchQuakes() {

    webPage := "http://ds.iris.edu/seismon/eventlist/index.phtml"

    resp, err := http.Get(webPage)

    if err != nil {
        log.Fatal(err)
    }

    defer resp.Body.Close()

    if resp.StatusCode != 200 {
        log.Fatalf("failed to fetch data: %d %s", resp.StatusCode, resp.Status)
    }

    doc, err := goquery.NewDocumentFromReader(resp.Body)

    if err != nil {
        log.Fatal(err)
    }

    doc.Find("tbody tr").Each(func(j int, tr *goquery.Selection) {

        if j >= 10 {
            return
        }

        e := Earthquake{}

        tr.Find("td").Each(func(ix int, td *goquery.Selection) {
            switch ix {
            case 0:
                e.Date = td.Text()
            case 1:
                e.Latitude = td.Text()
            case 2:
                e.Longitude = td.Text()
            case 3:
                e.Magnitude = td.Text()
            case 4:
                e.Depth = td.Text()
            case 5:
                e.Location = strings.TrimSpace(td.Text())
            case 6:
                e.IrisId = td.Text()
            }
        })

        quakes = append(quakes, e)

    })

    table := tablewriter.NewWriter(os.Stdout)
    table.SetHeader([]string{"Date", "Location", "Magnitude", "Longitude",
        "Latitude", "Depth", "IrisId"})
    table.SetCaption(true, "Last ten earthquakes")

    for _, quake := range quakes {

        s := []string{
            quake.Date,
            quake.Location,
            quake.Magnitude,
            quake.Longitude,
            quake.Latitude,
            quake.Depth,
            quake.IrisId,
        }

        table.Append(s)
    }

    table.Render()
}

func main() {

    fetchQuakes()
}

该示例从 Iris 数据库检索十个最新的地震。它以表格格式打印数据。

type Earthquake struct {
    Date      string
    Latitude  string
    Longitude string
    Magnitude string
    Depth     string
    Location  string
    IrisId    string
}

数据分组在 Earthquake 结构中。

var quakes []Earthquake

结构将存储在 quakes 切片中。

doc.Find("tbody tr").Each(func(j int, tr *goquery.Selection) {

定位数据很简单;我们查找 tbody 标签内的 tr 标签。

e := Earthquake{}

tr.Find("td").Each(func(ix int, td *goquery.Selection) {
    switch ix {
    case 0:
        e.Date = td.Text()
    case 1:
        e.Latitude = td.Text()
    case 2:
        e.Longitude = td.Text()
    case 3:
        e.Magnitude = td.Text()
    case 4:
        e.Depth = td.Text()
    case 5:
        e.Location = strings.TrimSpace(td.Text())
    case 6:
        e.IrisId = td.Text()
    }
})

quakes = append(quakes, e)

我们创建一个新的 Earthquake 结构,用表格行数据填充它,并将结构放入 quakes 切片中。

table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"Date", "Location", "Magnitude", "Longitude",
    "Latitude", "Depth", "IrisId"})
table.SetCaption(true, "Last ten earthquakes")

我们创建一个新表来显示数据。数据将在标准输出(控制台)中显示。我们为表创建标题和说明。

for _, quake := range quakes {

    s := []string{
        quake.Date,
        quake.Location,
        quake.Magnitude,
        quake.Longitude,
        quake.Latitude,
        quake.Depth,
        quake.IrisId,
    }

    table.Append(s)
}

该表以字符串切片作为参数;因此,我们将结构转换为切片,并使用 Append 将切片附加到表中。

table.Render()

最后,我们渲染表格。

$ go run earthquakes.go
+------------------------+--------------------------------+-----------+-----------+----------+-------+------------+
|          DATE          |            LOCATION            | MAGNITUDE | LONGITUDE | LATITUDE | DEPTH |   IRISID   |
+------------------------+--------------------------------+-----------+-----------+----------+-------+------------+
|  17-AUG-2021 07:54:31  | TONGA ISLANDS                  |      4.9  |  -174.01  |  -17.44  |   45  |   11457319 |
|  17-AUG-2021 03:10:50  | SOUTH SANDWICH ISLANDS REGION  |      5.7  |   -24.02  |  -58.04  |   10  |   11457233 |
|  17-AUG-2021 02:22:46  | LEYTE, PHILIPPINES             |      4.4  |   125.44  |   10.37  |  228  |   11457202 |
|  17-AUG-2021 02:19:28  | CHILE-ARGENTINA BORDER REGION  |      4.5  |   -67.28  |  -24.27  |  183  |   11457198 |
|  17-AUG-2021 01:30:26  | WEST CHILE RISE                |      4.9  |   -81.25  |  -44.38  |   10  |   11457192 |
|  17-AUG-2021 00:38:38  | AFGHANISTAN-TAJIKISTAN BORD    |      4.4  |    71.13  |   36.72  |  240  |   11457214 |
|                        | REG.                           |           |           |          |       |            |
|  16-AUG-2021 23:58:56  | NORTHWESTERN BALKAN REGION     |      4.6  |    16.28  |   45.44  |   10  |   11457177 |
|  16-AUG-2021 23:37:25  | SOUTH SANDWICH ISLANDS REGION  |      5.5  |   -26.23  |  -59.56  |   52  |   11457169 |
|  16-AUG-2021 20:50:34  | SOUTH SANDWICH ISLANDS REGION  |      5.5  |   -24.90  |  -60.25  |   10  |   11457139 |
|  16-AUG-2021 19:17:09  | SOUTH SANDWICH ISLANDS REGION  |      5.1  |   -26.77  |  -60.22  |   35  |   11457054 |
+------------------------+--------------------------------+-----------+-----------+----------+-------+------------+
Last ten earthquakes

来源

Goquery - Github 页面

在本文中,我们使用 goquery 包在 Go 中抓取网页/解析 HTML。

作者

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

列出所有 Go 教程