跳转至

Requests

约 1658 个字 350 行代码 3 张图片 预计阅读时间 10 分钟

HTTP for Humans™

官方项目的第三方模块

安装

用 pip 安装(注意拼写,后面有 s):

pip install requests

构建简单的 HTTP 请求

一个简单的 GET 请求

1
2
3
4
import requests                             # 同样要注意拼写
r = requests.get('http://httpbin.org/get')  # r 是一个 Response 对象
print(r.status_code)                        # HTTP 状态码
print(r.json())                             # 若 r 的响应是 JSON 的话,r.json() 返回的是表示 JSON 的字典
输出
200
{'args': {},
 'headers': {'Accept': '*/*',
  'Accept-Encoding': 'gzip, deflate',
  'Cache-Control': 'max-age=259200',
  'Host': 'httpbin.org',
  'User-Agent': 'python-requests/2.28.0',
  'X-Amzn-Trace-Id': 'Root=1-62b2dd69-786cb565338857bc160bd757'},
 'origin': '10.94.5.157, 58.213.34.25',
 'url': 'http://httpbin.org/get'}

urllib 对比

urllib 需要先构建请求,再获取响应;Requests 在构建请求时即可获取响应。

urllib 需要、得到的数据类型是特有的,需要用特有的方法去构建、解析;Requests 需要、得到的数据类型通常是基本的,绝大多数不需要特别构建、解析。

urllib
from urllib import request, parse

data = parse.urlencode([
    ('param1', 1),
    ('param2', 'hello'),
    ('param3', '测试')
])

req = request.Request('http://httpbin.org/post')
req.add_header('User-Agent', 'ding_urllib_test')

with request.urlopen(req, data=data.encode('utf-8')) as f:
    print('Status:', f.status, f.reason)
    for k, v in f.getheaders():
        print('%s: %s' % (k, v))
    print('Data:', f.read().decode('utf-8'))
Requests
import requests

data = {
    'param1': 1,
    'param2': 'hello',
    'param3': '测试'
}
header = {'User-Agent': 'ding_requests_test'}

with requests.post('http://httpbin.org/post', data=data, headers=header) as f:
    print('Status:', f.status_code, f.reason)
    for k, v in f.headers.items():
        print('%s: %s' % (k, v))
    print('Data:', f.text)

基本原理

底层基于第三方模块 urllib3;下面省略模块、对象名

Requests 基本原理
Requests 基本原理

构建请求所用的方法语句

这些方法的实质为 requests.request(HTTP方法的小写字符串, URL[, ...]),最终返回 requests.Response 对象。

1
2
3
4
requests.get / post / put / delete / head / options / patch (
    url,
    [参数[, 其他参数...]]
)

常用参数如下:

参数名 数据类型 含义
params 字典 /元组列表 / bytes 发送到 URL 的参数
data 字典 / 元组列表 / bytes / 文件对象 发送到请求主体的参数
headers 字典 定义首部行
json 字典 发送到请求主体的 JSON(可代替 data
files 包含文件对象的字典 发送到请求主体的文件
cookies 字典或 CookieJar 对象 和请求一并发送的 Cookie
auth 元组 需要验证身份时填的信息
timeout 浮点数或元组 超时时间
allow_redirects 布尔值 是否允许重定向(默认为是)
proxies 字典 代理设置
verify 布尔值或字符串 是否验证 TLS 证书(默认为是),或指定 CA 证书路径
stream 布尔值 若为否,立即下载响应内容
cert 字符串或元组 SSL 客户端密钥(pem),或表示 (证书名, 密钥) 的元组

构建带参数的请求

参数分 params(放在请求 URL)和 data(放在请求实体)。

data 可以为字典、元组列表、bytes、文件对象、字符串等类型。

会自动进行 URL 编码,自动添加需要的首部。

1
2
3
4
5
6
r = requests.post(
    'http://httpbin.org/post',
    params={'p1': 1, 'p2': 'hello测试'},
    data={'d1': 2, 'd2': 'hello测试主体'}
)
print(r.text)
输出
{
  "args": {
    "p1": "1", 
    "p2": "hello\u6d4b\u8bd5"
  }, 
  "data": "", 
  "files": {}, 
  "form": {
    "d1": "2", 
    "d2": "hello\u6d4b\u8bd5\u4e3b\u4f53"
  }, 
  "headers": {
    "Accept": "*/*", 
    "Accept-Encoding": "gzip, deflate", 
    "Cache-Control": "max-age=0", 
    "Content-Length": "49", 
    "Content-Type": "application/x-www-form-urlencoded", 
    "Host": "httpbin.org", 
    "User-Agent": "python-requests/2.28.0", 
    "X-Amzn-Trace-Id": "Root=1-62b91e38-0f3db4366037212279043c37"
  }, 
  "json": null, 
  "origin": "10.94.5.157, 114.221.187.200", 
  "url": "http://httpbin.org/post?p1=1&p2=hello\u6d4b\u8bd5"
}

构建请求主体为 JSON 的请求

请求主体为 JSON 字符串时,建议使用 json 参数,而非 data。前者可以自动把请求首部的 Content-type 填为 application/json

1
2
3
4
5
r = requests.post(
    'http://httpbin.org/post',
    json={'d1': 2, 'd2': 'hello测试主体'}
)
print(r.text)
输出
{
  "args": {}, 
  "data": "{\"d1\": 2, \"d2\": \"hello\\u6d4b\\u8bd5\\u4e3b\\u4f53\"}", 
  "files": {}, 
  "form": {}, 
  "headers": {
    "Accept": "*/*", 
    "Accept-Encoding": "gzip, deflate", 
    "Cache-Control": "max-age=259200", 
    "Content-Length": "48", 
    "Content-Type": "application/json", 
    "Host": "httpbin.org", 
    "User-Agent": "python-requests/2.28.0", 
    "X-Amzn-Trace-Id": "Root=1-62b92789-345d433772b19eb102fa385a"
  }, 
  "json": {
    "d1": 2, 
    "d2": "hello\u6d4b\u8bd5\u4e3b\u4f53"
  }, 
  "origin": "10.94.5.157, 114.221.187.200", 
  "url": "http://httpbin.org/post"
}

发送文件

files 里面最常见的结构如下:

1
2
3
4
5
6
7
8
{
    字段名字符串: (
        文件名字符串,
        文件对象换句话说就是流或表示文件内容的字符串,
        MIME 类型字符串,
        {其他字段名字符串: 其他字段值, ...}
    ), ...
}
1
2
3
4
[
    (字段名, (文件名, 内容, MIME, 其他字段)),
    ... # 此写法可以使用同一字段名发送多个文件
]

文件要在二进制模式下打开。

如果混有文件和数据,分别写到 filesdata 中,Requests 会自动帮忙处理好。

猜测文件的 MIME 类型

如果你事先不知道文件的 MIME 类型,可以使用标准库 mimetypes 猜测:

1
2
3
4
5
>>> import mimetypes
>>> mimetypes.guess_type('get.json')
('application/json', None)
>>> mimetypes.guess_type('get.json')[0]
'application/json'

file1 = open('get.json', 'rb')
file2 = open('avatar.jpg', 'rb')
file3 = open('avatar_work.png', 'rb')
files = [
    ('file1', ('g.json', file1, mimetypes.guess_type('get.json')[0])),
    ('file2', ('测试1.jpg', file2, 'image/jpeg')),
    ('file2', ('测试2.png', file3, 'image/png', {'test': 'istest'}))
]
data = {
    'data1': 'hello',
    'data2': '测试'
}
r = requests.post('http://httpbin.org/post', data=data, files=files)
print(r.text)
file1.close()
file2.close()
file3.close()

Response 对象的属性和方法

查看 Response 对象的属性和方法,显示所有可用的属性和方法(不包含以下划线开头的):

1
2
3
4
5
6
7
8
9
>>> list(filter(lambda s: not s.startswith('_'), r.__dir__()))
[
    'status_code',  'headers',  'raw',      'url',      'encoding',
    'history',  'reason',   'cookies',  'elapsed',  'request',
    'connection',   'ok',       'is_redirect',  'is_permanent_redirect',
    'next',     'apparent_encoding',        'iter_content', 'iter_lines',
    'content',  'text',     'json',     'links',        'raise_for_status', 
    'close'
]
  • status_code:整数,HTTP 状态代码:

    >>> r.status_code
    200
    
  • ok:布尔值,HTTP 状态代码是否小于 400

    >>> r.ok
    True
    

    该属性还用于整个对象的布尔值(__bool__()):

    1
    2
    3
    4
    5
    6
    >>> bad_r = requests.get('https://httpbin.org/status/404')
    >>> bool(r), bool(bad_r)
    (True, False)
    >>> print('r') if r else print('XrX'); print('bad_r') if bad_r else print('Xbad_rX')
    r
    Xbad_rX
    
  • raise_for_status():如果想要在请求发生 4XX 或 5XX 错误时抛出异常,可以使用它:

    如果有错误,抛出 requests.exceptions.HTTPError

    1
    2
    3
    4
    5
    6
    >>> bad_r = requests.get('https://httpbin.org/status/404')
    >>> bad_r.raise_for_status()
    Traceback (most recent call last):
      File "requests/models.py", line 832, in raise_for_status
        raise http_error
    requests.exceptions.HTTPError: 404 Client Error
    

    否则,返回 None

    >>> r.raise_for_status()
    None
    
  • reason:字符串,HTTP 状态的文本:

    1
    2
    3
    4
    >>> r.reason
    'OK'
    >>> bad_r.reason
    'NOT FOUND'
    
  • url:字符串,最终请求的 URL。如请求时有 params 参数,也会同时把参数写进去:

    1
    2
    3
    >>> rr = requests.get('http://httpbin.org/get', params={'param1': 1, 'param2': 'hello', 'param3': '测试'})
    >>> rr.url
    'http://httpbin.org/get?param1=1&param2=hello&param3=%E6%B5%8B%E8%AF%95'
    
  • headers:字典,响应首部行。键为字段名,值为字段值

    >>> r.headers
    {'Date': 'Thu, 23 Jun 2022 05:54:46 GMT', 'Content-Type': 'application/json', 'Content-Length': '358', 'Server': 'gunicorn/19.9.0', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Credentials': 'true', 'X-Cache': 'MISS from ts.com', 'X-Cache-Lookup': 'MISS from ts.com:3128', 'Via': '1.1 ts.com (squid/3.5.20)', 'Connection': 'keep-alive'}
    
  • cookies:返回 Cookies,数据类型为 requests.cookies.RequestCookieJar ,里面是 Cookie 信息。

    可以像字典一样操作;也可以转为字典:

    1
    2
    3
    4
    5
    6
    7
    >>> rr = requests.get('https://www.baidu.com')
    >>> rr.cookies
    <RequestsCookieJar[Cookie(version=0, name='BDORZ', value='27315', port=None, port_specified=False, domain='.baidu.com', domain_specified=True, domain_initial_dot=True, path='/', path_specified=True, secure=False, expires=1656400630, discard=False, comment=None, comment_url=None, rest={}, rfc2109=False)]>
    >>> rr.cookies['BDORZ']
    '27315'
    >>> rr.cookies.get_dict()
    {'BDORZ': '27315'}
    
  • encoding:字符串,响应的编码。默认情况下通过响应首部行的 charset 字段得出,但是可以更改该属性的值,以强制定义编码:

    1
    2
    3
    >>> r.encoding
    'utf-8'
    >>> r.encoding = 'ISO-8859-1'
    
  • apparent_encoding:同上,但是使用 chardet 模块,通过内容猜测得出:

    1
    2
    3
    4
    5
    6
    7
    8
    >>> r = requests.get('http://httpbin.org/get', params={'ces': '测试'})
    >>> r.encoding
    'utf-8'
    >>> r.apparent_encoding
    'ascii'     # 因为该网站返回的 JSON 里面把非 ASCII 的字符都转义了
    >>> rr = requests.get('https://www.baidu.com')
    >>> rr.apparent_encoding
    'utf-8'
    
    网站返回的 JSON
    1
    2
    3
    4
    5
    6
    7
    8
    {
      "args": {
        "ces": "\u6d4b\u8bd5"
      }, 
      "headers": {
        ...(省略)...
        }
    }
    
  • links:字典,返回响应首部的 Link 字段的信息;如无,返回空字典

    Link 字段的格式如下:

    Link: < uri-reference >; param1=value1; param2="value2"
    
    >>> r.links
    {}
    
    >>> ar = requests.get('http://httpbin.org/response-headers?Link=%3C%2Fstatic%2Ffavicon.ico%3E%3B%20rel%3D%22icon%22%3B%20type%3D%22image%2Fpng%22')
    >>> ar.links
    {'icon': {'url': '/static/favicon.ico', 'rel': 'icon', 'type': 'image/png'}}
    
    >>> ar = requests.get(http://httpbin.org/response-headers?Link=%3C%2Fstatic%2Ffavicon.ico%3E%3B%20rel%3D%22icon%22%3B%20type%3D%22image%2Fpng%22&Link=%3C%2Fflasgger_static%2Fswagger-ui.css%3E%3B%20rel%3D%22stylesheet%22%3B%20type%3D%22text%2Fcss%22')
    >>> ar.links
    {'icon': {'url': '/static/favicon.ico', 'rel': 'icon', 'type': 'image/png'},
     'stylesheet': {'url': '/flasgger_static/swagger-ui.css',
      'rel': 'stylesheet',
      'type': 'text/css'}}
    
  • elapsed:表示响应时间,数据类型为 datetime.timedelta

    1
    2
    3
    4
    5
    6
    >>> r.elapsed
    datetime.timedelta(seconds=6, microseconds=665815)
    >>> r.elapsed.microseconds
    665815          # 整数,表示去掉秒数的微秒数
    >>> r.elapsed.total_seconds()
    6.665815        # 浮点数,表示秒数
    

    这里的响应时间,指的是从“发送请求的第一个字节”开始,到“解析完响应的首部行”为止,与响应主体的大小无关。

  • content:字节对象,响应主体(如果想下载文件,取这个)

    >>> r.content
    b'{\n  "args": {\n    "param1": "1", \n    "param2": "hello", \n    "param3": "\\u6d4b\\u8bd5"\n  }, \n  "headers": {\n    "Accept": "*/*", \n    "Accept-Encoding": "gzip, deflate", \n    "Cache-Control": "max-age=0", \n    "Host": "httpbin.org", \n    "User-Agent": "python-requests/2.28.0", \n    "X-Amzn-Trace-Id": "Root=1-62b41052-74863b8a34e457ae201382fc"\n  }, \n  "origin": "10.94.5.157, 58.213.34.25", \n  "url": "http://httpbin.org/get?param1=1&param2=hello&param3=\\u6d4b\\u8bd5"\n}\n'
    
  • text:同上,但数据类型是字符串

    >>> r.text
    '{\n  "args": {\n    "param1": "1", \n    "param2": "hello", \n    "param3": "\\u6d4b\\u8bd5"\n  }, \n  "headers": {\n    "Accept": "*/*", \n    "Accept-Encoding": "gzip, deflate", \n    "Cache-Control": "max-age=0", \n    "Host": "httpbin.org", \n    "User-Agent": "python-requests/2.28.0", \n    "X-Amzn-Trace-Id": "Root=1-62b41052-74863b8a34e457ae201382fc"\n  }, \n  "origin": "10.94.5.157, 58.213.34.25", \n  "url": "http://httpbin.org/get?param1=1&param2=hello&param3=\\u6d4b\\u8bd5"\n}\n'
    
  • json(**kwargs):如响应主体是 JSON 格式的文本,可以用该方法转为字典或列表这样的 Python 可以直接处理的格式

    >>> r.json()
    {'args': {'param1': '1', 'param2': 'hello', 'param3': '测试'},
     'headers': {'Accept': '*/*',
      'Accept-Encoding': 'gzip, deflate',
      'Cache-Control': 'max-age=0',
      'Host': 'httpbin.org',
      'User-Agent': 'python-requests/2.28.0',
      'X-Amzn-Trace-Id': 'Root=1-62b41052-74863b8a34e457ae201382fc'},
     'origin': '10.94.5.157, 58.213.34.25',
     'url': 'http://httpbin.org/get?param1=1&param2=hello&param3=测试'}
    
    • 该方法使用 complexjson 模块,兼容 json.loads() 的附加参数
    • 如出错,抛出 requests.exceptions.RequestsJSONDecodeError 异常
  • history:列表,表示重定向的历史

    • 如果请求被重定向,该属性记录了重定向的历史:里面每一个元素都是 Response 对象,表示重定向到的请求;重定向多少次,里面就有多少请求
    • 如果没有被重定向,该属性为空列表
    >>> r.history
    []
    >>> rr = requests.get('http://httpbin.org/absolute-redirect/3') # 重定向三次
    >>> rr.history
    [<Response [302]>, <Response [302]>, <Response [302]>]
    >>> for i in rr.history:
    ...     print(i.url)
    http://httpbin.org/absolute-redirect/3
    http://httpbin.org/absolute-redirect/2
    http://httpbin.org/absolute-redirect/1
    >>> rr.url
    http://httpbin.org/get      # rr.url 记录的是最终的 URL
    
  • is_redirect:布尔值,该请求是否被重定向(HTTP 状态码是否为 301302303307308

    重定向的请求的最后一步为 False,之前的步骤(history 里面的)的为 True

    接上例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    >>> r.is_redirect
    False
    >>> rr.is_redirect
    False
    >>> for i in rr.history:
    ...     print(i.is_redirect)
    True
    True
    True
    

    对应的原理图
    对应的原理图

  • is_permanent_redirect:布尔值,该请求是否被永久重定向(HTTP 状态码是否为 301308

    接上例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    >>> r.is_permanent_redirect
    False
    >>> rr.is_permanent_redirect
    False
    >>> for i in rr.history:
    ...     print(i.is_permanent_redirect)
    False
    False
    False
    
    永久重定向示例
    >>> prr = requests.get('http://httpbin.org/status/301')
    >>> prr.is_permanent_redirect
    False
    >>> prr.status_code
    200
    >>> prr.history
    [<Response [301]>, <Response [302]>]
    >>> for i in prr.history:
    ...     print(i.is_permanent_redirect)
    True
    False
    
  • iter_content()iter_lines():都是返回一个生成器,可迭代,迭代结果的类型都是字节类型

    • 前者依次迭代响应主体的一个字节
    >>> for i in r.iter_content():
    ...     print(type(i), i)
    <class 'bytes'> b'{'
    <class 'bytes'> b'\n'
    <class 'bytes'> b' '
    <class 'bytes'> b' '
    <class 'bytes'> b'"'
    <class 'bytes'> b'a'
    <class 'bytes'> b'r'
    <class 'bytes'> b'g'
    <class 'bytes'> b's'
    ...省略...
    
    • 后者依次迭代响应主体的一行
    >>> for i in r.iter_lines():
    ...     print(type(i), i)
    <class 'bytes'> b'{'
    <class 'bytes'> b'  "args": {'
    <class 'bytes'> b'    "param1": "1", '
    <class 'bytes'> b'    "param2": "hello", '
    <class 'bytes'> b'    "param3": "\\u6d4b\\u8bd5"'
    <class 'bytes'> b'  }, '
    <class 'bytes'> b'  "headers": {'
    <class 'bytes'> b'    "Accept": "*/*", '
    <class 'bytes'> b'    "Accept-Encoding": "gzip, deflate", '
    ...省略...
    
  • request:返回 requests.models.PreparedRequest 类,其中包含许多于请求相关的属性,如:

    • url:URL(如果有参数,也会写在里面)
    • body:请求实体,为 bytes 对象
      • 将请求实体的内容转为文本:r.request.body.decode(errors='ignore')
    • headers:请求首部
  • close():关闭请求
    • 执行完之后,最好关闭它
    • 关闭后仍然可以访问大部分内容
    • 也可以像打开文件一样,利用 with 语句构建请求

快速转换 cURL 命令行到 Python 的 Requests 代码

https://curlconverter.com/#python

操作步骤
操作步骤

先发送第一个请求:

import requests

headers = {
    'authority': 'zhuanlan.zhihu.com',
    'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
    'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
    'cache-control': 'no-cache',
    'dnt': '1',
    'pragma': 'no-cache',
    'sec-ch-ua': '" Not;A Brand";v="99", "Microsoft Edge";v="103", "Chromium";v="103"',
    'sec-ch-ua-mobile': '?0',
    'sec-ch-ua-platform': '"Windows"',
    'sec-fetch-dest': 'document',
    'sec-fetch-mode': 'navigate',
    'sec-fetch-site': 'same-origin',
    'sec-fetch-user': '?1',
    'upgrade-insecure-requests': '1',
    'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.53 Safari/537.36 Edg/103.0.1264.37',
}

r1 = requests.get('https://zhuanlan.zhihu.com/p/67995315', headers=headers)
print(r1.ok)    # True

此时的 Cookie:

>>> r1.cookies
<RequestsCookieJar[Cookie(version=0, name='_xsrf', value='c8ae5332-e338-4129-9579-44f487066ea3', port=None, port_specified=False, domain='.zhihu.com', domain_specified=True, domain_initial_dot=True, path='/', path_specified=True, secure=False, expires=None, discard=True, comment=None, comment_url=None, rest={}, rfc2109=False), Cookie(version=0, name='_zap', value='cb4cf9ea-6a01-4dbd-923e-64e957fe82be', port=None, port_specified=False, domain='.zhihu.com', domain_specified=True, domain_initial_dot=True, path='/', path_specified=True, secure=False, expires=1719385521, discard=False, comment=None, comment_url=None, rest={}, rfc2109=False), Cookie(version=0, name='d_c0', value='"AuDRplQSKRWPTv6PBJ7S-5wHj4FKp0HZ9Dw=|1656313521"', port=None, port_specified=False, domain='.zhihu.com', domain_specified=True, domain_initial_dot=False, path='/', path_specified=True, secure=False, expires=1750921521, discard=False, comment=None, comment_url=None, rest={}, rfc2109=False), Cookie(version=0, name='KLBRSID', value='0a401b23e8a71b70de2f4b37f5b4e379|1656313521|1656313521', port=None, port_specified=False, domain='zhuanlan.zhihu.com', domain_specified=False, domain_initial_dot=False, path='/', path_specified=True, secure=False, expires=None, discard=True, comment=None, comment_url=None, rest={}, rfc2109=False)]>

再发送另一个请求:

1
2
3
4
headers['referer'] = 'https://zhuanlan.zhihu.com/p/67995315'

r2 = requests.get('https://zhuanlan.zhihu.com/p/22396872', cookies=r1.cookies, headers=headers)
print(r2.ok)    # True

该请求的 Cookie:

>>> r2.cookies
<RequestsCookieJar[Cookie(version=0, name='KLBRSID', value='0a401b23e8a71b70de2f4b37f5b4e379|1656313636|1656313521', port=None, port_specified=False, domain='zhuanlan.zhihu.com', domain_specified=False, domain_initial_dot=False, path='/', path_specified=True, secure=False, expires=None, discard=True, comment=None, comment_url=None, rest={}, rfc2109=False)]>

可以发现之前的 Cookie 未随之而来;因为 r2.cookies 只记录了这个请求传回的 Cookie,未进行叠加。

但实际上,浏览器里面此前的 Cookie 是会保留的,下次请求会一并发送出去。

手动叠加

RequestsCookieJar 对象没法直接叠加,但可以转为字典之后叠加,字典可以作为 Cookie 传入请求:

1
2
3
4
5
c = r1.cookies.get_dict()   # 转为字典
c |= r2.cookies.get_dict()  # 更新,这种更新方式可以兼顾原值和新值,有冲突时以后者为准
headers['referer'] = r2.url
r3 = requests.get('https://zhuanlan.zhihu.com/p/28037988', headers=headers, cookies=c)
print(r3.ok)    # True

利用 Session 对象

requests.sessions.Session 对象可以创建一个会话,会话中建立的请求会共享一些信息,如 Cookie。

s = requests.session()

为了简化代码,可以定义这个会话的首部字段:

s.headers = headers

发送第一个请求:

r = s.get('https://zhuanlan.zhihu.com/p/67995315')
print(r.ok) # True

之后不需要动 Cookie,会自动发送;为了更好地模拟,可以把首部的 referer 字段改一下:

1
2
3
4
5
6
s.headers['referer'] = r.url
r = s.get('https://zhuanlan.zhihu.com/p/22396872')
print(r.ok)     # True
s.headers['referer'] = r.url
r = s.get('https://zhuanlan.zhihu.com/p/28037988')
print(r.ok)     # True

此时的 Cookie:

>>> s.cookies
<RequestsCookieJar[Cookie(version=0, name='_xsrf', value='6a39fa7a-6aeb-48b3-9f29-edc6cb5c9f52', port=None, port_specified=False, domain='.zhihu.com', domain_specified=True, domain_initial_dot=True, path='/', path_specified=True, secure=False, expires=None, discard=True, comment=None, comment_url=None, rest={}, rfc2109=False), Cookie(version=0, name='_zap', value='6f4096aa-1eb5-45bc-b070-7d292b70217d', port=None, port_specified=False, domain='.zhihu.com', domain_specified=True, domain_initial_dot=True, path='/', path_specified=True, secure=False, expires=1719387572, discard=False, comment=None, comment_url=None, rest={}, rfc2109=False), Cookie(version=0, name='d_c0', value='"AOAfkycaKRWPTtpqrENfucPtK8LHbpY9aJY=|1656315572"', port=None, port_specified=False, domain='.zhihu.com', domain_specified=True, domain_initial_dot=False, path='/', path_specified=True, secure=False, expires=1750923572, discard=False, comment=None, comment_url=None, rest={}, rfc2109=False), Cookie(version=0, name='KLBRSID', value='5430ad6ccb1a51f38ac194049bce5dfe|1656315642|1656315572', port=None, port_specified=False, domain='zhuanlan.zhihu.com', domain_specified=False, domain_initial_dot=False, path='/', path_specified=True, secure=False, expires=None, discard=True, comment=None, comment_url=None, rest={}, rfc2109=False)]>

Session 对象也可以像 Response 对象一样,使用 close() 关闭。