1.使用goquery爬取初始静态HTML文件中的元素
在用golang编写爬虫的过程中,goquery提供了非常方便的对于静态html页面元素提前的接口.
比如这种直接出现在出现在静态hmtl文件中的元素,就可以直接爬取
doc.Find("div[class="List-item"]").
Each(func(i int, selection *goquery.Selection) {
txt := selection.Find("div[class="RichContent-inner"]").
Find("span").Text()}
goquery支持这种Xpath风格的查询,在找到对应的节点后可以直接获取其中的属性值.
但是很快我又发现了问题,就是在一个页面中他的想法在元素审查这个界面有10个,但是在静态html文件中只有2个,也就是说审查元素和我们http请求获得的源码不一致.
所谓查看网页源代码,就是别人服务器发送到浏览器的原封不动的代码。这是爬虫获得的代码.
你那些在源码中找不到的代码(元素),那是在浏览器执行js动态生成的,这些能在审查元素中看到.
通过审查元素就看到就是浏览器处理过的最终的html代码。
在这里可行的解决方案之一就是采集在浏览器中最终生成的元素,这时就需要借助我们的第二个工具--chromedp.
2. 使用chromedp获取网页执行动态js后的代码(即审查元素)
chromedp 本质上就是一个浏览器调度包,它与Selenium非常类似,但是使用起来更加方便,更易拓展,而且对golang更加友好
go get -u github.com/chromedp/chromedp #安装chromedp
使用chromedp(当然,你必须安装了chrome浏览器),我们可以将需要浏览的网站的url传入,然后等待网页把全部的动态js执行完毕,生成最后的html文件,这时再获取其中的元素,便可以读取所有的元素.
chromedp的使用过程会大量牵涉到golang 中context包的应用, 想要深入理解的小伙伴可以去系统的学习一下这个包的使用.
options := []chromedp.ExecAllocatorOption{
chromedp.Flag("headless",false),
chromedp.Flag("blink-settings","imageEnable=false"),
chromedp.UserAgent(`Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko)`),
}
c,_ := chromedp.NewExecAllocator(context.Background(),options...)
chromeCtx, cancel := chromedp.NewContext(c,chromedp.WithLogf(log.Printf))
_ = chromedp.Run(chromeCtx, make([]chromedp.Action, 0, 1)...)
timeOutCtx, cancel := context.WithTimeout(chromeCtx,60*time.Second)
defer cancel()
log.Printf("chrome visit page%sn",url)
var htmlContent string
err := chromedp.Run(timeOutCtx,
chromedp.Navigate(url),
//需要爬取的网页的url
chromedp.WaitVisible(`div[class="List-item"]`),
//等待某个特定的元素出现
chromedp.OuterHTML(`document.querySelector("body")`,&htmlContent,chromedp.ByJSPath),
//生成最终的html文件并保存在htmlContent文件中
)
if err!=nil {
log.Fatal(err)
}
代码中的Useragent可以在浏览器中输入chrome://version获取
随后依然使用goquery来读取这些你想要的元素,这里要注意把获得的html文件(原string类型)转化为 Reader类型.
doc, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
if err != nil {
log.Fatal(err)
}
至此,想法的爬取已经结束.
3. 拓展chromedp函数爬取想法下面的热评
获取了所有的想法之后,接下来需要获取的就是想法下面的热评了.
问题不大,只需要点击一个按钮之后他就可以出现,在chromedp.run的条件里面加点击语句就和等待语句可以了
chromedp.WaitVisible(`div[class="List-item"]`),
chromedp.Click(`button[class="Button ContentItem-action Button--plain Button--withIcon Button--withLabel"]`),
//Click会点击第一个匹配的元素
chromedp.WaitVisible(`div[class="CommentListV2"]`),
//直到等到评论列表出现
但是这里出现了问题,一个网页page有多个想法,每个想法又有自己的评论,也就是说每个想法的评论按钮都需要点击,这是一个批量的操作,这时chromedp自身所提供的函数Click()已经无法满足需求了,也就是说需要自己造轮子.
在这里我直接把函数贴出来,其实非常简单,拥有些许编程功力的人在阅读了源码中的Click()函数应该都可以完成.只是有几点需要注意一下
func MultiClick(sel interface{}, opts ...QueryOption) QueryAction {
return QueryAfter(sel, func(ctx context.Context, execCtx runtime.ExecutionContextID, nodes ...*cdp.Node) error {
if len(nodes) < 1 {
return fmt.Errorf("selector %q did not return any nodes", sel)
}
fmt.Printf("nodes:%dn",len(nodes))
for i := 0; i < len(nodes); i+=2 {
MouseClickNode(nodes[i]).Do(ctx)
Query("div[class="Modal Modal--default signFlowModal"]",append(opts,NodeVisible)...)
//1.等待要求登录窗口弹出
Click("button[class="Button Modal-closeButton Button--plain"]")
//2.关闭要求登录窗口
}
return nil
}, append(opts, NodeVisible)...)
}
- 迭代变量 i 每次的自增是2, 这个具体原因我也说不清楚,似乎是因为chromedp中的Query每次寻找某一元素都会产生两个结果,所以我们要每次都跳过一个伴随生成的元素.
- MouseClickNode()函数会自动将页面滚动到要点击的地方,如果需要点击处不在当前页面中的话. 而后台程序移动页面的速度非常的均匀和迅速,会导致阿乎的监控程序监测出非人为操作,从而弹出登录窗口.类似下图.
我原本想做一个模拟登录的过程,但是奈何阿乎的验证码系统有点难搞,所以我选择了最简单最暴力的方法,等待这个登录窗口出现之后,直接关闭这个登录窗口,即上述代码的1、2处.
最后,在chromedp.run下面加入相应的action即可
err := chromedp.Run(timeOutCtx,
chromedp.Navigate(url),
chromedp.WaitVisible(`div[class="List-item"]`),
chromedp.Sleep(2*time.Second),
chromedp.MultiClick(`button[class="Button ContentItem-action Button--plain Button--withIcon Button--withLabel"]`),
//加入自己写的MultiClick函数
chromedp.WaitVisible(`div[class="CommentListV2"]`),
chromedp.OuterHTML(`document.querySelector("body")`,&htmlContent,chromedp.ByJSPath),
)
至此,大功告成.