如何写出好爬虫

写爬虫可以说是一个程序员必修课,因为上手简单,成效明显,深受各大培训机构和教学材料的青睐。于是无数新手也加入了造轮子的队列,写的爬虫满天飞。

说爬虫上手简单,是有原因的,只要了解了 http 请求和 html/xml 结构,谁都能做出一个可用的爬虫,再辅以一个好用的请求库和解析库,完成老板的任务简直就是分分钟的事情,老婆孩子再也不用担心我加班了有木有?

———————- 下面是转折的分割线 ———————-

但是,这样写出来的爬虫真的达到了生产级别吗,答案是否定的。码农界常说的一个词是 Robust,来看看怎样让我们的爬虫变得更加 Robust

编码问题

互联网上存在各种各样的编码,虽然现代的浏览器都能智能的识别编码,但是对于爬虫来说它们却是需要跨过的第一道坎。

多数网站都会在响应头中给出本站编码,我们只需要检测响应头中 content-type encoding 就能准确的得到站点的编码。再不济也会在 html meta 中标识出编码内容,检测 meta 中的 charset 属性就行了,例如 <meta http-equiv="Content-Type" content="text/html" charset="utf-8"> 就表示该网页使用的是 utf8 编码。

如果站长丧心病狂到什么提示都不给,那么我们就只能靠猜了,iconv 是一个不错的库,可以检测文本内容的编码,而且准确率比较高,在各种语言中也能找到对应的 iconv 模块。

如果最后还是检测不出对应的编码,那就默认当做目前最常见的 utf8 编码吧,这段简单的代码片段可以说明检测编码的过程。

压缩问题

某些网站会在响应内容是使用 gzip 压缩以便节约流量(例如整天被爬的知乎同学),遇到这种站点,爬虫也需要做特殊的解析才能得到最后的纯文本内容。

解析的过程与检测编码类似,先通过响应头检测网站使用的是什么压缩编码,然后使用对应的解压缩方法解压内容即可,使用压缩的网站会在响应头 Content-Encoding 中加上压缩编码,例如 Content-Type: gzip 就表示该网页使用的是 gzip 压缩,这段代码可以说明检测压缩编码的过程

添加 User-Agent

下面的问题就是与内容提供者斗智斗勇的过程了,有些站点会识别爬虫行为然后屏蔽掉一些爬虫的请求。我们要做的就是尽量让我们的爬虫看起来,像个人。

首先请求头中必须添加 User-Agent,如果你还不知道 user-agent 是什么,出门左转看了wiki再回来。由于 ip 轻易无法修改,但是 user-agent 却是可以任意修改的,如果内容网站条件较宽,只使用 user-agent 识别爬虫的话,那么我们就乖乖的写上一些常用浏览器的 user-agent,如果你不知道有哪些,去这里可以查到所有的 ua。

至于专业的搜索引擎,内容网站欢迎还来不及,怎么会屏蔽呢。所以它们一般都会使用自己的特定 ua 标识,例如 Baidu spider,Google spider 等,普通野生的爬虫就不要参考了。

合并相同请求

如果我们的内容提供者地址是由用户添加的,难免碰到会有重复地址的情况,这时最好就是将这些链接给合并掉,以免重复请求。例如 A 和 B 同时添加了链接 abc,那么我们的爬虫只需请求一次 abc 然后将内容分别返回给 A 和 B 就行了,或者请求 abc 之后缓存一段时间,当其他用户添加这个链接时,返回缓存内容就行了。我们的宗旨就是用最少的请求干最多的事。

错误

有的时候爬虫在请求网站时会碰到一些错误,这些有的是由于内容网站的错误,有些是由于权限或受到了屏蔽,在大多数情况下面这些错误都可以通过 http status code 来分辨(如果不知道什么是 http status code,再出门去看一遍 wiki)。

如果区分每个 http status code 太麻烦,有一个基本的原则就是看 code 前缀,例如:

  • 3xx 开头的表示这个地址被跳转到其他地址了,爬虫可以根据响应头跟进,很多库自动做了跳转跟进,返回最终网址的内容,所以找到合适的请求库最重要,例如request
  • 4xx 表示内容网站拒绝了你的请求,有可能是你的 ip 被屏蔽,或者这个地址需要验证。遇到这种情况,可以等待一段时间再次请求,并逐渐增加请求间隔,直到返回的响应头为 2xx。一般的屏蔽会在一段时间后解除。
  • 5xx 表示内容网站挂了或你的服务器网络挂了,遇到这种情况,我们能做的就只能等,等内容网站修复错误。

连续错误次数过多的网站就不要再爬取了,以便节约带宽资源

请求频率

上面提到了错误重试的问题,这一节我们聊一下调整请求频率以防止被屏蔽。

如果你的爬虫现在可以正常访问内容网站,不要玩的太 high,尽量约束一下自己的请求频率,一分钟一次已经是很多网站能够忍耐的底线了,如果内容更新不频繁,可以设置为 20 分钟一次。

此外,还能制定一些比较聪明的策略,例如发现网站内容较上次没有更新,那么下次请求间隔设置为 1.5 倍,依次递增,直到你设定的请求间隔上限。如果内容有了更新,再把请求间隔重置为最小值。这样既不影响及时得到网站的更新,也会尽量减小被屏蔽的风险。

如果你的爬虫 ip 已经被屏蔽了(怎样判断被屏蔽请参考上文),那么就消停一会儿,设定一个较长的请求间隔再尝试,直至解除屏蔽。

分布式

爬虫受制于单台服务器的带宽和请求数限制,往往在高配置的机器中也无法发挥最大的效能。所以将爬虫分布在多台低配高带宽的服务器上是比较合理的做法。至于如果分布式,这个话题聊起来就没完了,可以使用最基础的消息队列(例如 zmq, rabbitmq 或 redis)和经典的 master/worker 结构,来实现多台机器协同工作。

大数据

大数据没有以前那么火了,但是为了提高这篇文章的逼格,仍然是一个值得一提的话题。

我们知道互联网的世界是开放的,很多内容其实不需要我们亲自去爬取,搜索引擎已经帮我们收录了这些网站内容,合理利用搜索引擎的 site: 命令,有时候可以得到比亲自爬取更满意的结果。

Don’t be evil

大部分网站会在站点根目录下包含一个 robots.txt 文件,可以通过 http://域名/robots.txt 来访问,里面的内容表明了站长对于爬虫的限定,如果发现某些内容是站长不希望你爬取的,那么还是乖乖绕过吧。如果不知道 robots.txt 是什么,再去一次 wiki。

结语

以上简单介绍了一下我多年以来作为一名非大数据非分布式非社区明星开发的经验,抛砖引玉,希望能对读者有所帮助。

Comments