请求、响应和反爬虫

网络爬虫

浏览数:254

2019-2-24

本文分为如下部分

  • 引言
  • 反爬虫之user-agent
  • cookies设置
  • response对象
  • HTTP状态码详解

引言

学习爬虫,更清楚地了解访问网页获取内容的过程是非常有必要的。

我们平常浏览网页的过程其实是这样的:点击一个网页链接,浏览器会帮我们向这个网页发送请求,该网站服务器接到这个请求之后,判断这个请求是否合理,合理则将网页信息返回到我们浏览器中,返回的过程称为响应;不合理则阻止这次访问。发送请求和返回请求的过程可以想象成我知乎私信你一条消息,你看到了之后给我回了一条。

举一个例子,在这个网页中,右键-检查,刷新页面,选择Network,可以看到下面这个界面

将Response Headers折叠起来,下拉看到

上面的Response Headers和Request Headers分别是响应头信息和请求头信息

  • Request Headers是浏览器向网站发送请求时携带的信息,类似身份证,对面网站通过判断这些信息来决定是否接受你的这次请求。这就好比我给你发私信时,你会看到我的昵称和头像,还有一些其他信息,你会根据这些来判断是不是要回我的消息。
  • Response Headers则是网页传回响应时携带的信息

这些信息又什么用呢?或者说和我们爬虫有什么关系呢?这些都是在使用requests请求设置参数、提取内容时体现的

反爬虫之user-agent

我们可以用下面这个命令对这个网站发送请求

import requests
r = requests.get('https://zhuanlan.zhihu.com/python-programming')

这是我们以前常做的事情,但是我们来看一下这次返回的结果是什么样的

>>> r.text
'<html><body><h1>500 Server Error</h1>\nAn internal server error occured.\n</body></html>\n'

它没有包含我们想要的信息,这更像一个报错。这是因为我们发送请求时携带的信息没有通过检验,或者说对方网站把我们这个请求识别成了爬虫。

我们可以来检验一下我们携带了什么信息,看一下Request Headers

>>> r.request.headers
{'User-Agent': 'python-requests/2.18.4', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'Connection': 'keep-alive'}

其中最重要的是User-Agent,顾名思义,就是指谁来代替我们访问网页的,这里显示是python中的requests库发送的请求。我们从网站设计者的角度出发思考,网站设计是给人用的,人通过浏览器访问是OK的,但是一个python库来访问是个什么情况,肯定是爬虫了。所以网站设计者只要制定这样一条规则:检查请求者的UA(User-Agent),如果不是浏览器,就拒绝其访问。这样就成功把我们的访问禁止了,这就是最简单的反爬虫,禁UA

为什么要反爬虫呢?

  • 首先,网站肯定不希望自己的信息被随便拿去使用,如果有人轻易拿到了知乎的所有文章信息,创建一个平台,并通过比知乎更好的算法将合适的内容推荐给用户,那么知乎网站的很多用户自然就会被吸引到那个新的平台上,这显然是知乎不愿意看到的
  • 第二,爬虫是自动化程序,它一秒向网站发出千百次请求,如果有非常多的爬虫在抓这个网站,那么网站服务器就会面临比较大的压力。所以反爬虫的一个作用是设置壁垒,阻止大量小白随意爬取;同时限制其他爬虫的抓取速度,防止服务器过载

同时,我们要注意一点,网站面向的对象是那些浏览网页的人,网站需要照顾的是他们的用户体验,他们不需要考虑做爬虫的人的用户体验,因为网站不是用来爬的,爬虫和网站甚至是敌人的关系。所以网页要尽量识别爬虫的特征,将确定为爬虫的请求封杀掉。而如果是真正用浏览器的用户则一定要确保他们不会被封,防止用户体验受损,这种关系其实是宁可放过一千也不误杀一个。

接下来,我们言归正传。

python库的UA被禁了,那么我们就好奇,浏览器帮我们发请求时,它携带的UA是什么呢?

我现在用的是chrome浏览器,上面截图中请求头信息没有截全,补充如下

从这里的UA能看出这是一个chrome浏览器发出的请求。

那么我们会想,如果requests发送请求时能伪装成chrome就可以绕过这道反爬虫机制了,只要把代码改成下面这样

import requests
headers = {'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36'}
r = requests.get('https://zhuanlan.zhihu.com/python-programming', headers = headers)

上面的UA就是从浏览器里复制过来的。这时用r.text打印出来的就是网页源代码。

总结一下,通常拿到一个网页,先不用加headers请求一次,如果没有返回想要的结果,首先考虑加一个headers,很多时候就可以了,如果不可以再考虑其他的。

cookies设置

我们现在来请求知乎首页,因为之前那个知乎页面需要设置user-agent,所以这里我们也加上

import requests
headers = {'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36'}
r = requests.get('https://www.zhihu.com/', headers = headers)
r.status_code # 200,表示请求成功
r.text # 网页源代码如下所示

可以看到这是一个等着我们登录的界面,如果我要抓取我知乎的首页内容,就需要登录进我的账号才能获取。(之前那个网页是不需要登录就可以看的,而首页是必须登录账号才可以看)

这时读者可能会奇怪,为什么我用浏览器输入这个网址自动进入的就是我登录完的页面,而requests进入的就是待登录页面。那是因为浏览器记住了你的登录状态,当你曾经登录过这个网站,它就创建了一个cookies,这个cookies存储在你的浏览器中。当你下次打开浏览器的时候,浏览器就直接使用这个cookies,使你不需要重新输入账号密码。我们可以从刚刚的截图中看到

request headers 里面有一项cookie,表示浏览器帮你请求网页的时候携带了这个cookie,所以你就可以直接进入登录过的页面。同样,如果requests携带这个cookies,也可以直接访问到登录后的数据。代码如下

import requests
mycookie = '' # 将上图中的cookie对应部分复制过来,以字符串形式传入mycookie变量
headers = {'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36',
           'cookie': mycookie}
r = requests.get('https://www.zhihu.com/', headers = headers)
r.status_code # 200,表示请求成功
r.text

(因为我如果公布自己的cookie则我的账户就可以被别人操作了,所以这里不写在代码中,读者可以自己贴自己的cookie)
这时我们会发现拿到了首页的文章信息。

关于cookie,读者可以选择application进入下面这个页面

方框处右键-clear,然后你刷新这个页面,会发现变成了这样

也就是清除了cookie之后,网页就需要重新登录,这也更说明了登录状态是通过cookie保持的。

总结一下,即当你请求时发现对方要求你登录之后才会把信息给你,你可以加上cookie就可以用登录后的状态来访问网页获取信息了。我们不需要了解cookie是怎么工作的,只要拿过来用就可以了。

response对象

上面代码中我们对r这个变量提取了这两个属性

r.text
r.status_code

下面我们来更详细地介绍这个Response对象(即上面的r变量)。

Response对象,顾名思义,就是包含了网页返回信息的对象,我们可以通过提取属性的方式提取其中的信息。这些信息里有我们要提取的网页源代码,也有我们请求、响应的信息,有助于我们查看请求的状态。常用的有下面这些

r.status_code # 提取请求状态码
r.url # 当前请求的url
r.text # 网页源代码字符型
r.content # 网页源代码bytes型
# 两种编码
r.encoding
r.apparent_encoding

主要讲下面两点

1.r.status_code

这是HTTP状态码,表示请求的响应状态。由三位数字组成,这三位数中最重要的是第一位数字

  • 2开头的表示请求成功最常见的是200,这是最好的结果,请求没有任何问题
  • 3开头表示重定向,即你请求的URL可能是无效的,自动转而去请求了另外一个URL,常见301
  • 4开头表示客户端错误,即我们这里发的是错的。比如常见的404表示链接是无效的
  • 5开头表示服务器错误,即那边接收时发生了错误

更加详细的状态码介绍可以参考维基百科

本文下一节会具体举例什么时候会出现什么样的状态码。

2.源代码与编码

r.textr.content都是网页源代码,前者是我们可以直接处理的字符串形式,而后者是二进制形式,他们之间的相互转化是通过编码与解码完成的,二者之间相互转化使用的字符集是r.encoding,而r.apparent_encoding是网页真实的编码方式。即下图源代码开头标注的部分

在抓取网页的时候,可能会出现这样的情况:r.text的结果是一些看不懂的字符,即默认使用的字符集r.encoding是错误的,这样我们就无法提取到正确的信息,此时我们就需要使用r.content使用正确的字符集r.apparent_encoding转化成我们真正想要的字符。可以看下面这个例子


上面截图是我在这篇文章中举的一个例子,这是一个字符编码的系列文章,涉及到了编码的方方面面,对编码不是很了解的可以通过专栏目录查找这个系列文章学习,这里就不赘述编码方面的细节了。

除了上面列出的这些,以后可能还会遇到r.json(),当网页源代码是json格式的数据时,直接这样就可以转化成python对象,和json.loads功能类似。

另一个需要提起的是r.request,如果用r.headers则返回的是response的headers,如果我们想获取发送请求时带的headers,就要r.request.headers

HTTP状态码详解

2开头

2开头表示请求成功,一般就是返回200,例子如下

>>> r = requests.get('https://bj.lianjia.com/ershoufang/')
>>> r.status_code
200

3开头

3开头的状态码表示重定向,即你不能访问当前访问的链接获取资源,它会自动转到一个新的链接上去,即重新定位到一个新的位置。

1.scrapy的日志例子

举一个例子

https://bj.lianjia.com/ershoufang
https://bj.lianjia.com/ershoufang/

这两个链接中,下面这个链接是比较规范的,而如果我们请求时使用的是上面的链接,它会自动重定向到下面的链接上,比如用scrapy爬虫时就出现过这样的日志信息(看from to 从哪个链接跳转到哪个链接)

DEBUG: Redirecting (301) to <GET http://bj.lianjia.com/ershoufang/> from <GET https://bj.lianjia.com/ershoufang>
[scrapy.downloadermiddlewares.redirect] DEBUG: Redirecting (301) to <GET https://bj.lianjia.com/ershoufang/> from <GET http://bj.lianjia.com/ershoufang/>
[scrapy.core.engine] DEBUG: Crawled (200) <GET https://bj.lianjia.com/ershoufang/> (referer: None)

这个日志信息表示,在抓取过程中,先请求的是

https://bj.lianjia.com/ershoufang

301重定向到

http://bj.lianjia.com/ershoufang/

再301重定向到

https://bj.lianjia.com/ershoufang/

此时返回200即正常请求到页面。

重定向不影响我们获取到数据,但是这样多次请求会影响获抓取速度,所以最好还是规范自己的URL,定位到真正资源的位置。

2.requests的例子

对于requests来说,它内部有自动重定向的设计,所以即使是不规范的URL,也会自动重定向然后返回200,看下面例子

>>> requests.get('https://bj.lianjia.com/ershoufang').status_code
200

但是我们修改一个参数就可以知道它其实是重定向过的

# 输入不规范的URL
>>> r = requests.get('https://bj.lianjia.com/ershoufang', allow_redirects=False)
>>> r.status_code
301
>>> r.text
''

# 输入规范的URL
>>> r = requests.get('https://bj.lianjia.com/ershoufang/',allow_redirects=False)
>>> r.status_code
200

allow_redirects=False不允许重定向时,不规范的URL就会返回301,且不会自动重定向,无法获得数据,而规范的URL就会正常请求。

3.r.history

其实我们也可以不使用allow_redirects参数就可以探知返回的200过程中是有301的,如下所示

>>> r = requests.get('https://bj.lianjia.com/ershoufang')
>>> r.url # 请求的url和传入的不一样,已经被重定向过了
'https://bj.lianjia.com/ershoufang/'
>>> r.status_code
200
>>> r.history # 看请求过程中有没有历史请求,发现有一次301的重定向
[<Response [301]>]
>>> r.history[0].url # 提取可以看出这次被重定向的网址,就是我们输入的URL
'https://bj.lianjia.com/ershoufang'

4开头

1.404

最常见的404即页面不存在,比如这个https://stackoverflow.com/1111,输入浏览器就会显示page not found,其实就是404.

用requests库请求也是如此

>>> r = requests.get('https://stackoverflow.com/1111')
>>> r.status_code
404

这种情况说明客户端错误,即你输入的链接是错误的,这个链接根本就不存在。

2.403

403表示服务器理解你的请求,但是拒绝执行它,可能是识别出了你是爬虫,它也完全可以返回一个404.

>>> r = requests.get('http://worldagnetwork.com/')
>>> r.status_code
403
>>> headers = {'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36'}
>>> r = requests.get('http://worldagnetwork.com/', headers = headers)
>>> r.status_code
200

上面代码表示最开始禁止访问,携带UA之后就允许正常访问了。

5开头

我们直接看下面这个例子

>>> r = requests.get('https://zhuanlan.zhihu.com/python-programming')
>>> r.status_code
500
>>> headers = {'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36'}
>>> r = requests.get('https://zhuanlan.zhihu.com/python-programming', headers = headers)
>>> r.status_code
200

5开头的状态码表示服务端发生错误,上面情况说明你发的请求被服务端拒绝了,从而无法获得数据,返回500。这是知乎的反爬虫限制的结果,如果带上UA再访问就可以正常请求,返回状态码也变成200.

我们可以看到,这种情况和刚刚403那种情况发生的原因是一样的,但是返回的状态码却不同,其实这是对方服务器自己定义的。他不想让你访问它的网页,有的说是我们服务端不想你访问,就是5开头,如果他说是你自己的请求有问题,那就返回4开头的。

所以关键只在请求是否成功,不是200的状态码最好都去检查一下是哪里出了问题。

专栏信息

专栏主页:python编程

专栏目录:目录

爬虫目录:爬虫系列目录

版本说明:软件及包版本说明