requests的使用

2.1 节我们了解了 urllib 库的基本用法,其中确实有不方便的地方,例如处理网页验证和 Cookie 时,需要写 Opener 类和 Handler 类来处理。另外实现 POST、PUT 等请求时的写法也不太方便。

为了更加方便地实现这些操作,产生了更为强大的库——requests。 有了它,Cookie、登录验证、代理设置等操作都不是事儿。

接下来,让我们领略一下 requests 库的强大之处吧。

准备工作

在开始学习之前,请确保已经正确安装好 requests 库,如果尚未安装,可以使用 pip3 来安装:

pip3 install requests

更加详细的安装说明可以参考 https://setup.scrape.center/requests

实例引入

urllib 库中的 urlopen 方法实际上是以 GET 方式请求网页,requests 库中相应的方法就是 get 方法,是不是感觉表意更百接一些? 下面通过实例来看一下:

import requests

r = requests.get('https://www.baidu.com/')
print(type(r))
print(r.status_code)
print(type(r.text))
print(r.text[:100])
print(r.cookies)

运行结果如下:

<class 'requests.models.Response'>
200
<class 'str'>
<!DOCTYPE html>
<!--STATUS OK--><html> <head><meta http-equiv=content-type content=text/html;charse
<RequestsCookieJar[<Cookie BDORZ=27315 for .baidu.com/>]>

这里我们调用 get 方法实现了与 urlopen 方法相同的操作,返回一个 Response 对象,并将其存放在变量 r 中,然后分别输出了响应的类型、状态码,响应体的类型、内容,以及 Cookie

观察运行结果可以发现,返回的响应类型是 requests.models.Response,响应体的类型是字符串 strCookie 的类型是 RequestsCookieJar

使用 get 方法成功实现一个 GET 请求算不了什么,requests 库更方便之处在于其他请求类型依然可以用一句话完成,实例如下:

import requests

r = requests.get('https://www.httpbin.org/get')
r = requests.post('https://www.httpbin.org/post')
r = requests.put('https://www.httpbin.org/put')
r = requests.delete('https://www.httpbin.org/delete')
r = requests.patch('https://www.httpbin.org/patch')

这里分别用 postputdelete 等方法实现了 POST、PUT、DELETE 等请求。是不是比 urllib 库简单太多了?

其实这只是冰山一角,更多的还在后面。

GET请求

HTTP 中最常见的请求之一就是 GET 请求,首先来详细了解一下利用 requests 库构建 GET 请求的方法。

基本示例

下面构建一个最简单的 GET 请求,请求的链接为 https://www.httpbin.org/get,该网站会判断客户端发起的是否为 GET 请求,如果是,那么它将返回相应的请求信息:

import requests

r = requests.get('https://www.httpbin.org/get')
print(r.text)

运行结果如下:

{
  "args": {},
  "headers": {
    "Accept": "*/*",
    "Accept-Encoding": "gzip, deflate",
    "Host": "www.httpbin.org",
    "User-Agent": "python-requests/2.32.3",
    "X-Amzn-Trace-Id": "Root=1-6794bd91-7be031434a3533974025f44c"
  },
  "origin": "34.80.45.10",
  "url": "https://www.httpbin.org/get"
}

可以发现,我们成功发起了 GET 请求,返回结果中包含请求头、URL、IP 等信息。

那么,对于 GET 请求,如果要附加额外的信息,一般怎样添加呢?例如现在想添加两个参数 nameage,其中 namegermeyage 是 25,于是 URL 就可以写成如下内容:

https://www.httpbin.org/get?name=germey&age=25

要构造这个请求链接,是不是要直接写成这样呢?

r = requests.get('https://www.httpbin.org/get?name=germey&age=25[https://www.]')

这样也可以,但是看起来有点不人性化哎?这些参数还需要我们手动去拼接,实现起来着实不优雅。

一般情况下,我们利用 params 参数就可以直接传递这种信息了,实例如下:

import requests

data = {
    'name': 'germey',
    'age': 25
}
r = requests.get('https://httpbin.org/get', params=data)
print(r.text)

运行结果如下:

{
  "args": {
    "age": "25",
    "name": "germey"
  },
  "headers": {
    "Accept": "*/*",
    "Accept-Encoding": "gzip, deflate",
    "Host": "httpbin.org",
    "User-Agent": "python-requests/2.32.3",
    "X-Amzn-Trace-Id": "Root=1-6794bee0-7c11738f2ff03c840f3e32b5"
  },
  "origin": "34.80.45.10",
  "url": "https://httpbin.org/get?name=germey&age=25"
}

上面我们把 URL 参数以字典的形式传给 get 方法的 params 参数,通过返回信息我们可以判断,请求的链接自动被构造成了 https://www.httpbin.org/get?name=germey&age=25 ,这样我们就不用自己构造 URL 了,非常方便。

另外,网页的返回类型虽然是 str 类型,但是它很特殊,是 JSON 格式的。所以,如果想直接解析返回结果,得到一个 JSON 格式的数据,可以直接调用 json 方法。实例如下:

import requests

r = requests.get('https://www.httpbin.org/get')
print(type(r.text))
print(r.json())
print(type(r.json()))

运行结果如下:

<class 'str'>
{'args': {}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Host': 'www.httpbin.org', 'User-Agent': 'python-requests/2.32.3', 'X-Amzn-Trace-Id': 'Root=1-6794c02c-4865d05421b098245885ef69'}, 'origin': '34.80.45.10', 'url': 'https://www.httpbin.org/get'}
<class 'dict'>

可以发现,调用 json 方法可以将返回结果(JSON 格式的字符串)转化为字典。

但需要注意的是,如果返回结果不是 JSON 格式,就会出现解析错误,抛出 json.decoder.JSONDecodeError 异常。

抓取网页

上面的请求链接返回的是 JSON 格式的字符串,那么如果请求普通的网页,就肯定能获得相应的内容了。我们以一个实例页面 https://ssr1.scrape.center/ 作为演示,往里面加入一点提取信息的逻辑,将代码完善成如下的样子:

import requests
import re

r = requests.get('https://ssr1.scrape.center/')
pattern = re.compile('<h2.*?>(.*?)</h2>', re.S)
titles = re.findall(pattern, r.text)
print(titles)

这个例子中,我们用最基础的正则表达式来匹配所有的标题内容。关于正则表达式,会在 2.3 节详细介绍,这里其只作为实例来配合讲解。

运行结果如下:

['肖申克的救赎 - The Shawshank Redemption', '霸王别姬 - Farewell My Concubine', '泰坦尼克号 - Titanic', '罗马假日 - Roman Holiday', '这个杀手不太冷 - Léon', '魂断蓝桥 - Waterloo Bridge', '唐伯虎点秋香 - Flirting Scholar', '喜剧之王 - The King of Comedy', '楚门的世界 - The Truman Show', '活着 - To Live']

我们发现,这里成功提取出了所有电影标题,只需一个最基本的抓取和提取流程就完成了。

抓取二进制数据

在上面的例子中,我们抓取的是网站的一个页面,实际上它返回的是一个 HTML 文档。要是想抓取图片、音频、视频等文件,应该怎么办呢?

图片、音频、视频这些文件本质上都是由二进制码组成的,由于有特定的保存格式和对应的解析方式,我们才可以看到这些形形色色的多媒体。所以,要想抓取它们,就必须拿到它们的二进制数据。

下面以示例网站的站点图标为例来看一下:

import requests

r = requests.get('https://scrape.center/favicon.ico', verify=False)
print(r.text)
print(r.content)

这里抓取的内容是站点图标,也就是浏览器中每一个标签上显示的小图标,如图 2-3 所示。

上述实例将会打印 Response 对象的两个属性,一个是 text,另一个是 content

行结果如图 2-4 和图 2-5 所示,分别是 r.textr.content 的结果。

可以注意到,r.text 中出现了乱码,r.content 的前面带有一个 b,代表这是 bytes 类型的数据。由于图片是二进制数据,所以前者在打印时会转化为 str 类型,也就是图片直接转化为字符串,理所当然会出现乱码。

上面的运行结果我们并不能看懂,它实际上是图片的二进制数据。不过没关系,我们将刚才提取到的信息保存下来就好了,代码如下:

import requests

r = requests.get('https://scrape.center/favicon.ico')
with open('favicon.ico', 'wb') as f:
    f.write(r.content)

这里用了 open 方法,其第一个参数是文件名称,第二个参数代表以二进制写的形式打开文件,可以向文件里写入二进制数据。

上述代码运行结束之后,可以发现在文件夹中出现了名为 favicon.ico 的图标,如图 2-6 所示。

这样,我们就把二进制数据成功保存成了一张图片,这个小图标被我们成功爬取下来了。

同样地,我们也可以用这种方法获取音频和视频文件。

添加请求头

我们知道,在发起 HTTP 请求的时候,会有一个请求头 Request Headers,那么怎么设置这个请求头呢?

很简单,使用 headers 参数就可以完成了。

在刚才的实例中,实际上是没有设置请求头信息的,这样的话,某些网站会发现这并不是一个由正常浏览器发起的请求,于是可能会返回异常结果,导致网页抓取失败。

要添加请求头信息,例如这里我们想添加一个 User-Agent 字段,就可以这么写:

import requests

headers = {
    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36'
}
r = requests.get('https://ssr1.scrape.center/', headers=headers, verify=False)
print(r.text)

当然,可以在这个 headers 参数中添加任意其他字段信息。

POST请求

前面我们了解了最基本的 GET 请求,另外一种比较常见的请求方式是 POST。使用 requests 库实现 POST 请求同样非常简单,实例如下:

import requests

data = {'name': 'germey', 'age': '25'}
r = requests.post('http://httpbin.org/post', data=data)
print(r.text)

这里还是请求 https://www.httpbin.org/post,该网站可以判断请求是否为 POST 方式,如果是,就返回相关的请求信息。

运行结果如下:

{
  "args": {},
  "data": "",
  "files": {},
  "form": {
    "age": "25",
    "name": "germey"
  },
  "headers": {
    "Accept": "*/*",
    "Accept-Encoding": "gzip, deflate",
    "Content-Length": "18",
    "Content-Type": "application/x-www-form-urlencoded",
    "Host": "httpbin.org",
    "User-Agent": "python-requests/2.32.3",
    "X-Amzn-Trace-Id": "Root=1-6794de4d-757f7a8a2610dc81780b4f19"
  },
  "json": null,
  "origin": "34.80.45.10",
  "url": "http://httpbin.org/post"
}

可以发现,我们成功获得了返回结果,其中 form 部分就是提交的数据,这证明 POST 请求成功发送了。

响应

请求发送后,自然会得到响应。在上面的实例中,我们使用 textcontent 获取了响应的内容。

此外,还有很多属性和方法可以用来获取其他信息,例如状态码、响应头、Cookie 等。实例如下:

import requests

r = requests.get('https://ssr1.scrape.center/')
print(type(r.status_code), r.status_code)
print(type(r.headers), r.headers)
print(type(r.cookies), r.cookies)
print(type(r.url), r.url)
print(type(r.history), r.history)

这里通过 status_code 属性得到状态码、通过 header 属性得到响应头、通过 cookies 属性得到 Cookie、通过 url 属性得到 URL、通过 history 属性得到请求历史。并将得到的这些信息分别打印出来。

运行结果如下:

<class 'int'> 200
<class 'requests.structures.CaseInsensitiveDict'> {'Date': 'Sat, 25 Jan 2025 12:57:39 GMT', 'Content-Type': 'text/html; charset=utf-8', 'Content-Length': '41667', 'Connection': 'keep-alive', 'X-Frame-Options': 'DENY', 'X-Content-Type-Options': 'nosniff', 'Expires': 'Sat, 25 Jan 2025 13:01:05 GMT', 'Cache-Control': 'max-age=600', 'Strict-Transport-Security': 'max-age=15724800; includeSubDomains'}
<class 'requests.cookies.RequestsCookieJar'> <RequestsCookieJar[]>
<class 'str'> https://ssr1.scrape.center/
<class 'list'> []

可以看到,headerscookies 这两个属性得到的结果分别是 CaseInsensitiveDictRequestsCookieJar 对象。

由第 1 章我们知道,状态码是用来表示响应状态的,例如 200 代表我们得到的响应是没问题的,上面例子输出的状态码正好也是 200,所以我们可以通过判断这个数字知道爬虫爬取成功了。

requests 库还提供了一个内置的状态码查询对象 requests.codes,用法实例如下:

import requests

r = requests.get('https://ssr1.scrape.center/')
exit() if not r.status_code == requests.codes.ok else print('Request Successfully')

这甲通过比较返回码和内置的表示成功的状态码,来保证请求是否得到了正常响应,如果是,就输出请求成功的消息,否则程序终止运行,这里我们用 requests.codes.ok 得到的成功状态码是 200。

这样我们就不需要再在程序里写状态码对应的数字了,用字符串表示状态码会显得更加直观。

当然,肯定不能只有 ok 这一个条件码。

下面列出了返回码和相应的查询条件:

# 信息性状态码
100: ('continue',),
101: ('switching_protocols',),
102: ('processing',),
103: ('checkpoint',),
122: ('uri_too_long', 'request_uri_too_long'),

# 成功状态码
200: ('ok', 'okay', 'all_ok', 'all_okay', 'all_good', '\\o/', '✔'),
201: ('created',),
202: ('accepted',),
203: ('non_authoritative_info', 'non_authoritative_information'),
204: ('no_content',),
205: ('reset_content', 'reset'),
206: ('partial_content', 'partial'),
207: ('multi_status', 'multiple_status', 'multi_stati', 'multiple_stati'),
208: ('already_reported',),
226: ('im_used',),

# 重定向状态码
300: ('multiple_choices',),
301: ('moved_permanently', 'moved', '\\o-'),
302: ('found',),
303: ('see_other', 'other'),
304: ('not_modified',),
305: ('use_proxy',),
306: ('switch_proxy',),
307: ('temporary_redirect', 'temporary_moved', 'temporary'),
308: ('permanent_redirect',
'resume_incomplete', 'resume'), # These 2 to be removed in 3.0

# 客户端错误状态码
400: ('bad_request', 'bad'),
401: ('unauthorized',),
402: ('payment_required', 'payment'),
403: ('forbidden',),
404: ('not_found', '-o-'),
405: ('method_not_allowed', 'not_allowed'),
406: ('not_acceptable',),
407: ('proxy_authentication_required', 'proxy_auth', 'proxy_authentication'),
408: ('request_timeout', 'timeout'),
409: ('conflict',),
410: ('gone',),
411: ('length_required',),
412: ('precondition_failed', 'precondition'),
413: ('request_entity_too_large',),
414: ('request_uri_too_large',),
415: ('unsupported_media_type', 'unsupported_media', 'media_type'),
416: ('requested_range_not_satisfiable', 'requested_range', 'range_not_satisfiable'),
417: ('expectation_failed',),
418: ('im_a_teapot', 'teapot', 'i_am_a_teapot'),
421: ('misdirected_request',),
422: ('unprocessable_entity', 'unprocessable'),
423: ('locked',),
424: ('failed_dependency', 'dependency'),
425: ('unordered_collection', 'unordered'),
426: ('upgrade_required', 'upgrade'),
428: ('precondition_required', 'precondition'),
429: ('too_many_requests', 'too_many'),
431: ('header_fields_too_large', 'fields_too_large'),
444: ('no_response', 'none'),
449: ('retry_with', 'retry'),
450: ('blocked_by_windows_parental_controls', 'parental_controls'),
451: ('unavailable_for_legal_reasons', 'legal_reasons'),
499: ('client_closed_request',),

# 服务端错误状态码
500: ('internal_server_error', 'server_error', '/o\\', 'x'),
501: ('not_implemented',),
502: ('bad_gateway',),
503: ('service_unavailable', 'unavailable'),
504: ('gateway_timeout',),
505: ('http_version_not_supported', 'http_version'),
506: ('variant_also_negotiates',),
507: ('insufficient_storage',),
509: ('bandwidth_limit_exceeded', 'bandwidth'),
510: ('not_extended',),
511: ('network_authentication_required', 'network_auth', 'network_authentication')

例如想判断结果是不是 404 状态,就可以用 requests.codes.not_found 作为内置的状态码做比较。

高级用法

通过本节前面部分,我们已经了解了 requests 库的基本用法,如基本的 GET、POST 请求以及 Response 对象。本节我们再来了解一些 requests 库的高级用法,如文件上传、Cookie 设置、代理设置等。

文件上传

我们知道使用 requests 库可以模拟提交一些数据。除此之外,要是有网站需要上传文件,也可以用它来实现,非常简单,实例如下:

import requests

files = {'file': open('favicon.ico', 'rb')}
r = requests.post('https://www.httpbin.org/post', files=files)
print(r.text)

在前一节,我们保存了一个文件 favicon.ico,这次就用它来模拟文件上传的过程。需要注意,favicon.ico 需要和当前脚本保存在同一目录下。如果手头有其他文件,当然也可以上传这些文件,更改下代码即可。

运行结果如下:

{
  "args": {},
  "data": "",
  "files": {
    "file": "data:application/octet-stream;base64,AAABAA..."
  },
  "form": {},
  "headers": {
    "Accept": "*/*",
    "Accept-Encoding": "gzip, deflate",
    "Content-Length": "4433",
    "Content-Type": "multipart/form-data; boundary=37731a954b8e31728b6e7b0e14c47f19",
    "Host": "www.httpbin.org",
    "User-Agent": "python-requests/2.32.4",
    "X-Amzn-Trace-Id": "Root=1-68512463-18403cd853cf7a323044985c"
  },
  "json": null,
  "origin": "171.34.210.142",
  "url": "https://www.httpbin.org/post"
}

以上结果省略部分内容,上传文件后,网站会返回响应,响应中包含 files 字段和 form 字段,而 form 字段是空的,这证明文件上传部分会单独用一个 files 字段来标识。

Cookie设置

前面我们使用 urllib 库处理过 Cookie,写法比较复杂,有了 requests 库以后,获取和设置 Cookie 只需一步即可完成。

我们先用一个实例看一下获取 Cookie 的过程:

import requests

r = requests.get('https://www.baidu.com')
print(r.cookies)
for key,value in r.cookies.items():
    print(key + '='+ value)

运行结果如下:

<RequestsCookieJar[<Cookie BDORZ=27315 for .baidu.com/>]> BDORZ=27315

这里我们首先调用 cookies 属性,成功得到 Cookie,可以发现它属于 RequestCookieJar 类型。然后调用 items 方法将 Cookie 转化为由元组组成的列表,遍历输出每一个 Cookie 条目的名称和值,实现对 Cookie 的遍历解析。

当然,我们也可以直接用 Cookie 来维持登录状态。下面以 GitHub 为例说明一下,首先我们登录 GitHub,然后将请求头中的 Cookie 内容复制下来,如图 2-7 所示。

图2-7 请求头中的 Cookie 内容

(省略)

可以将图 2-7 中框起来的这部分内容替换成你自己的 Cookie,将其设置到请求头里面,然后发送请求,实例如下:

import requests

headers = {
    'Cookie': '__octo=GH1.1.1849343058.1576602081; _ga=GA1.2.90460451.15 76602111; _Host-user_session_same_site=nbDv62KHnjP4N5KYqQNYZ2O8waeq smNgxFnFC88rnV7gTYQW_; _device_id=a7ca73be0e8f1a8idie2ebb5349f9075; user_session=nbDv62KHnjP4N5KYqQNYZ2O8waeqsmNgxFnFC88rnV7gTYQW_; logged_in=yes; dotcom_user=Germey; tz=Asia%2FShanghai; has_recent_activity=1; _gat=1; _gh_sess=your_session_info',
    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36',
}

r = requests.get('https://github.com/', headers=headers)
print(r.text)

运行结果如图 2-8 所示。

图2-8 运行结果

(省略)

可以发现,结果中包含了登录后才能包含的结果,其中有我的 GitHub 用户名信息,你如果尝试一下,同样可以得到你的用户信息。

得到这样类似的结果,说明用 Cookie 成功模拟了登录状态,这样就能爬取登录之后才能看到的页面了。

当然,也可以通过 cookies 参数来设置 Cookie 的信息,这里我们可以构造一个 RequestsCookieJar 对象,然后对刚才复制的 Cookie 进行处理以及赋值,实例如下:

import requests

cookies = '_octo=GH1.1.1849343058.1576602081; _ga=GA1.2.90460451.1576602111; _Host-user_session_same_site=nbDv62KHnjP4N5KYqQNYZ2O8waeqsmNgxFnFC88rnV7gTYQW_; _device_id=a7ca73be0e8f1a8idie2ebb5349f9075; user_session=nbDv62KHnjP4N5KYqQNYZ2O8waeqsmNgxFnFC88rnV7gTYQW_; logged_in=yes; dotcom_user=Germey; tz=Asia%2FShanghai; has_recent_activity=1; _gat=1; _gh_sess=your_session_info'
jar = requests.cookies.RequestsCookieJar()
headers = {
    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36'
}
for cookie in cookies.split(';'):
    key, value = cookie.split('=', 1)
    jar.set(key, value)
r = requests.get('https://github.com/', cookies=jar, headers=headers)
print(r.text)

这里我们首先新建了一个 RequestCookieJar 对象,然后利用 split 方法对复制下来的 Cookie 内容做分割,接着利用 set 方法设置好每个 Cookie 条目的键名和键值,最后通过调用 requests 库的 get 方法并把 RequestCookieJar 对象通过 cookies 参数传递,最后即可获取登录后的页面。

测试后,发现同样可以正常登录。

Session维持

直接利用 requests 库中 getpost 方法的确可以做到模拟网页的请求,但这两种方法实际上相当于不同的 Session,或者说是用两个浏览器打开了不同的页面。

设想这样一个场景,第一个请求利用 requests 库的 post 方法登录了某个网站,第二次想获取成巧登录后的自已的个人信息,于是又用了一次 requests 库的 get 方法去请求个人信息页面。

这实际相当于打开了两个浏览器,是两个完全独立的操作,对应两个完全不相关的 Session,那么能够成功获取个人信息吗?当然不能。

有人可能说,在两次请求时设置一样的 Cookie 不就行了?可以,但这样做显得很烦,我们有更简单的解决方法。

究其原因,解决这个问题的主要方法是维持同一个 Session,也就是第二次请求的时候是打开一个新的浏览器选项卡而不是打开一个新的浏览器。但是又不想每次都设置 Cookie,该怎么办呢?这时候出现了新的利器 Session 对象。

利用 Session 对象,我们可以方便地维护一个 Session,而且不用担心 Cookie 的问题,它会自动帮我们处理好。

我们先做一个小实验吧,如果沿用之前的写法,实例如下:

import requests

requests.get('https://www.httpbin.org/cookies/set/number/123456789')
r = requests.get('https://www.httpbin.org/cookies')
print(r.text)

这里我们请求了一个测试网址 https://www.httpbin.org/cookies/set/number/123456789。请求这个网址时,设置了一个 Cookie 条目,名称是 number,内容是 123456789。随后又请求了 https://www.httpbin org/cookies,以获取当前的 Cookie 信息。

这样能成功获取设置的 Cookie 吗?试试看。

运行结果如下:

{
  "cookies": {}
}

发现并不能。

我们再用刚才所说的 Session 试试看:

import requests

s = requests.Session()
s.get('https://www.httpbin.org/cookies/set/number/123456789')
r = s.get('https://www.httpbin.org/cookies')
print(r.text)

再看下运行结果:

{
  "cookies": {"number":"123456789"}
}

可以看到 Cookie 被成功获取了!这下能体会到同一个 Session 和不同 Session 的区别了吧!

所以,利用 Session 可以做到模拟同一个会话而不用担心 Cookie 的问题,它通常在模拟登录成功之后,进行下一步操作时用到。

Session 在平常用得非常广泛,可以用于模拟在一个浏览器中打开同一站点的不同页面,第 10 章会专门来讲解这部分内容。

SSL证书验证

现在很多网站要求使用 HTTPS 协议,但是有些网站可能并没有设置好 HTTPS 证书,或者网站的 HTTPS 证书可能并不被 CA 机构认可,这时这些网站就可能出现 SSL 证书错误的提示。

例如这个实例网站:https://ssr2.scrape.center/,如果用 Chrome 浏览器打开它,则会提示 “您的连接不是私密连接” 这样的错误,如图 2-9 所示。

图2-9 错误提示

(省略)

我们可以在浏览器中通过一些设置来忽略证书的验证。

但是如果想用 requests 库来请求这类网站,又会遇到什么问题呢?我们用代码试一下:

import requests

response = requests.get("https://ssr2.scrape.center/")
print(response.status_code)

运行结果如下:

requests.exceptions.SSLError:HTTPSConnectionPool(host='ssr2.scrape.center',port=443):Max retries exceeded with url:/(Caused by SSLError(SSLCertVerificationError(1,'[SSL:CERTIFICATE_VERIFY_FAILED) certificate verify failed: unable to get local issuer certificate(_ssl.c:1056)')))

可以看到,直接抛出了 SSLError 错误,原因是我们请求的 URL 的证书是无效的。

那如果我们一定要爬取这个网站,应该怎么做呢?可以使用 verify 参数控制是否验证证书,如果将此参数设置为 False,那么在请求时就不会再验证证书是否有效。如果不设置 verify 参数,其默认值是 True,会自动验证。

于是我们改写代码如下:

import requests

response=requests.get('https://ssr2.scrape.center/',verify=False)
print(response.status_code)

这样就能打印出请求成功的状态码了:

/usr/local/lib/python3.7/site-packages/urllib3/connectionpool.py:857:InsecureRequestwarning:Unverified HTTPS request is being made.Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#ssl-warnings InsecureRequestwarning)
200

不过我们发现其中报了一个警告,它建议我们给它指定证书。我们可以通过设置忽略警告的方式来屏蔽这个警告:

import requets
from requests.packages import urllib3

urllib3.disable_warning()
reponse = requests.get('https://ssr2.scrape.center/', verify=False)
print(reponse.status_code)

或者通过捕获警告到日志的方式忽略警告:

import logging
import requests

logging.captureWarning(True)
reponse = requests.get('https://ssr2.scrape.center/', verify=False)
print(reponse.status_code)

当然,我们也可以指定一个本地证书用作客户端证书,这可以是单个文件(包含密钥和证书)或一个包含两个文件路径的元组:

import requests

response = requests.get('https://ssr2.scrape.center/', cert=('/path/server.crt', '/path/server.key'))
print(response.status_code)

当然,上面的代码是演示实例,我们需要有 crtkey 文件,并且指定他们的路径。另外注意,本地私有证书的 key 必须是解密状态,加密状态的 key 是不支持的。

超时设置

在本机网络状况不好或者服务器网络响应太慢甚至无响应时,我们可能会等待特别久的时间才能接收到响应,甚至到最后因为接收不到响应而报错。为了防止服务器不能及时响应,应该设置一个超时时间,如果超过这个时间还没有得到响应,就报错。这需要用到 timeout 参数,其值是从发出请求到服务器返回响应的时间。实例如下:

import requests

r = requests.get('https://www.httpbin.org/get', timeout=1)
print(r.status_code)

通过这样的方式,我们可以将超时时间设置为 1 秒,意味着如果 1 秒内没有响应,就抛出异常。

实际上,请求分为两个阶段:连接(connect)和读取(read)。

上面设置的 timeout 是用作连接和读取的 timeout 的总和。

r = requests.get('https://www.httpbin.org/get', timeout=(5,30))

如果想永久等待,可以直接将 timeout 设置为 None,或者不设置直接留空,因为默认取值是 None。这样的话,如果服务器还在运行,只是响应特别慢,那就慢慢等吧,它永远不会返回超时错误的。其用法如下:

r = requests.get('https://www.httpbin.org/get', timeout=None)

或直接不加参数:

r = requests.get('https://www.httpbin.org/get')

身份认证

2.1 节我们讲到,在访问启用了基本身份认证的网站时(例如 https://ssr3.scrape.center/ ),首先会弹出一个认证窗口,如果 2-10 所示。

image 2025 06 17 16 28 50 300
Figure 1. 图2-10 弹出的认证窗口

这个网站就是启用了基本身份认证,2.1 节我们可以利用 urllib 库来实现身份的校验,但实现起来相对烦琐。那在 requests 库中怎么做呢?当然也有办法。

我们可以使用 requests 库自带的身份认证功能,通过 auth 参数即可设置,实例如下:

import requests
from requests.auth import HTTPBasicAuth

r = requests.get('https://ssr3.scrape.center/', auth=HTTPBasicAuth('admin', 'admin'))
print(r.status_code)

这个实例网站的用户名和密码都是 admin,在这里我们可以直接设置。

如果用户名和密码正确,那么请求时就会自动认证成功,返回 200 状态吗;如果认证失败,则返回 401 状态码。

当然,如果参数都传入一个 HTTPBasicAuth 类,就显得有点烦琐了,所以 requests 库提供了一个更简单的写法,可以直接传一个元组,他会默认使用 HTTPBasicAuth 这个类来认证。

所以上面的代码可以直接简写如下:

import requests

r = requests.get('https://ssr3.scrape.center/', auth=('admin', 'admin'))
print(r.status_code)

此外,requests 库还提供了其它认证方式,如 OAuth 认证,不过此时需要安装 oauth 包,安装命令如下:

pip3 install requests_oauthlib

使用 OAuth1 认证的示例方法如下:

import requests
from requests_oauthlib import OAuth1

url = 'https://api.twitter.com/1.1/account/verify_credentials.json'
auth = OAuth1("YOUR_APP_KEY", "YOU_APP_SECRET", 'USER_OAUTH_TOKEN', 'USER_OAUTH_TOKEN_SECRET')
requests.get(url, auth=auth)

代理设置

某些网站在测试的时候请求几次,都能正常获取内容。但是一旦开始大规模爬取,面对大规模且频繁的请求时,这些网站就可能弹出验证码,或者跳转到登录认证页面,更甚者可能会直接封禁客户端的,导致在一定时间段内无法访问。

那么,为了防止这种情况发生,我们需要设置代理来解决这个问题,这时就需要用到 proxies 参数。可以用这样的方式设置,

import requests

proxies = {
  'http': 'http://10.10.10.10:1080',
  'https': 'http://10.10.10.10:1080'
}
requests.get('https://www.httpbin.org/get', proxies=proxies)

当然,直接运行这个实例可能不行,因为这个代理可能是无效的,可以直接搜索寻找有效的代理并替换试验一下。

若代理需要使用上文所述的身份认证,可以使用类似 http://user:password@host:port 这样的语法来设置代理,实例如下:

import requests

proxies = {'https': 'http://user.password@10.10.10.10:1080/',}
requests.get('https://www.httpbin.org/get', proxies=proxies)

除了基本的 HTTP 代理外,requests 库还支持 SOCKS 协议的代理。

首先,需要安装 socks 这个库:

pip3 install "requests[socks]"

然后就可以使用 SOCKS 协议代理了,实例如下:

proxies = {
    'http': 'socks5://user:password@host:port',
    'https': 'socks5://user:password@host:port'
}
requests.get('https://www.httpbin.org/get', proxies=proxies)

Prepared Request

我们当然可以直接使用 requests 库的 getpost 方法发送请求,但有没有想过,这个请求在 requests 内部是怎么实现的呢?

实际上,requests 在发送请求的时候,是在内部构造了一个 Request 对象,并给这个对象赋予了各种参数,包括 urlheadersdata 等,然后直接把这个 Request 对象发送出去,请求成功后会再得到一个 Response 对象,解析这个对象即可。

那么 Request 对象是什么类型呢?实际上它就是 Prepared Request。

我们深入一下,不用 get 方法,直接构造一个 Prepared Request 对象来试试,代码如下:

from requests import Request, Session

url = 'http://www.httpbin.org/post'
data = {'name': 'germey'}
headers = {
    'User-Agent': ''
}
s = Session()
req = Request('POST', url, data=data, headers=headers)
prepped = s.prepare_request(req)
r = s.send(prepped)
print(r.text)

这里我们引入了 Request 类,然后用 urldataheaders 参数构造了一个 Request 对象,这时需要再调用 Session 类的 prepare_request 方法将其转换为一个 PreparedRequest 对象,再调用 send 方法发送,运行结果如下:

{
  "args": {},
  "data": "",
  "files": {},
  "form": {
    "name": "germey"
  },
  "headers": {
    "Accept": "*/*",
    "Accept-Encoding": "gzip, deflate",
    "Content-Length": "11",
    "Content-Type": "application/x-www-form-urlencoded",
    "Host": "www.httpbin.org",
    "User-Agent": "",
    "X-Amzn-Trace-Id": "Root=1-685127d2-459135812d25d95a15a1565d"
  },
  "json": null,
  "origin": "171.34.210.142",
  "url": "http://www.httpbin.org/post"
}

可以看到,我们达到了与 POST 请求同样的效果。

有了 Request 这个对象,就可以将请求当作独立的对象来看待,这样在一些场景中我们可以直接操作这个 Request 对象,更灵活地实现请求的调度和各种操作。

总结

本节的 requests 库的基本用法就介绍到这里了,怎么样?有没有感觉它比 urllib 库使用起来更为方便。本节内容需要好好掌握,后文我们会在实战中使用 requests 库完成一个网站的爬取,顺便巩固 requests 库的相关知识。