尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Python Requests库实战:从基础GET/POST到健壮API客户端构建

Python Requests库实战:从基础GET/POST到健壮API客户端构建 1. 从“能跑就行”到“稳如老狗”网络请求的实战分水岭刚接触Python做网络请求时很多人觉得这玩意儿太简单了不就是requests.get(url)和requests.post(url, data)两行代码的事吗我当年也这么想直到在一个生产环境的数据同步脚本里栽了大跟头。脚本跑了半个月都好好的突然在某天凌晨挂了日志里就一行冷冰冰的ConnectionError上游API的变更通知邮件早就被淹没在收件箱里。那次故障让我明白会用requests发请求只是幼儿园毕业真正理解如何构建一个健壮、可观测、可维护的请求流程才是从脚本小子到靠谱工程师的关键一步。GET和POST这两个HTTP最基础的动词里面门道可一点也不少。怎么优雅地传递参数怎么处理各种诡异的返回超时了怎么办重试机制怎么加返回值怎么解析才不容易崩这些细节教科书不会细讲但线上环境会分分钟教你做人。今天我就结合这些年踩过的坑和总结的经验跟你聊聊怎么把requests用出花来让你写的每一个请求都“稳如老狗”。2. GET请求你以为的简单处处是细节GET请求常用于获取数据参数直观地挂在URL后面。但就是这种“简单”最容易让人放松警惕。2.1 基础用法与参数传递的三种姿势最基础的GET请求看起来人畜无害import requests response requests.get(https://api.example.com/data) print(response.text)但一旦需要传参选择就来了。requests允许你通过params参数以字典形式传递查询参数它会自动帮你完成URL编码和拼接这是最推荐的方式。params { page: 1, limit: 20, keyword: Python 教程, filter: latest } response requests.get(https://api.example.com/search, paramsparams) print(response.url) # 输出https://api.example.com/search?page1limit20keywordPython%E6%95%99%E7%A8%8Bfilterlatest注意看keyword参数空格被正确编码成了号或%20这是手动拼接字符串时极易出错的地方。如果你拿到一个已经是拼接好的URL字符串也可以直接传给get方法但这样就失去了requests帮你管理编码的优势。第二种姿势是参数直接拼接在URL里。这在快速测试时很常见但非常不推荐用于正式代码因为很容易出错且难以维护。# 不推荐手动拼接易出错 url https://api.example.com/search?page1keywordPython教程 response requests.get(url)这里“教程”两个字如果没有被正确编码某些服务端可能无法识别。更危险的是如果参数值里包含或?这样的特殊字符会彻底破坏URL结构。第三种姿势使用params传入一个由(key, value)元组组成的列表。这在你需要传递多个相同键的参数时非常有用比如?tagpythontagrequests。params [(tag, python), (tag, requests), (sort, asc)] response requests.get(https://api.example.com/articles, paramsparams) print(response.url) # 输出https://api.example.com/articles?tagpythontagrequestssortasc注意关于参数编码requests默认使用utf-8。但如果你的参数包含非ASCII字符而目标服务器使用其他编码虽然这不符合现代标准但老旧系统确实存在你可能需要先自行编码。例如params{q: 中文.encode(gbk)}但这种情况极其罕见优先怀疑服务器端实现是否规范。2.2 超时与重试给请求系上“安全带”网络是不稳定的。忽略超时设置是新手最常犯的致命错误之一。没有超时的请求意味着你的程序可能会永远挂起耗尽资源。# 危险没有超时设置 # response requests.get(https://api.slow-server.com/data) # 正确总是设置超时 try: response requests.get(https://api.example.com/data, timeout5) # 连接读取总超时5秒 # 或者更精细的控制 # response requests.get(url, timeout(3.05, 27)) # 连接超时3.05秒读取超时27秒 except requests.exceptions.Timeout: print(请求超时服务器可能过载或网络不佳。) # 这里应该加入重试或告警逻辑 except requests.exceptions.ConnectionError: print(网络连接错误如DNS解析失败、拒绝连接等。)超时参数timeout可以是一个浮点数代表总时间也可以是一个元组(connect_timeout, read_timeout)分别控制连接和读取阶段。超时之后怎么办重试。但重试不能无脑进行。一个健壮的重试策略需要考虑以下几点重试次数通常3次是一个平衡点。退避策略立即重试可能会加剧服务器压力。采用指数退避例如等待1秒、2秒、4秒后再重试。重试条件只对特定的异常进行重试如超时Timeout、连接错误ConnectionError而对于客户端错误HTTP 4xx如404未找到或服务器明确返回的错误则不应重试。我们可以借助urllib3库requests底层使用的库或者tenacity库来实现高级重试。这里展示一个使用requests内置适配器配置重试的常用方法from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry # 定义重试策略 retry_strategy Retry( total3, # 总重试次数不包括第一次请求 backoff_factor1, # 退避因子等待时间 backoff_factor * (2^(重试次数-1)) 秒 status_forcelist[429, 500, 502, 503, 504], # 遇到这些HTTP状态码会重试 allowed_methods[GET, POST] # 只对GET和POST方法重试 ) # 创建适配器并挂载到Session adapter HTTPAdapter(max_retriesretry_strategy) session requests.Session() session.mount(https://, adapter) session.mount(http://, adapter) try: response session.get(https://api.example.com/unstable, timeout5) print(response.status_code) except requests.exceptions.RetryError as e: print(f在多次重试后仍然失败: {e})这个配置意味着如果请求超时或返回429太多请求、500内部服务器错误等状态码它会自动重试最多3次并且重试间隔会逐渐变长。2.3 处理响应状态码、头部与内容编码拿到response对象后第一件事永远是检查状态码。response requests.get(https://api.example.com/data, timeout5) if response.status_code 200: # 成功处理数据 data response.json() # 假设返回JSON elif response.status_code 404: print(请求的资源不存在。) elif response.status_code 401: print(未经授权需要检查Token或API Key。) elif response.status_code 429: print(请求过于频繁被限流了。) # 可以读取响应头中的Retry-After信息 retry_after response.headers.get(Retry-After) if retry_after: print(f请等待 {retry_after} 秒后再试。) else: # 其他未处理的错误抛出异常或记录日志 response.raise_for_status() # 如果状态码不是200抛出HTTPError异常raise_for_status()是一个便捷方法当状态码在400-599之间客户端或服务器错误时会抛出一个HTTPError异常可以帮你快速失败。响应头response.headers是一个类似字典的对象包含了服务器返回的所有头部信息。有些API会在头部返回分页信息、速率限制情况等元数据。print(f内容类型: {response.headers.get(Content-Type)}) print(f请求ID用于追踪: {response.headers.get(X-Request-Id)}) print(f速率限制: {response.headers.get(X-RateLimit-Limit)}/{response.headers.get(X-RateLimit-Remaining)})接下来是响应内容。response.text返回的是解码后的字符串。requests会尝试根据响应头中的Content-Type来猜测编码如果猜不到则使用ISO-8859-1。这可能导致中文乱码。最稳妥的方式是手动指定编码# 方式1如果知道服务器编码如utf-8 response.encoding utf-8 print(response.text) # 方式2使用response.content获取字节流然后手动解码 content_bytes response.content # 可以尝试chardet库检测编码需安装 # import chardet # encoding chardet.detect(content_bytes)[encoding] # text content_bytes.decode(encoding) text content_bytes.decode(utf-8) # 在确定编码时使用对于二进制内容如图片、文件直接使用response.content。# 下载图片 response requests.get(https://example.com/image.jpg, timeout10) if response.status_code 200: with open(downloaded_image.jpg, wb) as f: f.write(response.content)3. POST请求数据交付的艺术与陷阱POST请求通常用于创建或提交数据其复杂性在于数据可以放在不同地方以不同格式传输。3.1 表单提交、JSON负载与文件上传POST请求的数据体主要有三种形式1. 表单数据application/x-www-form-urlencoded这是网页表单默认的提交格式。在requests中使用data参数传递一个字典。form_data { username: test_user, password: secret_pass, # 注意实际中密码绝不应该明文传输 remember_me: on } response requests.post(https://example.com/login, dataform_data)requests会自动将字典编码成key1value1key2value2的格式并设置Content-Type为application/x-www-form-urlencoded。2. JSON数据application/json这是现代API最常用的数据交换格式。使用json参数requests会自动将Python对象字典、列表等序列化为JSON字符串并设置正确的Content-Type。json_payload { title: My New Post, body: This is the content of the post., tags: [python, api], published: True } response requests.post(https://api.example.com/posts, jsonjson_payload)这比手动使用json.dumps()然后传给data参数要方便和安全得多因为它确保了编码和头部正确。3. 多部分表单数据multipart/form-data主要用于文件上传也可以混合普通字段。使用files参数。# 上传单个文件 with open(report.pdf, rb) as f: files {document: (report.pdf, f, application/pdf)} response requests.post(https://api.example.com/upload, filesfiles) # 混合上传文件和普通字段 files {file: (image.png, open(image.png, rb), image/png)} data {description: A beautiful screenshot} response requests.post(https://api.example.com/upload, filesfiles, datadata)注意文件必须以二进制模式(rb)打开。files字典的值可以是一个三元组(filename, fileobj, content_type)也可以只是一个打开的文件对象。3.2 头部信息定制与认证很多时候API需要特定的头部信息比如认证令牌、指定接受的返回格式等。headers { Authorization: Bearer YOUR_ACCESS_TOKEN_HERE, # JWT Token认证 User-Agent: MyApp/1.0 (contactmyapp.com), # 自定义User-Agent礼貌且便于对方识别 Accept: application/json, # 明确告诉服务器我希望接收JSON Content-Type: application/json, # 当使用data参数传JSON时需要手动设置 } # 对于JSON请求更推荐用json参数它会自动设置Content-Type # 所以上面的headers里可以去掉Content-Type response requests.post(https://api.example.com/secure-endpoint, json{data: value}, headersheaders)关于认证requests提供了更简洁的写法# HTTP Basic Auth response requests.get(https://api.example.com/protected, auth(username, password)) # Bearer Token Auth (更常见) from requests.auth import HTTPBearerAuth bearer_auth HTTPBearerAuth(YOUR_TOKEN) response requests.get(https://api.example.com/protected, authbearer_auth) # 或者直接放在headers里如上面所示3.3 处理复杂响应与流式请求POST请求的响应处理与GET类似但有时你会遇到更复杂的情况。处理流式响应当下载大文件时为了避免一次性加载到内存可以使用流模式。url https://example.com/large-video.mp4 response requests.get(url, streamTrue, timeout30) # 注意设置较长的超时 if response.status_code 200: with open(video.mp4, wb) as f: # 以块的形式写入文件 for chunk in response.iter_content(chunk_size8192): if chunk: # 过滤掉keep-alive带来的空块 f.write(chunk)设置streamTrue后requests不会立即下载整个响应体。iter_content方法允许你以指定大小的块迭代内容。处理非JSON响应API可能返回XML、HTML或其他格式。你需要根据Content-Type来解析。if application/json in response.headers.get(Content-Type, ): data response.json() elif application/xml in response.headers.get(Content-Type, ): # 使用lxml或xml.etree.ElementTree解析 # import xml.etree.ElementTree as ET # root ET.fromstring(response.content) pass else: # 当作纯文本处理 text response.text4. 返回值解析的深水区异常、JSON与性能获取返回值只是第一步安全、高效地解析它才是体现功力的地方。4.1 结构化数据解析JSON的坑与技巧response.json()是解析JSON响应的首选方法但它会直接抛出json.decoder.JSONDecodeError异常如果响应体不是合法的JSON比如服务器返回了一个HTML错误页面。try: data response.json() except requests.exceptions.JSONDecodeError as e: print(fJSON解析失败响应内容可能不是JSON。状态码{response.status_code}) print(f响应文本前500字符{response.text[:500]}) # 记录日志并可能将原始响应内容保存下来供排查 with open(error_response.html, w, encodingresponse.encoding or utf-8) as f: f.write(response.text) data None这是一个非常重要的防御性编程实践。我曾经遇到过网关返回502错误时抛回一个Nginx的HTML错误页面如果直接调用.json()程序会崩溃。对于复杂的JSON使用jsonpath-ng或jmespath库可以更方便地提取嵌套很深的数据比一层层写字典索引更清晰、更健壮。# 假设返回的JSON结构复杂 # response_json {store: {book: [{title: Python, price: 39.99}, ...], ...}} # 使用jmespath (需安装: pip install jmespath) import jmespath expression jmespath.compile(store.book[?price 30].title) # 查找价格大于30的书名 expensive_books expression.search(response.json())4.2 统一错误处理与日志记录分散在各处的try...except会让代码难以维护。一个好的实践是创建一个通用的请求函数或类封装错误处理和日志。import logging import time from typing import Any, Dict, Optional logging.basicConfig(levellogging.INFO) logger logging.getLogger(__name__) def safe_request(method: str, url: str, **kwargs) - Optional[requests.Response]: 一个安全的请求包装函数包含日志、重试和基本错误处理。 start_time time.time() try: response requests.request(method, url, **kwargs) elapsed time.time() - start_time logger.info(f[{method.upper()}] {url} - Status: {response.status_code}, Time: {elapsed:.2f}s) # 可以在这里根据状态码决定是否抛出异常 response.raise_for_status() return response except requests.exceptions.Timeout as e: logger.error(f请求超时: {url} - {e}) # 触发告警 except requests.exceptions.ConnectionError as e: logger.error(f连接错误: {url} - {e}) except requests.exceptions.HTTPError as e: logger.error(fHTTP错误 ({e.response.status_code}): {url} - {e.response.text[:200] if e.response else str(e)}) except requests.exceptions.RequestException as e: logger.error(f请求异常: {url} - {e}) return None # 使用示例 resp safe_request(GET, https://api.example.com/data, timeout5, params{q: test}) if resp: data resp.json() # 处理数据这个函数记录了请求的耗时、状态并集中处理了常见异常。在生产环境中你还可以加入更详细的上下文信息如请求ID、用户ID等。4.3 连接池管理与性能优化默认情况下requests的每个请求都会建立和关闭一个新的TCP连接这在频繁请求同一主机时效率很低。使用Session对象可以复用连接显著提升性能。# 低效做法每次请求都新建连接 for i in range(10): r requests.get(https://api.example.com/items) # 高效做法使用Session with requests.Session() as session: # 可以为session统一设置headers、auth、重试策略等 session.headers.update({User-Agent: MyCrawler/1.0}) adapter HTTPAdapter(max_retriesRetry(total2, backoff_factor1)) session.mount(https://, adapter) for i in range(10): r session.get(https://api.example.com/items) # 连接被复用Session对象在with语句块结束后会自动关闭所有连接。对于长期运行的程序你可以全局维护一个Session实例。此外调整HTTPAdapter的pool_connections和pool_maxsize参数可以优化连接池行为。pool_connections是缓存的连接池数量默认为10pool_maxsize是每个连接池的最大连接数默认为10。对于需要大量并发请求到同一主机的爬虫或微服务客户端适当调大这些值可能有益但也要注意不要耗尽系统资源或对目标服务器造成压力。5. 实战案例构建一个健壮的API客户端让我们把这些知识点串联起来构建一个用于调用某个假设的“文章发布平台API”的简单客户端。这个客户端需要处理认证、分页获取文章、发布新文章和错误处理。import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry import logging from typing import List, Dict, Any, Optional logger logging.getLogger(__name__) class ArticleAPIClient: 文章平台API客户端 def __init__(self, base_url: str, api_key: str): self.base_url base_url.rstrip(/) self.api_key api_key self.session self._create_session() def _create_session(self) - requests.Session: 创建并配置一个可重用的Session session requests.Session() # 配置重试策略 retry_strategy Retry( total3, backoff_factor0.5, # 重试间隔0.5s, 1s, 2s status_forcelist[429, 500, 502, 503, 504], allowed_methods[GET, POST, PUT] ) adapter HTTPAdapter(max_retriesretry_strategy, pool_connections10, pool_maxsize20) session.mount(https://, adapter) session.mount(http://, adapter) # 设置默认头部 session.headers.update({ Authorization: fBearer {self.api_key}, User-Agent: ArticleClient/1.0, Accept: application/json, Content-Type: application/json }) return session def _make_request(self, method: str, endpoint: str, **kwargs) - Optional[Dict[str, Any]]: 内部请求方法统一处理日志和错误 url f{self.base_url}/{endpoint.lstrip(/)} logger.debug(fMaking {method.upper()} request to {url}) try: # 确保使用session并设置默认超时 kwargs.setdefault(timeout, (3.05, 30)) response self.session.request(method, url, **kwargs) logger.info(f{method.upper()} {url} - Status: {response.status_code}) # 对于204 No Content等没有返回体的成功请求 if response.status_code 204: return None response.raise_for_status() # 尝试解析JSON if response.content: # 响应体可能为空 return response.json() return None except requests.exceptions.JSONDecodeError as e: logger.error(fFailed to decode JSON from {url}. Response: {response.text[:500]}) raise ValueError(fInvalid JSON response from server: {response.text[:200]}) from e except requests.exceptions.RequestException as e: logger.error(fRequest failed for {url}: {e}) raise # 将异常抛给上层调用者处理 def get_articles(self, page: int 1, per_page: int 20) - List[Dict[str, Any]]: 分页获取文章列表 endpoint /api/v1/articles params {page: page, per_page: per_page} data self._make_request(GET, endpoint, paramsparams) return data.get(articles, []) if data else [] def create_article(self, title: str, content: str, tags: List[str]) - Optional[Dict[str, Any]]: 创建一篇新文章 endpoint /api/v1/articles json_payload { article: { title: title, content: content, tags: tags } } return self._make_request(POST, endpoint, jsonjson_payload) def upload_article_cover(self, article_id: int, image_path: str) - bool: 为文章上传封面图片 endpoint f/api/v1/articles/{article_id}/cover try: with open(image_path, rb) as f: files {file: (image_path, f, image/jpeg)} # 注意上传文件时我们覆盖了session默认的JSON Content-Type response self.session.post( f{self.base_url}/{endpoint}, filesfiles, timeout(10, 60) # 文件上传需要更长的超时时间 ) response.raise_for_status() return True except FileNotFoundError: logger.error(fImage file not found: {image_path}) return False except requests.exceptions.RequestException as e: logger.error(fFailed to upload cover: {e}) return False def close(self): 关闭session释放连接 self.session.close() # 使用示例 if __name__ __main__: client ArticleAPIClient(base_urlhttps://api.article-platform.com, api_keyyour-secret-key) try: # 获取第一页文章 articles client.get_articles(page1) print(fFetched {len(articles)} articles.) # 创建一篇新文章 new_article client.create_article( titlePython Requests实战, content这是一篇关于如何用好Requests库的文章..., tags[Python, HTTP, Tutorial] ) if new_article: print(fArticle created with ID: {new_article.get(id)}) # 上传封面假设有封面图片 # client.upload_article_cover(new_article[id], cover.jpg) finally: client.close() # 确保资源被释放这个客户端类展示了几个关键实践使用Session复用TCP连接统一设置认证头和默认配置。配置重试对临时性故障如网络波动、服务器5xx错误自动重试。统一错误处理在_make_request方法中集中处理网络异常和JSON解析错误。灵活的请求方法封装了不同场景的请求GET带参、POST JSON、上传文件。资源管理提供了close方法并在使用示例中用了try...finally确保Session被关闭。6. 调试与排查当请求不按预期工作时即使代码写得再严谨也难免会遇到问题。掌握有效的调试手段至关重要。1. 查看实际发出的请求有时候你以为你发的请求和你实际发出的请求不一样。使用requests的调试日志或直接打印预处理后的请求对象可以帮你确认。import logging # 启用urllib3的调试日志输出到控制台 logging.basicConfig(levellogging.DEBUG) # 或者更精细地控制 import http.client http.client.HTTPConnection.debuglevel 1 # 发起请求你会看到详细的HTTP报文 response requests.get(https://httpbin.org/get, params{test: value})注意在生产环境不要开启DEBUG日志因为会输出敏感信息如Authorization头。一个更安全的方式是使用requests的PreparedRequest对象req requests.Request(POST, https://httpbin.org/post, json{key: value}) prepared req.prepare() print(fMethod: {prepared.method}) print(fURL: {prepared.url}) print(fHeaders: {dict(prepared.headers)}) print(fBody preview: {prepared.body[:100] if prepared.body else None}) # 然后你可以用session.send(prepared)来实际发送它2. 使用外部工具辅助调试当代码逻辑复杂时可以借助像httpbin.org这样的服务来测试请求。它能回显你发送的所有信息。# 测试GET参数 r requests.get(https://httpbin.org/get, params{name: value}) print(r.json()[args]) # 会输出 {name: value} # 测试POST JSON r requests.post(https://httpbin.org/post, json{data: test}) print(r.json()[json]) # 会输出 {data: test} # 测试头部 r requests.get(https://httpbin.org/headers, headers{X-My-Header: Foo}) print(r.json()[headers][X-My-Header]) # 会输出 Foo对于更复杂的场景特别是调试与第三方API的交互使用像mitmproxy这样的中间人代理工具是终极武器。它能拦截、查看并修改你程序发出的所有HTTP/HTTPS流量让你清清楚楚地看到请求和响应的每一个字节。配置requests使用代理很简单proxies { http: http://127.0.0.1:8080, https: http://127.0.0.1:8080, # 注意mitmproxy的HTTPS代理也是http协议 } response requests.get(https://api.example.com, proxiesproxies, verifyFalse) # 注意verifyFalse仅用于调试自签名证书切记verifyFalse会禁用SSL证书验证仅在调试本地或受信任的代理时使用绝不要在生产环境中使用否则会面临中间人攻击风险。3. 理解常见的错误响应400 Bad Request你的请求格式有问题检查JSON语法、参数类型、必填字段。401 Unauthorized认证失败检查Token/API Key是否过期、格式是否正确、是否有权限访问该端点。403 Forbidden认证成功但权限不足。404 Not FoundURL路径错误或资源不存在。422 Unprocessable Entity请求格式正确但语义错误如字段值不符合业务规则。响应体通常会包含详细的错误信息。429 Too Many Requests触发了速率限制。需要降低请求频率并检查响应头中的Retry-After。5xx Server Error服务器端问题。你需要做的是1) 实现重试机制对500, 502, 503, 5042) 记录错误详情并告警3) 联系API提供方。处理这些错误时除了状态码务必仔细阅读响应体里面往往包含了具体的错误信息。一个健壮的程序应该能优雅地处理这些错误而不是直接崩溃。写网络请求代码从“能跑”到“跑得稳”中间隔着一大堆细节。这些细节就是区分普通使用者和资深开发者的地方。希望这些案例和经验能帮你少踩一些坑写出更可靠、更易维护的代码。记住每一个对外部服务的请求都是一个潜在的故障点善待它们就是善待你自己半夜被叫起来处理告警的睡眠时间。
返回列表