前一两年抓过某工商信息网站, 几三周时间大约抓了过千万多万张页面. 那时由于公司没啥经费, 报销又拖得很久, 不想花钱在很多机器和带宽上, 所以当时花了较多精力研究如何让一台爬虫机器达到抓取极限.
Python 爬虫这两年貌似成为了一项必备技能, 无论是搞技术的, 做产品的, 数据分析的, 金融的, 初创公司做冷启动的, 都想去抓点数据回来玩玩. 这里面绝大多数一共都只抓几万或几十万条数据, 这个数量级其实大可不必写爬虫, 使用 Chrome 插件 web scraper 或者让 selenium 驱动 Chrome 就好了, 会为你节省很多分析网页结构或研究如何登陆的时间.
本篇只关注如何让爬虫的抓取性能最大化上, 没有使用 scrapy 等爬虫框架, 就是多线程 + Python requests 库搞定.
对一个网站定向抓取几十万张页面一般只用解决访问频率限制问题就好了. 对机器内存, 硬盘空间, URL 去重, 网络性能, 抓取间隙时间调优一般都不会在意. 如果要设计一个单台每天抓取上百万张网页, 共有一亿张页面的网站时 , 访问频率限制问题就不是最棘手的问题了, 上述每一项都要很好解决才行. 硬盘存储, 内存, 网络性能等问题我们一项项来拆解.
Python 直播学习群: 556370268, 这里有资源共享, 技术解答, 还有小编从最基础的 Python 资料到项目实战的学习资料都有整理, 希望能帮助你更好的学习 python.
一, 优化硬盘存储
所以千万级网页的抓取是需要先设计的, 先来做一个计算题. 共要抓取一亿张页面, 一般一张网页的大小是 400KB 左右, 一亿张网页就是 1 亿 X200KB=36TB . 这么大的存储需求, 一般的电脑和硬盘都是没法存储的. 所以肯定要对网页做压缩后存储, 可以用 zlib 压缩, 也可以用压缩率更好的 bz2 或 pylzma .
但是这样还不够, 我们拿天眼查的网页来举例. 天眼查一张公司详情页的大小是 700KB .
image
对这张网页 zlib 压缩后是 100KB.
image
一亿个 100KB(9TB) 还是太大, 要对网页特殊处理一下, 可以把网页的头和尾都去掉, 只要 body 部分再压缩. 因为一张 html 页面里 < head></head > 和 < footer></footer > 大都是公共的头尾信息和 JS/CSS 代码, 对你以后做正文内容抽取不会影响 (也可以以后做内容抽取时把头尾信息补回去就好).
来看一下去掉头尾后的 HTML 页面大小是 300KB, 压缩后是 47KB.
image
一亿张就是 4T, 差不多算是能接受了. 京东上一个 4T 硬盘 600 多元.
二, 优化内存, URL 去重
再来说内存占用问题, 做爬虫程序为了防止重复抓取 URL, 一般要把 URL 都加载进内存里, 放在 set() 里面. 拿天眼查的 URL 举例:
https://www.tianyancha.com/company/23402373
这个完整 URL 有 44 个字节, 一亿个 URL 就是 4G, 一亿个 URL 就要占用 4G 内存 , 这还没有算存这一亿个 URL 需要的数据结构内存, 还有待抓取 URL, 已抓取 URL 还保存在内存中的 HTML 等等消耗的内存.
所以这样直接用 set() 保存 URL 是不建议的, 除非你的内存有十几个 G.
一个取巧的办法是截断 URL. 只把 URL:
https://www.tianyancha.com/company/23402373
的后缀: 23402373 放进 set() 里, 23402373 只占 8 个字节, 一亿个 URL 占 700 多 M 内存.
但是如果你是用的野云主机, 用来不断拨号用的非正规云主机, 这 700 多 M 内存也是吃不消的, 机器会非常卡.
就还需要想办法压缩 URL 的内存占用, 可以 使用 BloomFilter 算法 , 是一个很经典的算法, 非常适用海量数据的排重过滤, 占用极少的内存, 查询效率也非常的高. 它的原理是把一个字符串映射到一个 bit 上, 刚才 23402373 占 8 个字节, 现在只占用 1 个 bit(1 字节 = 8bit), 内存节省了近 64 倍, 以前 700M 内存, 现在只需要 10 多 M 了.
BloomFilter 调用也非常简单, 当然需要先 install 安装 bloom_filter:
from bloom_filter import BloomFilter
不过奇怪, bloom 里没有公有方法来判断 URL 是否重复, 我用的 contains () 方法, 也可能是我没用对, 不过判重效果是一样的.
三, 反抓取访问频率限制
单台机器, 单个 IP 大家都明白, 短时间内访问一个网站几十次后肯定会被屏蔽的. 每个网站对 IP 的解封策略也不一样, 有的 1 小时候后又能重新访问, 有的要一天, 有的要几个月去了. 突破抓取频率限制有两种方式, 一种是研究网站的反爬策略. 有的网站不对列表页做频率控制, 只对详情页控制. 有的针对特定 UA,referer, 或者微信的 H5 页面的频率控制要弱很多. 我在这两篇文章有讲到《 爬虫小偏方: 绕开登陆和访问频率控制 》《 爬虫小偏方二: 修改 referer 后可以不用登录了 》.
另一种方式就是多 IP 抓取, 多 IP 抓取又分 IP 代理池和 adsl 拨号两种, 我这里说 adsl 拨号的方式, IP 代理池相对于 adsl 来说, 我觉得收费太贵了. 要稳定大规模抓取肯定是要用付费的, 一个月也就 100 多块钱.
adsl 的特点是可以短时间内重新拨号切换 IP,IP 被禁止了重新拨号一下就可以了. 这样你就可以开足马力疯狂抓取了, 但是一天只有 24 小时合 86400 秒, 要 如何一天抓过百万网页, 让网络性能最大化也是需要下一些功夫的 , 后面我再详说.
至于有哪些可以 adsl 拨号的野云主机, 你在百度搜 "vps adsl", 能选择的厂商很多的. 大多宣称有百万级 IP 资源可拨号, 我曾测试过一段时间, 把每次拨号的 IP 记录下来, 有真实二三十万 IP 的就算不错了.
选 adsl 的一个注意事项是, 有的厂商拨号 IP 只能播出 C 段和 D 段 IP,110(A 段).132(B 段).3(C 段).2(D 段),A 和 B 段都不会变, 靠 C,D 段 IP 高频次抓取对方网站, 有可能对方网站把整个 C/D 段 IP 都封掉.
C/D 段加一起 255X255 就是 6 万多个 IP 全都报废, 所以要选拨号 IP 范围较宽的厂商. 你要问我哪家好, 我也不知道, 这些都是野云主机, 质量和稳定性本就没那么好. 只有多试一试, 试的成本也不大, 买一台玩玩一个月也就一百多元, 还可以按天买.
上面我为什么说不用付费的 IP 代理池?
因为比 adsl 拨号贵很多, 因为全速抓取时, 一个反爬做得可以的网站 10 秒内就会封掉这个 IP, 所以 10 秒就要换一个 IP, 理想状况下一天 86400 秒, 要换 8640 个 IP.
如果用付费 IP 代理池的话, 一个代理 IP 收费 4 分钱, 8640 个 IP 一天就要 345 元. adsl 拨号的主机一个月才 100 多元.
adsl 拨号 Python 代码
怎么拨号厂商都会提供的, 建议是用厂商提供的方式, 这里只是示例:
Windows 下用 os 调用 rasdial 拨号:
import os
Linux 下拨号:
import os
四, 网络性能, 抓取技术细节调优
上面步骤做完了, 每天能达到抓取五万网页的样子, 要达到百万级规模, 还需把网络性能和抓取技术细节调优.
1. 调试开多少个线程, 多长时间拨号切换 IP 一次最优.
每个网站对短时间内访问次数的屏蔽策略不一样, 这需要实际测试, 找出抓取效率最大化的时间点. 先开一个线程, 一直抓取到 IP 被屏蔽, 记录下抓取耗时, 总抓取次数, 和成功抓取次数. 再开 2 个线程, 重复上面步骤, 记录抓取耗时, 总的和成功的抓取次数. 再开 4 个线程, 重复上面步骤. 整理成一个表格如下, 下图是我抓天眼查时, 统计抓取极限和细节调优的表格:
image
爬虫多线程时间统计
从上图比较可以看出, 当有 6 个线程时, 是比较好的情况. 耗时 6 秒, 成功抓取 80-110 次. 虽然 8 个线程只耗时 4 秒, 但是成功抓取次数已经在下降了. 所以线程数可以设定为开 6 个.
开多少个线程调试出来了, 那多久拨号一次呢?
从上面的图片看到, 貌似每隔 6 秒拨号是一个不错的选择. 可以这样做, 但是我选了另一个度量单位, 就是每总抓取 120 次就重新拨号. 为什么这样选呢? 从上图也能看到, 基本抓到 120 次左右就会被屏蔽, 每隔 6 秒拨号其实误差比较大, 因为网络延迟等各种问题, 导致 6 秒内可能抓 100 次, 也可能抓 120 次.
2.requests 请求优化
要优化 requests.get(timeout=1.5) 的超时时间, 不设置超时的话, 有可能 get() 请求会一直挂起等待. 而且野云主机本身性能就不稳定, 长时间不回请求很正常. 如果要追求抓取效率, 超时时间设置短一点, 设置 10 秒超时完全没有意义. 对于超时请求失败的, 大不了以后再二次请求, 也比设置 10 秒的抓取效率高很多.
3. 优化 adsl 拨号等待时间
上面步骤已算把单台机器的抓取技术问题优化到一个高度了, 还剩一个优化野云主机的问题. 就是每次断开拨号后, 要等待几秒钟再拨号, 太短时间内再拨号有可能又拨到上一个 IP, 还有可能拨号失败, 所以要等待 6 秒钟 (测试值). 所以要把拨号代码改一下:
import os
而且 os.popen('rasdial 网络名称 adsl 账号名 adsl 密码') 拨号完成后, 你还不能马上使用, 那时外网还是不可用的, 你需要检测一下外网是否联通.
我使用 ping 功能来检测外网连通性:
import os
code 为 0 时表示联通, 不为 0 时还要重新拨号. 而 ping 也很耗时间的, 一个 ping 命令会 ping 4 次, 就要耗时 4 秒.
image
上面拨号等待 6 秒加上 ping 的 4 秒, 消耗了 10 秒钟. 上面猿人学 Python 说了, 抓 120 次才用 6 秒, 每拨号一次要消耗 10 秒, 而且是每抓 120 次就要重拨号, 想下这个时间太可惜了, 每天 8 万多秒有一半时间都消耗在拨号上面了, 但是也没办法.
当然好点的野云主机, 除了上面说的 IP 范围的差异, 就是拨号质量差异. 好的拨号等待时间更短一点, 拨号出错的概率要小一点.
通过上面我们可以轻松计算出一组抓取的耗时是 6 秒, 拨号耗时 10 秒, 总耗时 16 秒. 一天 86400 秒, 就是 5400 组抓取, 上面说了一组抓取是 120 次. 一天就可以抓取 5400X120=64 万张网页.
按照上述的设计就可以做到一天抓 60 多万张页面, 如果你把 adsl 拨号耗时再优化一点, 每次再节约 2-3 秒, 就趋近于百万抓取量级了.
另外野云主机一个月才 100 多, 很便宜, 所以你可以再开一台 adsl 拨号主机, 用两台一起抓取, 一天就能抓一百多万张网页. 几天时间就能镜像一个过千万网页的网站.
知识 Tips:
1. 为什么不用异步抓取?
没必要, 这里的整个抓取关键是网络性能, 而不是程序性能. 用异步把程序性能提高了, 单位时间的抓取次数是提高了, 但是这样反而会击中对方网站的访问频率控制策略.
2. 要计算对方的带宽压力, 不要抓取太过分了
抓取归抓取, 但不要影响对方网站, 把对方网站带宽都打满了.
一个中小型网站的带宽在 5M 以内, 大一点的网站带宽可能 10-30M, 超大型的另算.
一张网页 300KB, 对方一般会压缩后传输给浏览器, 就按压缩后 30KB 算, 你的爬虫一秒请求 20 次, 带宽就是 600KB. 可能一个网站每天都有几十个爬虫都在爬, 我们按有 10 个爬虫在同时抓取, 就是这些爬虫一秒内就要消耗 600KBX10=6M 带宽.
再加上还有正规爬虫, 人家网站上的正常用户访问这些, 算下来可能一共要消耗 10M 带宽. 一般的大中型网站都是吃不消的.
Python 直播学习群: 556370268, 这里有资源共享, 技术解答, 还有小编从最基础的 Python 资料到项目实战的学习资料都有整理, 希望能帮助你更好的学习 python.
来源: http://www.jianshu.com/p/8c2466d633e7