☰
Python入门首选requests库:从HTTP协议到爬虫与429限流排查
2026/10/13 8:25:13 网站建设 项目流程

1. 为什么Python入门第一个该学透的库是requests

1.1 requests在Python生态里的真实地位

很多人觉得requests就是爬虫专用库,这个理解不算错,但严重低估了它的价值。Python入门阶段,你可能要处理接口调试、自动化脚本、数据采集、量化策略、运维监控,甚至给同事写个批量工具——这些场景里,requests几乎是绕不开的底层依赖。它不是“某个领域的库”,而是Python网络世界的通用方言。

我见过不少初学者绕开requests先去啃“更高级”的异步框架(比如aiohttp),结果写出来的代码既难调试又容易踩并发坑。原因很简单:异步框架对网络协议、事件循环、协程调度的理解要求比requests高一个量级。而requests把HTTP请求封装成了一次“函数调用”,让你能把精力放在业务逻辑上,而不是手写一堆socket解析。先把requests吃透,再用它作为跳板去理解更底层的协议、异步模型,这条路比一上来就挑战硬核框架顺畅得多。

1.2 发请求之前,先搞懂HTTP到底在做什么

requests是好用,但它替你封装的那层东西,你得知道是什么。HTTP本质上是一个“请求-响应”协议:客户端发一段结构化文本给服务器,服务器解析后返回另一段文本。整个过程和一个很日常的场景很像——你去餐厅吃饭:

  • 你坐下,服务员过来,你报出菜名——这就是请求方法(GET是“我看一眼菜单”,POST是“我要点这道菜”)。
  • 你填了个电话号码给服务员备注——这就是**请求头(Headers)**里的元信息,比如User-Agent告诉服务端“我是用什么客户端来的”。
  • 厨师做好菜,服务员端上来——这是响应。
  • 菜牌上的编号和最终上桌的菜品一一对应——这就是状态码(Status Code):200表示“有这道菜,正常上桌”,404表示“菜单上没有这道菜”,500表示“后厨出事了”,429则是“我忙不过来,你先别点菜了”。

你真正写代码时,不一定需要手撸HTTP报文,但看到状态码、请求头、响应体这些词,脑海里得有这张“餐厅流程图”。否则遇到问题时,你连从哪一步开始排查都不知道。

1.3 一次requests.get()背后到底发生了什么

很多人用requests就是“照抄代码”,却不理解一行requests.get(url)到底做了多少事。拆开看,它在几毫秒到几秒内替你完成了:

  • URL解析与参数编码
  • DNS域名解析
  • TCP连接建立(三次握手)
  • TLS/SSL握手(如果URL是HTTPS)
  • 构造并发送HTTP请求报文
  • 接收并解析响应头、响应体
  • 自动处理cookie、重定向、连接复用等细节

这些步骤里任何一步失败,requests都会抛异常。比如DNS解析失败抛requests.exceptions.ConnectionError,连接超时抛requests.exceptions.ConnectTimeout,请求被服务端拒绝返回4xx状态码时,requests会把它包装成HTTPError但不会主动抛它(后面我会讲为什么)。

所以说,requests的价值不是“让你不用懂网络”,而是“让你在懂网络的前提下,把精力从底层重复劳动中解放出来”。写Python入门阶段的项目,没有比这个库更适合当“HTTP协议活教材”的。

2. 环境准备与第一个请求:安装细节与踩坑实录

2.1 Python3环境与pip安装

在装requests之前,先确保本机有一个能用的Python3环境。终端里输入:

python3 --version

如果提示找不到命令,先去官网下载对应系统的安装包。装完后顺手确认pip可用:

pip3 --version

有了pip,安装requests就一句话的事:

pip3 install requests

安装完成后,强烈建议先验证一下版本,因为很多“奇怪报错”都源于版本过旧或升级到一半的半成品状态:

import requests print(requests.__version__)

如果输出一个类似2.31.0的版本号,说明环境正常。这里提醒一句:尽量在虚拟环境中做这件事。我知道对新手来说“虚拟环境”这个词有点吓人,但你只需要一条命令:

python3 -m venv myenv source myenv/bin/activate # Windows下是 myenv\Scripts\activate

好处是:不同项目之间依赖互不污染,以后你装了一百个库把系统Python搞崩的概率会低很多。我早期就是图省事直接在系统Python里pip install,结果某次升级系统自带的某个库,把整个开发环境搞到崩溃,重装系统才缓过来。这个坑是真的疼。

2.2 新手一定会遇到的提示:defaulting to user installation

你执行pip install requests时,很可能看到这样一段提示:

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

很多新手看到这行英文就慌了,以为装失败了。其实翻译过来就是:当前Python的全局site-packages目录没有写权限,所以pip自动改道,把库装到你当前用户自己的目录下了。

这不一定是个错误——Python官方推荐的就是“能装到用户目录就装用户目录”,避免影响系统级Python。但如果你发现import requests能用而pip show requests显示的位置和你预期不符,也不用慌,这是pip的正常策略。当然,如果你希望装到系统全局,要么给目录加权限(不推荐,容易破坏环境),要么使用虚拟环境(推荐)。

顺便回答一个高频问题——“python中requests库如何升级”。看版本、升级、检查,三步走:

pip show requests # 查看当前版本和安装位置 pip install --upgrade requests # 升级到最新版 pip install requests==2.28.1 # 指定版本安装

升级前注意:如果手头有跑在旧环境的生产项目,先看一眼项目里有没有用到requests的旧参数(比如早期版本的verify、hooks等行为差异),避免升级后行为不一致。我遇到过某次升级后urllib3版本跟着变了,某些连接池行为出现细微差别,排查了半天才发现是版本升级引起的。

2.3 第一个请求:requests.get()的核心用法

环境没问题,代码就走起来。先跑一个最简单的GET:

import requests response = requests.get('https://httpbin.org/get') print(response.status_code) print(response.text)

初次运行,你可能看到几百行JSON格式的响应体。这个httpbin.org是专门用来测试HTTP请求的工具站,强烈推荐新手拿它当“练手沙盒”。它会原样回显你发出的请求参数、Headers、IP,方便你验证自己对requests的每项配置是否生效。

如果打印response.status_code得到200,说明这一步你已经从“听说过requests”跨到了“亲手跑通requests”。接下来要做的,就是把这一个请求拆解出各种花样——给它加参数、加请求头、加超时、加session。下一节一个个说。

3. 核心API拆解:参数、请求头、Session与响应对象

3.1 params、headers、timeout:写进每个请求的必备三件套

很多新人只会写requests.get(url),一旦遇到需要传参、伪装浏览器的场景就处处卡壳。实际上,请求的三大核心配置是params、headers、timeout,它们的配合能解决80%的日常请求需求。

params:把查询参数拼进URL的工具。新手最常见的错误是手动拼URL:

# 不推荐:手动拼接容易漏编码 url = "https://httpbin.org/get?name=张三&age=18" # 推荐:交给requests处理编码 params = { "name": "张三", "age": 18 } response = requests.get("https://httpbin.org/get", params=params) print(response.url)

当你用第二段代码时,requests会自动对中文、特殊符号做URL编码。尤其当参数里出现&、#这类容易“污染”URL段结构的字符时,手动拼接简直是自找麻烦。我调研过不少爬虫项目,报错原因最后都落在“URL没编码”上。

headers:请求头的字典。最常设置的是User-Agent(标识客户端类型)、Referer(标识来源页)、Cookie(身份信息)。用法:

headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36" } response = requests.get("https://httpbin.org/headers", headers=headers)

为什么要设置User-Agent?因为很多服务端会检查“你是不是一个正经浏览器”。requests默认的User-Agent是python-requests/x.x.x,服务器一看就知道你在用脚本访问,轻则返回泛化数据,重则直接拒绝。把User-Agent伪装成浏览器是最基础的反爬应对手段,但注意,这只能对付最基础的简单反爬规则,不代表你可以用它在别人网站上乱爬数据——务必遵守目标网站的robots协议和法律法规。

timeout:这是我在任何入门教程里都会强调的参数,没有例外。看这段代码:

# 危险写法:没有超时限制,服务端不响应时程序会一直挂着 response = requests.get("https://example.com/slow") # 正确写法:连接和读取阶段分别设置超时 response = requests.get("https://example.com/slow", timeout=(3, 10))

timeout=(3, 10)的意思是:建立连接最多等3秒,拿到响应头后读取数据最多等10秒。连接阶段超时会抛ConnectTimeout,读取阶段超时会抛ReadTimeout。没有timeout的请求,一旦对方服务器“装死”,你的脚本就会一直卡在那里,看起来像死循环。我在生产环境见过因为少写一个timeout,导致定时任务挂了整整一晚上的事故。

3.2 响应对象:status_code、text、json()、content怎么选

响应对象Response上有几个属性/方法,新手最容易混淆,我这里用一个表格讲明白它们的区别和适用场景:

响应元素返回类型什么时候用注意点
response.status_codeint判断请求是否成功2xx表示成功,4xx是客户端问题,5xx是服务端问题
response.textstr想直接看文本内容使用response.encoding解码,编码不对会乱码
response.contentbytes下载文件、图片、二进制流不涉及编码问题,适合直接写文件
response.json()dict/list接口返回JSON格式时必须先确认content-type是application/json,否则抛JSONDecodeError
response.headersCaseInsensitiveDict查响应头信息键不区分大小写,response.headers['Content-Type']和response.headers['content-type']等价

实际经验是:**能先判断状态码,就绝不直接response.json()。**比较稳妥的写法:

response = requests.get("https://httpbin.org/get", timeout=5) if response.status_code == 200: data = response.json() print(data["headers"]) else: print(f"请求失败,状态码:{response.status_code}")

如果你用response.json()解析的不是JSON格式,或者JSON结构不完整,会直接抛异常,脚本就中断了。所以“先看状态码,再解析响应体”应该成为肌肉记忆。还有一个常见误区:很多人用response.text看到一串乱码就以为网站有问题,其实往往是编码判断错了。下一节爬虫流程里我会专门讲编码处理。

3.3 Session会话:让多个请求共享Cookie、Headers和连接池

同一个网站上,你通常需要连续发送多个请求,比如“先登录,再访问个人中心”。这时如果每次都用requests.get(),Curl每次都是独立的,Cookie传不过去,服务器就会觉得你是陌生人。

requests.Session()就是为了解决这个问题存在的。它像一个“带状态的浏览器标签页”:

session = requests.Session() session.headers.update({ "User-Agent": "Mozilla/5.0 ..." }) # 第一次请求:登录接口,服务端返回Cookie并自动存储在session里 session.post("https://httpbin.org/cookies/set", params={"name": "test"}) # 第二次请求:自动携带session里存的Cookie response = session.get("https://httpbin.org/cookies") print(response.json())

Session还能复用TCP连接,所以对频繁请求的场景更有性能优势。但注意:Session用完最好调用session.close(),或者用上下文管理器with requests.Session() as s:。如果一直new Session不关闭,连接池里的空闲连接会越积越多,最终可能出现“Too many open files”之类的系统级报错。早期我的爬虫脚本就有这个问题,长时间跑下来网络连接数爆炸,只能重启进程,后来才意识到是Session没关导致的。

3.4 POST请求:data和json到底传哪个

POST请求常见于提交表单、登录、调用API。requests里传body有两个参数,一个是data,一个是json,它们的行为有本质区别:

# data:application/x-www-form-urlencoded 表单格式 requests.post("https://httpbin.org/post", data={"name": "张三"}) # json:application/json JSON格式 import json requests.post("https://httpbin.org/post", json={"name": "张三"})

用data时,requests会按表单格式编码,服务端接收到的是一段key=value&key2=value2字符串;用json时,requests会先把字典转成JSON字符串,再发送。绝大多数现代Web API都要求json格式,所以如果你对接的是一个接口文档里写着“request body为JSON”的服务,就直接用json参数。

再强调一个小知识点:requests.post(url, json=payload)等价于requests.post(url, data=json.dumps(payload), headers={"Content-Type": "application/json"}),但前者更简洁,也不用你自己记着加Content-Type头。初学者别再手动json.dumps了,直接交给你就完事儿。

4. 用requests写第一个爬虫:从单页抓取到完整流程

4.1 实战目标:抓取一个简单页面并解析内容

与其背概念,不如直接写一个完整的爬虫示例。这里我用https://httpbin.org/json作为目标,它返回一组固定JSON数据,适合用来验证流程。

import requests def fetch_json(url): headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" } response = requests.get(url, headers=headers, timeout=(5, 10)) if response.status_code == 200: return response.json() else: raise Exception(f"请求失败: {response.status_code}") if __name__ == "__main__": data = fetch_json("https://httpbin.org/json") print(data)

这一步你已经完成了一个最小可用的爬虫骨架:有请求头、有超时、有状态判断、有数据解析。后续所有爬虫项目,不管目标网站多复杂,骨架基本就是这个。千万别一开始就想着“怎么爬淘宝”“怎么爬抖音”,先把这类玩具网站抓通了,再逐步加编码处理、异常处理、重试逻辑。

4.2 编码与乱码处理:为什么text出来是乱码

爬虫新手遇到最多的问题就是:状态码200,但response.text打印出来全是乱码。原因很简单:HTTP响应头里有一个Content-Type字段,里面通常带有charset=utf-8之类的编码声明,requests用这个声明去解码。但有些网站响应头里没带charset,或者声明和实际内容编码不一致,比如页面实际上是GBK编码,声明却是UTF-8,那么用response.text解码出来自然就是歪的。

处理方式有两种:

# 方案一:手动指定编码 response.encoding = "gbk" print(response.text) # 方案二:让requests通过内容本身去推测编码 response.encoding = response.apparent_encoding print(response.text)

apparent_encoding是requests基于内容字节统计推测出来的编码,对乱码问题是个很好的兜底方案。但注意,它需要遍历内容做统计,会牺牲一些性能。所以我的习惯是:先从响应头里找charset,找不到再用apparent_encoding。实际抓取中文网站时,“声明UTF-8实际GBK”和“声明GBK实际UTF-8”的情况我都遇到过,编码检测这步真的不能省。

4.3 异常处理与重试逻辑

网络请求本身是不稳定的,DNS抖动、连接被重置、超时、限流——随时可能发生。所以爬虫代码里,异常处理比业务逻辑更要紧。requests的异常体系以requests.exceptions为根,我平时至少会捕获这四个:

import requests from requests.exceptions import RequestException, Timeout, HTTPError, ConnectionError try: response = requests.get(url, timeout=(3, 5)) response.raise_for_status() # 状态码非2xx时主动抛出HTTPError except Timeout: print("请求超时,稍后重试") except HTTPError as e: print(f"HTTP错误,状态码: {e.response.status_code}") except ConnectionError: print("连接失败,检查网络或域名是否可访问") except RequestException as e: print(f"其他请求异常: {e}")

这里有个容易被忽视的点:response.raise_for_status()。默认情况下,requests不会因为你收到404或500就抛异常,它只负责“收到响应”,不负责“响应正确”。如果你不主动检查状态码、不调用raise_for_status(),那么遇到服务端返回错误页面时,你的程序可能还在傻乎乎地解析错误页面HTML。我建议把它当成“请求成功”的守门员,放在每个请求后面。

至于重试,最简单的原则是先“有限重试”,再“人工介入”。比如遇到429或5xx,睡眠几秒后重试最多3次。不要无限重试,否则对方服务器一限流,你这边就变成自杀式轰炸。后面第5节我会讲怎么配置更专业的重试策略。

4.4 反爬初体验:User-Agent伪装与请求频率控制

第一次拿httpbin.org跑通后,你会忍不住想去抓点真实网站。这个过程中,你碰到的第一个“墙”往往是反爬机制。最基础的反爬检查就是看User-Agent。你发给httpbin.org/headers看看,如果输出里有个Python-Requests字样,说明你的身份太明显了。

应对的办法是伪装成浏览器,把User-Agent复制成你常用浏览器的完整UA字符串。但不建议把UA伪造得太乱,比如某些防护严格的服务还会校验UA和实际浏览器行为是否匹配。作为入门阶段,先学会设置一个合理UA,再配合time.sleep(2)控制请求频率,模拟一个“人在正常看网页”的节奏,就已经能解决很多简单站点的反爬问题。

再次强调:爬虫一定要尊重目标网站的robots.txt和公开声明,不碰需要登录才能访问的个人数据,不以任何方式给服务器造成过载压力。学习阶段测试时用公开接口就好,别拿别人的业务系统练手。这既是技术规范,也是底线。

5. 高频报错排查:429 Too Many Requests 与重试上限

5.1 “exceeded retry limit, last status: 429”到底在说什么

如果你写过一个脚本、跑过一次定时任务、或者调用过第三方API,大概率见过这么一段报错:

exceeded retry limit, last status: 429 too many requests

这句话翻译过来是“重试次数已用尽,最后一次响应的状态码是429”。429这个数字在HTTP世界里含义非常明确:服务器认为你在一段时间内发送了太多请求,触发了它预设的速率限制。服务器没有把你当恶意用户封死,而是以429的状态码告诉你“太快了,先歇会儿”。

值得注意的是,这个报错通常不是requests直接抛出来的。requests默认遇到429会正常返回Response对象,而不会重试。真正抛出这个报错的多半是第三方封装库、框架、或者你通过HTTPAdapter配置的重试机制——它们会按既定次数去重试,每次重试之间没有合适退避,重试耗尽后,就把“最后的状态码是429”作为异常抛了出来。所以看到这个报错,说明你已经走入了“有重试逻辑”的层次,只需做两件事:分析为什么会被限流、调整重试策略和请求频率。

5.2 根因分析:是先有“请求过多”,还是先有“重试不当”

我遇到过很多次429报错,认真复盘后发现,真正的原因往往不是单一的。可以从这几个方向排查:

服务端限流设置:很多公开API对未认证请求的速率限制低得惊人,比如“每分钟最多20次”。如果你并发写着8个线程疯狂发请求,撞上429只是时间问题。

代理IP质量:如果用了代理池,代理节点可能是很多人共享的,一旦某个节点已被限流,你发什么请求都会撞429。换成独享代理或调整节点,问题通常会消失。

User-Agent过于集中:多个并发请求共用同一个UA,加上请求频率又高,服务端很容易识别出“这不是一个正常人”。

重试退避策略缺失:这是最容易被忽略的。很多重试逻辑写的是“失败了就立刻重试”,比如:

for i in range(3): try: return requests.get(url) except: continue # 没有睡眠,没有退避

这样遇到429重试3次,三次几乎是瞬间打的,服务器连续收到你的请求,会继续返回429,重试自然全部无效。最终就出现了“exceeded retry limit, last status: 429”。限流触发后立刻重试是重试的大忌,你必须等待,等限流窗口过去再发。

5.3 一个完整的429排查链路复现

为了让你以后遇到429不慌,我复盘一次比较典型的排查过程,你也可以照着这个思路走:

第一步,复现报错。写个小脚本,单请求打到目标接口,发现能正常200返回。说明问题不是“接口本身坏了”,而是“访问频率超过了阈值”。

第二步,统计失败分布。给脚本加上日志,记录每次请求的时间戳和状态码。跑一段时间后你会发现,前十几个请求全部200,从某个时刻开始出现连续的429。这就定位到了“第几次请求触发限流”。

第三步,检查重试配置。看自己代码里有没有设置重试,重试间隔是多少。如果发现是“失败就立即重试”,先改成“失败后等待2到5秒再重试”,并且只重试一次做验证。

第四步,控制源头发送速率。把并发数降下来,或者加大每次请求之间的间隔。比如原来每秒10次,改成每秒2次、每4次之间sleep 1秒。再跑,429基本消失。

第五步,如果还是429。去看是不是代理IP的问题。换一个全新节点,如果问题消失,就是原节点被限流了;查看是不是目标接口要求携带特定Headers,比如APIToken,有些服务不携带认证会给出更严格的限流阈值。

这五步走完,绝大多数429都能定位到具体原因。真正难处理的是“服务端限流窗口很长,比如按小时限制总量”这种情况,那只能靠节流和排队,没有捷径。

5.4 用HTTPAdapter配置请求重试策略

requests内置的重试能力,主要通过HTTPAdapter和urllib3.Retry实现。这是官方推荐的做法,比手写for i in range(3)要专业得多,尤其适合程序在长时间运行、需要自动处理临时故障的场景。

看这段配置:

import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry def build_session(retries=3, backoff_factor=0.5): session = requests.Session() retry_strategy = Retry( total=retries, # 总重试次数 backoff_factor=backoff_factor, # 退避因子:每次重试等待 0.5*2^(n-1) 秒 status_forcelist=[429, 500, 502, 503, 504], # 遇到这些状态码才重试 allowed_methods=["GET", "POST"] # 只对幂等/指定方法重试 ) adapter = HTTPAdapter(max_retries=retry_strategy) session.mount("http://", adapter) session.mount("https://", adapter) return session if __name__ == "__main__": s = build_session() response = s.get("https://httpbin.org/status/429", timeout=5) print(response.status_code)

重点说几个参数的意义:

  • total=3:单个请求最多重试3次,不会无限重试。
  • backoff_factor=0.5:重试等待时间按指数退避策略计算。第1次重试等待0.5秒,第2次1秒,第3次2秒。这样设计是因为服务端限流后需要时间恢复,指数退避比固定间隔更合理。
  • status_forcelist:只有明确列出的状态码才触发重试。不是所有状态码都值得重试,比如401(未认证)重试多少次都没用,404也不会因为重试就变成200。
  • allowed_methods=["GET", "POST"]:只对特定方法重试。POST有时候不是幂等的,重复提交可能会导致业务数据重复,所以用起来要谨慎。

这套配置挂上之后,“exceeded retry limit”就变成了一个“可预期的、受控的”事件,而不是脚本突然崩溃。但这里要泼盆冷水:重试策略是最后一道防线,不是第一声警报。真正的解决之道永远是“从源头把请求频率控制在合理范围”,重试只是兜底,不是躺赢的借口。

5.5 从根上减少429:请求频率控制与并发设计

很多朋友跑爬虫或者批量脚本时,觉得“我加了重试就不会出问题了”。但实际生产经验告诉我:好的请求模型是“前面少发,后面稳定”,而不是“前面猛发,后面拼命重试”。几个经过验证的做法:

控制请求间隔。最简单也最有效的办法:

import time for url in urls: response = requests.get(url, timeout=5) print(response.status_code) time.sleep(1) # 每请求一次,固定间隔1秒

具体的间隔时长没有标准答案,取决于目标服务的限流阈值。你可以从1秒开始尝试,如果还出现429就加到2秒、3秒。

并发度保守设置。如果你用ThreadPoolExecutor,建议初始并发数从2到4开始跑,观察稳定后再说。不要一上来就开32线程,那不是请求,是给小网站做压力测试。

做本地缓存/去重。同一个数据,本地已经有副本了,就别重复请求拿完整响应,改用Etag或条件请求。减少请求总量,才是对限流最友好、对服务器最尊重的方案。

预留限流余量。如果对方限流是“每秒钟最多10次”,你自动调低到每秒6到8次。两三个小时跑完比“5分钟跑完但因为限流卡半天”高效得多。我曾经为了“跑得快”把并发调很高,结果不仅数据没抓到多少,还把对方服务打到限流,最后连正常访问的请求也被临时封了,教训非常深刻。

6. 进阶使用与我的几点实战经验

6.1 SSL验证、代理与大文件下载

入门阶段你可能不关心SSL验证,但对接真实接口时早晚会碰到。某些自建证书、内网服务,证书链可能不完整,requests会抛requests.exceptions.SSLError。最简单的临时处理是verify=False:

response = requests.get("https://self-signed.badssl.com/", verify=False)

但这是临时方案,不是长期方案。生产环境里更稳妥的做法是把你信任的证书加到verify参数里,比如:

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

verify=False会同时产生一个警告InsecureRequestWarning,你可以用urllib3.disable_warnings()关掉它,但请记住,这相当于你打开了“不验明对方身份”的门,只适用于完全受控的内网环境。我在本地开发时偶尔用它,但一到正式环境必配证书路径,绝不偷懒。

代理的使用也很常见:

proxies = { "http": "http://127.0.0.1:7890", "https": "http://127.0.0.1:7890" } response = requests.get("https://httpbin.org/get", proxies=proxies)

这里特别提醒:一些网络调试工具默认会开启系统代理,你用requests时如果没显式指定代理而是自动读了系统代理,请求可能走了一条“你没想到的路”。排查网络问题时先看看requests.get(..., proxies={"http": None, "https": None})是不是绕开了代理才正常,很多谜之超时就这么解决的。写到这我突然意识到上面这个端口号只是随便举例,你完全不需要关心具体值,只管记住“requests支持代理字典”这个事实即可。

6.2 大文件下载与流式读取

下载大文件时,如果你是response.content一次性读完再写文件,内存占用会非常难看。假设一个500MB的压缩包,直接content读进来,内存直接爆炸。正确做法是走流式读取:

import requests url = "https://example.com/bigfile.zip" response = requests.get(url, stream=True, timeout=(5, 30)) response.raise_for_status() with open("bigfile.zip", "wb") as f: for chunk in response.iter_content(chunk_size=1024 * 1024): f.write(chunk)

stream=True告诉requests不要急着把整个响应体都下载到内存,而是边读边写入文件。iter_content每次拉取1MB的数据块。这样即使文件有几十个GB,内存消耗也几乎可以忽略。顺带一个小技巧:中断续传时,配合Range请求头还能实现断点下载,这个进阶玩法等你用到再去深挖。

6.3 并发请求:数组大了,先想清楚要不要并发

很多人一谈到“提高效率”就想上并发。但并发带来两个问题:一是对目标服务器的压力成倍增加,容易触发429;二是代码复杂度上升,调试难度加倍。所以我的建议是:小任务先串行,串行确实太慢再上并发,并发数从保守开始调。

如果非要并发,最简单很低门槛的方案是用concurrent.futures。它和requests配合起来很顺手:

import concurrent.futures import requests URLS = ["https://httpbin.org/get"] * 10 def fetch(url): response = requests.get(url, timeout=5) return len(response.content) with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor: results = list(executor.map(fetch, URLS)) print(results)

注意这段代码里,每个线程都直接requests.get,没有共用Session。如果想共用同一个Session,多线程里必须加锁或依赖Session自身的线程安全保证,否则可能出现连接池并发问题。对新手来说,我建议先用多线程独立请求,等理解透了再考虑外层封装Session池。

6.4 requests升级维护与兼容性注意

最后聊聊升级维护这件“小事”。很多人装完requests就再也不管了,直到某天跑一个项目报错说urllib3 v2.0 only supports OpenSSL 1.1.1+,才知道问题出在基础库版本不兼容上。

我的维护习惯是:

  • 每隔一段时间执行pip list --outdated,看看requests和urllib3是否有新版。
  • 升级前先看release notes,特别是涉及verify、timeout、Session等行为变动的说明。
  • 升级后立刻跑一遍自己项目的基础测试用例,确认没有行为回归。
  • 如果项目要长期维护,可以把requests的版本号写死在requirements.txt里,升级时手动确定,避免某次顺手升级把线上项目搞挂。

real talk:requests这个库的核心API设计得非常稳定,过去这么多年变化并不大。所以如果你用着旧版本,业务代码基本不会有兼容问题;真正的大坑往往来自它的依赖们——urllib3、certifi、charset-normalizer——这些底层库升级后,行为可能会变化,间接影响requests。所以排查requests异常时,看下依赖版本总没错。

6.5 我踩过的几个坑,说给新手当预防针

最后一节不啰嗦他们官方文档里的东西,就说点个人手记。

第一个坑是“升级requests后urllib3版本冲突”。有次我图省事直接升级了urllib3,结果requests还是旧版,两者组合后不断抛InsecureRequestWarning,代码本身没变,但日志全被警告刷屏。后来统一升到兼容版本,一切恢复正常。从此我升级任何库,都会带上“依赖一起升”。

第二个坑是“Session不关闭,连接数爆炸”。有段时间我写的定时任务每天跑一次,每次new Session但不close,跑了不到两周,系统报错“Too many open files”。查了半天才发现,Session对象虽然会随着进程退出被GC回收,但在长驻进程里如果不手动close,连接池的空闲连接不会立刻释放。从那之后我全改成了with requests.Session() as s:的写法。

第三个坑是“重试逻辑写得太猛,把对方服务打到限流”。这个之前提过,但我还是想说:requests本身只是个工具,真正决定你项目命运的是你用工具的节奏和底线。遇到429,正确情绪不是“我要重试到它给我200”,而是“我的请求速率要调整到它不给我429”。

学习requests这门功课,其实学的不只是一个库的API,而是“如何优雅地跟远端服务打交道”。你把请求频率控制好、异常处理写全、重试策略配置合理,就已经比很多“只顾抓数据不管对方死活”的脚本强了。基础打牢后,再去看看httpx、aiohttp这些进阶的选择,会轻松很多。一步一步来,不用急。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询