基础知识
HTTP协议
我们浏览网页的浏览器和手机应用客户端与服务器通信几乎都是基于HTTP协议,而爬虫可以看作是一个另类的客户端,它把自己伪装成浏览器或者手机应用客户端,按照自己的逻辑贪婪的向服务器索取数据,如何向服务器索取数据,所以了解HTTP协议就显得很有必要了。
HTTP协议中文名称是超文本传输协议,是一个基于请求与响应模式的、无状态的、应用层的协议,常基于TCP的连接方式。请求和响应模式很好理解,客户端发送请求,服务器响应客户端的请求,就像学校食堂打菜一样,你和打菜阿姨说要哪份菜,她才会给你盛哪份菜。
无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。形象点说,可以把服务器看成是没有记忆的大学食堂打饭打菜,在每次请求中,阿姨并不知道你之前有没有打过菜,也不知道你是不是合法的学生,所以你只能一边举着学生证一边和阿姨说我要这个菜,阿姨看到你的学生证后才会给你打菜,而这个学生证就是你每次需要重传的数据信息。
当我们在浏览器地址栏中输入http://www.bilibili.com 并敲入回车后,浏览器会构造HTTP请求发送到服务器,在收到服务器的HTTP响应后,浏览器会解析页面,继续向服务器请求图片、视频、js脚本等数据,直到页面加载完成,最终展示给我们的就是B站主页了。这是我用Fiddler抓的包,展示的是HTTP最原生的面貌,接下来我就根据这张图具体的讲解HTTP协议,以及写爬虫需要关注的一些点。
HTTP请求由三部分组成,分别是: 请求行、消息报头、请求正文。 在接收和解释请求消息后,服务器返回一个HTTP响应消息,HTTP响应也是由三个部分组成,分别是:状态行、消息报头、响应正文。
HTTP方法
HTTP请求的请求行以一个方法符号开头,以空格分开,后面跟着请求的URI和协议的版本。请求方法都是大写,有很多种,常见的有GET POST DELETE PUT,各种方法之间的区别不大。
这里罗列了一些常用的方法,一般来讲,GET表示向服务器请求URI对应的资源,POST表示向服务器提交数据,DELETE表示删除数据,PUT表示修改数据。但这都是一种约定,没有强制的要求,如果你碰见用DELETE方法提交数据也没必要大惊小怪。在实际写爬虫的过程中,我们只需要按照抓包请求构造数据即可,没有必要在意用了什么方法。
报头字段
重点讲解几个写爬虫需要关注的字段
- User-Agent 出现在请求报头中,表示客户端的操作系统、浏览器型号版本等信息。服务器可以根据此报头向客户端返回不同的页面以适应客户端。有些网站(知乎)会校验此报头,不填写或者不主流的报头都不能拿到正常的页面。因此自己在写爬虫的时候最好将从浏览器中拷贝到代码中。
- Cookie 出现在请求抱头中,前面我们说过HTTP是基于请求与响应模式并且无状态的协议,之前举了打菜阿姨的例子,Cookie就相当于每次请求中的学生证,它可以记录用户的身份信息。当我们自己写爬虫的时候,如果需要登陆,并且登陆又有验证码或者短信验证时,最简单的方法就是从浏览器中把cookie拷贝到爬虫中,就可以骗过服务器了。
- Set-Cookie 出现在响应抱头中,让客户端更新页面关联的Cookie,还是拿食堂阿姨的例子,如果你的响应报头有这个字段,意思就是阿姨重新给你了一个学生证,下次打饭你得用最新的学生证,原来的学生证不好使。如果你在模拟浏览器或者客户端登陆,需要将此报头更新已有的Cookie,不过Scrapy和requests都可以自动更新,因此不需要你再手动设置。
- Content-Type 标明请求正文或者响应正文的格式,客户端或者服务器会根据此字段选择合适的方式解析正文内容,以下是一些常见的值
- Content-Length 标明请求正文或者响应正文的长度,在使用requests构造请求的时候,我们不需要显式的加上此字段,requests会根据请求正文自动计算添加。
- Content-Encoding 在某些情况下,正文会讲过压缩后传输,此字段会指明压缩的类型(gzip和压缩参数)
- Transfer-Encoding 如果正文内容过长,HTTP协议允许将此字段设置为chunked,然后分块传输响应正文
- Connection 在HTTP1.1之前的版本,不支持持久连接,所谓的持久链接意思就是:HTTP协议一般通过TCP协议实现,客户端和服务器经过TCP三次握手建立连接,在请求和响应结束之后,此连接继续保持,当之后还有请求的时候,就不需要重新通过三次握手再建立连接,这样可以极大的降低客户端和服务器的IO负载。
在自己写爬虫的时候,我们可以根据浏览器的抓包数据有选择的添加一些请求报头,其实大部分情况下都可以直接使用浏览器中的请求头,为了避免不必要的麻烦,尽可能像的模仿浏览器总是没有错的。
响应码
响应消息的第一行的状态行包括HTTP的协议版本、状态码、状态码含义。按照约定
- 2xx表示请求成功
- 3xx表示重定向
- 4xx表示客户端错误(403 Forbiden 404 Not Found)
- 5xx表示服务器错误(502 网关错误)
更多HTTP参考:
- http://www.ruanyifeng.com/blog/2016/08/http.html (阮一峰)
- https://book.douban.com/subject/10746113/ (HTTP权威指南)
爬虫开发
一般来说开发爬虫的过程是这样的
- 抓包分析获取数据的URL
- 通过python从上一步的URL获取数据
- 从上一步获取的HTML页面或者JSON数据中解析出感兴趣的数据
- 存储数据
下面就讲解这四个关键点
抓包发包工具
写爬虫的第一步就是分析想要的数据浏览器是通过什么URL拿到的,抓包也就在所难免。最好用的抓包工具当然是谷歌浏览器了,右键检查,选中网络,重新刷新页面就可以看到加载此网页所有的HTTP请求了,如果此链接有跳转地址,跳转之前的HTTP请求会被清掉,所以记得选上preserve log,尤其是登陆的时候,一般都会有跳转。
再介绍另外两个HTTP抓包工具——Fiddler和Charles,分别在windows和macos使用。它们可以为我们展示更多HTTP的细节,将请求和响应都调至Raw模式下,我们就可以一睹HTTP请求和响应的真实面貌。
通过抓包分析出具体的URL后,想进一步确认自己构造的参数和报头能否正确获取到数据,应该怎么做呢?不怕,postman可以帮你,你可以很轻松的选择方法,定义header,添加各种类型的body。
python请求数据
讲完了基本的HTTP协议知识后,大家可能会疑问那我该如何模仿浏览器或者手机客户端去向服务器发送HTTP请求呢?python的原生库urllib、第三方库requests、pycurl等都支持HTTP协议,既然有这么多工具可以用,大家可能就又有疑问该选择哪个工具了。在此我特地安利大家用一下requests,它让爬虫变得如此简单,让你再也不用为字符编码、重定向、cookie、响应解压缩烦恼了。如果你坚持用原生的库,那么有以下问题需要你一一解决,这些都是当年自己趟过的坑,绝非危言耸听。
- 需要自己判断服务器返回数据的编码格式,如果这个地方你不能正确判断,那恭喜你之后的每一步,你都必须面对乱码的问题
- 重定向,urllib不能自动判断重定向,需要自己解析重定向的链接并重新请求
- 如果模拟登陆,你必须要手动保证Cookie正确更新和发送
- 很多情况下响应正文是压缩过的,需要做解压处理
- 对于比较长的响应正文,服务器会将正文分段传输,所以还需要你做拼接操作
- 原生的urllib对HTTPS和持久连接都支持不好
当你花了一整天,写了好几百行的代码终于解决上面的问题后,而你旁边的同事可能早已经把数据下载完并愉快的约妹子去了。所以用requests吧,兄弟们用了都说好。下面我用两个例子讲解一下如何用requests获取想要的数据,并教你如何解决这些问题:
- 如何发送不同方法的请求
- 如何保存cookie
- 如何添加代理
- 如何处理编码问题
B站
假如我想下载B站里面某位小姐姐所有上传的视频,应该怎么办呢?首先你需要找到这位小姐姐的视频主页
但是通过谷歌浏览器右键查看页面源码,没有从html中找到这些视频的播放信息,唯一的可能就是视频数据是通过js脚本调用服务器获取,然后生成的这张页面。爬虫小白可能会疑问,难道我需要像浏览器一样分析js脚本,然后模拟js执行吗?其实不用这么复杂,只需要简单的分析抓包结构,就可以找到请求URL了。
获取视频的URL: http://space.bilibili.com/ajax/member/getSubmitVideos?mid=79415852&pagesize=30&tid=0&page=1&keyword=&order=senddate
那么问题又来了,这个URL的其他参数是干啥的呢?凭经验,mid肯定是这位小姐姐的用户id,page和pagesize是负责分页用的,keyword和是用来搜索的关键字,order是排序选项,剩下的tid是干啥的呢?其实写爬虫很多时候都会遇到这种问题,不知道某个参数的含义,也不确定正确的取值范围,需要一些尝试和运气,这里我们不管它就好。而返回的字段中有一个aid,那肯定是视频的id,有这个就可以自己拼接出播放链接了。
是不是很简单,通过response.ok查看请求是否正确返回,因为此接口的数据为json格式,直接通过response.json()就可以直接拿到格式化的数据。
知乎
虽然现在知乎对未登录用户展示的内容越来越多,但是仍会有一些限制,用爬虫模拟登陆可以之后再去爬取数据,可以避免很多不必要的麻烦,现在就讲一讲如何用requests模拟用户登陆。
还是和之前一样,在登陆页面打开谷歌浏览器的抓包窗口,输入用户名和密码点击确定,然后在茫茫请求中找到发送登陆信息的那个HTTP请求即可,功夫不负有心人,我们终于找到了登陆的请求。
等等,请求里面还有一个_xsrf,这是一个什么鬼参数,其实呢这是一个防止跨站请求伪造而生成的一个随机数,可以通过解析https://www.zhihu.com/#signin 页面获取,这一部分我在下面会讲解如何HTML获取数据,现在假设我们已经拿到这个数据了,如何将用户名和密码登陆呢?
如果我们想要自动保存Cookie信息,只需要生成一个Session对象,之后所有的请求通过此对象完成,requests会像浏览器一样自动更新cookie信息,并在每次请求的时候加上cookie,因此在成功的发送post登陆请求之后,就可以用session在保持登陆状态请求数据了。需要注意的是在请求的时候我特意去掉了Cookie和Content-Length报头,因为requests会自动加上,所以不需要我们特意关注。
更多关于requests的使用可以查看官方文档:
英文: http://docs.python-requests.org/en/master/
中文: http://cn.python-requests.org/zh_CN/latest/user/quickstart.html
python解析数据
因为个人在解析数据的时候遇到过很多编码的坑,所以在继续讲解之前告诉大家一些如何避免编码问题的方法。python2中有两种字符串:unicode和str,它们分别对应python3中的str和bytes。如何定义这两种类型的变量在下图中给大家列出来了。
以python3为例讲解这两种类型的区别。python3中的str每一个字符可以存储一个英文字母、一个汉字甚至一个emoji表情,它可以通过特定的编码方式,例如utf-8或者gbk生成bytes,在不同的编码格式下,可能需要2-3个字符常能表示一个汉字。bytes可以指定解码格式解码生成str,如果指定的解码格式不匹配,就会导致乱码问题。为了避免乱码问题,最好的方式就是使用str,等到需要写入文件或者数据库的时候,再指定写入的编码格式,用好这个准则,我们可以避免百分之九十的编码问题。
HTTP响应的数据格式有很多,例如文本、json、html,对应的解析方式也很多。通用一点,用python内置库正则匹配找到想要的数据,但是这种方法相对来说比较麻烦,而且不好维护,比较适合文本类型的数据,但HTTP响应正文基本都是json和HTML,这种方式适用面比较窄。
当请求的数据是json格式时,我们可以很方便的用requests反序列化返回内容,取出感兴趣的数据。但是当HTTP返回的数据是html的时候,我们该如何操作,就像刚才知乎登陆的例子中,如何快速从html中解析想要的数据呢?
专门用来解析html的第三方库有很多,例如beautifulsoup、pyquery。个人推荐使用pyquery,因为它可以使用jquery的方式选取元素,而且支持xpath,以后上手scrapy会很容易。继续上面登陆知乎的例子,登陆时需要的_xsrf实际上在 https://www.zhihu.com/#signin 页面里面,只要先请求到这个页面,然后解析出_xsrf,配合之前的登陆请求,我们就可以完整的实现用python模拟浏览器登陆知乎了。
使用起来是不是相当的简单,我们只要通过谷歌浏览器找到对应DOM元素,根据属性名就可以非常快速的找到想要的数据。需要注意的是response.content和response.text,这都是返回的body正文,但是前者是bytes,后者是str,requests已经帮助我们把响应正文按照正确的编码格式进行了解码,根据我们之前的阐述的原则,尽量使用str,所以26这个地方我用的是response.text。
更多关于pyquery的使用可以参考官方文档: https://pythonhosted.org/pyquery/
存储数据
根据数据量的不同以及数据类型的不同,数据的存储选择也很多。如果爬取的是图片、音频、视频等多媒体文件,直接按照文件形式存储就好了。如果是一些文本,数字等数据,一般有这么几种选择:
- 输出到屏幕
- 写入文件(txt csv)
- 写入数据库 (mysql sqlite mongo)
如果数据量非常小,可以选择直接输出到屏幕(这种情况貌似也不需要爬虫),因为终端存储的数据量很少,而且因为没有持久化,关闭窗口就意味着数据丢失,不建议使用。
在数据量小且不愿意折腾数据库的情况下,可以把爬取的数据写入文件,但是这种情况不能随取随用,也不方便做数据分析,需要手动处理。
当数据量较多,而且需要快捷的分析数据,推荐使用数据库存储数据,大型的数据库mysql, mongo功能齐全,可以分方便的进行数据分析,而且也很容易实现分布式扩展,当你需要多进程甚至多机器运行爬虫的时候,这些数据库可能是最好的选择。sqlite相对来说功能要少很多,python原生支持,依赖少,数据量不算太大的情况下可以考虑使用。
因为这部分内容太多太深,感兴趣的童鞋如果想深入了解一些,这里列出一些文档供大家参考:
python操作sqlite: http://www.runoob.com/sqlite/sqlite-python.html
python操作mysql: http://www.runoob.com/python/python-mysql.html
python操作mongo: https://www.oschina.net/question/54100_27233
爬虫示例
下面给出一个简单的例子,为大家展示如何使用上述python库实现一个完整的爬虫。一些热门的知乎话题最多有1000条精华回答,这个例子就是爬取这些精品答案。图示页面就是回答列表,每页有二十个答案,最多有五十页。但是此页面没有完整的回答信息,需要通过显示全部对应的链接进入回答详情页才能获取完整的答案,所以我们的爬虫策略就是通过回答列表找到所有精华回答链接,再通过回答链接获取内容。而且这些页面不需要登陆也能访问,因此可以省去模拟登陆。
开发环境
python是跨平台语言,但不同平台不同版本的python略微有一些差异,考虑到大家使用windows平台的较多,我在windows和ubuntu的python3.5验证过此代码,其他平台和其他python版本下可能需要做一些修改。集成开发环境推荐使用Pycharm,这是一个跨平台良心IDE,各大操作系统下都有免费的社区版本可以下载。
运行代码
代码链接
https://github.com/LiuRoy/sakura/blob/master/spider/crawl.py
https://github.com/LiuRoy/sakura/blob/master/spider/tables.sql
安装依赖库:
pip install requests pyquery SQLAlchemy
运行代码:
python scrawl.py
代码解释
通过谷歌浏览器抓包分析,可以通过 https://www.zhihu.com/topic/19553155/top-answers?page=2 页面获取每个话题不同分页下的回答链接 https://www.zhihu.com/question/27189372/answer/38712987 ,在此页面中就可以获取问题、回答、点赞数、标签等信息。
因为数据量不大,采用sqlite存储,可以很方便的用命令行或者桌面客户端查看数据。
反爬虫策略和应对方式
稍微大一些的网站都会有一些反爬虫策略,简单一点的根据User-Agent过滤,例如知乎,我们只需要设置为和浏览器相同即可。复杂一点的会限制非登陆用户,也只需要按照之前例子中的方式登陆,或者干脆先用浏览器登陆好,然后在第一次访问的时候带上浏览器中的cookie就好,实现起来难度不大。但是有不少网站,例如豆瓣和github,在检测到某一客户端频繁访问后,会直接封锁ip,这个问题解决起来就相当的棘手和蛋疼了。
解决方法也挺简单,我们只需要找到足够多的代理ip就可以了,只要策略得当,短时间内不要过度频繁的使用同一ip代码,或者当某一ip地址被封锁后马上切换到其他的ip代理,这样就可以保证高效的爬取数据了。那如何找到代理ip并且如何使用了,其实免费的代理ip很多,我们用百度搜索代理ip就可以找到很多网站,例如:http://www.ip181.com/。
找到代理ip后,就可以用上面的方式很轻松的使用代理ip了,但是网上免费的代理ip质量不好,很多不可用,而且速度慢、不稳定,请求的时候最好设置一下超时时间。我之前在爬github的时候,会专门写一个爬虫从这些网站搜集代理ip地址,一旦检测到被github封锁,就随机选取一个代理ip使用,如果发现代理ip不可用,不断的更换知道可用的代理ip为止,每个代理ip使用的次数也会有一定的限制,保证爬虫在整个执行期间不会因为ip封锁而不可用。
异常及性能
曾经我遇到过这样的状态,写好并运行爬虫一个小时之后,因为网络抖动或者某一种特殊的页面导致解析失败,整个爬虫运行终止,这就蛋疼了。如果日志打印不充分,我连运行失败的原因都清楚,更别说修复问题,即使修复好重新运行,又要花一个小时才能到刚才的进度,费时费力。
爬虫出现异常的情况实在是太多:网络抖动、解析页面失败、反爬虫被屏蔽、写入数据异常、代码逻辑错误等等都会导致进程终止,而一般爬虫需要数小时甚至数天数周的运行,每一次的运行失败都是时间巨大的浪费。因此一定需要在可能出现异常的地方接住异常,保证爬虫不会终止,并记录日志,这些错误日志不仅可以快速的帮助我们定位错误,在出错不多的情况下,我们甚至不需要修改代码重新运行,只需要人肉补全这些数据就好。
如果发现自己的爬虫运行效率太低,爬取速度太慢,并发对于提升爬虫速度是一个不错解决方案,因为GIL的存在,多进程并发模式对于python提速更优,我们可以使用生产者消费者的模式将爬虫任务进行拆分,从而实现多进程或者分布式。一般来说可以将HTTP请求的数据、解析数据、存储数据分别用不同的进程实现,这些进程之间通过消息队列进行通信,保证每个进程无状态,就可以非常容易的实现多进程扩展。即使某一类进程出现异常,也不需要重新启动所有的进程,只需要修复好代码重新启动,就可以实现断点续爬了。
一些常用的分布式工具:redis、rabbitmq、scrapy、celery、you-get
redis: http://www.redis.cn/
rabbitmq: http://www.rabbitmq.com/documentation.html
scrapy: http://scrapy-chs.readthedocs.io/zh_CN/0.24/intro/tutorial.html
celery: http://docs.jinkan.org/docs/celery/
you-get: https://github.com/soimort/you-get/wiki/中文说明