news 2026/9/29 18:14:45

从requests到Session与重试:Python HTTP请求实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从requests到Session与重试:Python HTTP请求实战指南

凌晨两点,我盯着终端里疯狂滚动的报错日志——exceeded retry limit, last status: 429 too many requests——那是我用Python写的一个数据采集脚本,跑了不到一小时就被服务端限流拍死在沙滩上。老实说,这类错误对用requests库的开发者来说不陌生,真正让我难受的不是429本身,而是我发现自己对requests的理解还停留在“发个get()拿到响应”的层面,一旦遇到重试策略、会话保持、版本管理这些实战问题,完全是在靠猜。

requests是Python生态里使用率最高的HTTP客户端库,没有之一。爬虫、接口测试、自动化脚本、后端服务调用,几乎都绕不开它。但很多人(包括当时的我)对它的认知是断层的——会用requests.get(url),却讲不清参数怎么传、Session为什么能保持登录、429限流怎么通过重试机制优雅化解。这篇文章我不打算写官方文档的翻译版,而是基于我自己踩过的坑,把requests从安装到实战、从get()到重试机制的完整链路拆开讲一遍,希望能给正在用requests做爬虫或接口调用的朋友一些真正可复用的经验。

1. 为什么Python写HTTP请求绕不开requests

1.1 从urllib到requests:一次开发者体验的跨越

Python标准库其实自带HTTP客户端——urllib。我早期写爬虫用的就是它,说实话能用,但痛苦。一个简单的GET请求,urllib要写urllib.request.urlopen(),处理响应要手动decode,设置超时要单独传timeout参数,抛出来的异常还得用urllib.error里的各种类去接,代码又长又绕。

requests的设计目标就是“让HTTP请求对人类友好”,K大神Kenneth Reitz做这个库时反复强调的核心理念:HTTP请求应该是自然的、可读的、符合直觉的。用requests写同样的GET请求:

import requests resp = requests.get("https://httpbin.org/get", timeout=10) print(resp.text)

三行完事。响应对象直接帮你搞定编码解析、JSON序列化、状态码判断这些杂活。这不是炫技,而是降低了所有Python开发者发起HTTP请求的门槛,所以不管做爬虫还是调API,大家第一反应都是import requests。

1.2 requests库到底帮我们干了什么

往深了说,requests并不是在底层重新发明了HTTP协议,它是对Python标准库http.client和urllib3的一层封装。像连接池管理、SSL验证、重试机制这些重活,实际上由urllib3完成,requests提供的是简洁的API层。

这点很重要,因为当你遇到exceeded retry limit这类错误时,排查方向其实会涉及两层:requests这层你能控制的是请求参数、Session配置、超时与重试的显式设置;而底层urllib3的默认行为(比如默认重试次数、连接池大小)也会影响最终表现。理解了分层关系,后面调参才有的放矢。

用户只需要写几行业务代码,剩下的TCP连接、TLS握手、HTTP报文构造、响应解析全部被吞进库内部处理。这也是它学起来快的原因——你不需要成为HTTP协议专家就能发请求,但如果想真正用好它,知道一点底层机制会非常有帮助。

1.3 安装与首次运行:版本先看清

安装requests的方式很简单,大多数时候一条命令就够了:

pip install requests

但pip install背后有几个隐藏坑。如果你是在Windows上手动装的Python,很可能遇到这样一段提示:

Defaulting to user installation because normal site-packages is not writeable

我不少朋友看到这句话直接懵了,其实内容很简单:当前Python环境的site-packages目录没有写权限,pip自动转成了--user安装模式,把包装进了当前用户目录。碰上这情况,建议先确认一下自己是不是应该用虚拟环境,别图省事硬往系统环境塞包。后面第5章我会单独讲环境和升级的问题,这里先不展开。

装完之后可以顺手验证下版本:

import requests print(requests.__version__)

我在2025年常用的版本是2.32.x,如果你看到2.31以下的老版本,建议找时间升级,因为涉及一些安全修复和连接行为优化。

2. 一个get()请求的一生:从URL构造到响应解析

2.1 params、headers、timeout的配合逻辑

热搜词里“requests库的get方法”排得很靠前,说明这是绝大多数人接触requests的第一步。get()最基础的用法确实是传一个URL,但真实场景里几乎没有只传URL就够的请求。

带参请求最常见的写法是:

import requests url = "https://api.example.com/search" params = {"keyword": "python", "page": 1, "limit": 20} headers = {"User-Agent": "Mozilla/5.0"} resp = requests.get(url, params=params, headers=headers, timeout=15)

这里要重点理解params的机制——requests不会让你手动在URL后面拼?keyword=python&page=1,它会把params字典里的键值对做URL编码后拼到地址上。这样做的好处一是方便扩展,二是字典里的特殊字符(比如中文、空格、&)会被正确处理。

headers是另一个常被忽略的参数。很多接口要对来源做校验,最基本的就是User-Agent。用requests默认的python-requests/x.x.x标识去访问某些网站,很容易被当成爬虫识别。我不是教你伪装成浏览器去欺骗站点,而是想说明:带上合理且真实的UA,是网络请求的基本礼貌,能显著降低被误伤的几率。

对于timeout,我的经验是必须在生产代码里设置,千万别省。不设timeout意味着你的程序可能因为服务端不响应而无限挂起,这在爬虫和定时任务里尤其致命。合理取值要看具体接口——一般外部API我给5到10秒,内网服务给3秒,批量采集时甚至可以给15秒以上,避免网络波动导致频繁超时。

2.2 响应对象里面的三个常用出口:text、content、json()

resp.text返回的是解码后的字符串,resp.content返回的是原始字节流,resp.json()则是直接帮你把JSON格式的响应体解析成字典或列表。很多新手分不清三者的使用场景,我做个简单的对照:

方法返回类型适用场景注意事项
resp.textstr网页源码、纯文本接口编码是requests根据响应头猜测的,可能不准
resp.contentbytes图片、文件下载、未知编码流保留原始数据,适合再编码或写文件
resp.json()dict/listJSON格式APIJSON解析失败会抛异常,要先判断状态码

实际工作中最忌一上来就resp.json()。如果服务端返回了500错误页或一个纯文本的报错信息,json解析会直接炸。更稳妥的做法是先用resp.status_code判断,或直接用resp.raise_for_status()——这个方法在HTTP状态码为4xx/5xx时会抛出requests.exceptions.HTTPError,省得你手写一堆if判断。

处理响应编码有几个经验:如果resp.text出现乱码,原因大概率是服务端没有声明字符集或声明错误,此时用resp.encoding手动指定目标编码再重新获取resp.text即可;如果是下载文件,务必要用resp.content配合二进制模式写文件,用text会导致文件损坏。

2.3 状态码处理与异常体系:别和if语句硬刚

我在很多项目里看到过这种代码:

resp = requests.get(url) if resp.status_code == 200: return resp.json() else: return None

不是说不能用,而是这个写法把大量可能发生的异常都吞掉了。requests本身有一套完善的异常体系,用好它代码会干净很多:

  • requests.exceptions.ConnectionError:连接失败,比如DNS解析不了、目标拒绝连接
  • requests.exceptions.Timeout:请求超时,包括连接超时和读取超时
  • requests.exceptions.HTTPError:HTTP状态码非2xx,配合raise_for_status()触发
  • requests.exceptions.RequestException:上面所有异常的基类

所以我的习惯是:

import requests from requests.exceptions import RequestException try: resp = requests.get(url, timeout=10) resp.raise_for_status() except RequestException as e: print(f"请求失败: {e}") else: return resp.json()

这样抛出来的错误信息包含具体的失败原因,排查问题时比拿到一个None清晰得多。

3. 爬虫实战:Session、请求头与频率控制

3.1 Session对象:为什么连续请求会吃闭门羹

爬虫初学者最容易遇到的困惑:我用requests.get()明明能拿到首页,为什么下一步带着Cookie去访问个人中心就提示未登录?原因很简单——每次直接调用get()创建的都是一次独立的连接,请求之间不共享Cookie,服务端自然认不出你。

requests.Session()解决的就是这个问题。Session对象在底层维护了一个连接池,并且自动管理Cookie。你第一次用Session请求时服务端返回的Set-Cookie会被存进Session的CookieJar里,后续同Session发起的请求会自动带上这些Cookie:

import requests session = requests.Session() # 第一次请求会携带登录信息,服务端返回Cookie session.post("https://example.com/login", data={"username": "test", "password": "123456"}) # 后续请求自动携带登录后的Cookie resp = session.get("https://example.com/my/profile")

除了Cookie,Session还会复用底层TCP连接,减少了重复建连的开销。批量请求几十上百条时,性能差距非常明显。我在采集脚本里基本都会建一个Session,再配上统一的headers和超时设置,整个会话期间的代码会干净很多:

session = requests.Session() session.headers.update({"User-Agent": "Mozilla/5.0"}) session.timeout = 10

3.2 请求头伪装与Cookie管理:合规与效率的平衡

接着说说请求头。很多接口只靠User-Agent做基础识别,所以一个符合常规浏览器习惯的UA能挡掉大量误伤。除了UA,Referer和Origin在部分站点会被校验,这个就取决于具体接口了,没有通用公式,但抓包看一次正常浏览器请求,照着填一般不会错。

Cookie方面,requests的Session已经足够好用,但有一个细节值得注意:如果需要手动往Session里塞Cookie,不要用字符串拼接,用requests.cookies构造:

from requests.cookies import cookiejar_from_dict session = requests.Session() session.cookies = cookiejar_from_dict({"sessionid": "abc123", "token": "xyz"})

这样CookieJar的结构是标准的,后续更新Cookie也能正常合并。

强调一下,这里说模拟请求头,指的是用合理的手段访问公开数据。抓取数据前务必确认目标站点是否允许爬虫,注意robots.txt,控制请求频率,不要对服务端造成压力。用requests写爬虫很容易,但实际上“能爬到”和“可以爬”是两码事,合规是第一位的。

3.3 频率控制:别让脚本把服务端打挂

大部分限流问题不是被刻意针对,而是客户端请求太密集触发了服务端保护。我的经验是,无论目标接口多“皮实”,批量请求之间必须加间隔。

最简单的做法是time.sleep(),但硬编码固定间隔有个坏处:间隔太短容易被封,间隔太长采集效率太低。更合理的策略是加入随机抖动:

import time import random time.sleep(random.uniform(0.5, 1.5))

这样从服务端看,你的请求间隔不是一个整齐的固定节奏,更像人类操作,触发风控的概率会低一些。再进阶一点,可以用一个自适应的限速器——遇到429就把间隔翻倍,持续正常就把间隔逐渐调小。这类逻辑我会放在一个独立的请求层里统一管理,而不是散落每个爬虫脚本中。

4. 429限流全排查:从报错到重试策略落地

4.1 先搞懂429到底在说什么

回到开头那个exceeded retry limit, last status: 429 too many requests。这个报错通常不是requests本身直接抛出来的,而是来自urllib3的Retry模块——请求触发了重试机制,但每次重试后服务端依然返回429,直到超过重试上限。

HTTP 429(Too Many Requests)的含义是“请求过于频繁,已被服务端限流”。服务端通过X-RateLimit-Limit和X-RateLimit-Remaining等响应头告知额度,或者通过Retry-After头告诉客户端“你需要等多少秒再试”。用requests写代码时,如果你不做任何重试配置,默认情况下请求发出后拿到429响应并不会自动重试,代码会直接以429状态码返回——很多朋友第一次遇到限流就是这么懵的。

当429出现时,问题不再是“怎么发送请求”,而是“如何科学地等待重试”。无脑重试第一波可能侥幸通过,但如果你完全不等待就立刻狂点,服务端会进一步收紧策略,甚至会直接封IP。

4.2 一次实测:从exceeded retry limit到跑通全流程

我之前写过一个小任务,需要批量调用某个公开的搜索接口,最初版本是这样的:

import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry session = requests.Session() retry = Retry( total=3, status_forcelist=[429, 500, 502], backoff_factor=0.5, ) adapter = HTTPAdapter(max_retries=retry) session.mount("http://", adapter) session.mount("https://", adapter)

配置里的backoff_factor=0.5很关键。Retry的退避时间计算公式是{backoff_factor} * (1 << retry_count),也就是第一次重试前等0.5秒,第二次等1秒,第三次等2秒,成指数递增。这套策略在大多数情况下有效,但还有一个容易忽略的点——status_forcelist里填入429后,urllib3会去读响应头Retry-After,如果目标接口返回了这个头,会优先使用其中的值作为等待时长。

那次任务一开始没配Retry-After的兼容逻辑,因为目标接口压根没返回Retry-After头,只靠指数退避硬扛。跑了几个小时后我发现日志里依然出现exceeded retry limit,复盘日志后定位到原因:那个接口的限流窗口是按分钟计算的,高峰时段每分钟只允许30次请求,而我的脚本在单线程+自动重试模式下,一分钟内发起请求数远超了额度。

4.3 正确的重试姿势:限速器、退避算法与业务降级

那次踩坑让我把重试策略升级成了“三层结构”,后面再没被429击穿过:

第一层,请求发出前控制速率。我会写一个简单的令牌桶或滑动窗口,保证每秒最多N个请求,从源头减少429触发概率。

第二层,重试时遵循指数退避,加上随机抖动。纯指数退避有个问题:如果所有客户端在同一时间被限流,大家同步等待相同的时间后同时重试,会再次制造一波流量尖峰。加一个随机扰动可以打散重试节奏。

import random import time def retry_with_backoff(func, max_retries=5, base_delay=1): for attempt in range(max_retries): try: return func() except requests.exceptions.RequestException as e: if attempt == max_retries - 1: raise delay = base_delay * (2 ** attempt) + random.uniform(0, 0.5) time.sleep(delay)

第三层,重试次数耗尽后的业务降级。程序里出现exceeded retry limit并不代表任务失败,更像是一个需要处理的业务分支:跳过当前批次、记录日志、稍后集中补偿。我在任务里会把这些失败请求写进一个待重试队列,等限流窗口过了再统一处理,整体任务不会因为单批失败而中断。

对于调用第三方服务的场景,务必要读一读对方接口文档里的限流说明。有的服务是按QPS限,有的是按每分钟请求总量限,有的会明确在响应头里给出配额值。不理解服务端的限流维度,客户端这边再怎么调退避参数都是盲调。

5. requests库的升级与安装环境避坑

5.1 升级前先搞清楚:你的包到底装在哪里

前面提到了pip install requests时可能遇到“Defaulting to user installation”的提示,这个问题在升级时同样常见。很多人执行pip install --upgrade requests后,用pip show requests查看版本已经更新了,但在Python里import requests后打印的版本号还是旧的,原因就是系统里有多个Python环境,pip装到了其中一个,解释器用的是另一个。

遇到这种情况,先别急着骂pip。用下面两条命令确认环境:

where python python -m pip show requests

where python能看到当前命令行里用的Python解释器完整路径。python -m pip能确保pip和当前Python解释器绑定,避免调用到别的pip。

5.2 升级requests的完整流程与常见报错

升级requests本身不复杂:

python -m pip install --upgrade requests

但有几个高频问题需要专门提。第一,如果你长期没升级过,pip版本可能太老,会遇到pip install命令报错或找不到新版本索引的情况。先升级pip:

python -m pip install --upgrade pip

第二,Windows上如果提示权限不足,优先用虚拟环境,而不是想都不想加sudo或管理员权限。为每个项目建独立的虚拟环境是成本最低的隔离方案,也彻底避开“多个环境互相污染”的坑。

第三,升级后别只盯着requests的版本,还要看一眼urllib3的版本。requests对urllib3有版本依赖限制,如果你单独装了一个过新或过旧的urllib3,可能破坏requests的运行环境。稳妥做法是直接用pip install --upgrade requests让它自己解析依赖,不要手动盲目改urllib3的版本。

5.3 版本锁定:为什么线上环境不能随手upgrade

开发环境把requests升到最新版没啥问题,但生产环境一定要锁定版本。requests的API设计很稳定,但小版本更新可能修正连接行为、安全策略,一旦线上某个脚本依赖旧行为,升级后可能突然出现登录失效、证书校验失败等诡异问题。

我用得比较多的是把项目的依赖写进requirements.txt:

requests==2.32.3 urllib3==2.x.y certifi==2024.x.y

固定版本后,不管是本地开发还是CI部署,依赖都能保持一致。每次要升级时单独开一个分支,跑完全量测试再合并,不能图省事直接在服务器上pip install --upgrade了事。

6. 进阶思路:requests不够用的时候怎么办

6.1 异步场景:requests和httpx、aiohttp的取舍

requests库有一个绕不开的软肋——它是一个同步库。一个请求发出去,在拿到完整响应之前,当前线程就卡在那里。如果任务是几十个请求串行跑,整体耗时基本就是所有请求耗时之和,性能天花板很明显。

做爬虫时如果追求采集效率,通常会切换到异步方案。httpx是一个很接近requests体验的替代品,API风格几乎无缝迁移,同时支持同步和异步调用;aiohttp则更底层一些,灵活度更高,但需要自己处理的东西也多。

一个常见的折中方案是:业务逻辑继续用requests,数据量大时再用concurrent.futures.ThreadPoolExecutor做多线程并发,规避requests的GIL限制。注意并发会放大请求频率,更要配合第4章讲的限速器使用,不然很容易把目标服务打挂。

6.2 证书、代理与自定义适配器的经验

requests默认用certifi提供的CA证书做HTTPS校验。某些公司内网或测试环境使用自签名证书,直接请求会报SSLError。对这种场景,正确做法是把自签名证书加进信任列表,或者用verify参数指定CA包路径:

resp = requests.get("https://internal.example.com", verify="/path/to/ca.pem")

而不是一关了之(verify=False)。关闭证书校验在调试时可以临时用,生产环境会引入中间人攻击的风险,建议不要这么干。

代理方面,requests通过proxies参数或环境变量HTTP_PROXY/HTTPS_PROXY支持HTTP代理。我在调试接口时会用一个本地代理工具抓包看请求细节,在公司网络环境访问外网接口也经常需要走代理,配置方式都是一样的:

proxies = {"http": "http://10.0.0.1:8080", "https": "http://10.0.0.1:8080"} resp = requests.get(url, proxies=proxies)

需要说明一句:这里讲的代理仅指标准HTTP代理,公网访问限制和网络接入问题请按所在网络环境规定执行,我不展开。

6.3 个人实践:搭建一套稳定可复用的请求基础层

说到最后,我想分享一个让我少踩很多坑的做法:不要在每个脚本里重复写requests调用,而是把常用逻辑抽成一个独立的请求基础层。它的核心职责包括统一的Session初始化、默认超时与重试配置、日志记录、错误分类、限速控制。

伪代码结构大致长这样:

class ApiClient: def __init__(self, base_url, concurrency_limit=5): self.session = requests.Session() self.session.headers.update({"User-Agent": "MyCrawler/1.0"}) # 配置重试策略与连接池 retry = Retry(total=3, backoff_factor=1, status_forcelist=[429, 500, 502, 503]) adapter = HTTPAdapter(pool_connections=100, pool_maxsize=100, max_retries=retry) self.session.mount("https://", adapter) def get_json(self, path, **kwargs): url = self.base_url + path resp = self.session.get(url, timeout=10, **kwargs) resp.raise_for_status() return resp.json()

所有具体业务只调用这个基础层,底层改连接池、改重试策略时,业务代码完全不用动。时间久了你会发现,这套基础层才是一个爬虫项目最值得维护的地方,它替业务侧屏蔽掉了HTTP协议、限流策略、网络异常这些真正的复杂度。

就像我在开头那次429事故里学到的——requests入门只要半天,把它用好却需要在真实流量里不断调试和积累。上面这套请求层的雏形,是我在多次“重试到崩溃、被限流到封IP”之后逐步改出来的。如果你现在还在每个脚本里随手写requests.get(),我确实建议找一个时间把这层抽出来,之后的维护成本会低非常多。至于限流和重试的调参,没有一次成型的神仙配置,我自己的做法是每接一个新目标服务,就先跑一个小规模的探针任务,观察响应码和响应头,再调整重试参数。这种“先探路再批量”的思路,比拿全量数据去撞限流阈值要稳得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 18:14:00

8个AI论文平台全流程拆解:从选题、文献、润色到查重答辩的实战指南

研究生毕业论文&#xff0c;十个人里有八个是被“选题”“文献”“润色”“图表”“查重”这五座大山轮番折磨过来的。我自己当年写论文的时候&#xff0c;还没有现在这么多AI平台可用&#xff0c;全靠人肉肝&#xff0c;到后期改格式改到怀疑人生。这几年辅导过不少师弟师妹&a…

作者头像 李华
网站建设 2026/9/29 18:12:55

WorkBuddy自动化实践:用deepseek-v4-flash生成AI日报并推送微信小程序

1. 为什么我要给 WorkBuddy 设一个“十点半闹钟”每天早上到工位&#xff0c;第一件事不是打开编辑器&#xff0c;而是先刷一遍昨天夜里各个渠道冒出来的消息&#xff1a;项目群里有没有人 我、待办列表里有没有逾期任务、昨天提交的几份材料有没有反馈、几个正在跑的数据任务…

作者头像 李华
网站建设 2026/9/29 18:12:41

Halcon与C#工业视觉框架架构设计:产线级稳定性与实装案例解析

做工业视觉上位机开发的朋友&#xff0c;应该都有类似的经历&#xff1a;Halcon负责"看得准"&#xff0c;C#负责"管得住"&#xff0c;两者凑在一起就是一套完整的视觉检测系统。我手头这套框架是在2.0版本的基础上改出来的&#xff0c;2.0当年在公司内部传…

作者头像 李华
网站建设 2026/9/29 18:12:29

技术平权下的一人公司:用标准化接口打磨个人业务系统

第一次看到“专知智库OPC研究院”这个名字时&#xff0c;我脑子里冒出来的其实是另一群OPC——工业自动化圈里的OPC UA、OPC Server&#xff0c;用C#连接西门子PLC的朋友对这个词一定不陌生。但往下看才反应过来&#xff0c;这里的OPC不是通信协议&#xff0c;而是One Person C…

作者头像 李华
网站建设 2026/9/29 18:11:56

Flutter在OpenHarmony上的电子合同搜索模块实战解析

把 Flutter 应用跑到 OpenHarmony 设备上&#xff0c;这个动作已经淘汰掉一批准备不足的团队&#xff1b;而要在电子合同签署App里把合同搜索做到又快又准&#xff0c;又会淘汰掉一批只会写列表页的开发者。我上个月刚完成公司“电子合同签署App”的 OpenHarmony 适配&#xff…

作者头像 李华