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