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