关于使用rod做了一个大规模爬虫的一些思考
昨天花了一点时间,优化了一个版本的rod爬虫,也算是为大规模采集某个外文站点来练手吧,我主要是抓取一个论坛上的一些内容,所以开始准备用colly来做这个事情,毕竟使用的次数比较多了,相对比较熟悉,但是这个站点想象的还是比我实际操作起来要复杂的,本文我会分成三部分,第一部分讲采集目标站点面临的一些困难,第二部分讲如何解决这些困难,第三部分是我在开发过程中萌生思考,想要解决最终也试图着力解决,但是在某个方向上稍微入手了一些,发现其实其实众里寻它千百度,那人却在灯火阑珊处,相信对于你做分布式爬虫或者会给出一些全新的思考,开始吧!
第一部分 采集目标站点面临的一些困难
- 首先这个站点是需要做一些交互的,虽然没有特别复杂的交互操作,但是对于rod内置的一些方法还有熟悉的过程,所以并非只需要做单独的dom节点指定解析
- 这个站点列表页进到详情页里,到的不是最终的详情页,而是详情汇总页,你还需要进一步再解析详情页来,如果你有过一些图片站的采集经历,一定有过相同的体验,因为这也是提高浏览量的有效方式
- 对应的站点有反爬机制,如何应对
- 针对于一些资源的采集,要过滤重复采集,简单的url去重并不能解决所有问题,要有个健壮性更强的方案
- 你如果对于golang的goroutine理解不清晰的话,不知道如何有效做并发控制,可能你不能把golang的高并发特性有效的发挥出来
第二部分 如何解决这些困难
-
第一个问题相对比较简单,你需要对于方法使用有个熟悉的过程,是个熟能生巧的过程,这里要强调一下,就是针对
page.MustWaitLoad()
的使用,当然我前面也尝试过一些自定义的处理方式,比如说我会定义ctx参数,目的是构建运行协程上下文,然后把我的超时时间设置在传递的browser对象的做page请求里面,发现失败率特别高,而后也采用过直接加sleep比较长的秒数,还是会有一定几率的请求失败,甚至也尝试过sleeper func() utils.Sleeper
效果类似于sleep,后来就过了一遍文档,终究文档才是最好的老师,就找到了page.MustWaitLoad()
,等待页面加载完全,这样才能有效进行解析 -
第二个问题,这里可能不同站点的结构不一样,具体问题具体分析,比如有些网站要你自己一些一些点交互ajax来跳页,有些网站是直接在详情汇总页能读到总页数,不同情形不同解法,我还见过一个国外的drupal站点的内容,详情页的内容的展示都需要你自己点击页面按钮或者下拉做主体内部部分的刷新,其实都是一些防爬手段。这里就要注意如果使用golang的并发特性,要注意浏览器的正解关闭,也就是一个goroutine结束任务,完成生命周期,你要调用
defer page.MustClose()
防止爆显存,非常重要,可能对于不同任务的生命周期,你需要有自己的理解,多尝试,关于goroutine特性的使用,我后面还会继续说! -
第三个问题,关于重复数据的过滤,最简单的方式就是用redis,比如你爬一些数据实时敏感性不高的数据,比如资讯,比如图片之类的,你用简单的方式即可,但是针对于页面频繁变动,且这个变动会影响最终结果准确性的数据,你肯定不可以这么粗暴,稍微上点强度,可以用简单的hash或者md5方式:
func hasPageChanged(newContent, oldHash string) bool {
newHash := sha256.Sum256([]byte(newContent))
return fmt.Sprintf("%x", newHash) != oldHash
}
或者: 页面screenshot截图比较的方式; 或者: 一些页面dom比较器,这比较小众了; 或者: 使用布隆过滤,这个redis新版本中已经内置成了扩展,安装之后就等于原生支持了,效率非常高,因为是使用位运算来做存储的,节省空间,多次哈希碰撞出现几率也小,当然肯定也是有缺点的,现在chatgpt都出来了,不需要我科普,自己感兴趣查一查; 或者: Trie(Prefix Tree) 等等等等..
- 这个解决方案也太多了,我使用的方式就是随机的浏览器头部加上随机的代理,这个代理可以自己找免费的,我用的付费的,免费的可用的ip太少了,挺浪费时间的
- 这点就非常关键了,goroutine其实就就需要你对于协程的生命周期有起码一些理解:这里放个题目吧,针对于我的这段代码,让你改成并发版本,你怎么改?
func visitAllPages(browser *rod.Browser, pageURL string, wg *sync.WaitGroup, logger *log.Logger) {
var pageNumbers []int
threadID, err := extractThreadID(pageURL, logger)
if err != nil {
logger.Println("Error extracting thread ID:", err)
return
}
logger.Println("Extracted thread ID:", threadID)
page, err := browser.Page(proto.TargetCreateTarget{URL: pageURL})
if err != nil {
log.Printf("Failed to create page: %v", err)
return
}
defer func() {
if err := page.Close(); err != nil {
log.Printf("Failed to close page: %v", err)
}
}()
page = page.Timeout(50 * time.Second)
selector := ".pagination .page-item a"
page.MustWaitLoad()
elements, err := page.Elements(selector)
if err != nil {
logger.Println(selector+" Page loaded failed:", pageURL)
}
if len(elements) == 0 {
logger.Println("No pagination found, scraping this page directly:", pageURL)
extractImageFromDetailPage(browser, pageURL, sleeper, wg, logger)
} else {
logger.Printf("Pagination found with %d elements\n", len(elements))
for _, el := range elements {
pageText := el.MustText()
if pageText != "«" && pageText != "»" && pageText != "..." {
pageNum, err := strconv.Atoi(pageText)
if err == nil {
pageNumbers = append(pageNumbers, pageNum)
} else {
logger.Println("Error converting page number:", err)
}
}
}
if len(pageNumbers) > 0 {
lastPage := pageNumbers[len(pageNumbers)-1] // The last page number
logger.Printf("Total pages found: %d\n", lastPage)
for pageNum := 1; pageNum <= lastPage; pageNum++ {
currentPageURL := fmt.Sprintf("https://domain/xx/%s/%d", threadID, pageNum)
logger.Printf("Visiting page: %s\n", currentPageURL)
extractImageFromDetailPage(browser, currentPageURL, sleeper, wg, logger)
}
} else {
logger.Println("No valid pagination found")
}
}
}
面对一个没有分布式的工具,你需要重复造轮子,搞个分布式功能给它吗?
这个仁者见仁,智者见智,我先给出我的结论,在云原生如火如荼的今天,你不需要对于没有分布式功能的单体应用做分布式版本,我的理由是:
首先,为什么我们想要做成分布式的功能,或者早期的单进程应用,你想做成并发版本,我们在考虑这些扩展的时候,实际我们在思考什么?
你应该有答案了,在性能有限的情况下最大限度的压缩性能,在性能不变的情况下通过加机器,整体提高执行效率,缩短时间!但这些行为终极目的是什么?
任务调度!
你可能要写个任务队列,把任务放到队列里,这样你起10台20台100台机器,可以一起消费队列里的任务,效率就提升了,在单机上面,你可能要用多开器,或者没有冲突的命名文件进行执行,就是为了压榨系统的最大性能!
可以kubernetes出现之后,这些都可以通过集群本身强大的调度功能来形成支持,你根本就不必再做什么任务队列了!
kubernetes本身也比较重,你可能觉得还是你自己实施一个业务逻辑出来比较合适,如果你觉得你能压缩硬件投资到非常高的收益,高过在分布式功能的研发上面,并且能实质性的达到kubernetes那个稳定的调度服务编排功能,也可以尝试!
到于并发版本,golang做成并发版本很简单,但是做到对于并发任务的完全可控,其实还是需要一些爬虫组织架构的,这个我会在我的内部课程里分享,感谢阅读!