1.使用goquery爬取初始静态HTML文件中的元素

在用golang编写爬虫的过程中,goquery提供了非常方便的对于静态html页面元素提前的接口.

618ca8fae5161914932b44864acb31df.png

比如这种直接出现在出现在静态hmtl文件中的元素,就可以直接爬取

3bc278903cce44e2fe7ca4a308e63136.png
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获取

3f7e65bf04034d6dfcc7e92a578856e4.png

随后依然使用goquery来读取这些你想要的元素,这里要注意把获得的html文件(原string类型)转化为 Reader类型.

doc, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
	if err != nil {
		log.Fatal(err)
	}

至此,想法的爬取已经结束.

3. 拓展chromedp函数爬取想法下面的热评

获取了所有的想法之后,接下来需要获取的就是想法下面的热评了.

7b015fd04c542f6235bee382b82edbe9.png

问题不大,只需要点击一个按钮之后他就可以出现,在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)...)
}
  1. 迭代变量 i 每次的自增是2, 这个具体原因我也说不清楚,似乎是因为chromedp中的Query每次寻找某一元素都会产生两个结果,所以我们要每次都跳过一个伴随生成的元素.
  2. MouseClickNode()函数会自动将页面滚动到要点击的地方,如果需要点击处不在当前页面中的话. 而后台程序移动页面的速度非常的均匀和迅速,会导致阿乎的监控程序监测出非人为操作,从而弹出登录窗口.类似下图.
9ea99e22975ba92fa377b8422a0588f7.png

我原本想做一个模拟登录的过程,但是奈何阿乎的验证码系统有点难搞,所以我选择了最简单最暴力的方法,等待这个登录窗口出现之后,直接关闭这个登录窗口,即上述代码的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),
	)

至此,大功告成.