1. requests库到底强在哪,为什么爬虫和接口调试都绕不开它
做Python开发这些年,我见过太多人一上来就问我"爬虫用什么库",我永远只会回答一个名字:requests。不是因为它完美无缺,而是因为它是目前Python生态里把HTTP请求这件事做得最"顺手"的库,没有之一。官方文档第一句话就说它是"给人类用的HTTP库",这句话一点不夸张。
先给没接触过的朋友一个直观感受。你用Python写一个最简单的HTTP请求,如果只用标准库urllib,代码写起来又长又绕,要手动处理URL编码、要手动构造请求头、响应还要自己去decode。但换成requests,三行代码搞定:
import requests resp = requests.get("https://httpbin.org/get") print(resp.status_code) print(resp.text)这背后隐藏了大量细节:连接建立、HTTP报文封装、响应解析、字符编码处理、Cookie存取,requests全都替你做了。你写爬虫、调接口、做数据采集、写自动化脚本,最核心的操作就是"发请求、拿响应",requests把你从繁琐的底层协议里解放出来,让你把精力集中在业务逻辑上。
这个库适合谁?我觉得覆盖范围极广:刚入门Python想学爬虫的新手,用它写第一个能跑的程序;做数据分析的人,用它拉取公开数据集;后端开发做接口联调,用它在脚本里快速验证;甚至做自动化运维的,也能用它写监控报警脚本。只要你的程序需要和Web打交道,requests就是一个永远绕不开的基础设施。
我最早接触requests是七年前,那时候网上爬虫教程清一色用urllib,后来requests一出来,社区几乎是以肉眼可见的速度迁移。为什么?因为它把"请求"这个动作简化到了极致,而且API设计得非常直觉化,你不需要背文档,猜都能猜出来该怎么用。这篇文章我就把requests从入门到实战的完整链路讲一遍,包括我踩过的坑、排查过的问题,全是实际经验,看完你就能直接上手写东西。
2. 环境准备与第一个请求:别小看安装这步
2.1 安装Python与requests库的几种姿势
先说环境。很多新手卡在第一步,不是requests难装,而是Python环境本身没配好。如果你用的是Windows,去python.org下载安装包时,务必勾选"Add Python to PATH"这个选项,否则装完在命令行里敲python会提示"不是内部或外部命令"。这一步忘了勾,后面全是坑。
装好Python之后,安装requests有两种方式。最推荐的是pip:
pip install requests如果你系统里同时装了Python 2和Python 3,可能需要用pip3,甚至在Windows上要用python -m pip install requests,避免装错环境。这一点我特别提醒一下,热词里专门有"windows安装python多个版本",这就是多版本环境的经典问题。我建议多版本用户一律用虚拟环境(venv)管理依赖,而不是全局pip装包,否则不同项目依赖的版本冲突会让人抓狂。
还有一种情况是离线安装。有些公司的开发环境是内网隔离的,没法直接pip,这时候需要在一台能联网的机器上下载whl文件,然后拷贝内网安装:
pip download requests -d ./packages pip install requests --no-index --find-links=./packages2.2 发送第一个GET请求并理解响应结构
环境准备好后,我们来发第一个真正意义上的请求。我习惯用httpbin.org这个在线测试服务来学习HTTP,因为它会把你发的请求原样返回,方便观察结构。
import requests resp = requests.get("https://httpbin.org/get") print(resp.status_code) # 200 print(resp.headers) # 响应头,是一个字典对象 print(resp.text) # 响应体的文本形式 print(resp.json()) # 如果响应是JSON,直接解析成字典这里有几个非常重要的细节。resp.status_code是HTTP状态码,200表示成功,404表示资源不存在,500表示服务器内部错误,状态码是判断请求是否成功的第一个信号。resp.headers是一个大小写不敏感的字典,你可以直接用resp.headers["Content-Type"]取值,不用管服务器返回时是Content-Type还是content-type。resp.text是requests根据响应头里的编码自动解码后的字符串,如果服务器没声明编码,可以手动指定resp.encoding = "utf-8"再访问resp.text。
需要强调一点:resp.json()虽然方便,但它内部只是调用json.loads(resp.text),如果响应体不是合法的JSON,会抛出异常。所以实际项目中,我建议先判断状态码和Content-Type,再决定是否调用json()。
2.3 带参数请求和自定义Headers:从入门到像浏览器
绝大多数接口都不会让你裸奔访问,至少要有查询参数。requests传URL参数的方式非常优雅,不需要自己拼URL:
params = { "keyword": "python", "page": 1, "page_size": 20 } resp = requests.get("https://httpbin.org/get", params=params) print(resp.url) # 打印实际请求的完整URLrequests会自动把字典转成URL编码后的查询字符串,中文、特殊字符都不用你操心。这在爬虫场景里特别实用,翻页、搜索、筛选条件,统统可以维护在一个字典里,逻辑清晰又容易改。
自定义Headers也是爬虫和接口调试的刚需。很多网站会检查User-Agent,如果你用默认的"python-requests/x.x.x",很容易被反爬策略拦截。最简单的伪装就是设置一个浏览器的User-Agent:
headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "Accept": "application/json, text/plain, */*", "Referer": "https://example.com" } resp = requests.get("https://httpbin.org/get", headers=headers)我个人的习惯是,把常用的headers组合封装成一个函数,返回一个公共请求头字典,这样多个爬虫项目能复用。还有个小技巧:如果你不确定某个请求需要哪些头,先用浏览器的开发者工具(F12)打开Network面板,刷新页面找到对应的请求,右键Copy as cURL,然后可以用一个叫curl2requests的小工具把它转成requests代码,调试效率能翻倍。
3. 进阶实战:Session、重试与429错误处理
3.1 Session对象:为什么爬虫必须用它
初学requests时,大部分人都是直接requests.get、requests.post这样裸调。这样当然能跑,但只要遇到需要登录的网站,就歇菜了。原因很简单:每次裸调requests,都是一次全新的、无状态的HTTP连接,服务器不会记得你上一次做过什么。
而Session对象相当于一个持久的"会话容器",它会自动保存Cookie,复用底层TCP连接。我打个比方,裸调requests就像每次去超市都重新办一张会员卡,Session则是把会员卡一直揣在兜里,第二次去直接刷。
session = requests.Session() session.headers.update({ "User-Agent": "Mozilla/5.0 ..." }) # 先登录,Cookie会被session自动保存 login_data = {"username": "test", "password": "123456"} session.post("https://httpbin.org/post", data=login_data) # 后续请求自动携带登录态 resp = session.get("https://httpbin.org/cookies") print(resp.text)这段代码里,登录接口返回的Set-Cookie会被Session自动保存,后续请求自动带上,你完全不感知。这种会话保持能力在写爬虫时特别关键,因为大多数网站的用户体系都依赖Cookie判断登录状态。
Session还有一个隐藏优势,就是连接复用。HTTP底层是TCP连接,频繁建立和断开连接非常耗时。Session内部维护了一个连接池,请求同一域名时会复用底层连接,速度能提升一个量级。这个优化在大量请求的场景下非常明显。
3.2 超时设置与重试机制:别再让程序卡死
接下来这块是我觉得requests最容易踩坑的地方,也是热词里反复出现的"exceeded retry limit, last status: 429 too many requests"这类报错的高发区。
先说超时。如果不设置timeout参数,requests默认会一直等下去,直到底层socket超时(这个时间很长,可能是几分钟)。实际爬虫场景里,一个请求卡住,整个程序就卡住了,后面的请求全排队,效率低到崩溃。所以我的铁律:所有请求必须设置timeout。
resp = requests.get("https://httpbin.org/delay/3", timeout=5)timeout=5表示连接和读取的总超时时间是5秒。如果想分开控制连接和读取的超时,可以传一个元组:
resp = requests.get("https://httpbin.org/delay/3", timeout=(3, 10))第一个值是连接超时(连不上服务器时就放弃),第二个值是读取超时(连接上了但数据迟迟不回时就放弃)。我通常设置(3, 10),连接3秒、读取10秒,比较平衡。
再说重试。网络请求是天然不稳定的,偶发超时、偶发5xx错误都很正常。很多人的代码一遇到请求失败就整个崩掉,这其实是不合理的,应该用重试机制来提升鲁棒性。requests本身没有内置重试逻辑,但它的底层urllib3提供了Retry类,可以通过HTTPAdapter挂载到Session上:
from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry session = requests.Session() retry = Retry( total=3, # 总重试次数 connect=3, # 连接失败重试次数 read=2, # 读取失败重试次数 backoff_factor=0.5, # 退避因子,重试间隔递增 status_forcelist=[500, 502, 503, 504] # 哪些状态码触发重试 ) adapter = HTTPAdapter(max_retries=retry) session.mount("http://", adapter) session.mount("https://", adapter)backoff_factor的机制是:第n次重试前的等待时间为backoff_factor * (2 ** (n-1))秒,也就是0.5秒、1秒、2秒这样指数递增。这个设计是为了避免重试风暴——如果服务器已经忙不过来了,你的请求还以超高频率打过去,只会让情况更糟。
3.3 429 Too Many Requests:遇到限流的正确姿势
热词里频繁出现429状态码,我专门来聊这个。HTTP 429表示"Too Many Requests",通俗说就是:你请求得太频繁了,服务器受不了了,让你歇会儿。
遇到429,最忌讳的做法是马上加大重试次数、缩短间隔继续猛打。正确的做法是尊重服务器的限流信号,退避重试。urllib3的Retry也对429做了特殊支持,你只需要在status_forcelist里加入429,再配合Retry-After头处理:
import time from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry def requests_with_retry(session, url, **kwargs): retry = Retry( total=5, backoff_factor=1, status_forcelist=[429, 500, 502, 503, 504], respect_retry_after_header=True ) adapter = HTTPAdapter(max_retries=retry) session.mount("https://", adapter) return session.get(url, **kwargs)respect_retry_after_header=True的意思是,如果服务器在响应头里返回了Retry-After字段(告诉你要等多少秒),urllib3会乖乖等待对应时间再重试。这是对服务器最基本的尊重,也是避免IP被封的关键。
我再分享一个实际经验:遇到429时,除了退避重试,更应该反思自己的请求频率是否合理。很多公开接口的文档里明确写了QPS限制,比如"每秒最多请求1次",那就应该在自己的代码里做好限速,而不是依赖服务器429来教做人。用time.sleep控制请求间隔,或者使用更高级的限流库,都是成熟的做法。
import time import requests def fetch_with_rate_limit(url, interval=2): time.sleep(interval) # 保证每次请求间隔至少2秒 resp = requests.get(url, timeout=10) return resp3.4 异常处理体系:让程序在出错时优雅降级
requests的异常体系不算复杂,但很多人处理得不对。它主要有以下几类异常:
| 异常类型 | 触发场景 | 处理建议 |
|---|---|---|
| requests.exceptions.ConnectionError | 网络不通、DNS解析失败、连接被拒绝 | 检查网络,稍后重试 |
| requests.exceptions.Timeout | 请求超时 | 增大timeout或重试 |
| requests.exceptions.TooManyRedirects | 重定向次数超过上限 | 检查URL是否有循环重定向 |
| requests.exceptions.HTTPError | HTTP状态码为4xx或5xx(需调用raise_for_status触发) | 按状态码区分处理 |
| requests.exceptions.JSONDecodeError | JSON解析失败 | 判断响应格式后再解析 |
推荐的写法是把请求封装成一个函数,统一捕获异常:
import requests from requests.exceptions import RequestException def safe_get(url, params=None, headers=None, timeout=(3, 10)): try: resp = requests.get(url, params=params, headers=headers, timeout=timeout) resp.raise_for_status() # 状态码4xx/5xx时抛出HTTPError return resp except requests.exceptions.Timeout: print(f"请求超时: {url}") except requests.exceptions.ConnectionError: print(f"连接失败: {url}") except requests.exceptions.HTTPError as e: print(f"HTTP错误: {e.response.status_code} - {url}") except RequestException as e: print(f"请求异常: {e}") return Noneraise_for_status()是一个被低估的方法。如果你不调用它,requests在收到404、500时也不会报错,resp.text里可能是一段HTML错误页面,你的代码还会继续往下跑,最后在一堆奇怪的错误里迷失方向。调用了它,状态码异常时立即抛异常,能让你第一时间发现问题。
我实际写爬虫时,异常处理、重试、日志这三者通常是配合使用的。重试解决偶发问题,异常处理兜底,日志记录每次失败的原因和上下文,方便事后排查。这三板斧下来,爬虫的稳定性会有一个质的飞跃。
4. 把requests用到极致:代理、SSL与性能优化
4.1 代理配置与SSL证书处理
爬虫写多了,必然会遇到代理的需求。可能是网站封了你的IP,可能需要绕过访问限制,也可能单纯是为了分散请求压力。requests配置代理非常简单,一个字典搞定:
proxies = { "http": "http://127.0.0.1:7890", "https": "http://127.0.0.1:7890" } resp = requests.get("https://httpbin.org/get", proxies=proxies)如果代理需要认证,在URL里带上用户名密码:
proxies = { "http": "http://user:password@127.0.0.1:7890" }这里我要特别强调合规和安全问题。如果你访问的是国内正常的公开网站和接口,完全不需要所谓的特殊网络工具。requests的代理功能最常应用于正常的开发调试场景——比如把请求转向本地抓包工具(Charles、Fiddler、mitmproxy)来分析报文。这个场景下,代理地址一般是127.0.0.1加端口号,目的只是让抓包工具能看到HTTP流量内容,帮助你排查问题,这才是requests代理配置的正确用法。
SSL证书验证是另一个高频问题。正常情况下,requests会验证HTTPS证书,这是安全的默认行为。但如果遇到自签名证书或公司内网的测试环境,验证会失败。你可以这样做:
# 方式一:关闭验证(仅限测试环境) resp = requests.get("https://self-signed.badssl.com", verify=False) # 方式二:指定CA证书路径 resp = requests.get("https://example.com", verify="/path/to/ca.crt")关闭verify=False后,requests会抛一个InsecureRequestWarning警告,提示你当前连接不安全。我的建议是:生产环境永远不要关闭SSL验证,如果确实需要用自签名证书,应该把证书加到信任列表里,用verify参数指定CA文件路径。这既是安全底线,也是职业素养。
4.2 连接池与性能优化
requests的设计目标是"简单好用",在极致性能上它确实不如aiohttp、httpx这类异步库,但通过合理配置,它的并发能力也够大多数场景用了。
性能优化的第一招是复用Session。前面提过Session内部维护连接池,默认每个域名10个连接。如果单线程爬虫觉得速度不够,可以用ThreadPoolExecutor配合Session,控制最大并发数:
from concurrent.futures import ThreadPoolExecutor import requests session = requests.Session() def fetch_one(url): try: resp = session.get(url, timeout=(3, 10)) return resp.status_code, len(resp.content) except Exception as e: return None, str(e) urls = [f"https://httpbin.org/get?id={i}" for i in range(50)] with ThreadPoolExecutor(max_workers=5) as executor: results = list(executor.map(fetch_one, urls))这里的关键是:Session对象是线程安全的,多个线程共享同一个Session没问题,连接池里的连接也会被复用。max_workers的取值建议从5开始,根据目标网站的反应调整。太快会被限流甚至封IP,太慢又没效率,这个度需要实际测试。
还有一个性能优化点是连接适配:
adapter = HTTPAdapter( pool_connections=20, # 连接池大小 pool_maxsize=20, # 每个主机的最大连接数 max_retries=3 )pool_connections是缓存连接的总数,pool_maxsize是同一主机可复用的连接数。调大这两个值能提升高并发下的吞吐量,但代价是内存占用更高。实际项目中,我通常保持默认,除非做压测时发现连接成为瓶颈。
4.3 流式下载与大文件处理
下载大文件时,一个经典错误是直接用resp.content把整个文件读进内存。如果文件有2GB,你的内存可能直接爆掉。正确的做法是启用流式模式,分块写入磁盘:
import requests url = "https://example.com/big-file.zip" resp = requests.get(url, stream=True, timeout=(5, 60)) if resp.status_code == 200: with open("big-file.zip", "wb") as f: for chunk in resp.iter_content(chunk_size=8192): if chunk: f.write(chunk)stream=True是核心,它让requests不会一次性下载全部内容,而是等你去迭代响应体。iter_content(chunk_size=8192)指定每次读取8KB,边下边写,内存占用始终在一个很小的范围内。
对于大文件下载,我还建议加一个进度显示。用tqdm这个库几行代码就能实现:
from tqdm import tqdm import requests resp = requests.get(url, stream=True, timeout=(5, 60)) total_size = int(resp.headers.get("Content-Length", 0)) chunk_size = 8192 with open("big-file.zip", "wb") as f: with tqdm(total=total_size, unit="B", unit_scale=True) as pbar: for chunk in resp.iter_content(chunk_size=chunk_size): if chunk: f.write(chunk) pbar.update(len(chunk))注意Content-Length头可能是0或不存在,这时total_size为0,进度条会显示不定长模式。这个细节很常见,很多新手会在这里困惑,其实不影响下载功能,只是显示上不准确。
5. 实战演练:一个完整的公开数据采集脚本
5.1 目标分析与合规检查
光说不练假把式。这一节我用一个完整的小项目,把前面讲的东西串起来。假设我们要采集一个公开的、允许爬取的API数据。为了安全合规,我用httpbin.org作为演示目标,它本身就是用来做HTTP测试的公开服务,没有任何访问限制,完全合法。
在写爬虫之前,我一直强调先做合规检查。核心就三条:一是检查网站的robots.txt,看是否允许爬虫访问目标路径;二是看网站的服务条款,是否有明确的爬取禁止声明;三是控制请求频率,不要给对方服务器造成压力。这不仅是道德问题,也是保护自己IP的风险管理。我见过太多人因为爬虫频率控制不当,导致IP被网站封禁,得不偿失。
5.2 完整代码实现与逐行解读
这个实战项目要做的事情是:从httpbin.org的延迟接口拉取多条数据,带重试、带超时、带限速、带错误日志,最后把成功的响应保存到本地JSON文件。
import json import time import logging import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" ) def create_session(): session = requests.Session() retry = Retry( total=3, backoff_factor=0.5, status_forcelist=[429, 500, 502, 503, 504] ) adapter = HTTPAdapter(max_retries=retry) session.mount("https://", adapter) session.headers.update({ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" }) return session def fetch_with_backoff(session, url, params=None): try: resp = session.get(url, params=params, timeout=(3, 10)) resp.raise_for_status() return resp.json() except requests.exceptions.Timeout: logging.warning("请求超时: %s", url) except requests.exceptions.HTTPError as e: logging.warning("HTTP错误: %s - %s", e.response.status_code, url) except requests.exceptions.ConnectionError: logging.warning("连接失败: %s", url) return None def main(): session = create_session() results = [] for i in range(1, 11): params = {"id": i} data = fetch_with_backoff( session, "https://httpbin.org/get", params=params ) if data: results.append({ "id": i, "args": data.get("args", {}), "user_agent": data.get("headers", {}).get("User-Agent", "") }) logging.info("成功获取第 %d 条数据", i) else: logging.error("第 %d 条数据获取失败", i) time.sleep(1) # 限速:每秒最多一个请求 with open("data.json", "w", encoding="utf-8") as f: json.dump(results, f, ensure_ascii=False, indent=2) logging.info("全部完成,共获取 %d 条数据", len(results)) if __name__ == "__main__": main()这段代码看着长,拆开看其实就几层:create_session负责构建带重试的会话,fetch_with_backoff负责发请求并统一处理异常,main函数控制业务流程和限速,日志记录每一环节的进展。这种分层结构是我写爬虫的基本框架,任何项目都可以套用。
5.3 抓取结果的结构化处理与存储
输出的JSON文件使用了ensure_ascii=False和indent=2两个参数,前者保证中文正常显示,后者让JSON格式化易读。这只是最基础的存储方式。
实际项目里,数据落地通常有几种选择。数据量小、结构简单的用JSON文件足够;需要查询筛选的,用SQLite或MySQL;如果只是临时跑一次,打印到终端也行。我记得有一次帮朋友抓几千条公开商品数据,存JSON文件没问题,但后续要做数据分析和可视化,还是导入Pandas更方便:
import pandas as pd df = pd.DataFrame(results) print(df.head())如果你的目标是数据分析和可视化,那么requests负责取数,Pandas负责清洗和分析,Matplotlib负责出图,这是一条非常顺的链路。热词里提到的"python数据分析与可视化"就是这么一套东西。
6. 高频报错与排查技巧实录
6.1 我最常被问到的几个requests报错
问的人多了,我把高频报错整理成一个速查表,方便你遇到问题时候能快速定位:
| 报错信息 | 出现原因 | 解决方案 |
|---|---|---|
| Max retries exceeded with url | 连接失败或超时,重试也失败 | 检查网络、URL是否正确、目标服务器是否可达 |
| ConnectionError: Max retries exceeded | 目标网站无法连接 | 确认域名解析正常,必要时用浏览器访问测试 |
| Too many redirects | URL存在循环重定向 | 检查URL配置,或用allow_redirects=False手动处理 |
| InsecureRequestWarning | 关闭了SSL验证 | 建议改用verify指定CA证书,而不是长期关闭验证 |
| SSLError: Certificate verify failed | SSL证书验证失败 | 更新CA证书库,或针对测试环境指定verify路径 |
| JSONDecodeError | 响应体不是合法JSON | 先resp.text确认返回内容,再考虑是否改用text解析 |
| Connection pool is full | 连接池耗尽 | 调大pool_maxsize,或检查代码是否存在连接泄漏 |
6.2 一个经典排查案例:429导致的连环问题
上个月有个朋友跑爬虫,程序跑着跑着突然报错:exceeded retry limit, last status: 429 too many requests。他问我是不是代理IP不行,我说你先别看代理,回去看看你的请求频率,这个报错的意思就是你的请求太过频繁被限流了,不是IP的问题,是频率的问题。
那次排查过程我记得很清楚。我让他先在代码里加了一行日志,打印每次请求的时间戳和URL,跑了两分钟后发现,他爬虫每秒钟发出去大概30个请求,而且对同一个域名。这种频率,大部分网站都会给429。解决方案也很简单:一是把并发数从10调降到3,二是每次请求间隔加0.5秒,三是在Retry里设置respect_retry_after_header=True,配合服务器的限流指示。改完之后,跑了一个小时,一个429都没再出现过。
这个案例我觉得很典型。很多人遇到429第一反应是"代理不够好"、"IP被拉黑",其实大概率是频率控制没做好。记住一个原则:能通过降低频率解决的问题,就不要用更多的IP去硬刚,这是爬虫能长期不被封的核心逻辑。
6.3 几个实用排查工具和技巧
除了会写requests代码,会"看"请求也很重要。我推荐几个排查思路:
第一,开启调试日志。requests基于urllib3,我们可以打开urllib3的调试日志,看到底层的HTTP交互细节:
import logging import requests logging.basicConfig(level=logging.DEBUG)这样会输出每次请求的连接、请求头、响应状态等信息。注意,这会打印大量日志,只建议在调试阶段开启。
第二,用抓包工具对比。如果requests请求失败,但浏览器访问正常,用Fiddler或Charles抓包,对比浏览器和requests发的请求头差异,很可能是少了某个必要的Header,比如Referer、Origin等。
第三,用状态码和响应头定位问题。收到403时,检查是否是User-Agent被识别了;收到404时,检查URL或路径参数是否正确;收到301/302时,检查是否要跟随重定向。这些都需要你对HTTP协议本身有基本的理解。
7. 聊聊我对requests的理解和一些小建议
requests这个库看起来简单,用起来也不难,但真正用好,需要对HTTP协议有基本的理解。比如状态码的含义、Header的作用、Cookie的机制,这些概念和requests的API是一一对应的。我的建议是,不要只停留在调API的层面,多花时间理解HTTP本身,你会发现requests的设计很多都是对HTTP语义的直观映射。
从长期使用的角度来看,我想再分享几个小建议。第一,管理好你的Session和请求频率,这是提升爬虫稳定性的核心;第二,尽量用类型标注给请求函数加上签名,时间久了你会感谢自己当初的规范;第三,requests和正则表达式配合使用,虽然效率不高,但很多小场景足够用了。如果你追求异步高性能,未来也可以关注 httpx 这个库,它的API风格和requests几乎一致,但支持异步,不过那是另一个话题了。
根据我个人经验,requests是Python生态里少有的"大而全又好用"的库,它能覆盖你90%的HTTP需求,而且踩坑成本极低。希望这篇文章能帮你把剩下那10%的坑也提前填上,让你少走点弯路。