如何让Python爬虫一天抓取100万张网页
文的文字及图片来源于网络,仅供学习、交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理。
作者: 王平 源自:猿人学python
ps:如有需要python学习资料的小伙伴可以加点击下方链接自行获取
前一两年抓过某工商信息网站,几三周时间大约抓了过千万多万张页面。那时由于公司没啥经费,报销又拖得很久,不想花钱在很多机器和带宽上,所以当时花了较多精力研究如何让一台爬虫机器达到抓取极限。
本篇偏爬虫技术细节,先周知。 python爬虫这两年貌似成为了一项必备技能,无论是搞技术的,做产品的,数据分析的,金融的,初创公司做冷启动的,都想去抓点数据回来玩玩。这里面绝大多数一共都只抓几万或几十万条数据,这个数量级其实大可不必写爬虫,使用 chrome 插件 web scraper 或者让 selenium 驱动 chrome 就好了,会为你节省很多分析网页结构或研究如何登陆的时间。
本篇只关注如何让爬虫的抓取性能最大化上,没有使用scrapy等爬虫框架,就是多线程+python requests库搞定。
对一个网站定向抓取几十万张页面一般只用解决访问频率限制问题就好了。对机器内存,硬盘空间,url去重,网络性能,抓取间隙时间调优一般都不会在意。如果要设计一个单台每天抓取上百万张网页,共有一亿张页面的网站时,访问频率限制问题就不是最棘手的问题了,上述每一项都要很好解决才行。硬盘存储,内存,网络性能等问题我们一项项来拆解。
一、优化硬盘存储
所以千万级网页的抓取是需要先设计的,先来做一个计算题。共要抓取一亿张页面,一般一张网页的大小是400kb左右,一亿张网页就是1亿x200kb=36tb 。这么大的存储需求,一般的电脑和硬盘都是没法存储的。所以肯定要对网页做压缩后存储,可以用zlib压缩,也可以用压缩率更好的bz2或pylzma 。
但是这样还不够,我们拿天眼查的网页来举例。天眼查一张公司详情页的大小是700kb 。
对这张网页zlib压缩后是100kb。
一亿个100kb(9tb)还是太大,要对网页特殊处理一下,可以把网页的头和尾都去掉,只要body部分再压缩。因为一张html页面里<head></head>和<footer></footer>大都是公共的头尾信息和js/css代码,对你以后做正文内容抽取不会影响(也可以以后做内容抽取时把头尾信息补回去就好)。
来看一下去掉头尾后的html页面大小是300kb,压缩后是47kb。
一亿张就是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:
1 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:
1 from bloom_filter import bloomfilter # 生成一个装1亿大小的 2 bloombloom = bloomfilter(max_elements=100000000, error_rate=0.1) 3 # 向bloom添加url bloom.add('https://www.tianyancha.com/company/23402373') #判断url是否在bloombloom.__contains__('https://www.tianyancha.com/company/23402373')
不过奇怪,bloom里没有公有方法来判断url是否重复,我用的contains()方法,也可能是我没用对,不过判重效果是一样的。
三、反抓取访问频率限制
单台机器,单个ip大家都明白,短时间内访问一个网站几十次后肯定会被屏蔽的。每个网站对ip的解封策略也不一样,有的1小时候后又能重新访问,有的要一天,有的要几个月去了。突破抓取频率限制有两种方式,一种是研究网站的反爬策略。有的网站不对列表页做频率控制,只对详情页控制。有的针对特定ua,referer,或者微信的h5页面的频率控制要弱很多。 另一种方式就是多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拨号:
1 import os # 拨号断开 2 os.popen('rasdial 网络链接名称 /disconnect') # 拨号 3 os.popen('rasdial 网络链接名称 adsl账号 adsl密码')
linux下拨号:
1 import os # 拨号断开 2 code = os.system('ifdown 网络链接名称')# 拨号 3 code = os.system('ifup 网络链接名称')
四、网络性能,抓取技术细节调优
上面步骤做完了,每天能达到抓取五万网页的样子,要达到百万级规模,还需把网络性能和抓取技术细节调优。
1.调试开多少个线程,多长时间拨号切换ip一次最优。
每个网站对短时间内访问次数的屏蔽策略不一样,这需要实际测试,找出抓取效率最大化的时间点。先开一个线程,一直抓取到ip被屏蔽,记录下抓取耗时,总抓取次数,和成功抓取次数。 再开2个线程,重复上面步骤,记录抓取耗时,总的和成功的抓取次数。再开4个线程,重复上面步骤。整理成一个表格如下,下图是我抓天眼查时,统计抓取极限和细节调优的表格:
从上图比较可以看出,当有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秒钟(测试值)。所以要把拨号代码改一下:
1 import os # 断开拨号 2 os.popen('rasdial 网络名称 /disconnect') 3 time.sleep(6) # 拨号 4 os.popen('rasdial 网络名称 adsl账号名 adsl密码')
而且 os.popen(‘rasdial 网络名称 adsl账号名 adsl密码’) 拨号完成后,你还不能马上使用,那时外网还是不可用的,你需要检测一下外网是否联通。
我使用 ping 功能来检测外网连通性:
1 import os 2 code = os.system('ping www.baidu.com')
code为0时表示联通,不为0时还要重新拨号。而ping也很耗时间的,一个ping命令会ping 4次,就要耗时4秒。
上面拨号等待6秒加上 ping 的4秒,消耗了10秒钟。上面猿人学python说了,抓120次才用6秒,每拨号一次要消耗10秒,而且是每抓120次就要重拨号,想下这个时间太可惜了,每天8万多秒有一半时间都消耗在拨号上面了,但是也没办法。
当然好点的野云主机,除了上面说的ip范围的差异,就是拨号质量差异。好的拨号等待时间更短一点,拨号出错的概率要小一点。
按照上述的设计就可以做到一天抓60多万张页面,如果你把adsl拨号耗时再优化一点,每次再节约2-3秒,就趋近于百万抓取量级了。
上一篇: python高级编程——网络编程(二)
下一篇: Linux--进程管理--06