
1. 从零上手 Request一个被低估的 HTTP 客户端利器做后端开发或者数据采集的朋友大概率都绕不开一个场景需要在自己的代码里主动发起 HTTP 请求去调用第三方接口、抓取页面数据、或者做服务间的联调测试。很多人第一反应是用curl拼命令行或者用标准库里的http.client、urllib硬写。我早期也是这么干的直到有一次要对接一个带鉴权、带重试、带超时控制的接口用标准库写了将近两百行代码调试了一下午最后发现连 Cookie 都没处理好。后来同事甩给我一个库说“你用这个试试”我花了十分钟改完代码量直接砍到二十行。那个库就是Requests。Requests 是 Python 生态里最流行的 HTTP 客户端库之一它的设计哲学就一句话让 HTTP 请求变得像喝水一样简单。你不需要手动处理连接池、编码转换、重定向、Cookie 持久化这些琐碎的事情它全部帮你封装好了。不管你是要做接口自动化测试、写爬虫、对接第三方支付回调、还是做微服务之间的健康检查Requests 都能覆盖。这篇文章我会从实际使用的角度出发把 Requests 的核心用法、参数细节、踩坑经验一次性讲透适合刚接触网络编程的新手也适合用了一段时间但没系统梳理过的老手。2. 为什么我最终选了 Requests 而不是其他方案2.1 标准库的痛点和 Requests 的降维打击Python 标准库里其实自带了好几个能发 HTTP 请求的模块urllib.request、http.client、urllib3都算。但它们有一个共同的问题API 设计太底层了。举个例子你只是想发一个带 JSON 体的 POST 请求用urllib.request大概要这么写import urllib.request import json data json.dumps({name: test, value: 123}).encode(utf-8) req urllib.request.Request( https://httpbin.org/post, datadata, headers{Content-Type: application/json}, methodPOST ) with urllib.request.urlopen(req, timeout10) as resp: body resp.read().decode(utf-8) print(json.loads(body))而用 Requests同样的功能import requests resp requests.post( https://httpbin.org/post, json{name: test, value: 123}, timeout10 ) print(resp.json())代码量差了将近三倍而且 Requests 的写法一眼就能看懂在干什么。标准库那版你得知道data要手动编码、Content-Type要手动设、响应要手动解码任何一个环节漏了都会出问题。Requests 把这些全部自动化了传json参数它自动序列化并设置请求头调resp.json()它自动反序列化。2.2 和其他第三方库的横向对比当然Python 的 HTTP 客户端不止 Requests 一个。httpx支持异步、aiohttp专攻异步、urllib3是 Requests 的底层依赖。我实际项目里这几个都用过说说我的选型逻辑库名同步支持异步支持上手难度适用场景Requests优秀不支持极低常规接口调用、脚本、测试httpx优秀优秀低需要异步且想保留 Requests 风格aiohttp不支持优秀中等高并发爬虫、异步服务urllib3良好不支持中等需要精细控制连接池如果你的场景是同步调用为主、并发量不大、追求开发效率Requests 就是最优解。它的文档质量、社区生态、Stack Overflow 上的问答数量都是碾压级别的遇到问题基本一搜就有答案。我见过太多人一上来就追求异步结果项目里总共就调三五个接口用异步反而把代码复杂度拉高了得不偿失。提示Requests 本身不支持异步。如果你的项目确实需要高并发异步请求建议直接上 httpx它的 API 和 Requests 高度相似迁移成本很低。2.3 安装与版本选择安装没什么好说的一条命令pip install requests但版本选择有个细节值得注意。Requests 在 2.32.0 之前的版本依赖chardet做编码检测之后换成了charset_normalizer。如果你在老项目里遇到编码识别不准的问题可以先确认一下版本。查看当前版本pip show requests我一般建议锁定一个较新的稳定版本比如2.32.x系列写进requirements.txt里避免不同环境装出不同版本导致行为不一致。另外Requests 依赖urllib3、certifi、idna、charset_normalizer这几个包pip会自动处理不用手动装。3. 核心 API 逐个拆解从 GET 到文件上传3.1 GET 请求与参数传递的正确姿势GET 请求是最常用的但参数怎么传有讲究。很多人习惯自己拼 URL# 不推荐的写法 url https://httpbin.org/get?name张三age25city北京 resp requests.get(url)这种写法的问题在于中文和特殊字符需要手动 URL 编码参数多了容易拼错而且可读性差。正确的方式是用params参数params { name: 张三, age: 25, city: 北京 } resp requests.get(https://httpbin.org/get, paramsparams) print(resp.url) # https://httpbin.org/get?name%E5%BC%A0%E4%B8%89age25city%E5%8C%97%E4%BA%ACRequests 会自动帮你做 URL 编码中文、空格、特殊符号都不用手动处理。如果某个参数需要传多个值比如tagpythontagjava可以传列表params {tag: [python, java, go]} resp requests.get(https://httpbin.org/get, paramsparams)这样会生成tagpythontagjavataggo。这个细节很多人不知道遇到需要传数组的接口时容易卡住。3.2 POST 请求的四种数据格式POST 请求比 GET 复杂因为请求体的格式有好几种。Requests 针对不同格式提供了不同的参数用错了接口就会报错。第一种表单提交application/x-www-form-urlencoded这是最传统的表单格式用data参数传字典resp requests.post( https://httpbin.org/post, data{username: admin, password: 123456} )Requests 会自动把Content-Type设成application/x-www-form-urlencoded并把字典编码成usernameadminpassword123456。第二种JSON 提交application/json现在 RESTful 接口基本都用 JSON用json参数resp requests.post( https://httpbin.org/post, json{username: admin, password: 123456} )注意这里传的是json而不是data。如果你把字典传给dataRequests 会当成表单处理服务端收到的Content-Type就不对接口大概率返回 400。这个坑我踩过不止一次尤其是对接一些文档写得不清楚的接口时一定要先确认对方要的是表单还是 JSON。第三种原始文本text/plain 或其他有些接口要求直接传一段字符串或者 XML这时候用data传字符串resp requests.post( https://httpbin.org/post, dataxmlnametest/name/xml, headers{Content-Type: application/xml} )第四种文件上传multipart/form-data文件上传用files参数files {file: open(report.pdf, rb)} resp requests.post(https://httpbin.org/post, filesfiles)如果要同时传文件和表单字段可以这样files {file: open(report.pdf, rb)} data {description: 月度报告} resp requests.post(https://httpbin.org/post, filesfiles, datadata)注意用files时不要手动设置Content-TypeRequests 会自动生成带boundary的 multipart 头。手动设置反而会导致服务端解析失败。3.3 响应对象的常用属性和方法发完请求拿到resp对象后怎么读取结果也有讲究。常用的属性有这些resp.status_codeHTTP 状态码比如 200、404、500resp.headers响应头是个大小写不敏感的字典resp.text响应体解码后的字符串resp.content响应体的原始字节处理图片、文件时用这个resp.json()把响应体解析成 Python 对象前提是内容是合法 JSONresp.encoding当前使用的编码可以手动改resp.elapsed从发请求到收到响应的时间关于编码有个经典坑如果服务端返回的Content-Type里没带charsetRequests 会默认用ISO-8859-1解码中文就会变成乱码。解决办法是手动指定resp.encoding utf-8 print(resp.text)或者直接用resp.content.decode(utf-8)自己解码。我一般习惯先看resp.headers.get(Content-Type)确认编码后再决定怎么处理。3.4 超时设置不设超时等于埋雷这是新手最容易忽略、老手最重视的一个参数。Requests 默认没有超时也就是说如果服务端不响应你的程序会一直卡在那里。生产环境里这是致命的。# 设置总超时 10 秒 resp requests.get(https://httpbin.org/get, timeout10) # 分别设置连接超时和读取超时 resp requests.get(https://httpbin.org/get, timeout(3.05, 27))元组形式里第一个值是连接超时建立 TCP 连接的时间第二个值是读取超时等待服务端返回数据的时间。为什么连接超时我写 3.05 而不是 3这是 Requests 官方文档里的建议值因为 DNS 解析本身可能消耗额外时间留一点余量能避免边界情况下的误判。提示超时时间设多少合适内网接口一般 3 到 5 秒够了公网接口建议 10 到 30 秒。如果是文件下载读取超时要设大一些或者干脆不设读取超时但设连接超时。4. 进阶实战会话、重试、代理与鉴权4.1 Session 会话保持与连接复用如果你要连续调同一个域名的多个接口用requests.get()每次都新建连接效率低而且 Cookie 不会自动保持。这时候应该用Sessionsession requests.Session() session.headers.update({User-Agent: MyApp/1.0}) # 第一次请求服务端 Set-Cookie session.post(https://httpbin.org/cookies/set, data{sessionid: abc123}) # 第二次请求Cookie 自动带上 resp session.get(https://httpbin.org/cookies) print(resp.json()) # {cookies: {sessionid: abc123}}Session 的好处有三个Cookie 自动持久化、底层 TCP 连接复用、公共请求头统一管理。我做过一个测试连续发 100 个请求到同一个域名用 Session 比不用 Session 快了将近 40%因为省去了反复建立 TCP 连接和 TLS 握手的时间。用完记得关闭session.close()或者用上下文管理器with requests.Session() as session: resp session.get(https://httpbin.org/get)4.2 重试机制让请求更健壮网络请求失败是常态尤其是调用第三方接口时。Requests 本身没有内置重试但可以通过urllib3的Retry配合HTTPAdapter实现from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry session requests.Session() retry Retry( total3, backoff_factor0.5, status_forcelist[500, 502, 503, 504], allowed_methods[GET, POST] ) adapter HTTPAdapter(max_retriesretry) session.mount(https://, adapter) session.mount(http://, adapter)这段配置的意思是最多重试 3 次每次间隔按 0.5 秒递增0.5、1、2 秒遇到 500、502、503、504 这些服务端错误时触发重试只对 GET 和 POST 方法生效。backoff_factor的递增逻辑是{backoff_factor} * (2 ** (retry_number - 1))所以第一次重试等 0.5 秒第二次等 1 秒第三次等 2 秒。这个指数退避策略能有效避免在服务端刚出问题时疯狂重试把它压垮。注意allowed_methods在旧版本里叫method_whitelist如果你用的是老版本 urllib3参数名要改一下。另外POST 请求重试要谨慎如果接口不是幂等的重试可能导致重复下单、重复扣款等问题。4.3 代理配置的三种方式有些场景下需要走代理比如抓取外部数据、调试不同网络环境。Requests 支持三种配置方式# 方式一字典传参 proxies { http: http://10.10.1.10:3128, https: http://10.10.1.10:1080 } resp requests.get(https://httpbin.org/get, proxiesproxies) # 方式二带认证的代理 proxies { https: http://user:pass10.10.1.10:3128 } # 方式三Session 级别全局配置 session requests.Session() session.proxies.update(proxies)如果代理是通过环境变量配置的Requests 会自动读取HTTP_PROXY和HTTPS_PROXY。但要注意NO_PROXY环境变量可以指定哪些地址不走代理格式是逗号分隔的域名列表。4.4 鉴权Basic Auth 与 Bearer Token接口鉴权是绕不开的。最简单的 HTTP Basic Authfrom requests.auth import HTTPBasicAuth resp requests.get( https://httpbin.org/basic-auth/admin/123456, authHTTPBasicAuth(admin, 123456) ) # 简写形式 resp requests.get( https://httpbin.org/basic-auth/admin/123456, auth(admin, 123456) )Bearer Token 更常见直接放请求头headers {Authorization: Bearer eyJhbGciOiJIUzI1NiIs...} resp requests.get(https://api.example.com/user, headersheaders)如果是 OAuth2 之类的复杂鉴权通常需要先调 token 接口拿 access_token再带着 token 调业务接口。我一般会封装一个函数在 token 过期时自动刷新def get_with_token(url, token_manager): headers {Authorization: fBearer {token_manager.get_token()}} resp requests.get(url, headersheaders) if resp.status_code 401: token_manager.refresh() headers[Authorization] fBearer {token_manager.get_token()} resp requests.get(url, headersheaders) return resp5. 踩坑实录与常见问题速查5.1 编码乱码问题的完整排查链路中文乱码是 Requests 使用中最常见的问题没有之一。排查思路按这个顺序走第一步看响应头里的 Content-Typeprint(resp.headers.get(Content-Type)) # 比如 text/html; charsetutf-8如果带了charsetRequests 会用它解码一般不会乱码。如果没带Requests 会走默认逻辑可能猜错。第二步手动指定编码resp.encoding utf-8 print(resp.text)第三步如果还不对用 apparent_encodingprint(resp.apparent_encoding) resp.encoding resp.apparent_encodingapparent_encoding是charset_normalizer库根据内容猜出来的编码准确率比默认逻辑高很多。但它是计算出来的每次访问都会重新分析所以拿到结果后最好缓存下来。第四步终极方案直接操作字节text resp.content.decode(gbk, errorsignore)有些老网站用的是 GBK 或 GB2312 编码而且响应头里不写这时候只能靠猜。errorsignore能跳过无法解码的字节避免直接抛异常。5.2 SSL 证书验证失败的几种情况调用 HTTPS 接口时偶尔会遇到SSLError。原因通常有三种服务端证书过期或自签名本地 CA 证书库太旧公司网络做了中间人拦截临时绕过验证的方法是verifyFalseresp requests.get(https://self-signed.example.com, verifyFalse)但这会同时触发一个警告。要关掉警告import urllib3 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)注意verifyFalse只建议在测试环境用。生产环境关闭证书验证等于把安全性完全放弃了中间人攻击可以随意篡改内容。正确的做法是把自签名证书加到信任链里或者用verify/path/to/ca.pem指定 CA 证书文件。5.3 重定向处理与历史记录Requests 默认会自动跟随重定向最多 30 次。有时候你需要看中间经过了哪些跳转resp requests.get(https://httpbin.org/redirect/3) print(resp.history) # [Response [302], Response [302], Response [302]] print(resp.url) # https://httpbin.org/getresp.history是一个列表按顺序记录了每次重定向的响应。如果不想自动跟随resp requests.get(https://httpbin.org/redirect/3, allow_redirectsFalse) print(resp.status_code) # 302 print(resp.headers[Location]) # 下一跳地址POST 请求遇到 301、302 时Requests 会把方法改成 GET这是符合浏览器行为的。但有些接口设计不规范期望 POST 重定向后还是 POST这时候就会出问题。解决办法是手动处理重定向或者用allow_redirectsFalse自己控制。5.4 常见问题速查表问题现象可能原因解决方法中文乱码编码识别错误手动设resp.encoding或用apparent_encoding请求卡死未设超时加timeout(3.05, 27)400 Bad Request数据格式不对确认用json还是data401 Unauthorized鉴权信息缺失或过期检查 token加自动刷新逻辑403 Forbidden被识别为爬虫加User-Agent等请求头SSLError证书验证失败测试环境verifyFalse生产环境配 CA连接被重置代理配置问题检查proxies参数和环境变量响应内容截断读取超时太短增大读取超时或流式读取5.5 流式下载大文件的正确做法下载大文件时如果直接用resp.content整个文件会先加载到内存里几百兆的文件能把内存吃满。正确做法是流式读取resp requests.get(https://example.com/bigfile.zip, streamTrue, timeout(3.05, 60)) with open(bigfile.zip, wb) as f: for chunk in resp.iter_content(chunk_size8192): if chunk: f.write(chunk)streamTrue让 Requests 不立即下载响应体iter_content按块读取chunk_size8192是每次读 8KB。这个大小可以根据网络情况调整内网可以设大一点比如 64KB公网建议 8KB 到 16KB。提示流式请求下resp.text和resp.content会报错必须用iter_content或iter_lines。另外记得在with块结束后连接才会释放或者手动调resp.close()。6. 我个人的几条实战心得用 Requests 这些年有几个习惯是我踩了坑之后养成的分享出来供参考。第一永远设超时。不管是脚本还是服务只要发请求就加timeout。我见过太多因为没设超时导致整个服务线程池被拖垮的案例。哪怕你设个 60 秒也比不设强。第二生产环境用 Session 而不是裸调。Session 的连接复用和 Cookie 管理能省很多事而且方便统一配置重试、代理、请求头。我一般会封装一个全局的 Session 实例在应用启动时初始化好。第三日志要打全。出问题时请求的 URL、方法、请求头、请求体、响应状态码、响应体前 500 个字符这些信息能帮你快速定位。我习惯在封装层里加一个开关调试时打开生产环境关掉避免日志爆炸。第四敏感信息不要硬编码。token、密码、代理认证信息这些走环境变量或配置中心别直接写在代码里。我见过有人把生产环境的 token 提交到公开仓库结果被扫出来盗用损失不小。第五注意请求频率。调第三方接口时看清楚对方的限流策略。该加 sleep 就加 sleep该用队列就用队列。把对方接口打挂了最后倒霉的还是自己。最后再分享一个小技巧如果你需要快速验证一个接口能不能通不用写代码直接在命令行里用python -m requests是不行的但可以用curl先试。不过 Requests 有个隐藏玩法在 Python 交互式环境里import requests r requests.get(https://httpbin.org/get) r.status_code r.json()这种即时反馈的方式比写完整脚本再跑要快得多调试接口时特别顺手。