第32章:源码剖析——models.py(Request/Response/PreparedRequest对象模型)
1. 项目背景业务场景某团队在对接一个需要自定义请求签名的 API 时发现签名总是验证失败——签名字符串中 URL 的 query string 与服务器记录的不一致。调试后发现requests.get(url, params{a: [1,2,3]})生成的 URL 中参数格式是a1a2a3而他们签名库期望的是a1,2,3。为了修复这个问题他们需要理解 requests 的 URL 编码逻辑——这就要深入models.py源码。另一个场景更棘手团队在response.json()返回的字典中修改了数据然后这个修改反直觉地影响了原始response.text。排查发现response.json()返回的是解析后的新对象不会反过来影响原始响应体。但开发却发现数据被改了——原来是他将response.json()的返回值赋值给了一个全局变量其他代码修改了这个变量。痛点问题一黑盒使用导致调试困难。当requests.get(url, paramsparams)生成的 URL 不符合预期时大多数开发者只能不断试错——改参数、加日志、抓包对比。但如果读过models.py中的PreparedRequest.prepare_url()源码就能清晰地理解 URL 的生成逻辑迅速定位问题。问题二Response 对象的延迟加载机制。response.content看起来是一个属性但它背后是一套复杂的延迟加载机制——raw流只在第一次访问content时才真正读取。这意味着如果你在访问content之前就关闭了底层连接再去访问会得到一个StreamConsumedError。问题三Request → PreparedRequest 的转换逻辑。第 22 章讲了 PreparedRequest 的使用但没有深入其内部实现。理解prepare_method、prepare_url、prepare_headers、prepare_body、prepare_cookies五个步骤的具体实现能在你调试为什么这个头没发出去时节省数小时。models.py 核心类关系图Request (用户构建的请求) │ method, url, headers, params, data, json, cookies... │ └── Session.prepare_request(Request) │ └── PreparedRequest (编码后的不可变请求) │ method, url, headers, body │ prepare_method() / prepare_url() / prepare_headers() │ prepare_body() / prepare_cookies() │ └── Session.send(PreparedRequest) │ └── Response (服务器返回的响应) │ status_code, headers, _content (延迟加载) │ text, json(), iter_content()2. 项目设计小胖对着日志里两个不同格式的 URL 困惑“大师我发现一个诡异的问题——我给params{tags: [a,b,c]}生成的 URL 里是tagsatagsbtagsc。但我同事说这个格式不符合服务端要求。这个 URL 到底是怎么生成的”大师“这个问题需要看models.py的源码。prepare_url()方法负责把params拼接到 URL 上。它内部调用的是标准库的urlencode(params, doseqTrue)——doseqTrue就是数组参数的编码方式。”小白推了推眼镜“那response.text和response.content的关系呢为什么有时候改了一个会影响另一个”大师“它们的实现机制完全不同。content是主存储——第一次访问时从raw流读取所有数据存到_content。text是一个 property它用encoding去解码content得到字符串。两者互不影响——text改不了contentcontent改不了原始流。”“但有一个微妙的点response.json()内部调用的是complexjson.loads(self.text)。如果服务端返回了 BOM\xef\xbb\xbf标准json.loads会报错因为 BOM 不是合法 JSON 开头。requests 没有自动处理 BOM——这是一个已知的限制。”小胖追问“那PreparedRequest的五个prepare_*方法中哪个最容易踩坑”大师“prepare_headers()和prepare_body()是最容易踩坑的。prepare_headers()会合并 Session.headers 请求 headers 隐式 headersHost、Content-Length 等。如果 Session 和请求中都有同名 header请求的优先。但 Content-Type 是个特例——如果你用了json参数requests 会自动设置Content-Type: application/json覆盖你在 headers 里手写的值。”“prepare_body()会根据你传的是data、json、files参数来决定 Content-Type 和 body 格式。如果你同时传了多个优先级是 files json data。这就是为什么有时你设了data和json会报TypeError。”# models.py 中的关键代码简化版classPreparedRequest:defprepare_url(self,url,params):# 拼接 params 到 URL query stringifparams:queryurlencode(params,doseqTrue)urlf{url}?{query}if?notinurlelsef{url}{query}self.urlurldefprepare_headers(self,headers):# 合并 headersself.headersCaseInsensitiveDict()forsourcein[session_headers,headers]:self.headers.update(source)# 设置隐式头ifHostnotinself.headers:self.headers[Host]parsed_url.netloc生活比喻技术映射料理机放原料→出成品Request → PreparedRequest真空包装不变形PreparedRequest 不可变性分期付款先订后付Response._content 延迟加载智能翻译机按语言解码response.encoding 决定 text 结果万能插座适配requests 兼容多种 params 格式3. 项目实战环境准备# 找到 requests 安装位置python-cimport requests; print(requests.__file__)# 源码位置: site-packages/requests/models.py分步实现步骤一PreparedRequest.prepare_url() 源码走读目标理解 params 如何被编码到 URL 中。# models.py 中 prepare_url 的核心逻辑简化注释版fromurllib.parseimporturlencode,urlparse,urlunparseimportrequestsdefdemo_prepare_url_flow():演示 requests 内部 URL 构建的完整链路importrequestsfromrequestsimportRequest,Session sessionSession()# 构建请求reqRequest(GET,https://httpbin.org/search,params{q:小胖,# 中文 - %E5%B0%8F%E8%83%96tags:[a,b],# 数组 - tagsatagsb (doseqTrue)page:1,# 数字 - page1},)preparedsession.prepare_request(req)print( URL 构建结果 )print(f完整 URL:{prepared.url})print(f路径部分:{prepared.path_url})# 源码关键逻辑:# 1. 解析原始 URL# 2. 将 params 用 urlencode(params, doseqTrue) 编码# 3. 拼接到 URL 的 query string 部分# 4. 处理已有 query string 的情况 (用 连接)# 验证 doseqFalse vs True 的区别fromurllib.parseimporturlencode params{tags:[a,b]}print(f\ndoseqFalse:{urlencode(params,doseqFalse)})# - tags%5B%27a%27%2C%27b%27%5D (把整个列表转字符串)print(fdoseqTrue:{urlencode(params,doseqTrue)})# - tagsatagsb (每个元素独立)session.close()demo_prepare_url_flow()步骤二Response 的延迟加载机制目标理解 content/text/json 的加载时机和关系。importrequestsdefdemo_response_lazy_loading():演示 Response 对象的内容加载机制resprequests.get(https://httpbin.org/get,streamTrue)# 此时还没有读取响应体print( 延迟加载演示 )print(f状态码:{resp.status_code}(已获取))print(f响应头: Content-Type{resp.headers.get(Content-Type)}(已获取))# raw 流对象——urllib3 的 HTTPResponseprint(f\nraw 流:{type(resp.raw).__name__})print(fraw 可读:{resp.raw.readable()})# 第一次访问 content——触发真正的读取contentresp.contentprint(f\ncontent 类型:{type(content).__name__})print(fcontent 长度:{len(content)}bytes)# 第二次访问 content——使用缓存 _contentcontent2resp.contentprint(f第二次 content: 同一对象?{contentiscontent2})# True!# text 是基于 content 的 propertyprint(f\ntext (前50):{resp.text[:50]})print(fencoding:{resp.encoding})# json() 是基于 text 的dataresp.json()print(f\njson 解析:{type(data).__name__})# 源码关键逻辑:# property# def content(self):# if self._content is False: # 首次访问# self._content b.join(self.iter_content(...))# return self._contentresp.close()demo_response_lazy_loading()步骤三Request 到 PreparedRequest 的完整转换链路目标手动实现 prepare 的五个步骤理解每一步做了什么。importrequestsdeftrace_prepare_pipeline():追踪 prepare 的五个步骤sessionrequests.Session()session.headers.update({X-Session-Header:session-value})reqrequests.Request(POST,https://httpbin.org/post?existing1,params{added:yes},headers{X-Custom:req-value},json{user:小胖},)# Step by step:preparedrequests.models.PreparedRequest()# ---- Step1: prepare_method ----# 将 method 大写化 (get - GET)prepared.prepare_method(req.method)print(fStep1 - method:{prepared.method})# ---- Step2: prepare_url ----# 拼接 params 到 URL, 处理已有 query stringprepared.prepare_url(req.url,req.params)print(fStep2 - url:{prepared.url})print(f path_url:{prepared.path_url})# ---- Step3: prepare_headers ----# 合并 Session.headers req.headers 隐式头prepared.prepare_headers(req.headers)print(f\nStep3 - headers:)fork,vinprepared.headers.items():print(f{k}:{v})# ---- Step4: prepare_body ----# 根据 data/json/files 决定 body 格式和 Content-Typeprepared.prepare_body(req.data,req.json,None)print(f\nStep4 - body (前100):{prepared.body[:100] if prepared.body else None})# ---- Step5: prepare_cookies ----# 从 Session.cookies 中选出匹配的 Cookieprepared.prepare_cookies(req.cookies)# 注意Session.prepare_request() 内部做了额外的 merge 工作# 包括 merge_environment_settings (代理、SSL)print(f\n完整准备后的 headers:)fullsession.prepare_request(req)forkinsorted(full.headers.keys()):print(f{k}:{full.headers[k]})session.close()trace_prepare_pipeline()可能遇到的坑及解决方法坑1prepare_url 对重复键的处理# params 中同一个 key 多次出现params[(sort,desc),(sort,asc)]# urlencode 生成: sortdescsortasc# 但服务端如何解析取决于框架: Flask - 取最后一个; Java - 取数组坑2response._content 和 _content_consumed# 如果设置了 streamTrue 并且开始迭代内容# 内部标志 _content_consumed 会被设为 True# 后续再调用 response.json() 会重置 _content_consumed 并重新读取# 这只在 streamTrue 时有效坑3CaseInsensitiveDict 的隐藏行为# response.headers 是 CaseInsensitiveDict# headers[Content-Type] headers[content-type] # True# 但在序列化时 (json.dumps) 会保留原始的 key 大小写测试验证importpytestimportrequestsfromrequestsimportRequest,SessionclassTestModelsInternals:验证 models.py 的内部行为deftest_prepare_url_appends_params(self):sSession()reqRequest(GET,https://httpbin.org/get,params{q:test})preps.prepare_request(req)assertqtestinprep.url s.close()deftest_content_caching(self):resprequests.get(https://httpbin.org/get)c1resp.content c2resp.contentassertc1isc2# 第二次访问使用缓存deftest_prepare_headers_merge(self):sSession()s.headers[X-Session]sreqRequest(GET,https://httpbin.org/get,headers{X-Req:r})preps.prepare_request(req)assertprep.headers[X-Session]sassertprep.headers[X-Req]rs.close()4. 项目总结核心源码要点方法关键源码逻辑常见问题prepare_url()urlencode(params, doseqTrue)拼接数组参数格式不符合服务端预期prepare_headers()合并 Session 请求 隐式头Content-Type 被 json 参数覆盖prepare_body()根据 data/json/files 决定编码data 和 json 同时传报 TypeErrorprepare_cookies()从 CookieJar 中按 Domain/Path 筛选Cookie 未被携带Domain 不匹配Response.content延迟从 raw 流读取并缓存streamTrue 时需手动 close适用场景调试 URL 参数编码不符合预期的问题排查请求头未发送或被覆盖的原因理解为什么 response.json() 报错但 text 正常自定义 PreparedRequest 时的行为预测注意事项params数组中元素的编码依赖doseqTrueprepare_headers后 Content-Type 可能被 body 参数覆盖response._content在streamTrue时是动态读取的CaseInsensitiveDict 的序列化需注意原始大小写常见踩坑经验案例一params{ids: [1,2]}导致服务端 400。Spring Boot 后端期望ids1,2但 requests 发送的是ids1ids2。根因prepare_url使用doseqTrue的编码。修复手动拼接params{ids: 1,2}或后端改为接收数组格式。案例二response.json()对 BOM 报错。某 PHP API 返回的 JSON 带 UTF-8 BOM\xef\xbb\xbfjson.loads无法解析。response.text能看到 BOM 但response.json()报 JSONDecodeError。根因requests 未自动处理 BOM。修复在调用json()前手动response.encoding utf-8-sig。思考题源码题阅读models.py中PreparedRequest.prepare_headers()的源码找出 Content-Length 头是在什么条件下被自动计算的如果用户手动设置了 Content-Length会发生什么设计题基于对Response内部机制的理解设计一个分片 JSON 解析器——对于返回 2GB JSON 数组的 API用streamTrueijson逐条解析而不爆内存。延伸阅读与资源Milvus向量数据库实战修炼从 0 到 1精通向量检索与生产落地后端工程师的 AI 转型第一课Ollama 与私有化大模型实战10倍开发者的 Dify 魔法书从零构建全栈 AI 应用后端工程师转型AI第一课-Ollama 与私有化大模型实战大型语言模型(LLM) vLLM 高性能推理落地实战Agent开发之LlamaIndex 实战修炼与源码进阶大语言模型Transformers 实战修炼与源码剖析