指南:從 GET 到文件上傳的 HTTP 請求全解析)
1. 從零上手 Request一個被低估的 HTTP 客戶端利器做后端開發(fā)或者數(shù)據(jù)采集的朋友大概率都繞不開一個場景需要在自己的代碼里主動發(fā)起 HTTP 請求去調(diào)用第三方接口、抓取頁面數(shù)據(jù)、或者做服務間的聯(lián)調(diào)測試。很多人第一反應是用curl拼命令行或者用標準庫里的http.client、urllib硬寫。我早期也是這么干的直到有一次要對接一個帶鑒權、帶重試、帶超時控制的接口用標準庫寫了將近兩百行代碼調(diào)試了一下午最后發(fā)現(xiàn)連 Cookie 都沒處理好。后來同事甩給我一個庫說“你用這個試試”我花了十分鐘改完代碼量直接砍到二十行。那個庫就是Requests。Requests 是 Python 生態(tài)里最流行的 HTTP 客戶端庫之一它的設計哲學就一句話讓 HTTP 請求變得像喝水一樣簡單。你不需要手動處理連接池、編碼轉換、重定向、Cookie 持久化這些瑣碎的事情它全部幫你封裝好了。不管你是要做接口自動化測試、寫爬蟲、對接第三方支付回調(diào)、還是做微服務之間的健康檢查Requests 都能覆蓋。這篇文章我會從實際使用的角度出發(fā)把 Requests 的核心用法、參數(shù)細節(jié)、踩坑經(jīng)驗一次性講透適合剛接觸網(wǎng)絡編程的新手也適合用了一段時間但沒系統(tǒng)梳理過的老手。2. 為什么我最終選了 Requests 而不是其他方案2.1 標準庫的痛點和 Requests 的降維打擊Python 標準庫里其實自帶了好幾個能發(fā) HTTP 請求的模塊urllib.request、http.client、urllib3都算。但它們有一個共同的問題API 設計太底層了。舉個例子你只是想發(fā)一個帶 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要手動設、響應要手動解碼任何一個環(huán)節(jié)漏了都會出問題。Requests 把這些全部自動化了傳json參數(shù)它自動序列化并設置請求頭調(diào)resp.json()它自動反序列化。2.2 和其他第三方庫的橫向?qū)Ρ犬斎籔ython 的 HTTP 客戶端不止 Requests 一個。httpx支持異步、aiohttp專攻異步、urllib3是 Requests 的底層依賴。我實際項目里這幾個都用過說說我的選型邏輯庫名同步支持異步支持上手難度適用場景Requests優(yōu)秀不支持極低常規(guī)接口調(diào)用、腳本、測試httpx優(yōu)秀優(yōu)秀低需要異步且想保留 Requests 風格aiohttp不支持優(yōu)秀中等高并發(fā)爬蟲、異步服務urllib3良好不支持中等需要精細控制連接池如果你的場景是同步調(diào)用為主、并發(fā)量不大、追求開發(fā)效率Requests 就是最優(yōu)解。它的文檔質(zhì)量、社區(qū)生態(tài)、Stack Overflow 上的問答數(shù)量都是碾壓級別的遇到問題基本一搜就有答案。我見過太多人一上來就追求異步結果項目里總共就調(diào)三五個接口用異步反而把代碼復雜度拉高了得不償失。提示Requests 本身不支持異步。如果你的項目確實需要高并發(fā)異步請求建議直接上 httpx它的 API 和 Requests 高度相似遷移成本很低。2.3 安裝與版本選擇安裝沒什么好說的一條命令pip install requests但版本選擇有個細節(jié)值得注意。Requests 在 2.32.0 之前的版本依賴chardet做編碼檢測之后換成了charset_normalizer。如果你在老項目里遇到編碼識別不準的問題可以先確認一下版本。查看當前版本pip show requests我一般建議鎖定一個較新的穩(wěn)定版本比如2.32.x系列寫進requirements.txt里避免不同環(huán)境裝出不同版本導致行為不一致。另外Requests 依賴urllib3、certifi、idna、charset_normalizer這幾個包pip會自動處理不用手動裝。3. 核心 API 逐個拆解從 GET 到文件上傳3.1 GET 請求與參數(shù)傳遞的正確姿勢GET 請求是最常用的但參數(shù)怎么傳有講究。很多人習慣自己拼 URL# 不推薦的寫法 url https://httpbin.org/get?name張三age25city北京 resp requests.get(url)這種寫法的問題在于中文和特殊字符需要手動 URL 編碼參數(shù)多了容易拼錯而且可讀性差。正確的方式是用params參數(shù)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 編碼中文、空格、特殊符號都不用手動處理。如果某個參數(shù)需要傳多個值比如tagpythontagjava可以傳列表params {tag: [python, java, go]} resp requests.get(https://httpbin.org/get, paramsparams)這樣會生成tagpythontagjavataggo。這個細節(jié)很多人不知道遇到需要傳數(shù)組的接口時容易卡住。3.2 POST 請求的四種數(shù)據(jù)格式POST 請求比 GET 復雜因為請求體的格式有好幾種。Requests 針對不同格式提供了不同的參數(shù)用錯了接口就會報錯。第一種表單提交application/x-www-form-urlencoded這是最傳統(tǒng)的表單格式用data參數(shù)傳字典resp requests.post( https://httpbin.org/post, data{username: admin, password: 123456} )Requests 會自動把Content-Type設成application/x-www-form-urlencoded并把字典編碼成usernameadminpassword123456。第二種JSON 提交application/json現(xiàn)在 RESTful 接口基本都用 JSON用json參數(shù)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參數(shù)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 響應對象的常用屬性和方法發(fā)完請求拿到resp對象后怎么讀取結果也有講究。常用的屬性有這些resp.status_codeHTTP 狀態(tài)碼比如 200、404、500resp.headers響應頭是個大小寫不敏感的字典resp.text響應體解碼后的字符串resp.content響應體的原始字節(jié)處理圖片、文件時用這個resp.json()把響應體解析成 Python 對象前提是內(nèi)容是合法 JSONresp.encoding當前使用的編碼可以手動改resp.elapsed從發(fā)請求到收到響應的時間關于編碼有個經(jīng)典坑如果服務端返回的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 超時設置不設超時等于埋雷這是新手最容易忽略、老手最重視的一個參數(shù)。Requests 默認沒有超時也就是說如果服務端不響應你的程序會一直卡在那里。生產(chǎn)環(huán)境里這是致命的。# 設置總超時 10 秒 resp requests.get(https://httpbin.org/get, timeout10) # 分別設置連接超時和讀取超時 resp requests.get(https://httpbin.org/get, timeout(3.05, 27))元組形式里第一個值是連接超時建立 TCP 連接的時間第二個值是讀取超時等待服務端返回數(shù)據(jù)的時間。為什么連接超時我寫 3.05 而不是 3這是 Requests 官方文檔里的建議值因為 DNS 解析本身可能消耗額外時間留一點余量能避免邊界情況下的誤判。提示超時時間設多少合適內(nèi)網(wǎng)接口一般 3 到 5 秒夠了公網(wǎng)接口建議 10 到 30 秒。如果是文件下載讀取超時要設大一些或者干脆不設讀取超時但設連接超時。4. 進階實戰(zhàn)會話、重試、代理與鑒權4.1 Session 會話保持與連接復用如果你要連續(xù)調(diào)同一個域名的多個接口用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 連接復用、公共請求頭統(tǒng)一管理。我做過一個測試連續(xù)發(fā) 100 個請求到同一個域名用 Session 比不用 Session 快了將近 40%因為省去了反復建立 TCP 連接和 TLS 握手的時間。用完記得關閉session.close()或者用上下文管理器with requests.Session() as session: resp session.get(https://httpbin.org/get)4.2 重試機制讓請求更健壯網(wǎng)絡請求失敗是常態(tài)尤其是調(diào)用第三方接口時。Requests 本身沒有內(nèi)置重試但可以通過urllib3的Retry配合HTTPAdapter實現(xiàn)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 這些服務端錯誤時觸發(fā)重試只對 GET 和 POST 方法生效。backoff_factor的遞增邏輯是{backoff_factor} * (2 ** (retry_number - 1))所以第一次重試等 0.5 秒第二次等 1 秒第三次等 2 秒。這個指數(shù)退避策略能有效避免在服務端剛出問題時瘋狂重試把它壓垮。注意allowed_methods在舊版本里叫method_whitelist如果你用的是老版本 urllib3參數(shù)名要改一下。另外POST 請求重試要謹慎如果接口不是冪等的重試可能導致重復下單、重復扣款等問題。4.3 代理配置的三種方式有些場景下需要走代理比如抓取外部數(shù)據(jù)、調(diào)試不同網(wǎng)絡環(huán)境。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)如果代理是通過環(huán)境變量配置的Requests 會自動讀取HTTP_PROXY和HTTPS_PROXY。但要注意NO_PROXY環(huán)境變量可以指定哪些地址不走代理格式是逗號分隔的域名列表。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 之類的復雜鑒權通常需要先調(diào) token 接口拿 access_token再帶著 token 調(diào)業(yè)務接口。我一般會封裝一個函數(shù)在 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庫根據(jù)內(nèi)容猜出來的編碼準確率比默認邏輯高很多。但它是計算出來的每次訪問都會重新分析所以拿到結果后最好緩存下來。第四步終極方案直接操作字節(jié)text resp.content.decode(gbk, errorsignore)有些老網(wǎng)站用的是 GBK 或 GB2312 編碼而且響應頭里不寫這時候只能靠猜。errorsignore能跳過無法解碼的字節(jié)避免直接拋異常。5.2 SSL 證書驗證失敗的幾種情況調(diào)用 HTTPS 接口時偶爾會遇到SSLError。原因通常有三種服務端證書過期或自簽名本地 CA 證書庫太舊公司網(wǎng)絡做了中間人攔截臨時繞過驗證的方法是verifyFalseresp requests.get(https://self-signed.example.com, verifyFalse)但這會同時觸發(fā)一個警告。要關掉警告import urllib3 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)注意verifyFalse只建議在測試環(huán)境用。生產(chǎn)環(huán)境關閉證書驗證等于把安全性完全放棄了中間人攻擊可以隨意篡改內(nèi)容。正確的做法是把自簽名證書加到信任鏈里或者用verify/path/to/ca.pem指定 CA 證書文件。5.3 重定向處理與歷史記錄Requests 默認會自動跟隨重定向最多 30 次。有時候你需要看中間經(jīng)過了哪些跳轉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這是符合瀏覽器行為的。但有些接口設計不規(guī)范期望 POST 重定向后還是 POST這時候就會出問題。解決辦法是手動處理重定向或者用allow_redirectsFalse自己控制。5.4 常見問題速查表問題現(xiàn)象可能原因解決方法中文亂碼編碼識別錯誤手動設resp.encoding或用apparent_encoding請求卡死未設超時加timeout(3.05, 27)400 Bad Request數(shù)據(jù)格式不對確認用json還是data401 Unauthorized鑒權信息缺失或過期檢查 token加自動刷新邏輯403 Forbidden被識別為爬蟲加User-Agent等請求頭SSLError證書驗證失敗測試環(huán)境verifyFalse生產(chǎn)環(huán)境配 CA連接被重置代理配置問題檢查proxies參數(shù)和環(huán)境變量響應內(nèi)容截斷讀取超時太短增大讀取超時或流式讀取5.5 流式下載大文件的正確做法下載大文件時如果直接用resp.content整個文件會先加載到內(nèi)存里幾百兆的文件能把內(nèi)存吃滿。正確做法是流式讀取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。這個大小可以根據(jù)網(wǎng)絡情況調(diào)整內(nèi)網(wǎng)可以設大一點比如 64KB公網(wǎng)建議 8KB 到 16KB。提示流式請求下resp.text和resp.content會報錯必須用iter_content或iter_lines。另外記得在with塊結束后連接才會釋放或者手動調(diào)resp.close()。6. 我個人的幾條實戰(zhàn)心得用 Requests 這些年有幾個習慣是我踩了坑之后養(yǎng)成的分享出來供參考。第一永遠設超時。不管是腳本還是服務只要發(fā)請求就加timeout。我見過太多因為沒設超時導致整個服務線程池被拖垮的案例。哪怕你設個 60 秒也比不設強。第二生產(chǎn)環(huán)境用 Session 而不是裸調(diào)。Session 的連接復用和 Cookie 管理能省很多事而且方便統(tǒng)一配置重試、代理、請求頭。我一般會封裝一個全局的 Session 實例在應用啟動時初始化好。第三日志要打全。出問題時請求的 URL、方法、請求頭、請求體、響應狀態(tài)碼、響應體前 500 個字符這些信息能幫你快速定位。我習慣在封裝層里加一個開關調(diào)試時打開生產(chǎn)環(huán)境關掉避免日志爆炸。第四敏感信息不要硬編碼。token、密碼、代理認證信息這些走環(huán)境變量或配置中心別直接寫在代碼里。我見過有人把生產(chǎn)環(huán)境的 token 提交到公開倉庫結果被掃出來盜用損失不小。第五注意請求頻率。調(diào)第三方接口時看清楚對方的限流策略。該加 sleep 就加 sleep該用隊列就用隊列。把對方接口打掛了最后倒霉的還是自己。最后再分享一個小技巧如果你需要快速驗證一個接口能不能通不用寫代碼直接在命令行里用python -m requests是不行的但可以用curl先試。不過 Requests 有個隱藏玩法在 Python 交互式環(huán)境里import requests r requests.get(https://httpbin.org/get) r.status_code r.json()這種即時反饋的方式比寫完整腳本再跑要快得多調(diào)試接口時特別順手。