资讯动态

Python requests库实战全解:爬虫与接口调试的必备技能

发布时间:2026/9/9 12:37:42 来源:尧图企业网站定制
1. requests库到底强在哪为什么爬虫和接口调试都绕不开它做Python开发这些年我见过太多人一上来就问我爬虫用什么库我永远只会回答一个名字requests。不是因为它完美无缺而是因为它是目前Python生态里把HTTP请求这件事做得最顺手的库没有之一。官方文档第一句话就说它是给人类用的HTTP库这句话一点不夸张。先给没接触过的朋友一个直观感受。你用Python写一个最简单的HTTP请求如果只用标准库urllib代码写起来又长又绕要手动处理URL编码、要手动构造请求头、响应还要自己去decode。但换成requests三行代码搞定import requests resp requests.get(https://httpbin.org/get) print(resp.status_code) print(resp.text)这背后隐藏了大量细节连接建立、HTTP报文封装、响应解析、字符编码处理、Cookie存取requests全都替你做了。你写爬虫、调接口、做数据采集、写自动化脚本最核心的操作就是发请求、拿响应requests把你从繁琐的底层协议里解放出来让你把精力集中在业务逻辑上。这个库适合谁我觉得覆盖范围极广刚入门Python想学爬虫的新手用它写第一个能跑的程序做数据分析的人用它拉取公开数据集后端开发做接口联调用它在脚本里快速验证甚至做自动化运维的也能用它写监控报警脚本。只要你的程序需要和Web打交道requests就是一个永远绕不开的基础设施。我最早接触requests是七年前那时候网上爬虫教程清一色用urllib后来requests一出来社区几乎是以肉眼可见的速度迁移。为什么因为它把请求这个动作简化到了极致而且API设计得非常直觉化你不需要背文档猜都能猜出来该怎么用。这篇文章我就把requests从入门到实战的完整链路讲一遍包括我踩过的坑、排查过的问题全是实际经验看完你就能直接上手写东西。2. 环境准备与第一个请求别小看安装这步2.1 安装Python与requests库的几种姿势先说环境。很多新手卡在第一步不是requests难装而是Python环境本身没配好。如果你用的是Windows去python.org下载安装包时务必勾选Add Python to PATH这个选项否则装完在命令行里敲python会提示不是内部或外部命令。这一步忘了勾后面全是坑。装好Python之后安装requests有两种方式。最推荐的是pippip install requests如果你系统里同时装了Python 2和Python 3可能需要用pip3甚至在Windows上要用python -m pip install requests避免装错环境。这一点我特别提醒一下热词里专门有windows安装python多个版本这就是多版本环境的经典问题。我建议多版本用户一律用虚拟环境venv管理依赖而不是全局pip装包否则不同项目依赖的版本冲突会让人抓狂。还有一种情况是离线安装。有些公司的开发环境是内网隔离的没法直接pip这时候需要在一台能联网的机器上下载whl文件然后拷贝内网安装pip download requests -d ./packages pip install requests --no-index --find-links./packages2.2 发送第一个GET请求并理解响应结构环境准备好后我们来发第一个真正意义上的请求。我习惯用httpbin.org这个在线测试服务来学习HTTP因为它会把你发的请求原样返回方便观察结构。import requests resp requests.get(https://httpbin.org/get) print(resp.status_code) # 200 print(resp.headers) # 响应头是一个字典对象 print(resp.text) # 响应体的文本形式 print(resp.json()) # 如果响应是JSON直接解析成字典这里有几个非常重要的细节。resp.status_code是HTTP状态码200表示成功404表示资源不存在500表示服务器内部错误状态码是判断请求是否成功的第一个信号。resp.headers是一个大小写不敏感的字典你可以直接用resp.headers[Content-Type]取值不用管服务器返回时是Content-Type还是content-type。resp.text是requests根据响应头里的编码自动解码后的字符串如果服务器没声明编码可以手动指定resp.encoding utf-8再访问resp.text。需要强调一点resp.json()虽然方便但它内部只是调用json.loads(resp.text)如果响应体不是合法的JSON会抛出异常。所以实际项目中我建议先判断状态码和Content-Type再决定是否调用json()。2.3 带参数请求和自定义Headers从入门到像浏览器绝大多数接口都不会让你裸奔访问至少要有查询参数。requests传URL参数的方式非常优雅不需要自己拼URLparams { keyword: python, page: 1, page_size: 20 } resp requests.get(https://httpbin.org/get, paramsparams) print(resp.url) # 打印实际请求的完整URLrequests会自动把字典转成URL编码后的查询字符串中文、特殊字符都不用你操心。这在爬虫场景里特别实用翻页、搜索、筛选条件统统可以维护在一个字典里逻辑清晰又容易改。自定义Headers也是爬虫和接口调试的刚需。很多网站会检查User-Agent如果你用默认的python-requests/x.x.x很容易被反爬策略拦截。最简单的伪装就是设置一个浏览器的User-Agentheaders { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Accept: application/json, text/plain, */*, Referer: https://example.com } resp requests.get(https://httpbin.org/get, headersheaders)我个人的习惯是把常用的headers组合封装成一个函数返回一个公共请求头字典这样多个爬虫项目能复用。还有个小技巧如果你不确定某个请求需要哪些头先用浏览器的开发者工具F12打开Network面板刷新页面找到对应的请求右键Copy as cURL然后可以用一个叫curl2requests的小工具把它转成requests代码调试效率能翻倍。3. 进阶实战Session、重试与429错误处理3.1 Session对象为什么爬虫必须用它初学requests时大部分人都是直接requests.get、requests.post这样裸调。这样当然能跑但只要遇到需要登录的网站就歇菜了。原因很简单每次裸调requests都是一次全新的、无状态的HTTP连接服务器不会记得你上一次做过什么。而Session对象相当于一个持久的会话容器它会自动保存Cookie复用底层TCP连接。我打个比方裸调requests就像每次去超市都重新办一张会员卡Session则是把会员卡一直揣在兜里第二次去直接刷。session requests.Session() session.headers.update({ User-Agent: Mozilla/5.0 ... }) # 先登录Cookie会被session自动保存 login_data {username: test, password: 123456} session.post(https://httpbin.org/post, datalogin_data) # 后续请求自动携带登录态 resp session.get(https://httpbin.org/cookies) print(resp.text)这段代码里登录接口返回的Set-Cookie会被Session自动保存后续请求自动带上你完全不感知。这种会话保持能力在写爬虫时特别关键因为大多数网站的用户体系都依赖Cookie判断登录状态。Session还有一个隐藏优势就是连接复用。HTTP底层是TCP连接频繁建立和断开连接非常耗时。Session内部维护了一个连接池请求同一域名时会复用底层连接速度能提升一个量级。这个优化在大量请求的场景下非常明显。3.2 超时设置与重试机制别再让程序卡死接下来这块是我觉得requests最容易踩坑的地方也是热词里反复出现的exceeded retry limit, last status: 429 too many requests这类报错的高发区。先说超时。如果不设置timeout参数requests默认会一直等下去直到底层socket超时这个时间很长可能是几分钟。实际爬虫场景里一个请求卡住整个程序就卡住了后面的请求全排队效率低到崩溃。所以我的铁律所有请求必须设置timeout。resp requests.get(https://httpbin.org/delay/3, timeout5)timeout5表示连接和读取的总超时时间是5秒。如果想分开控制连接和读取的超时可以传一个元组resp requests.get(https://httpbin.org/delay/3, timeout(3, 10))第一个值是连接超时连不上服务器时就放弃第二个值是读取超时连接上了但数据迟迟不回时就放弃。我通常设置(3, 10)连接3秒、读取10秒比较平衡。再说重试。网络请求是天然不稳定的偶发超时、偶发5xx错误都很正常。很多人的代码一遇到请求失败就整个崩掉这其实是不合理的应该用重试机制来提升鲁棒性。requests本身没有内置重试逻辑但它的底层urllib3提供了Retry类可以通过HTTPAdapter挂载到Session上from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry session requests.Session() retry Retry( total3, # 总重试次数 connect3, # 连接失败重试次数 read2, # 读取失败重试次数 backoff_factor0.5, # 退避因子重试间隔递增 status_forcelist[500, 502, 503, 504] # 哪些状态码触发重试 ) adapter HTTPAdapter(max_retriesretry) session.mount(http://, adapter) session.mount(https://, adapter)backoff_factor的机制是第n次重试前的等待时间为backoff_factor * (2 ** (n-1))秒也就是0.5秒、1秒、2秒这样指数递增。这个设计是为了避免重试风暴——如果服务器已经忙不过来了你的请求还以超高频率打过去只会让情况更糟。3.3 429 Too Many Requests遇到限流的正确姿势热词里频繁出现429状态码我专门来聊这个。HTTP 429表示Too Many Requests通俗说就是你请求得太频繁了服务器受不了了让你歇会儿。遇到429最忌讳的做法是马上加大重试次数、缩短间隔继续猛打。正确的做法是尊重服务器的限流信号退避重试。urllib3的Retry也对429做了特殊支持你只需要在status_forcelist里加入429再配合Retry-After头处理import time from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry def requests_with_retry(session, url, **kwargs): retry Retry( total5, backoff_factor1, status_forcelist[429, 500, 502, 503, 504], respect_retry_after_headerTrue ) adapter HTTPAdapter(max_retriesretry) session.mount(https://, adapter) return session.get(url, **kwargs)respect_retry_after_headerTrue的意思是如果服务器在响应头里返回了Retry-After字段告诉你要等多少秒urllib3会乖乖等待对应时间再重试。这是对服务器最基本的尊重也是避免IP被封的关键。我再分享一个实际经验遇到429时除了退避重试更应该反思自己的请求频率是否合理。很多公开接口的文档里明确写了QPS限制比如每秒最多请求1次那就应该在自己的代码里做好限速而不是依赖服务器429来教做人。用time.sleep控制请求间隔或者使用更高级的限流库都是成熟的做法。import time import requests def fetch_with_rate_limit(url, interval2): time.sleep(interval) # 保证每次请求间隔至少2秒 resp requests.get(url, timeout10) return resp3.4 异常处理体系让程序在出错时优雅降级requests的异常体系不算复杂但很多人处理得不对。它主要有以下几类异常异常类型触发场景处理建议requests.exceptions.ConnectionError网络不通、DNS解析失败、连接被拒绝检查网络稍后重试requests.exceptions.Timeout请求超时增大timeout或重试requests.exceptions.TooManyRedirects重定向次数超过上限检查URL是否有循环重定向requests.exceptions.HTTPErrorHTTP状态码为4xx或5xx需调用raise_for_status触发按状态码区分处理requests.exceptions.JSONDecodeErrorJSON解析失败判断响应格式后再解析推荐的写法是把请求封装成一个函数统一捕获异常import requests from requests.exceptions import RequestException def safe_get(url, paramsNone, headersNone, timeout(3, 10)): try: resp requests.get(url, paramsparams, headersheaders, timeouttimeout) resp.raise_for_status() # 状态码4xx/5xx时抛出HTTPError return resp except requests.exceptions.Timeout: print(f请求超时: {url}) except requests.exceptions.ConnectionError: print(f连接失败: {url}) except requests.exceptions.HTTPError as e: print(fHTTP错误: {e.response.status_code} - {url}) except RequestException as e: print(f请求异常: {e}) return Noneraise_for_status()是一个被低估的方法。如果你不调用它requests在收到404、500时也不会报错resp.text里可能是一段HTML错误页面你的代码还会继续往下跑最后在一堆奇怪的错误里迷失方向。调用了它状态码异常时立即抛异常能让你第一时间发现问题。我实际写爬虫时异常处理、重试、日志这三者通常是配合使用的。重试解决偶发问题异常处理兜底日志记录每次失败的原因和上下文方便事后排查。这三板斧下来爬虫的稳定性会有一个质的飞跃。4. 把requests用到极致代理、SSL与性能优化4.1 代理配置与SSL证书处理爬虫写多了必然会遇到代理的需求。可能是网站封了你的IP可能需要绕过访问限制也可能单纯是为了分散请求压力。requests配置代理非常简单一个字典搞定proxies { http: http://127.0.0.1:7890, https: http://127.0.0.1:7890 } resp requests.get(https://httpbin.org/get, proxiesproxies)如果代理需要认证在URL里带上用户名密码proxies { http: http://user:password127.0.0.1:7890 }这里我要特别强调合规和安全问题。如果你访问的是国内正常的公开网站和接口完全不需要所谓的特殊网络工具。requests的代理功能最常应用于正常的开发调试场景——比如把请求转向本地抓包工具Charles、Fiddler、mitmproxy来分析报文。这个场景下代理地址一般是127.0.0.1加端口号目的只是让抓包工具能看到HTTP流量内容帮助你排查问题这才是requests代理配置的正确用法。SSL证书验证是另一个高频问题。正常情况下requests会验证HTTPS证书这是安全的默认行为。但如果遇到自签名证书或公司内网的测试环境验证会失败。你可以这样做# 方式一关闭验证仅限测试环境 resp requests.get(https://self-signed.badssl.com, verifyFalse) # 方式二指定CA证书路径 resp requests.get(https://example.com, verify/path/to/ca.crt)关闭verifyFalse后requests会抛一个InsecureRequestWarning警告提示你当前连接不安全。我的建议是生产环境永远不要关闭SSL验证如果确实需要用自签名证书应该把证书加到信任列表里用verify参数指定CA文件路径。这既是安全底线也是职业素养。4.2 连接池与性能优化requests的设计目标是简单好用在极致性能上它确实不如aiohttp、httpx这类异步库但通过合理配置它的并发能力也够大多数场景用了。性能优化的第一招是复用Session。前面提过Session内部维护连接池默认每个域名10个连接。如果单线程爬虫觉得速度不够可以用ThreadPoolExecutor配合Session控制最大并发数from concurrent.futures import ThreadPoolExecutor import requests session requests.Session() def fetch_one(url): try: resp session.get(url, timeout(3, 10)) return resp.status_code, len(resp.content) except Exception as e: return None, str(e) urls [fhttps://httpbin.org/get?id{i} for i in range(50)] with ThreadPoolExecutor(max_workers5) as executor: results list(executor.map(fetch_one, urls))这里的关键是Session对象是线程安全的多个线程共享同一个Session没问题连接池里的连接也会被复用。max_workers的取值建议从5开始根据目标网站的反应调整。太快会被限流甚至封IP太慢又没效率这个度需要实际测试。还有一个性能优化点是连接适配adapter HTTPAdapter( pool_connections20, # 连接池大小 pool_maxsize20, # 每个主机的最大连接数 max_retries3 )pool_connections是缓存连接的总数pool_maxsize是同一主机可复用的连接数。调大这两个值能提升高并发下的吞吐量但代价是内存占用更高。实际项目中我通常保持默认除非做压测时发现连接成为瓶颈。4.3 流式下载与大文件处理下载大文件时一个经典错误是直接用resp.content把整个文件读进内存。如果文件有2GB你的内存可能直接爆掉。正确的做法是启用流式模式分块写入磁盘import requests url https://example.com/big-file.zip resp requests.get(url, streamTrue, timeout(5, 60)) if resp.status_code 200: with open(big-file.zip, wb) as f: for chunk in resp.iter_content(chunk_size8192): if chunk: f.write(chunk)streamTrue是核心它让requests不会一次性下载全部内容而是等你去迭代响应体。iter_content(chunk_size8192)指定每次读取8KB边下边写内存占用始终在一个很小的范围内。对于大文件下载我还建议加一个进度显示。用tqdm这个库几行代码就能实现from tqdm import tqdm import requests resp requests.get(url, streamTrue, timeout(5, 60)) total_size int(resp.headers.get(Content-Length, 0)) chunk_size 8192 with open(big-file.zip, wb) as f: with tqdm(totaltotal_size, unitB, unit_scaleTrue) as pbar: for chunk in resp.iter_content(chunk_sizechunk_size): if chunk: f.write(chunk) pbar.update(len(chunk))注意Content-Length头可能是0或不存在这时total_size为0进度条会显示不定长模式。这个细节很常见很多新手会在这里困惑其实不影响下载功能只是显示上不准确。5. 实战演练一个完整的公开数据采集脚本5.1 目标分析与合规检查光说不练假把式。这一节我用一个完整的小项目把前面讲的东西串起来。假设我们要采集一个公开的、允许爬取的API数据。为了安全合规我用httpbin.org作为演示目标它本身就是用来做HTTP测试的公开服务没有任何访问限制完全合法。在写爬虫之前我一直强调先做合规检查。核心就三条一是检查网站的robots.txt看是否允许爬虫访问目标路径二是看网站的服务条款是否有明确的爬取禁止声明三是控制请求频率不要给对方服务器造成压力。这不仅是道德问题也是保护自己IP的风险管理。我见过太多人因为爬虫频率控制不当导致IP被网站封禁得不偿失。5.2 完整代码实现与逐行解读这个实战项目要做的事情是从httpbin.org的延迟接口拉取多条数据带重试、带超时、带限速、带错误日志最后把成功的响应保存到本地JSON文件。import json import time import logging import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry logging.basicConfig( levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s ) def create_session(): session requests.Session() retry Retry( total3, backoff_factor0.5, status_forcelist[429, 500, 502, 503, 504] ) adapter HTTPAdapter(max_retriesretry) session.mount(https://, adapter) session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 }) return session def fetch_with_backoff(session, url, paramsNone): try: resp session.get(url, paramsparams, timeout(3, 10)) resp.raise_for_status() return resp.json() except requests.exceptions.Timeout: logging.warning(请求超时: %s, url) except requests.exceptions.HTTPError as e: logging.warning(HTTP错误: %s - %s, e.response.status_code, url) except requests.exceptions.ConnectionError: logging.warning(连接失败: %s, url) return None def main(): session create_session() results [] for i in range(1, 11): params {id: i} data fetch_with_backoff( session, https://httpbin.org/get, paramsparams ) if data: results.append({ id: i, args: data.get(args, {}), user_agent: data.get(headers, {}).get(User-Agent, ) }) logging.info(成功获取第 %d 条数据, i) else: logging.error(第 %d 条数据获取失败, i) time.sleep(1) # 限速每秒最多一个请求 with open(data.json, w, encodingutf-8) as f: json.dump(results, f, ensure_asciiFalse, indent2) logging.info(全部完成共获取 %d 条数据, len(results)) if __name__ __main__: main()这段代码看着长拆开看其实就几层create_session负责构建带重试的会话fetch_with_backoff负责发请求并统一处理异常main函数控制业务流程和限速日志记录每一环节的进展。这种分层结构是我写爬虫的基本框架任何项目都可以套用。5.3 抓取结果的结构化处理与存储输出的JSON文件使用了ensure_asciiFalse和indent2两个参数前者保证中文正常显示后者让JSON格式化易读。这只是最基础的存储方式。实际项目里数据落地通常有几种选择。数据量小、结构简单的用JSON文件足够需要查询筛选的用SQLite或MySQL如果只是临时跑一次打印到终端也行。我记得有一次帮朋友抓几千条公开商品数据存JSON文件没问题但后续要做数据分析和可视化还是导入Pandas更方便import pandas as pd df pd.DataFrame(results) print(df.head())如果你的目标是数据分析和可视化那么requests负责取数Pandas负责清洗和分析Matplotlib负责出图这是一条非常顺的链路。热词里提到的python数据分析与可视化就是这么一套东西。6. 高频报错与排查技巧实录6.1 我最常被问到的几个requests报错问的人多了我把高频报错整理成一个速查表方便你遇到问题时候能快速定位报错信息出现原因解决方案Max retries exceeded with url连接失败或超时重试也失败检查网络、URL是否正确、目标服务器是否可达ConnectionError: Max retries exceeded目标网站无法连接确认域名解析正常必要时用浏览器访问测试Too many redirectsURL存在循环重定向检查URL配置或用allow_redirectsFalse手动处理InsecureRequestWarning关闭了SSL验证建议改用verify指定CA证书而不是长期关闭验证SSLError: Certificate verify failedSSL证书验证失败更新CA证书库或针对测试环境指定verify路径JSONDecodeError响应体不是合法JSON先resp.text确认返回内容再考虑是否改用text解析Connection pool is full连接池耗尽调大pool_maxsize或检查代码是否存在连接泄漏6.2 一个经典排查案例429导致的连环问题上个月有个朋友跑爬虫程序跑着跑着突然报错exceeded retry limit, last status: 429 too many requests。他问我是不是代理IP不行我说你先别看代理回去看看你的请求频率这个报错的意思就是你的请求太过频繁被限流了不是IP的问题是频率的问题。那次排查过程我记得很清楚。我让他先在代码里加了一行日志打印每次请求的时间戳和URL跑了两分钟后发现他爬虫每秒钟发出去大概30个请求而且对同一个域名。这种频率大部分网站都会给429。解决方案也很简单一是把并发数从10调降到3二是每次请求间隔加0.5秒三是在Retry里设置respect_retry_after_headerTrue配合服务器的限流指示。改完之后跑了一个小时一个429都没再出现过。这个案例我觉得很典型。很多人遇到429第一反应是代理不够好、IP被拉黑其实大概率是频率控制没做好。记住一个原则能通过降低频率解决的问题就不要用更多的IP去硬刚这是爬虫能长期不被封的核心逻辑。6.3 几个实用排查工具和技巧除了会写requests代码会看请求也很重要。我推荐几个排查思路第一开启调试日志。requests基于urllib3我们可以打开urllib3的调试日志看到底层的HTTP交互细节import logging import requests logging.basicConfig(levellogging.DEBUG)这样会输出每次请求的连接、请求头、响应状态等信息。注意这会打印大量日志只建议在调试阶段开启。第二用抓包工具对比。如果requests请求失败但浏览器访问正常用Fiddler或Charles抓包对比浏览器和requests发的请求头差异很可能是少了某个必要的Header比如Referer、Origin等。第三用状态码和响应头定位问题。收到403时检查是否是User-Agent被识别了收到404时检查URL或路径参数是否正确收到301/302时检查是否要跟随重定向。这些都需要你对HTTP协议本身有基本的理解。7. 聊聊我对requests的理解和一些小建议requests这个库看起来简单用起来也不难但真正用好需要对HTTP协议有基本的理解。比如状态码的含义、Header的作用、Cookie的机制这些概念和requests的API是一一对应的。我的建议是不要只停留在调API的层面多花时间理解HTTP本身你会发现requests的设计很多都是对HTTP语义的直观映射。从长期使用的角度来看我想再分享几个小建议。第一管理好你的Session和请求频率这是提升爬虫稳定性的核心第二尽量用类型标注给请求函数加上签名时间久了你会感谢自己当初的规范第三requests和正则表达式配合使用虽然效率不高但很多小场景足够用了。如果你追求异步高性能未来也可以关注 httpx 这个库它的API风格和requests几乎一致但支持异步不过那是另一个话题了。根据我个人经验requests是Python生态里少有的大而全又好用的库它能覆盖你90%的HTTP需求而且踩坑成本极低。希望这篇文章能帮你把剩下那10%的坑也提前填上让你少走点弯路。

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

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

免费获取报价