相关技术 Golang+chromedp+goquery
技术简介
Chromedp
chromedp是一个更快、更简单的Golang库用于调用支持Chrome DevTools协议的浏览器,同时不需要额外的依赖(例如Selenium和PhantomJS)--- 需使用到Chrome浏览器
相关文档:https://pkg.go.dev/github.com/chromedp/chromedp
Chrome和Golang都与Google有着相当密切的关系,而Chrome DevTools其实就是Chrome浏览器按下F12之后的控制终端
下载:go get -u github.com/chromedp/chromedp
基本我们可以熟悉最常用的几个方法了:
- chromedp.NewContext() 初始化chromedp的上下文,后续这个页面都使用这个上下文进行操作
- chromedp.Flag("headless", true) 给 chromedp设置参数,设置为 无头模式 headless ,无头模式即Chrome浏览器的无GUI的命令行版浏览器,但功能上和我们平常使用的chrome没有区别,若该参数不设置为true,则在程序运行的时候,chromedp会拉取我们环境中的chrome浏览器,显示页面
- chromedp.Flag("blink-settings", "imagesEnabled=false")设置为不显示图片
- chromedp.UserAgent(
Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36
) UserAgent是用于设置默认用户代理头的命令行选项。 - chromedp.Run() 运行一个chrome的一系列操作
- chromedp.Sleep() 延时
- chromedp.Navigate() 将浏览器导航到某个页面
- chromedp.WaitVisible() 等候某个元素可见,再继续执行。
- chromedp.Click() 模拟鼠标点击某个元素
- chromedp.Value() 获取某个元素的value值
- chromedp.ActionFunc() 再当前页面执行某些自定义函数
- chromedp.Text() 读取某个元素的text值
- chromedp.Evaluate() 执行某个js,相当于控制台输入js
- network.SetExtraHTTPHeaders() 截取请求,额外增加header头
- chromedp.SendKeys() 模拟键盘操作,输入字符
- chromedp.Nodes() 根据xpath获取某些元素,并存储进入数组
- chromedp.NewRemoteAllocator
- chromedp.OuterHTML() 获取元素的outer html
- chromedp.Screenshot() 根据某个元素截图
- page.CaptureScreenshot() 截取整个页面的元素
- chromedp.Submit() 提交某个表单
- chromedp.WaitNotPresent() 等候某个元素不存在,比如“正在搜索。。。”
- chromedp.Tasks{} 一系列Action组成的任务
Goquery
在写爬虫的时候,想要对HTML内容进行选择和查找匹配时通常是不直接写正则表达式的:因为正则表达式可读性和可维护性比较差。用Python写爬虫这方面可选择的方案非常多了,其中有一个被开发者常用的库pyquery,而Golang也有对应的goquery,可以说goquery是jQuery的Golang版本实现。借用jQueryCSS选择器的语法可以非常方面的实现内容匹配和查找。
示例代码(总)
package main
import (
"context"
"fmt"
"log"
"strings"
"time"
"github.com/PuerkitoBio/goquery"
"github.com/chromedp/chromedp"
)
// 全局数据--列表
var htmlContent string
// 全局数据--详情
var infoContent string
// 初始化chromedb
func initchromedp() (c context.Context) {
// 初始化chromedp
options := []chromedp.ExecAllocatorOption{
chromedp.Flag("headless", true), // debug使用
chromedp.Flag("blink-settings", "imagesEnabled=false"),
chromedp.UserAgent(`Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36`),
}
//初始化参数,先传一个空的数据
options = append(chromedp.DefaultExecAllocatorOptions[:], options...)
c, _ = chromedp.NewExecAllocator(context.Background(), options...)
return c
}
//获取网站上爬取的数据
func GetHttpHtmlContent(url string, selector string, sel interface{}) {
// 初始化chromedb
c := initchromedp()
// create context
chromeCtx1, cancel := chromedp.NewContext(c, chromedp.WithLogf(log.Printf))
chromeCtx2, cancel := chromedp.NewContext(c, chromedp.WithLogf(log.Printf))
// 执行一个空task, 用提前创建Chrome实例
// chromedp.Run(chromeCtx1, make([]chromedp.Action, 0, 1)...)
// 创建一个上下文,超时时间为40s---首页
timeoutCtx1, cancel := context.WithTimeout(chromeCtx1, 40*time.Second)
// 创建一个上下文,超时时间为40s---列表
timeoutCtx2, cancel := context.WithTimeout(chromeCtx1, 40*time.Second)
// 创建一个上下文,超时时间为40s---详情
timeoutCtx3, cancel := context.WithTimeout(chromeCtx2, 40*time.Second)
// 第一步:初始化首页 爬取首页数据
err := chromedp.Run(timeoutCtx1, initvisitweb(url, selector, sel))
if err != nil {
log.Fatal(err)
}
// 获取首页数据
doc, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
if err != nil {
log.Fatal(err)
}
// 循环li标签
doc.Find("li").Each(func(i int, s *goquery.Selection) {
// 获取a标签中的标题数据
band := s.Find("a").Text()
// 获取span标签中的时间数据
time := s.Find("span").Text()
// 获取详情链接
href, _ := s.Find("a").Attr("href")
// 获取某消息详情html数据
err = chromedp.Run(timeoutCtx3, GetInfo(href))
infodoc, err := goquery.NewDocumentFromReader(strings.NewReader(infoContent))
if err != nil {
log.Fatal(err)
}
// 详情标题
// title := infodoc.Find("h1").Text()
// 详情基础信息---html
em, _ := infodoc.Find("h6").Find("em").Html()
// 详情内容---html
// content, _ := infodoc.Find(".content").Html()
fmt.Println(i, band, time, href, em)
})
defer cancel()
// 循环翻页
for i := 0; i < 2; i++ {
//执行下一页
err = chromedp.Run(timeoutCtx2, DoCrawler(selector, sel)) //执行爬虫任务
doc, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
if err != nil {
log.Fatal(err)
}
doc.Find("li").Each(func(i int, s *goquery.Selection) {
// 获取a标签中的标题数据
band := s.Find("a").Text()
// 获取span标签中的时间数据
time := s.Find("span").Text()
// 获取详情链接
href, _ := s.Find("a").Attr("href")
// 获取某消息详情html数据
err = chromedp.Run(timeoutCtx3, GetInfo(href))
infodoc, err := goquery.NewDocumentFromReader(strings.NewReader(infoContent))
if err != nil {
log.Fatal(err)
}
// 详情标题
// title := infodoc.Find("h1").Text()
// 详情基础信息---html
em, _ := infodoc.Find("h6").Find("em").Html()
// 详情内容---html
// content, _ := infodoc.Find(".content").Html()
fmt.Println(i, band, time, href, em)
})
}
// return htmlContent, nil
}
// 初始化访问
func initvisitweb(url string, selector string, sel interface{}) chromedp.Tasks {
return chromedp.Tasks{
chromedp.Navigate(url), // 将浏览器导航到某个页面
chromedp.WaitVisible(selector), // 等候某个元素可见,再继续执行。
chromedp.OuterHTML(sel, &htmlContent, chromedp.ByJSPath), //获取指定标签的html
}
}
// 任务 主要执行翻页功能和或者html
func DoCrawler(selector string, sel interface{}) chromedp.Tasks {
return chromedp.Tasks{
chromedp.Click(`#pageDec > span:nth-child(3)`), //点击翻页
chromedp.Sleep(1 * time.Second), // 延时
chromedp.WaitVisible(selector), // 等候某个元素可见,再继续执行。
chromedp.OuterHTML(sel, &htmlContent, chromedp.ByJSPath), //获取指定标签的html
}
}
// 获取详情
func GetInfo(href string) chromedp.Tasks {
return chromedp.Tasks{
chromedp.Navigate(href), // 将浏览器导航到某个页面
chromedp.WaitVisible("body > div.wrap > div.main > div.mt15.details-box > div.details-main"), // 等候某个元素可见,再继续执行。
chromedp.OuterHTML(`document.querySelector(".details-main")`, &infoContent, chromedp.ByJSPath), //获取指定标签的html
}
}
func main() {
// 获取html
GetHttpHtmlContent("https://www.henan.gov.cn/ywdt/szf/", "body > div.wrap > div.main > div.mt15.list-box", `document.querySelector(".mt15")`)
}
代码解析
1. 入口
参数分别为:
https://www.henan.gov.cn/ywdt/szf/ 爬取的地址
body > div.wrap > div.main > div.mt15.list-box 获取的元素 (参考下面翻页函数)
document.querySelector(".mt15") 从class="mt15"中获取**
func main() {
// 获取html
GetHttpHtmlContent("https://www.henan.gov.cn/ywdt/szf/", "body > div.wrap > div.main > div.mt15.list-box", `document.querySelector(".mt15")`)
}
2. 初始化Chromedp
初始化chromedp操作,重新设置chromedp使用"有头"的方式打开,以便于我们进行debug。"imagesEnabled=false"页面不显示图片,返回c context.Contex
// 初始化chromedp
func initchromedp() (c context.Context) {
// 初始化chromedp
options := []chromedp.ExecAllocatorOption{
chromedp.Flag("headless", true), // debug使用
chromedp.Flag("blink-settings", "imagesEnabled=false"),
chromedp.UserAgent(`Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36`),
}
//初始化参数,先传一个空的数据
options = append(chromedp.DefaultExecAllocatorOptions[:], options...)
c, _ = chromedp.NewExecAllocator(context.Background(), options...)
return c
}
3. 初始化页面
初始化访问,获取某网页首页html数据
// 初始化访问
func initvisitweb(url string, selector string, sel interface{}) chromedp.Tasks {
return chromedp.Tasks{
chromedp.Navigate(url), // 将浏览器导航到某个页面
chromedp.WaitVisible(selector), // 等候某个元素可见,再继续执行。
chromedp.OuterHTML(sel, &htmlContent, chromedp.ByJSPath), //获取指定标签的html
}
}
4. 翻页任务
处理翻页,操作点击下一页等待元素可见,获取下一页数据。
#pageDec > span:nth-child(3)
:页面F12,右键元素检查>右键标签Copy>Copy selector获取
imagePreview (2).png
// 任务 主要执行翻页功能和或者html
func DoCrawler(selector string, sel interface{}) chromedp.Tasks {
return chromedp.Tasks{
chromedp.Click(`#pageDec > span:nth-child(3)`), //点击翻页
chromedp.Sleep(1 * time.Second), // 延时
chromedp.WaitVisible(selector), // 等候某个元素可见,再继续执行。
chromedp.OuterHTML(sel, &htmlContent, chromedp.ByJSPath), //获取指定标签的html
}
}
5. 获取详情
跳转详情页面,等待元素可见,获取指定html
imagePreview (3).png
// 获取详情
func GetInfo(href string) chromedp.Tasks {
return chromedp.Tasks{
chromedp.Navigate(href), // 将浏览器导航到某个页面
chromedp.WaitVisible("body > div.wrap > div.main > div.mt15.details-box > div.details-main"), // 等候某个元素可见,再继续执行。
chromedp.OuterHTML(`document.querySelector(".details-main")`, &infoContent, chromedp.ByJSPath), //获取指定标签的html
}
}
6. 总代码逻辑:
GetHttpHtmlContent函数先调用initchromedp初始化,创建三个上下文,第一个用于进入首页,第二个用于翻页,第三个用于查看详情
1. 操作浏览器进入首页(此配置不会调出真实浏览器),获取首页数据,根据标题href获取详情数据
2. 第一步执行过后,当前页还是在首页,在循环里调用翻页操作获取下一页的数据以及下一页所有文章详情
3. 注:循环次数为页面的页数(后期可优化)
//获取网站上爬取的数据
func GetHttpHtmlContent(url string, selector string, sel interface{}) {
// 初始化chromedb
c := initchromedp()
// 初始化上下文
chromeCtx1, cancel := chromedp.NewContext(c, chromedp.WithLogf(log.Printf))
chromeCtx2, cancel := chromedp.NewContext(c, chromedp.WithLogf(log.Printf))
// 执行一个空task, 用提前创建Chrome实例
// chromedp.Run(chromeCtx1, make([]chromedp.Action, 0, 1)...)
// 创建一个上下文,超时时间为40s---首页
timeoutCtx1, cancel := context.WithTimeout(chromeCtx1, 40*time.Second)
// 创建一个上下文,超时时间为40s---列表
timeoutCtx2, cancel := context.WithTimeout(chromeCtx1, 40*time.Second)
// 创建一个上下文,超时时间为40s---详情
timeoutCtx3, cancel := context.WithTimeout(chromeCtx2, 40*time.Second)
// 第一步:初始化首页 爬取首页数据
err := chromedp.Run(timeoutCtx1, initvisitweb(url, selector, sel))
if err != nil {
log.Fatal(err)
}
// 获取首页数据
doc, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
if err != nil {
log.Fatal(err)
}
// 循环li标签
doc.Find("li").Each(func(i int, s *goquery.Selection) {
// 获取a标签中的标题数据
band := s.Find("a").Text()
// 获取span标签中的时间数据
time := s.Find("span").Text()
// 获取详情链接
href, _ := s.Find("a").Attr("href")
// 获取某消息详情html数据
err = chromedp.Run(timeoutCtx3, GetInfo(href))
infodoc, err := goquery.NewDocumentFromReader(strings.NewReader(infoContent))
if err != nil {
log.Fatal(err)
}
// 详情标题
// title := infodoc.Find("h1").Text()
// 详情基础信息---html
em, _ := infodoc.Find("h6").Find("em").Html()
// 详情内容---html
// content, _ := infodoc.Find(".content").Html()
fmt.Println(i, band, time, href, em)
})
defer cancel()
// 循环翻页
for i := 0; i < 2; i++ {
//执行下一页
err = chromedp.Run(timeoutCtx2, DoCrawler(selector, sel)) //执行爬虫任务
doc, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
if err != nil {
log.Fatal(err)
}
doc.Find("li").Each(func(i int, s *goquery.Selection) {
// 获取a标签中的标题数据
band := s.Find("a").Text()
// 获取span标签中的时间数据
time := s.Find("span").Text()
// 获取详情链接
href, _ := s.Find("a").Attr("href")
// 获取某消息详情html数据
err = chromedp.Run(timeoutCtx3, GetInfo(href))
infodoc, err := goquery.NewDocumentFromReader(strings.NewReader(infoContent))
if err != nil {
log.Fatal(err)
}
// 详情标题
// title := infodoc.Find("h1").Text()
// 详情基础信息---html
em, _ := infodoc.Find("h6").Find("em").Html()
// 详情内容---html
// content, _ := infodoc.Find(".content").Html()
fmt.Println(i, band, time, href, em)
})
}
// return htmlContent, nil
}