开发复杂爬虫系统的经验与思考
共 8550字,需浏览 18分钟
·
2021-07-27 19:10
大家好,欢迎来到 Crossin的编程教室 !
爬虫系统是很多Python开发者会遇到的需求。在开发中,往往会踩到各种无法预知的坑。今天给大家分享一篇关于爬虫系统开发的经验总结,让大家在技术上少走弯路。
(本文仅作技术层面的探讨,对于爬虫及互联网数据的使用,请遵循知识产权及相关法律法规!)
前言
爬虫是互联网业务开发中重要的一项技术,我们之前积累了不少爬虫使用的经验,在此分享给大家,希望能对大家之后的业务发展提供一些技术选型方向上的思路。
我们将会从以下几点来分享我们的经验:
爬虫的应用场景 爬虫的技术选型 实战详解:复杂场景下的爬虫解决方案 爬虫管理平台
爬虫的应用场景
在生产上,爬虫主要应用在以下几种场景
搜索引擎,Google,百度这种搜索引擎公司每天启动着无数的爬虫去抓取网页信息,才有了我们使用搜索引擎查询资料的便捷、全面、高效 冷数据启动时丰富数据的主要工具,新业务开始时,需要抓取数据填充以便早期的测试运行 数据服务或聚合的公司,比如天眼查,企查查,西瓜数据等等 提供横向数据比较,聚合服务,比如说电商中经常需要有一种比价系统
爬虫的技术选型
接下来我们就由浅入深地为大家介绍爬虫常用的几种技术方案
简单的爬虫
说起爬虫,大家可能会觉得技术比较高深,会立刻联想到使用像 Scrapy 这样的爬虫框架,这类框架确实很强大,那么是不是一写爬虫就要用框架呢?非也!要视情况而定,如果我们要爬取的接口返回的只是很简单,固定的结构化数据(如JSON),用 Scrapy 这类框架的话有时无异于杀鸡用牛刀,不太经济!
举个简单的例子,业务中有这么一个需求:需要抓取育学园中准妈妈从「孕4周以下」~「孕36个月以上」每个阶段的数据
对于这种请求,bash 中的 curl 足堪大任!
首先我们用 charles 等抓包工具抓取此页面接口数据,如下
通过观察,我们发现请求的数据中只有 month 的值(代表孕几周)不一样,所以我们可以按以下思路来爬取所有的数据:
1、 找出所有「孕4周以下」~「孕36个月以上」对应的 month 的值,构建一个 month 数组 2、 构建一个以 month 值为变量的 curl 请求,在 charles 中 curl 请求我们可以通过如下方式来获取
3、 依次遍历步骤 1 中的 month,每遍历一次,就用步骤 2 中的 curl 和 month 变量构建一个请求并执行,将每次的请求结果保存到一个文件中(对应每个孕期的 month 数据),这样之后就可以对此文件中的数据进行解析分析。
示例代码如下,为了方便演示,中间 curl 代码作了不少简化,大家明白原理就好
#!/bin/bash
## 获取所有孕周对应的 month,这里为方便演示,只取了两个值
month=(21 24)
## 遍历所有 month,组装成 curl 请求
for month in ${month[@]};
do
curl -H 'Host: yxyapi2.drcuiyutao.com'
-H 'clientversion: 7.14.1'
...
-H 'birthday: 2018-08-07 00:00:00'
--data "body=month%22%3A$month" ## month作为变量构建 curl 请求
--compressed 'http://yxyapi2.drcuiyutao.com/yxy-api-gateway/api/json/tools/getBabyChange' > $var.log ## 将 curl 请求结果输出到文件中以便后续分析
done
而如果你熟悉Python,用requests库也是两三行代码的事。(参考:让你的爬虫开发效率提升8倍)
看了上面的例子,是否觉得爬虫不过如此,没错,业务中很多这种简单的爬虫实现可以应付绝大多数场景的需求!
脑洞大开的爬虫解决思路
按以上介绍的爬虫思路可以解决日常多数的爬虫需求,但有时候我们需要一些脑洞大开的思路,简单列举两个
1、 去年有同学给了一个有关奶粉的 url 的链接
https://m.tmall.com/mblist/de_9n40_AVYPod5SU93irPS-Q.html
,他们希望能提取此文章的信息,同时找到其中所有提到奶粉
关键字的文章并提取其内容, 这就需要用到一些搜索引擎的高级技巧了, 我们注意到,url 是以以下形式构成的
https://m.tmall.com/mblist/de_ + 每篇文章独一无二的签名
利用搜索引擎技巧我们可以轻松搞定这个需求
对照图片,步骤如下:
首先我们用在百度框输入高级查询语句「奶粉 site:m.tmall.com inurl:mblist/de_」,点击搜索,就会显示出此页中所有天猫精选中包含奶粉的文章 title 注意地址栏中浏览器已经生成了搜索的完整 url,拿到这个 url 后,我们就可以去请求此 url,此时会得到上图中包含有 3, 4 这两块的 html 文件 拿到步骤 2 中获取的 html 文件后,在区域 3 每一个标题其实对应着一个 url(以 <a href> ..... </a>)的形式存在,根据正则表达式就可以获取每个标题对应的 url,再请求这些 url 即可获取对应的文章信息。 同理,拿到步骤 2 中获取的 html 文件后,我们可以获取区域 4 每一页对应的 url,再依次请求这些 url,然后重复步骤 2,即可获取每一页天猫精选中包含有奶粉的文章
通过这种方式我们也巧妙地实现了需求,这种爬虫获取的数据是个 html 文件,不是 JSON 这些结构化数据,我们需要从 html 中提取出相应的 url 信息(存在 <a> 标签里),可以用正则,也可以用 xpath 来提取。
比如 html 中有如下 div 元素
<div id="test1">大家好!</div>
可以用以下的 xpath 来提取
data = selector.xpath('//div[@id="test1"]/text()').extract()[0]
就可以把「大家好!」提取出来,需要注意的是在这种场景中,「依然不需要使用 Scrapy 这种复杂的框架」,在这种场景下,由于数据量不大,使用单线程即可满足需求
2、 某天有同学又提了一个需求,想爬取网上的视频
通过抓包我们发现每个视频的 url 都很简单,输入到浏览器查看也能正常看视频,于是我们想当然地认为直接通过此 url 即可下载视频,但实际我们发现此 url 是分片的(m3u8,为了优化加载速度而设计的一种播放多媒体列表的档案格式),下载的视频不完整,后来我们发现打开`http://www.flvcd.com/`网站
输入视频地址转化一下就能拿到完整的视频下载地址
「如图示:点击「开始GO!」后就会开始解析视频地址并拿到完整的视频下载地址」
进一步分析这个「开始GO!」按钮对应的请求是「http://www.flvcd.com/parse.php?format=&kw= + 视频地址」,所以只要拿到视频地址,再调用 flvcd 的视频转换请求即可拿到完整的视频下载地址,通过这种方式我们也解决了无法拿到完整地址的问题。
复杂的爬虫设计
上文我们要爬取的数据相对比较简单, 数据属于拿来即用型,实际上我们要爬取的数据大部分是非结构化数据(html 网页等),需要对这些数据做进一步地处理(爬虫中的数据清洗阶段),而且每个我们爬取的数据中也很有可能包含着大量待爬取网页的 url,也就是说需要有 url 队列管理,另外请求有时候还需求登录,每个请求也需要添加 Cookie,也就涉及到 Cookie 的管理,在这种情况下考虑 Scrapy 这样的框架是必要的!不管是我们自己写的,还是类似 Scrapy 这样的爬虫框架,基本上都离不开以下模块的设计
url 管理器
网页(HTML)下载器, 对应 Python 中的urllib2, requests等库
(HTML)解析器,主要有两种方式来解析
下图详细解释了各个模块之间是如何配合使用的
正则表达式 以css, xpath为代表的结构化解析(即将文档以DOM树的形式重新组织,通过查找获取节点进而提取数据的方式), Python中的 html.parser,BeautifulSoup,lxml 皆是此类范畴
首先调度器会询问 url 管理器是否有待爬取的 url 如果有,则获取出其中的 url 传给下载器进行下载 下载器下载完内容后会将其传给解析器做进一步的数据清洗,这一步除了会提取出有价值的数据,还会提取出待爬取的URL以作下一次的爬取 调度器将待爬取的URL放到URL管理器里,将有价值的数据入库作后续的应用 以上过程会一直循环,直到再无待爬取URL
可以看到,像以上的爬虫框架,如果待爬取 URL 很多,要下载,解析,入库的工作就很大,就会涉及到多线程,分布式爬取。Python 由于其本身支持多线程,协程等特性,来实现这些比较复杂的爬虫设计就绰绰有余了,同时由于 Python 简洁的语法特性,吸引了一大波人写了很多成熟的库,各种库拿来即用,很是方便,大名鼎鼎的 Scrapy 框架就是由于其丰富的插件,易用性俘获了大批粉丝。所以接下来我们就简要介绍一下 Scrapy,同时也来看看一个成熟的爬虫框架是如何设计的。
我们首先要考虑一下爬虫在爬取数据过程中会可能会碰到的一些问题,这样才能明白框架的必要性以后我们自己设计框架时该考虑哪些点
url 队列管理:比如如何防止对同一个 url 重复爬取(去重),如果是在一台机器上可能还好,如果是分布式爬取呢 Cookie 管理:有一些请求是需要帐号密码验证的,验证之后需要用拿到的 Cookie 来访问网站后续的页面请求,如何缓存住 Cookie 以便后续进一步的操作 多线程管理:前面说了如果待爬取URL很多的话,加载解析的工作是很大的,单线程爬取显然不可行,那如果用多线程的话,管理又是一件大麻烦 User-Agent 与动态代理的管理: 目前的反爬机制其实也是比较完善的,如果我们用同样的UA,同样的IP不节制地连续对同一个网站多次请求,很可能立马被封, 此时我们就需要使用 random-ua ,动态代理来避免被封 动态生成数据的爬取:一般通过 GET 请求获取的网页数据是包含着我们需要的数据的,但有些数据是通过 Ajax 请求动态生成,这样的话该如何爬取 DEBUG 爬虫管理平台: 爬虫任务多时,如何查看和管理这些爬虫的状态和数据
从以上的几个点我们可以看出写一个爬虫框架还是要费不少功夫的,幸运的是,scrapy 帮我们几乎完美地解决了以上问题,让我们只要专注于写具体的解析入库逻辑即可, 来看下它是如何实现以上的功能点的
url 队列管理: 使用 scrapy-redis 插件来做 url 的去重处理,利用 redis 的原子性可以轻松处理url重复问题 Cookie管理: 只要做一次登录校验,就会缓存住Cookie,在此后的请求中自动带上此Cookie,省去了我们自己管理的烦恼 多线程管理: 只要在中间件中指定线程次数 CONCURRENT_REQUESTS = 3
,scrapy就可以为我们自己管理多线程操作,无需关心任何的线程创建毁灭生命周期等复杂的逻辑User-Agent与动态代理的管理: 使用 random-useragent
插件为每一次请求随机设置一个UA,使用蚂蚁(mayidaili.com)等代理为每一个请求头都加上proxy
这样我们的 UA 和 IP 每次就基本都不一样了,避免了被封的窘境动态数据(通过 ajax 等生成)爬取: 使用 Selenium + PhantomJs
来抓取抓动态数据DEBUG: 如何有效测试爬取数据是否正确非常重要,一个不成熟的框架很可能在我们每次要验证用 xpath,正则等获取数据是否正确时每一次都会重新去下载网页,效率极低,但Scray-Shell 提供了很友好的设计,它会先下载网页到内存里,然后你在 shell 做各种 xpath 的调试,直到测试成功! 使用 SpiderKeeper+Scrapyd 来管理爬虫, GUI 操作,简单易行
可以看到 Scrapy 解决了以上提到的主要问题,在爬取大量数据时能让我们专注于写爬虫的业务逻辑,无须关注 Cookie 管理,多线程管理等细节,极大地减轻了我们的负担,很容易地做到事半功倍!
(注意
! Scrapy 虽然可以使用 Selenium + PhantomJs
来抓取动态数据,但随着 Google 推出的 puppeter 的横空出世,PhantomJs 已经停止更新了,因为 Puppeter 比 PhantomJS 强大太多,所以如果需要大量地抓取动态数据,需要考虑性能方面的影响,Puppeter 这个 Node 库绝对值得一试,Google 官方出品,强烈推荐)
理解了 Scrapy 的主要设计思路与功能,我们再来看下如何用 Scrapy 来开发我们某个音视频业务的爬虫项目,来看一下做一个音视频爬虫会遇到哪些问题
音视频爬虫实战
一、先从几个方面来简单介绍我们音视频爬虫项目的体系
1、四个主流程
爬取阶段 资源处理(包括音频,视频,图片下载及处理) 正式入库 后处理阶段
2、目前支持的功能点
各类视频音频站点的爬取 内容同步更新 视频截图(视频内容无封面) 视频转码适配(flv 目前客户端不支持)
3、体系流程分布图
二、分步来讲下细节
1. 爬虫框架的技术选型
说到爬虫,大家应该会很自然与 python 划上等号,所以我们的技术框架就从 python 中比较脱颖而出的三方库选。scrapy 就是非常不错的一款。相信很多其他做爬虫的小伙伴也都体验过这个框架。
那么说说这个框架用了这么久感受最深的几个优点:
request 触发底层采用的是 python 自带的 yield 协程,可以节省内容的同时,回调式的编程方式也显得优雅舒适 对于 html 内容的高效筛选处理能力,selecter 的 xpath 真的很好用 由于迭代时间已经很长了,具备了很完善的扩展 api,例如:middlewares 就可以全局 hook 很多事件点,动态 ip 代理就可以通过 hook request_start 实现
2. 爬虫池 db 的设计
爬虫池 db 对于整个爬取链路来说是非常重要的关键存储节点,所以在早教这边也是经历了很多次的字段更迭。
最初我们的爬虫池 db 表只是正式表的一份拷贝,存储内容完全相同,在爬取完成后,copy 至正式表,然后就失去相应的关联。这时候的爬虫池完全就是一张草稿表,里面有很多无用的数据。
后来需要看爬虫的具体来源,这时候爬虫池里面即没有网站源链接,也无法根据正式表的专辑 id 对应到爬虫池的数据内容。所以,爬虫池 db 做出了最重要的一次改动。首先是建立爬虫池数据与爬取源站的关联,即source_link 与 source_from 字段,分别代表内容对应的网站原链接以及来源声明定义。第二步则是建立爬虫池内容与正式库内容的关联,为了不影响正式库数据,我们添加 target_id 对应到正式库的内容 id 上。此时,就可以满足获取爬取内容具体来源的需求了。
后续又发现,在大量的爬虫数据中筛选精品内容需要一些源站数据的参考值,例如:源站播放量等,此时爬虫池db 和正式库 db 存储内容正式分化,爬虫池不再只是正式库的一份拷贝,而是代表源站的一些参考数据以及正式库的一些基础数据。
而后来的同步更新源站内容功能,也是依赖这套关系可以很容易的实现。
整个过程中,最重要的是将本来毫无关联的 「爬取源站内容」 、 「爬虫池内容」 、 「正式库内容」 三个区块关联起来。
3. 为什么会产生资源处理任务
本来的话,资源的下载以及一些处理应该是在爬取阶段就可以一并完成的,那么为什么会单独产生资源处理这一流程。
首先,第一版的早教爬虫体系里面确实没有这一单独的步骤,是在scrapy爬取过程中串行执行的。但是后面发现的缺点是:
scrapy 自带的 download pipe 不太好用,而且下载过程中并不能并行下载,效率较低 由于音视频文件较大,合并资源会有各种不稳定因素,有较大概率出现下载失败。失败后会同步丢失掉爬取信息。 串行执行的情况下,会失去很多扩展性,重跑难度大。
针对以上的问题,我们增加了爬虫表中的中间态,即资源下载失败的状态,但保留已爬取的信息。然后,增加独立的资源处理任务,采用 python 的多线程进行资源处理。针对这些失败的内容,会定时跑资源处理任务,直到成功为止。(当然一直失败的,就需要开发根据日志排查问题了)
三、遇到的问题和解决方案
资源下载阶段经常出现中断或失败等问题【方案:将资源下载及相关处理从爬取过程中独立出来,方便任务重跑】 虽然是不同平台,但是重复资源太多,特别是视频网站【方案:资源下载前根据title匹配,完全匹配则过滤,省下了多余的下载时间消耗】 大量爬取过程中,会遇到ip被封的情况【方案:动态 ip 代理】 大型视频网站资源获取规则频繁替换(加密,视频切割,防盗链等),开发维护成本高【方案:you-get 三方库,该库支持大量的主流视频网站的爬取,大大减少开发维护成本】 爬取源站内容仍在更新中,但是我们的平台内容无法更新【方案:db 存入原站链接,根据差异性进行更新】 专辑爬取任务媒介存于服务器文本文件中,并需开发手动命令触发,耗费人力【方案:整合脚本逻辑,以 db 为媒介,以定时任务检测触发】 需要添加一些类似原站播放量等的数据【方案:之前爬虫表在将数据导入正式表后失去关联,现在建立起关联,在爬虫表添加爬虫原站相关数据字段】 接口中没有媒体文件相关信息,而自己平台需要,例如:时长【方案:ffmpeg 支持的媒体文件解析】 下载后的视频很多在客户端无法播放【方案:在资源上传前,进行格式和码率验证,不符合则进行相应的转码】
四、最后做下总结
对于上述爬虫结构体系,不一定能通用于所有的需求场景,但是同类问题的思考与解决方案确是可以借鉴与应用于各个业务线的,相信对大家会有不少启发
爬虫管理平台
当爬虫任务变得很多时,ssh+crontab 的方式会变得很麻烦, 需要一个能随时查看和管理爬虫运行状况的平台,
SpiderKeeper+Scrapyd 目前是一个现成的管理方案,提供了不错的UI界面。功能包括:
1.爬虫的作业管理:定时启动爬虫进行数据抓取,随时启动和关闭爬虫任务
2.爬虫的日志记录:爬虫运行过程中的日志记录,可以用来查询爬虫的问题
3.爬虫运行状态查看:运行中的爬虫和爬虫运行时长查看
总结
从以上的阐述中,我们可以简单地总结一下爬虫的技术选型
如果是结构化数据(JSON 等),我们可以使用 curl,或 requests 这些简单办法来处理即可 如果是非结构化数据(html 等),此时 bash 由于无法处理这类数据,需要用正则, xpath 来处理,也可以用 BeautifulSoup 来处理 如果待爬取的 url 很多,单线程无法应付,就需要多线程来处理了,又或者需要 Cookie 管理,动态 ip 代理等,这种情况下我们就得考虑 scrapy 这类高性能爬虫框架了
根据业务场景的复杂度选择相应的技术可以达到事半功倍的效果。我们在技术选型时一定要考虑实际的业务场景。
如果文章对你有帮助,欢迎转发/点赞/收藏~
作者:码海
_往期文章推荐_