资讯动态

Python requests模块下载:从入门到工程化文件下载实战

发布时间:2026/10/15 1:25:15 来源:尧图企业网站定制
简介requests是Python生态中广受好评的HTTP客户端库相比标准库urllib2其API更简洁、语义更清晰这份压缩包正是围绕requests模块整理而成适合Python初学者、爬虫开发者以及需要在项目中快速封装网络请求的工程人员。压缩包共121个文件主体为76个Python脚本配合20个rst文档、5个HTML示例页面以及少量配置文件、许可证和辅助脚本构成一套完整的源码加文档示例集合压缩后仅643KB轻量易用。目前已有7014人学习或下载在同类资源中关注度较高。读者可从中看到requests库的典型调用方式、参数传递、会话保持、异常处理等高频场景代码并通过rst与HTML文档理解模块的设计思路与扩展方法。相比直接阅读官方文档这份资源更贴近实际工程能帮助快速上手并规避urllib2时代的繁琐写法适合边看边练。1. python requests 模块下载装完只是开始搜索“python requests 模块下载”的人多半不是缺一条 pip install 命令而是装完之后不知道下一步怎么写才稳。requests 是 Python 生态里使用面最广的 HTTP 客户端库底层封装 urllib3把 URL 拼接、请求头组装、响应解码、连接复用这些重复劳动全部收拢成几个参数。它适合两类人一类是刚写脚本的开发者想用最短代码完成接口请求和文件下载另一类是写过一段时间脚本、但没系统处理过超时、重试、断点续传的熟手。这份笔记从安装开始把 requests 的参数语义、流式下载、异常排查和工程化配置串成一条完整链路。你能看到的不只是“怎么写”还有“为什么这么写”以及“哪里会翻车”。2. 环境准备与首条请求虚拟环境、pip 安装与响应读取requests 的安装门槛极低但“安装”这件事里藏着环境隔离、版本锁定、离线部署三个细节。在生产服务器上我一般会新建虚拟环境而不是直接往系统 Python 里塞包在离线内网环境则需要提前把依赖包下载成 wheel 文件。这一章先把环境搞定再发第一条真实请求。2.1 虚拟环境与 pip 安装锁定版本与离线部署虚拟环境解决的是“脏依赖”问题。系统 Python 通常被多个项目共用你装一个 requests 2.32.3另一个项目可能就需要旧版直接覆盖会产生连锁故障。所以常规做法是先建隔离环境再安装依赖。python -m venv req_env source req_env/bin/activate pip install requests2.32.3 python -c import requests; print(requests.__version__)第一行创建虚拟环境目录第二行激活Windows 下执行 req_env\Scripts\activate第三行安装并锁定版本最后一步验证安装结果。requests 自身还依赖 urllib3、certifi、idna、charset_normalizerpip 安装时自动拉取不需要手工处理。参数说明2.32.3 是硬锁定版本防止某次升级改变行为如果网络到官方源慢可以在命令末尾加 -i 参数指向可用的 PyPI 镜像速度差别很明显。离线内网环境是另一个常见场景。我一般会在一台能联网的机器上执行 pip download requests2.32.3 -d ./wheels把依赖的 wheel 全装进一个目录然后整个目录拷进内网用 pip install --no-index --find-links./wheels requests 完成安装。注意 --no-index 的含义是强制不从远端索引拉包只认本地目录避免内网环境里 pip 还傻等网络。这段的坑在于手工拷贝时容易漏掉 requests 的间接依赖你在外网打包时最好直接对 requests 做 pip downloadpip 会把依赖树一并拉下来。2.2 首条 GET 请求text、content、json 三种响应体环境就绪后第一条请求用 GET 打到一个测试接口。下面这个例子请求 example.com 的接口并分别演示三种读取响应体的方式。example.com 是保留示例域名你实际对接时换成目标地址即可。import requests resp requests.get( https://example.com/api/info, params{page: 1}, timeout(3, 5), ) print(resp.status_code) print(resp.headers.get(Content-Type)) print(resp.text[:200]) print(resp.content[:200])requests.get 返回一个 Response 对象status_code 是 HTTP 状态码text 是按响应头 charset 解码后的字符串content 是未经解码的原始字节。一次请求同时拿到两种形态是因为 requests 在底层已经把响应体完整读进内存text 和 content 只是两种视图。如果你对接的是一个 JSON 接口直接 resp.json() 更方便它会用 json 库解析 content 并自动处理编码。text 和 content 的选择容易踩坑当服务器响应头没有 charset 字段时requests 会用兜底编码猜测猜错之后 text 就是乱码content 反而完好。所以我在脚本里习惯优先用 content需要字符串时手动 decode(utf-8, errorsreplace)。errors 参数的作用是遇到无法解码的字节时用替换符兜底而不是直接抛异常把整个程序打断。状态码处理方面不要只看 status_code 等于 200 就继续我一般会调 resp.raise_for_status()它在 4xx/5xx 时自动抛 HTTPError比手动写 if 判断更省事异常信息里还带 URL 和状态码排查时一眼就能定位。2.3 POST 与 data/json接口对接最容易翻车的地方GET 拿到数据之后第二个高频操作是 POST 提交。requests 的 post 方法里有两组容易混淆的参数data 和 json。直接用例子看差异。import requests # 表单提交Content-Type 为 application/x-www-form-urlencoded resp requests.post( https://example.com/form, data{name: test}, timeout(3, 5), ) # JSON 提交Content-Type 为 application/json resp requests.post( https://example.com/api, json{name: test}, timeout(3, 5), ) print(resp.status_code) print(resp.text[:300])用 data 传字典时requests 会把数据编码成表单格式用 json 传字典时它会先做 JSON 序列化再设置 Content-Type: application/json。后端框架就是靠这个 Content-Type 决定把请求体解析成表单字段还是 JSON 对象。所以接口文档写的要求是 JSON你却用 data服务端大概率读不到字段返回 400 或者干脆没反应。我遇到过最典型的情况是登录接口文档上写着 body 是 JSON代码里用的是 data{username: ..., password: ...}本地测了很久都不通抓包一看 Content-Type 是表单。改成 json 之后一次通过。如果你是给第三方系统写对接脚本先抓包确认目标接口实际接受的 Content-Type再决定用哪个参数。还有个隐藏点json 传中文时不需要手动做 URL 编码requests 会把整个对象序列化为 UTF-8 的 JSON 字符串编码这一层它替你做完了。3. 请求参数与运行机制Params、Headers、超时与重定向requests 的 API 设计得很“平”但平不代表没有语义差别。params 管 URL 查询串data 和 json 管请求体headers 管服务端对“你是谁”的判断timeout 管请求的生死线。这一章把这几组参数放在同一个画面里讲清楚每个参数的适用边界。3.1 params / data / json三种传参方式选哪个这三个参数不是等价物它们修改的是 HTTP 报文的不同部分。参数作用位置编码方式典型场景paramsURL 查询串自动拼接为 ?a1b2GET 查询、翻页、过滤data请求体application/x-www-form-urlencoded表单登录、老接口json请求体application/jsonREST API、嵌套结构params 适合 GET 请求requests 会把字典按 keyvalue 拼到 URL 后面并且处理特殊字符的转义。中文和空格在 URL 里不能直接出现params 自动做完百分号编码你不需要手工 urllib.parse.urlencode。data 和 json 的区别上一章已经演示过同一份字典换一个参数Content-Type 就变了。很多人在一个接口上反复测试不通过根源就是没看懂这三者的边界。写一个工具脚本时我习惯把接口参数按这三类拆开查询条件放 params表单放 data结构化业务数据放 json。拆分的价值不在美观而在排查抓包时你能直接从报文格式判断是自己传错了还是服务端解析错了。如果你传的是 dict 但字段值是一个列表data 会把它编码成多个同名键json 会保留为数组这两种表达对应后端完全不同的解析逻辑对接前先确认目标接口对重复字段的约定。3.2 请求头伪装User-Agent、Referer 与 Session 组合requests 默认的 User-Agent 是 python-requests/2.32.3 这样的字符串服务端一眼就能识别是脚本。很多站点对这类请求直接 403所以第一步是换成浏览器 UA。import requests ua (Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36) s requests.Session() s.headers.update({ User-Agent: ua, Referer: https://example.com/, Accept-Language: zh-CN,zh;q0.9, }) resp s.get(https://example.com/api/data, timeout(3, 5)) print(resp.status_code)这里用 Session 而不是直接 requests.get有两个原因第一Session 内部维护 CookieJar登录接口返回的 Set-Cookie 会被自动保存并带上后续请求第二同一个 Session 会复用 TCP 连接频繁调接口时减少三次握手开销。headers.update 设置的请求头对整个 Session 内所有请求生效省去每个请求单独传 headers 的重复代码。Referer 是另一道常见校验。下载类接口尤其喜欢检查 Referer判断请求是不是从站点页面点进来的直接裸请求会返回 403 或 304。遇到这种情况把浏览器开发者工具里网络面板的实际请求复制出来Headers 里的 UA、Referer、Origin、Cookie 逐项对照填进你自己的请求。我一般会先在浏览器里抓一次真实请求把关键请求头同步进脚本而不是凭经验猜。注意 Session 的请求头是“合并策略”Session.headers 里的值会被单次请求传入的 headers 覆盖但只影响那一次不会污染 Session 全局配置。多账号切换时用 Session 逐个创建实例比手动改 Cookie 头要干净。3.3 timeout 与重定向连接语义和跳转处理timeout 是很多人用了但没真正理解的参数。它控制的是“连接建立”和“数据读取”两个阶段的超时。import requests resp requests.get( https://example.com/redirect, allow_redirectsFalse, timeout(3, 5), ) if resp.status_code in (301, 302, 303, 307, 308): print(Location:, resp.headers.get(Location))timeout(3, 5) 的含义3 秒内建立 TCP 连接5 秒内读到数据。如果只传一个数字两个阶段共用同一个值。设置连接超时是因为连不上的服务器没必要等太久设置读超时是因为服务器接受了请求但迟迟不返回数据不能让线程无限挂起。allow_redirectsFalse 时requests 不会自动跟随 3xx 跳转你可以从 Location 头取出真实目标地址再做二次处理。文件下载场景里CDN 链接经常先返回 302 到实际存储节点默认跟随是没问题的但如果你要记录最终落盘 URL用 resp.url 能看到跳转后的地址。重定向还有一个和 stream 相关的点streamTrue 时默认仍然跟随重定向但重定向耗掉的等待时间会计入你的流程总时长。如果目标服务器对跳转接口本身响应很慢请求会卡在跳转这一步而不是卡在文件下载。排查这类问题时打开 resp.history它记录了所有跳转的响应对象历史长度就是跳转次数。很多“下载卡住”的玄学问题最后查出来是重定向环节超时文件接口本身没毛病。你在循环里复用同一个 Session 调大量 URL 时也一样timeout 记得用元组形式别图省事传一个数字。4. 文件下载实战流式读取、断点续传与完整性校验把 requests 用在文件下载上才算碰到它最容易出问题的地方。对一个大文件直接 resp.content内存和磁盘双重告急。这一章给出大文件下载的完整方案先讲流式读取的标准姿势再讲 Range 实现断点续传最后补一个 MD5 校验保证下载完的文件是完整的。4.1 流式读取大文件下载的标准姿势先看反面示例。很多人在下载文件时这么写resp requests.get(https://example.com/file.zip, timeout(5, 30)) with open(file.zip, wb) as f: f.write(resp.content)文件小于 50MB 时这么写没什么体感但到了几百 MB 甚至 GB 级resp.content 会先把整个响应体加载进内存内存占用瞬间拉高机器内存不足时直接 OOM。正确的方案是让响应体流式进入边接收边写磁盘。import requests def download_file(url, local_path): with requests.get(url, streamTrue, timeout(5, 30)) as resp: resp.raise_for_status() total int(resp.headers.get(Content-Length, 0)) downloaded 0 with open(local_path, wb) as f: for chunk in resp.iter_content(chunk_size8192): if not chunk: continue f.write(chunk) downloaded len(chunk) if total: print(f\r{downloaded / total * 100:.1f}%, end) print(\ndownload done)streamTrue 告诉 requests 不要等整个响应体到达再返回值而是连接保持打开响应体数据分块到达。iter_content(chunk_size8192) 按 8KB 一块迭代每一块拿到就立刻写入磁盘。8192 这个值是我常用的起始值磁盘 IO 和内存占用之间比较均衡追求速度可以调到 65536但在弱网环境大块反而容易被半路切断导致本次迭代拿到的数据长度小于预期。参数说明里有两个细节值得记一是 with 语句管理 response无论下载中途是否抛异常response 都会关闭底层的 TCP 连接能正常释放回连接池二是 Content-Length 不一定存在分块传输编码的响应就没有这个头total 取 0 时进度条退化成只显示 “download done”脚本仍然可以跑完。还有一个容易忽略的行为iter_content 内部会处理响应解码拿到的是解码后的字节不是你直接在网络上抓到的压缩字节。也就是说即使响应头里 Content-Encoding: gzip你写入磁盘的也是解压后的数据不需要自己做解压。4.2 Range 断点续传从 200 到 206 的差异处理下载任务中断后从头再来在弱网环境里非常折磨人。HTTP 协议本身支持 Range 头服务端可以只返回文件从某个偏移量开始的后半段。requests 对 Range 没有做专门的封装你只需要在 headers 里声明它。import os import requests def resume_download(url, local_path): existing os.path.getsize(local_path) if os.path.exists(local_path) else 0 headers {Range: fbytes{existing}-} resp requests.get(url, headersheaders, streamTrue, timeout(5, 30)) if resp.status_code 206: mode ab else: mode wb existing 0 total int(resp.headers.get(Content-Length, 0)) existing done existing with open(local_path, mode) as f: for chunk in resp.iter_content(chunk_size8192): if chunk: f.write(chunk) done len(chunk) print(f\r{done / total * 100:.1f}%, end) print(\nresume done)逻辑说明分三步。第一步os.path.getsize 读取本地已下载字节数文件不存在时记 0。第二步把偏移量写进 Range 头bytes1024- 表示从第 1024 字节开始返回。第三步服务端支持 Range 时返回 206 Partial Content此时文件用追加模式 ab 写existing 保留服务端不支持时返回 200 全量内容此时必须回退到覆盖模式 wbexisting 归零否则文件会越写越长。这里最容易记混的是 Content-Length。206 响应里的 Content-Length 是“剩余字节数”不是整个文件大小所以计算 total 要加 existing。如果你在续传完成后发现文件大小对不上优先检查这一行。还有一个边界情况续传时原文件已经下载完Range 头会变成 bytes完整大小-此时服务端返回 416 Range Not Satisfiable代码需要单独处理一般做法是删除已存在文件重新下载或者直接返回“已完成跳过”。我在脚本里习惯先比较 existing 和总大小相等就提前 return避免多打一次请求。4.3 完整性校验MD5 分块计算与 Content-Range 核对文件下载完成不意味着内容正确。中间网络闪断、磁盘抖动都可能让文件损坏。HTTP 协议不保证响应体落盘后的完整性所以下载器一般会拿服务端提供的 MD5 或文件大小做校验。requests 场景下做 MD5 校验要用分块读取一次性 read 一个大文件同样会吃满内存。import hashlib def md5_of_file(path, chunk_size8192): h hashlib.md5() with open(path, rb) as f: while True: block f.read(chunk_size) if not block: break h.update(block) return h.hexdigest()这个函数把文件按 8KB 分块喂给 md5 累加器最终得到整个文件的校验值和服务器下发的 MD5 字符串比对即可。如果服务器只给了 Content-Length 没有给 MD5有一个低成本替代方案断点续传完成后从响应头里找 Content-Range它的格式是 bytes start-end/total把 total 部分取出来和本地文件实际大小比对。不一致就说明续传链路里有数据错位删除重下。文件校验要考虑性能。GB 级文件跑 MD5 本身就是一二十秒的量级别在下载回调里同步做放下载后单独跑。对下载器里的每个文件都校验能挡掉大部分“文件怎么打不开”的玄学问题。另外服务端给的 MD5 有时候是文件未压缩前的哈希而 requests 在流式模式下已经帮你做了解码这时候直接对比可能不相等。遇到这种情况检查服务端文档里校验值针对的是传输字节还是原始内容必要时对压缩字节再做一次哈希。5. requests 下载避坑高频问题与排查清单requests 用起来顺手恰恰是这种“顺手”把很多 HTTP 细节盖住了出了问题反而不容易定位。这一章整理我实际排查中遇到最多的五个问题每个都按现象、原因、解决三步记录。你按顺序对号入座多数情况能省下半天时间的抓包。5.1 乱码响应文本与文件名的编码问题现象resp.text 打印出来中文全是乱码或者下载的文件名变成 %E4%B8%AD%E6%96%87 这样的百分号串。 原因响应头没有 charset 时requests 用 ISO-8859-1 兜底解码遇到 UTF-8 的中文必然乱文件名乱码则是服务端把中文名做了 URL 百分号编码requests 不会自动帮你解。 解决先检查 resp.encoding 和 resp.apparent_encoding两者不一致时手动指定 resp.encoding resp.apparent_encoding再取 resp.text。文件名则用 urllib.parse.unquote 解一次。注意 apparent_encoding 是靠内容猜的短页面可能猜错要求高就固定用 utf-8 试失败再换 gbk。5.2 内存暴涨不能用 resp.content 下载大文件现象下载一个 500MB 的安装包脚本内存占用冲到 1GB 以上在内存小的机器上直接被系统杀掉。 原因resp.content 会一次性把整个响应体加载进内存下载文件时没有走流式。 解决见上一章streamTrue 加 iter_content 分块写磁盘。判断标准很简单目标文件超过 50MB就不要碰 resp.content另一个隐蔽点是 resp.json() 也会加载全文大 JSON 接口同样有内存压力用 resp.iter_content 自己拼或干脆换更小的分页接口。5.3 timeout 失效DNS 解析与总耗时的空白地带现象timeout(3, 5) 明明设置了但一个请求卡了 30 秒才抛异常或者干脆卡死不动。 原因timeout 只作用于 socket 连接和读操作覆盖不到 DNS 解析阶段也不限制整个请求的总耗时。DNS 服务器无响应时resolve 过程是独立的系统调用requests 的 timeout 管不到它。 解决在 request 之前给 socket 层设置兜底socket.setdefaulttimeout(3)。这个设置对当前进程内所有 socket 生效DNS 解析超时也会被约束。如果还要限制总耗时用 concurrent.futures.ThreadPoolExecutor 包一层future.result(timeout10) 超时后直接放弃。注意放弃前要把对应的 response 关闭避免连接泄漏。5.4 SSL 警告与 verifyFalse测试环境专用开关现象requests.get(..., verifyFalse) 后控制台刷 InsecureRequestWarning一行接一行看着像报错。有些脚本作者为了省事直接不去管甚至把 verifyFalse 带进了生产环境。 原因verifyFalse 关闭了 SSL 证书校验urllib3 按设计会输出警告提醒你当前不在安全通道里。 解决测试环境可以显式压掉警告urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)并在代码里注释“仅测试自签名证书用”。生产环境坚持 verifyTrue遇到证书报错先查证书链、根证书、系统时间而不是关校验。真实生产事故里有一类就是手滑把 verifyFalse 传到生产导致中间人攻击风险直接暴露。5.5 连接复用后的偶发 ConnectionError现象同一个 Session 连续请求几十次后某次突然抛 ConnectionError: Connection aborted。 原因服务端或中间设备把空闲 keep-alive 连接断开了客户端不知道下一次请求还走旧连接数据没发出去就报错。 解决捕获 ConnectionError 后重建 Session 重试一次这是成本最低的方案更稳的是给 Session 挂 HTTPAdapter把连接池大小调小一点、重试调起来见下一章。不要在不加任何容错的情况下让长跑脚本裸奔下载类任务尤其明显断连一次就任务失败太亏。6. 再进一步Session 复用与重试机制的正确用法到这一步单次请求和文件下载都能跑通。剩下的问题变成“脚本长时间运行怎么不出事”。requests 底层的连接和重试控制都在 urllib3 里通过 HTTPAdapter 暴露出来。不显式配置时requests 不会自动重试连接失败就直接抛异常。对下载类任务一次闪断就失败是不可接受的。import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry def build_session(retries3): retry Retry( totalretries, connectretries, readretries, backoff_factor0.5, status_forcelist[500, 502, 503, 504], ) adapter HTTPAdapter( max_retriesretry, pool_connections10, pool_maxsize20, ) s requests.Session() s.mount(https://, adapter) s.mount(http://, adapter) return sRetry 的几个参数值得展开参数作用建议值total总重试次数上限3backoff_factor退避间隔系数间隔系数*2^(重试次数-1)0.5status_forcelist命中这些状态码时也重试[500, 502, 503, 504]total3 配合 backoff_factor0.5重试间隔约为 0.5s、1s、2s后续请求有退避不会对服务端造成瞬时压力。status_forcelist 里不要放 401 和 403账号密码错误重试也会被拒403 往往是风控重试只会加速封禁。HTTPAdapter 的 pool_connections 和 pool_maxsize 控制连接池容量单机脚本 10 和 20 足够不要盲目调大。重试也有边界。Retry 只对特定异常类型和状态码生效DNS 解析失败、超时类型需要配合 retry 参数里的其他选项且重试的请求是透明的日志里不容易看出来发生过重试。所以生产脚本我会在调用侧再加一层日志捕获 RequestException 时打印异常类型、URL、已重试次数方便事后复盘。另外requests 的异常体系有一个总入口 requests.RequestException所有网络相关异常都从它派生。脚本里不需要逐个 except只捕获这一个就能拿到大部分故障但注意它会吞掉异常细节打印时用 repr(e) 保留完整信息。我以前写定时任务习惯性不配 Retry某天网络抖动脚本原地连着重试了几百次把一个内部接口打到 5xx。从那以后我强制自己凡是走 requests 的下载脚本必须统一从 build_session 创建 Session显式写清重试次数和退避参数再在入口处加一层异常日志。这个习惯让后来所有定时任务的稳定性都上了一个台阶。requests 的坑大多是弯路踩出来的把这些配置固化进模板后面的人接手也少受折腾希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑