如何使用goquery在Golang中进行网页抓取/HTML解析

Gogoquery教程展示了如何使用goquery在Golang中进行网页抓取/HTML解析。goqueryAPI类似于jQuery。

goquery基于net/html包和CSSSelectorlibrarycascadia。

$ go get github.com/PuerkitoBio/goquery

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

$ go version
go version go1.18.1 linux/amd64

我们使用Go版本1.18。

gogoquery获取标题

下面的例子,我们得到一个网页的标题。

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

gogoquery读取本地文件

以下示例读取本地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文件。

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:

goquery从HTML字符串中读取

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

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]

gogoquery过滤词

以下示例过滤单词。

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遍历过滤后的结果。我们将每个过滤后的单词添加到单词切片中。

fmt.Println(words)

最后,我们打印切片。

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

gogoquery联合词

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

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]

gogoquery获取链接

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

package main

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

    "github.com/PuerkitoBio/goquery"
)

func getLinks() {

    webPage := "https://golang.org"

    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)
})

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

GoqueryStackOverflow问题

我们将获得有关Raku标签的最新StackOverflow问题。

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
...

gogoquery获取地震

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

$ go get github.com/olekukonko/tablewriter

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

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包在Go中抓取网页/解析HTML。

列出所有Go教程。

赞(0) 打赏

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏