Featured image of post 解剖Requests:从Python小白到源码猎手的进阶之路

解剖Requests:从Python小白到源码猎手的进阶之路

一次HTTP请求背后的模块化设计与代码抽象艺术

学而不能致用的人是一头背着书的牛马。蠢驴是否知道它背上背着的是一堆书而不是一捆柴?—— 本杰明·富兰克林

前言

2025年2月底,我回顾了前五年学习工作经历以及技术发展趋势后,在编程语言方面树立以Python与C++为主的编程技能,Java、Rust、Go等其他语言为辅,这样我需要推动自己将Pyhon和C++的技能尽可能达到精通,熟记这两门语言核心知识。正如我在最近阅读的《富兰克林自传》中所看到,富兰克林早期以印刷技能作为自己的谋生手段,而近三年我也以编程技能作为自己的谋生手段,未来路还很长,不能止步于此,需要不断学习其他领域与学科知识,学以致用,不能成为芒格口中的“铁锤人”。

Python语言大概是在我五六年前开始正式作为工作语言,当时以《Head First Python》书籍入门,在读研期间通过阅读与实践深度学习相关项目,如Numpy、Pytorch、mmdetection、mmocr等,对Python语言有了更深的应用理解。最近通过阅读《流畅的Python》第2版,对该语言进行系统性地回顾与熟记;此次阅读Requests源码,一是对最近学习的一个总结与应用,二是给自己未来通过Python实现更大更复杂项目打下坚实的基础。

谁适合阅读这篇文章?

如何更好地阅读本篇文章?

项目简介

这里直接引用官方对Requests的相关介绍,我们在后面源码解析中会逐步展开代码中是如何实现这些功能的。

A simple, yet elegant, HTTP library. Requests是一个简单而优雅的 HTTP 库。

Supported Features & Best–Practices 支持的功能和最佳实践

Requests is ready for the demands of building robust and reliable HTTP–speaking applications, for the needs of today.
Requests已准备好满足构建强大且可靠的 HTTP 应用程序的需求,以满足当今时代的需求。

  • Keep-Alive & Connection Pooling 保持连接和连接池
  • International Domains & URLs 国际域名和 URL
  • Sessions with Cookie Persistence 具有Cookie 持久性的会话机制
  • Browser-style SSL Verification 浏览器风格的 SSL 验证
  • Basic/Digest Authentication 基本身份验证和摘要身份认证
  • Familiar dict–like Cookies 熟悉的类似dict的 Cookies
  • Automatic Content Decompression and Decoding 内容自动解压缩和解码
  • Muti-part File Uploads 多部分文件上传
  • SOCKS Proxy Support 支持 SOCKS 代理
  • Connection Timeouts 连接超时
  • Streaming Downloads 流式下载
  • Automatic honoring of .netrc 自动遵循.netrc配置
  • Chunked HTTP Requests 分块 HTTP 请求

项目结构

我们再来看一下Requests的项目的顶层结构,并简单介绍一下各个目录与文件的作用。 这里采用eza -a -T -L 1 --group-directories-first命令查看项目目录结构。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.
├── .git    # Git 版本控制系统的目录,包含项目的版本历史和配置
├── .github    # GitHub 相关的配置文件和工作流,如持续集成(CI)配置等
├── docs    # 文档目录,包含使用指南、API 文档等
├── ext    # 外部扩展或依赖项,现已逐步弃用
├── src    # 源码目录,包含主要的接口和功能实现代码
├── tests    # 测试目录,包括单元测试与集成测试代码,覆盖请求发送、异常处理、Cookie 管理等关键功能。
├── .coveragerc 
├── .git-blame-ignore-revs
├── .gitignore
├── .pre-commit-config.yaml   # Git钩子配置文件,在提交代码前自动执行,用于代码规范检查、格式化等
├── .readthedocs.yaml    # Read the Docs 平台用于配置项目构建过程的配置文件
├── AUTHORS.rst
├── HISTORY.md
├── LICENSE
├── Makefile    # 自动化任务的文件,如构建、测试
├── MANIFEST.in
├── NOTICE
├── pyproject.toml
├── README.md
├── requirements-dev.txt
├── setup.cfg
├── setup.py    # 安装和打包配置文件,定义项目的元数据(如名称、版本、依赖项)并支持 pip install 和 setuptools 进行管理。
└── tox.ini

上面项目组织结构中,我们可以学习到一个广泛遵循的约定,即将项目的源码、测试、文档等内容分别放置在不同的目录中,以便于管理和维护。通常将源代码放在 src/ 目录,测试代码放在 tests/ 目录,文档资料放在 docs/ 目录,并使用 LICENSE 文件声明开源许可。

其他文件是一些Python项目工具的配置文件,如 pyproject.toml、setup.py、requirements-dev.txt、tox.ini 等,关于这些项目构建和运行的配置文件,平时在使用到这些工具时多多留意就好啦~

源码解析

本想寻找两三个简单实用的示例,无意间查看源码目录的src/requests/__init__.py文件,发现已经有两个示例了,我们在这里直接运行它们。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
(common-env) ➜  requests git:(main) python
Python 3.12.9 | packaged by Anaconda, Inc. | (main, Feb  6 2025, 12:55:12) [Clang 14.0.6 ] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import requests
>>> r = requests.get('https://www.python.org')
>>> r.status_code
200
>>> payload = dict(key1='value1', key2='value2')
>>> r = requests.post('https://httpbin.org/post', data=payload)
>>> print(r.text)
{
  "args": {}, 
  "data": "", 
  "files": {}, 
  "form": {
    "key1": "value1", 
    "key2": "value2"
  }, 
  "headers": {
    "Accept": "*/*", 
    "Accept-Encoding": "gzip, deflate", 
    "Content-Length": "23", 
    "Content-Type": "application/x-www-form-urlencoded", 
    "Host": "httpbin.org", 
    "User-Agent": "python-requests/2.32.3", 
    "X-Amzn-Trace-Id": "Root=1-67d7be38-6126bbec2e56917a24eda1a3"
  }, 
  "json": null, 
  "origin": "45.86.73.64", 
  "url": "https://httpbin.org/post"
}

下面正式开始对Requests源码进行解析,从src/requests/__init__.py文件开始。

__init__.py文件

以下截取文件代码中的核心部分,省略部分使用三行# ...表示。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# ...
# ...
# ...
from . import packages, utils
from .__version__ import (
    __author__,
    __author_email__,
    __build__,
    __cake__,
    __copyright__,
    __description__,
    __license__,
    __title__,
    __url__,
    __version__,
)
from .api import delete, get, head, options, patch, post, put, request
from .exceptions import (
    ConnectionError,
    ConnectTimeout,
    FileModeWarning,
    HTTPError,
    JSONDecodeError,
    ReadTimeout,
    RequestException,
    Timeout,
    TooManyRedirects,
    URLRequired,
)
from .models import PreparedRequest, Request, Response
from .sessions import Session, session
from .status_codes import codes

logging.getLogger(__name__).addHandler(NullHandler())

# FileModeWarnings go off per the default.
warnings.simplefilter("default", FileModeWarning, append=True)

我们知道在Python中,__init__.py文件的作用包含以下三点:

  1. 标记包目录:文件存在即表示该目录是一个Python包,允许导入子模块(空文件也可)。
  2. 初始化包:包被导入时自动执行,用于(1)定义包级变量/函数(如版本号、工具函数);(2)批量导入子模块,简化外部调用,如from . import submodule
  3. 控制导入行为:(1)通过__all__指定from package import *时导出的模块列表;(2)隐藏内部实现,仅暴露特定接口。

具体到上面展示的Requests中__init__.py文件核心代码,主要包含了:

  • 导入子模块,如packagesutils
  • 导入版本信息,如__version__
  • 导入API接口,如deletegetheadoptionspatchpostputrequest
  • 导入异常类,如ConnectionErrorConnectTimeoutHTTPErrorJSONDecodeError等;
  • 导入请求和响应模型类,如PreparedRequestRequestResponse
  • 导入会话管理类,如Sessionsession
  • 导入状态码类,如codes
  • 进行日志模块的初始化。

在阅读和解析其他模块源码之前,我们先来看下Requests核心模块的架构设计。其源码目录结构如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
(common-env) ➜  requests git:(main) ✗ lz src/requests 
src/requests
├── __init__.py
├── __version__.py
├── _internal_utils.py
├── adapters.py
├── api.py
├── auth.py
├── certs.py
├── compat.py
├── cookies.py
├── exceptions.py
├── help.py
├── hooks.py
├── models.py
├── packages.py
├── sessions.py
├── status_codes.py
├── structures.py
└── utils.py

Requests的核心模块主要包含api.pysessions.pyadapters.pymodels.pyutils.pystatus_codes.py,下面通过PlantUML图展示Requests的核心模块架构设计。

Requests核心模块架构设计

api.py模块

在查看api.py模块源码之前,我们将通过运行测试用例并逐步跟踪代码执行流程,这样可以更好地理解代码的执行逻辑。

理解 tests/ 目录结构

将该目录下文件按照功能分类,方便查找和阅读。

  • 核心功能测试test_requests.py
  • 适配器与底层传输test_adapters.py, test_lowlevel.py
  • 工具与辅助功能test_utils.py, utils.py
  • 数据结构与内部类test_structures.py
  • 钩子与扩展功能test_hooks.py
  • 测试基础设施testserver/, test_testserver.py, certs/
  • 兼容性与环境验证compat.py
  • 帮助与文档test_help.py
  • 包与安装验证test_packages.py
  • 测试框架配置conftest.py, __init__.py

安装测试依赖

运行下面命令安装测试依赖,包含 pytest 测试框架和 pytest-httpbin 本地 HTTP 服务模拟。

1
2
uv pip install -e .[socks]  # 包含 socks 代理等可选依赖
uv ip install pytest pytest-httpbin  # 测试框架和本地 HTTP 服务模拟

运行测试用例并进入调试模式

这里我们选择运行test_HTTP_200_OK_GET_ALTERNATIVE测试方法,调试程序会在进入方法后暂停,方便我们跟踪代码执行流程。

1
pytest tests/test_requests.py -k test_HTTP_200_OK_GET_ALTERNATIVE --trace

接下来使用 n(下一步)、s(进入函数)等命令跟踪代码。

api.py模块源码解析

本章我们重点关注api.py模块,尤其是其中比较常用的requests.get()requests.post()方法,上面示例的测试用例函数test_HTTP_200_OK_GET_ALTERNATIVE中使用了requests.Request()requests.Session()方法,我们在这里先不做详细展开。

我在test_requests.py文件中找到了test_HTTP_200_OK_GET_WITH_PARAMS测试用例,作为项目源码入手的起点,我们将逐步调试并解析这个测试用例。

此外,通过命令行中调试代码虽然方便,但是面对现代大型Python项目,采用IDE提供的调试工具更加方便。因此,创建以下VS Code调试配置文件。

.vscode/launch.json具体内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Debug Tests",
            "type": "debugpy",
            "request": "launch",
            "module": "pytest",
            "args": ["tests/test_requests.py::TestRequests::test_HTTP_200_OK_GET_WITH_PARAMS"],
            "cwd": "${workspaceFolder}",  // 指定工作目录
            "justMyCode": false,  // 允许跟踪 Requests 源码
        }
    ]
}

同时,配置.vscode/settings.json文件,指定 Python 解释器路径。这样,按下 F5 键即可在 VS Code 中调试test_HTTP_200_OK_GET_WITH_PARAMS测试用例。

注意:在这里我踩了一个坑,一开始配置的.vscode/launch.json文件中configurations的args参数写成了"args": ["tests/test_requests.py::test_HTTP_200_OK_GET_WITH_PARAMS", "--trace"],导致调试时一直提示找不到测试用例,后来将测试类TestRequests::加上即可。分析我犯的错误的原因,主要有两个:
(1)利用人工智能文件编辑提示功能,自动生成了tests/test_requests.py:test_HTTP_200_OK_GET_ALTERNATIVE,我想当然得用test_HTTP_200_OK_GET_WITH_PARAMS将其替换;
(2)没有熟悉pytest的用法,尤其是区分pytest test_sample.py::TestMath::test_addpytest test_sample.py -k test_add的区别,前者需指定测试类和测试方法,后者可以模糊匹配测试方法。

配置完后,可以在测试代码旁打断点,再按 F5 键开始调试,这样就可以在 VS Code 中逐步调试测试用例代码了。如下图:

VS Code代码调试

参考资料

知识点记忆

  • 提示词:模块、包。在Python中,模块是一个单独的 .py 文件,包是一个包含多个模块的文件夹,且必须有 __init__.py 文件。
  • 提示词:主流工具。构建、打包和分发工具setuptools、测试框架pytest、代码格式化工具black、自动排序导入工具isort、代码静态分析工具flake8、自动化测试环境管理工具tox、文档生成工具Sphinx。
All Rights Reserved.(所有权利保留。禁止未经授权的复制或再分发。)
使用 Hugo 构建
主题 StackJimmy 设计