很多新手在接触动态页面爬虫时,第一反应就是把 Playwright 或 Selenium 拉出来,模拟浏览器慢慢等页面渲染完,再把 HTML 抓下来解析。这个思路不算错,但属于“最重”的方案。我做了这么多年的数据采集,有一个非常深刻的体会:只要页面是动态加载的,优先找它的 API 接口,然后用 Requests 直接请求,往往比“老老实实渲染页面”稳定得多、快得多,也省事得多。这篇内容我会从“为什么要找接口”“怎么用 Playwright 的 Network 能力把接口揪出来”讲到“最后如何回到 Requests 完成稳定抓取”,全程用可落地的代码和真实的踩坑经验说话,适合刚入门的爬虫新手,也适合被动态页面折腾到头秃的朋友。
标题里那句“更稳定”不是口号,而是我吃过多次亏之后的总结。如果你正在爬一个数据不断刷新、接口请求一堆的前后端分离站点,或者你写好的 Playwright 脚本动不动就超时、被限制,那么掌握“先找 API,再用 Requests 实现”这条路径,会让你每天的采集任务轻松非常多。接下来我会按实战顺序把整条链路拆开讲。
1. 为什么动态页面的“正道”是找接口,而不是硬爬渲染结果
1.1 动态页面到底“动”在哪:Ajax 渲染机制入门
所谓动态页面,本质上是“页面骨架 + 数据分开传输”。服务器返回的 HTML 往往只是一个空壳,里面的列表、表格、卡片数据,都是浏览器加载完 HTML 以后,再通过一段段 JavaScript 向后端发起异步请求拿到的,这个过程通常叫 Ajax(Asynchronous JavaScript and XML),或者用更现代的 fetch 方式实现。
你可以把这种页面理解成餐厅的外卖流程:你拿到手的不是一整盘做好的菜(完整 HTML),而是一张菜单和一副刀叉(页面框架),真正想吃的菜(数据)需要你通过服务员(JavaScript)去后厨(后端接口)另外点单。浏览器会自动帮你点单并上菜,所以人类用户看到的页面是完整的。但爬虫没有“浏览器自动点单”这个动作,直接用 Requests 抓 HTML 自然什么都拿不到。这就是为什么很多新手一抓动态页面就傻眼:源码里根本没有自己要的数据。
想拿数据,有两条路。第一条是用 Playwright 这类浏览器自动化工具,让浏览器真实渲染整个页面,等数据“上桌”以后再读取最终 HTML。第二条就是直接找到“后厨点菜单”的接口地址,模拟那个点单动作,从接口响应里把数据拿回来,绕开浏览器渲染这个环节。
刚开始学爬虫的人往往只会第一条路,因为直觉上“模拟浏览器总是对的”,但这条路坑很多。第二章我会详细说为什么。先记住一个结论:动态页面背后的数据交换,是通过一个个明确的 API 完成的,这些 API 才是数据的真正源头。我们写爬虫,目标就是数据的源头。
1.2 无脑 Playwright 去渲染的四个坑
用 Playwright 整页渲染再抓取,看起来“所见即所得”,实际操作中会遇到一堆问题,我挑四个最典型的讲。
第一是“等不到数据”。页面渲染不是瞬时的,接口请求也有快有慢。你写page.wait_for_selector()等某个元素出现,网络一抖或者接口超时,等了 10 秒 20 秒还没出来,脚本就卡死在那里。就算你加了超时控制,重试逻辑也得跟着写,代码复杂度一下就上去了。
第二是“资源开销大”。每跑一次爬虫就要启动一个完整浏览器内核,CPU 和内存的消耗比纯 Requests 高出一个数量级。如果任务是大量页面循环采集,比如一口气要抓几千个详情页,用 Playwright 一轮跑下来,普通电脑风扇能转得跟起飞一样,速度还慢。
第三是“稳定性偏弱”。浏览器渲染要依赖前端 JS 的正常执行,页面代码一旦改版,某个按钮 ID 变了、某个弹窗逻辑改了,你的选择器或者等待条件就可能失效。而且部分站点会做反自动化的检测,检测到 webdriver 特征就弹出验证码,直接打断你整个采集流程。
第四是“数据掺杂杂质”。渲染完的页面里包含大量布局标签、样式类名、无关节点,你要的数据埋在里面,最终还得靠 BeautifulSoup 或 XPath 二次提取。等于你花大代价渲染了一整个页面,最后只用其中 5% 的内容,得不偿失。
对比之下,直接请求 API 就是另一个画风:一个GET或POST请求甩过去,服务器直接返回一份干净的 JSON,你要的字段整整齐齐躺里面,解析成本极低。
1.3 “API 优先”到底优先在哪:一个生活化类比
我一直跟来咨询我爬虫问题的朋友说,把“渲染页面”和“请求接口”两种方案放在一起比较,就像你去一家餐厅,明明可以扫桌子上的点餐码直接下单,却非要喊服务员过来,把菜单从头到尾背一遍,让服务员再去后厨重复一遍你说的话。前者一步到位,后者多了一层转手,效率低还容易出错。
所谓“API 优先”,就是把这个“点餐码”找出来,直接用程序和它对话。优势非常直观:一是响应快,省掉浏览器加载 JS、渲染 DOM 的时间;二是数据干净,JSON 结构一看就懂;三是成本低,不需要起一个浏览器进程,对机器要求低,也更容易做分布式采集。当然,这条路也有门槛——你得先找到那个接口,并且要处理接口的鉴权和参数签名问题。但“找接口”这件事完全可以靠一套标准化的方法解决掉,后面几章就是讲这个方法的。
2. 用 Playwright 的 Network 能力,从请求堆里“揪出”目标接口
2.1 在 Playwright 里监听网络请求:page.on("request") 这么用
很多人以为要用 Playwright 就得等页面渲染完再抓内容,其实 Playwright 最强的能力之一是它对网络层的监听。你可以让浏览器在后台真实运行页面,同时把浏览器发出的每一个网络请求都记录成日志,这就相当于装了一个自动化的“抓包器”。
Playwright 提供了page.on("request")和page.on("response")两套事件。前者在请求发出时触发,你能拿到请求的 URL、方法、请求头、POST 请求体;后者在响应返回时触发,你可以读取响应状态码和响应内容。下面是一个最小可用的监听示例:
import asyncio from playwright.async_api import async_playwright async def capture_requests(url): async with async_playwright() as p: browser = await p.chromium.launch(headless=True) page = await browser.new_page() captured = [] async def on_response(response): if response.status == 200: captured.append({ "url": response.url, "status": response.status, "content_type": response.headers.get("content-type", "") }) page.on("response", on_response) await page.goto(url, wait_until="networkidle") await browser.close() return captured # 使用示例 requests_list = asyncio.run(capture_requests("https://example.com")) for req in requests_list: print(req["url"], req["content_type"])这段代码跑起来之后,你会看到页面的所有请求一个不落被记录下来。要注意wait_until="networkidle"这个参数,它的意思是等网络连接基本空闲之后再继续,这样能保证大部分异步请求都已经发出并返回了。如果页面有下拉加载、滚动分页这种交互式请求,你在监听过程中还得主动模拟滚动,否则后面的数据接口不会触发。这是一个非常容易忽略的细节,我一开始做动态列表页时,经常因为没滚动页面,导致后续翻页接口一个都没抓到,还以为是站点没有更多数据了。
2.2 从海量请求中筛出“真命天子”:特征识别法
打开了监听,你会发现一个页面能发出几十上百个请求,图片、CSS、字体、统计脚本、广告 SDK,全都在里面。怎么找出真正返回业务数据的那个接口?这就需要用“特征识别法”。我总结了三个筛选方向,基本可以应对绝大多数场景。
第一,按类型筛。只保留 XHR(XMLHttpRequest) 或 Fetch 请求。这两类请求才是页面异步拿数据的主力,图片、CSS 这些资源请求可以直接扔掉。在 Playwright 里可以通过请求的resource_type字段判断,取值是xhr或fetch的就是我们要重点关注的对象。判断逻辑可以这样加:
async def on_request(request): if request.resource_type in ("xhr", "fetch"): print("候选接口:", request.url, request.method)第二,按关键词筛。大多数后端接口的 URL 都带有和业务相关的英文单词,比如你要爬“王者战绩”,URL 里一般会带summoner、profile、stats之类关键词;要爬电商订单,接口里大概率会有order、list、detail。在监听结果里直接搜索这些关键词,很快就能锁定目标。实际抓取时,我会直接把所有xhr请求的 URL 拉到本地文本里,用编辑器搜一轮,基本百发百中。
第三,按响应内容筛。这就更稳了。拿不准哪个接口才是“真数据接口”时,可以对每个响应做一次内容嗅探:如果响应体的格式是 JSON,并且里面包含了你要抓的字段名,那就把它标记为命中。比如你要爬的英雄胜率,可以在响应体里搜"winRate"这个字段,谁返回了它,谁就是目标接口。这个方法可以写成自动化脚本,每个响应都快速做一次字符串命中判断,把命中的 URL 打印出来。
这三种方法可以组合使用,而且组合起来的效果是 1+1>2 的。我自己在实际项目里就是先过滤xhr,再过滤关键词,最后落到具体响应看 JSON 结构,基本三轮下来接口就锁死了。
2.3 把 Network 会话导出成 HAR 留档
手动在代码里打印请求日志当然可以,但遇到复杂页面时,逐条看打印信息效率太低。更推荐的做法是:用 Playwright 把整个网络会话导出成一个 HAR 文件(HTTP Archive 格式),再用浏览器开发者工具的“导入 HAR”功能慢慢看。
Playwright 提供了browser.new_context(record_har_path="session.har"),只要在创建上下文时指定 HAR 输出路径,浏览器跑完整个流程后,所有请求和响应快照都会自动保存到文件里。代码大概长这样:
from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.launch(headless=True) context = browser.new_context( record_har_path="captured.har", record_har_url_filter="*api*" ) page = context.new_page() page.goto("https://example.com") page.wait_for_timeout(3000) # 给异步请求留时间 browser.close()参数record_har_url_filter="*api*"是我常用的一个过滤技巧,它只记录 URL 里包含api的请求,HAR 文件会干净很多。如果你不确定接口 URL 长什么样,这个参数可以先不加,等导出之后再筛。
HAR 文件里除了请求 URL 和方法,还记录了完整的请求头、请求体、响应状态和响应内容,甚至精确到毫秒级的耗时。这对后续分析接口参数、理解接口调用逻辑极有帮助。我自己的习惯是,每一次接到新的采集目标,先花 20 分钟跑一遍 HAR 导出,把接口链路彻底搞清楚再写正式代码,这 20 分钟通常能省下后面好几小时的排错时间。
3. 不写代码也能找 API:开发者工具手动分析实操(王者战绩场景)
3.1 打开 F12 面板,先别急着抓,先看 Network
用 Playwright 自动监听网络请求是“偏自动化”的方案,但平时做一次性分析、或者仅仅想快速验证一个接口是否存在时,直接用浏览器开发者工具反而更快。这个方法不需要你写任何代码,只需要打开浏览器按 F12。
这里我拿一个很典型的搜索关键词场景“Python 爬虫查王者战绩”举例子。假设你要查某个玩家的历史战绩,你当然可以打开战绩查询页面,等它整个渲染完,然后去解析表格。但更快的方式是提前打开开发者工具的 Network 面板,勾选上方的“Fetch/XHR”过滤器,再刷新页面。此时面板里剩下的请求,就基本都是页面在异步获取数据时发出的接口请求了。
你不需要懂前端,只要观察这些请求的返回内容。逐个点击面板里的请求条目,右侧会弹出详情,里面有 Headers、Payload、Preview、Response 几个标签页。Preview 和 Response 会直接展示服务端返回的数据内容。当你点到某一个请求,发现 Preview 里出现玩家名称、段位、胜负场次这些你要的数据,那这一定就是你要的接口。
这个过程本质上和第二章讲的“特征识别法”是同一个逻辑,只不过把工具从 Playwright 换成了浏览器自带的面板。对刚入门的朋友来说,先用这个方式找接口,对理解“动态页面数据从哪里来”非常有帮助。
3.2 三个关键字筛出接口:XHR、搜索词、Preview
在 Network 面板里手动找接口时,我习惯遵守一套固定的筛选流程,可以极大避免“眼睛看花”的问题。
第一步,锁定 Fetch/XHR 过滤标签。普通的文档请求(Document)、图片(Img)、样式表(Stylesheet)都不是我们要找的,先排除。第二步,利用面板右上角的搜索框输入业务关键词。如果我正在爬“王者战绩”,我会直接键入summoner或profile,面板会自动过滤 URL 中包含这些词的请求。如果拿不准关键词,也可以输入api或query之类通用词碰碰运气。第三步是看响应内容,这一步最硬核。点击请求后,直接切到 Preview 标签页,它会把 JSON 数据格式化展开成树状结构,比看原始字符串舒服一百倍。如果你在这个树状结构里找到了你要的字段名,接口身份就完全确定了。
这套流程熟练之后,找出一个页面的核心接口平均只要两三分钟。而且它不挑站点,前后端分离的页面基本都适用。我见过有的新手一上来就在 Elements 面板里翻 DOM 结构,翻了半天也找不到数据在哪。其实数据根本不在 DOM 里,数据在 Network 里,方向对了,效率自然就上来了。
3.3 如果接口返回加密数据,后面要怎么想
有时候你会遇到一种情况:接口找到了,URL 也正确,但 Response 里返回的是一串看不懂的字符串,或者字段名是data: "加密内容"这种形式,根本读不出有效数据。遇到这种情况别慌,这通常是接口做了响应加密。加密的前端逻辑一定写在某个 JavaScript 文件里面,解密过程要么是 AES、RSA,要么是自定义算法,而且大概率有对应的密钥逻辑。
如果遇到响应加密,我的建议分两步走。第一步,回到 Network 面板,在 JS 资源里搜索decrypt、AES、CryptoJS等关键词,看看能不能定位到解密函数,理解解密过程。第二步,检查接口返回里是否带了encrypt_flag、encoding之类的开关参数,有时候加密是可以手动关掉的。实在解不开,就退回 Playwright 渲染方案,让浏览器替你完成解密,再从渲染后的页面里提取数据。这种“能用 API 就用 API,API 被加密了就退回渲染兜底”的思路,是动态页面采集里很重要的平衡手段。
4. 从“看到接口”到“请求直达”:用 Requests 稳定调通数据
4.1 复制 cURL 并转换为 Requests 调用
找到接口 URL 只是第一步,真正决定成败的是把浏览器那次请求“照抄”成代码里的请求。这里有个几乎零成本的技巧:在开发者工具的 Network 面板里,右键点击目标请求,选择“复制为 cURL”。
cURL 命令里包含了请求 URL、请求方法、全部请求头、请求体,可以说是浏览器请求的一份“完整体检报告”。拿到这个命令之后,可以直接把它转换成 Python 代码。如果你懒的手工转换,可以用在线工具或者 Python 的curl_cffi、requests帮你解析,不过对于学习阶段,我更推荐手工对照着写一遍,这样你能真正理解请求头的每一部分。
一个从王者战绩查询页面复制出来的 cURL 命令,转换后大概长这样:
import requests url = "https://api.example.com/summoner/profile" headers = { "User-Agent": "Mozilla/5.0 ...", # 浏览器标识,极其重要 "Accept": "application/json", "Referer": "https://example.com/player", # 有些接口会校验来源页 "Origin": "https://example.com", } params = { "summonerId": "123456789", "season": "current", } resp = requests.get(url, headers=headers, params=params, timeout=10) print(resp.status_code) print(resp.json())这里有三个地方我要特别提醒。第一,headers里的每一项都尽量保留,不要觉得多余就删掉。很多后端只认带齐 referer 和 origin 的请求,少了任何一个就直接返回 403。第二,params和data的区别要弄清楚,GET 用参数拼 URL,POST 用请求体传数据,搞混了接口大概率报错。第三,如果请求里有动态的 token 或者签名参数,就必须搞清楚它的生成逻辑,硬编码进去的 token 一旦过期,代码立刻就会失效。
4.2 补全 Headers 和动态参数:这样才不会 401/403
调用 API 最常碰到的错误就是 401 和 403。401 是未认证,403 是没权限,两者在爬虫语境下的原因高度相似:要么是请求头不全被识别成了“非浏览器”,要么是携带的认证凭证失效,要么是 cookie 状态不对。
先说请求头的问题。很多站点的后端会对User-Agent做白名单校验,非浏览器标识一律拒绝。所以我强烈建议,User-Agent字段直接复制浏览器真实的 UA 字符串,不要用 Python-requests 默认的python-requests/...。同理,Referer和Origin要填页面真实的来源,很多站点用它们做 CSRF 防护和防盗链校验,这两个字段不带上,就算你 UA 正确也可能被拦。
再说 token 的问题。现在的接口几乎都会带 token 鉴权,token 可能放在请求头里叫Authorization,也可能放在 cookie 里。如果你在浏览器 Network 面板里看到Authorization: Bearer xxxxx,说明这个接口需要登录态。简单的做法是把浏览器请求里的 token 复制出来,硬编码进代码;但 token 通常有有效期,更稳妥的做法是先写一个登录请求,用 Requests 登录后把返回的 token 存起来,再带着这个 token 去请求数据接口。
讲究一点的站点还会在接口请求里带签名参数,比如sign、timestamp、nonce。这种参数的生成规则一般在前端 JS 里。找规则的办法是到 Sources 面板里搜索sign=或者从 Network 面板里找到生成这些参数的 JS 文件,静下心读一读算法逻辑。如果算法特别复杂,也可以考虑用 Playwright 在页面里执行 JS 动态生成参数,再把参数传给 Requests。这种“混合方案”我在实际项目里用得不少,既保证了请求的轻量,又绕过了参数生成的坑。
4.3 一个完整 Requests 抓取流程示例(含重试与异常处理)
找到接口、理清参数之后,就该把整个流程写成健壮的代码了。很多新手写的抓取代码只有“GET + 解析”,遇到网络抖动就崩,这其实不是 requests 库的锅,而是少了异常处理和重试机制。一个能真正跑稳定任务的 Requests 抓取流程,至少应该包含三块:会话复用、超时控制、重试机制。
先看一个参照代码:
import requests import time import json from requests.adapters import HTTPAdapter def create_session(retries=3): session = requests.Session() # 挂载重试适配器,对连接错误和部分状态码自动重试 adapter = HTTPAdapter(max_retries=retries) session.mount("https://", adapter) session.mount("http://", adapter) session.headers.update({ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...", "Accept": "application/json", "Referer": "https://example.com/player", "Origin": "https://example.com", }) return session def fetch_player_profile(session, summoner_id): url = "https://api.example.com/summoner/profile" params = {"summonerId": summoner_id} for attempt in range(3): try: resp = session.get(url, params=params, timeout=10) if resp.status_code == 200: return resp.json() elif resp.status_code == 429: wait_time = 2 ** attempt print(f"触发限频,等待{wait_time}秒...") time.sleep(wait_time) else: print(f"请求失败: {resp.status_code} {resp.text[:200]}") except requests.Timeout: print(f"第{attempt + 1}次请求超时,即将重试...") except requests.RequestException as e: print(f"请求异常: {e}") time.sleep(0.5) return None session = create_session() profile = fetch_player_profile(session, "123456789") if profile: print(json.dumps(profile, ensure_ascii=False, indent=2))这里重点解释几个设计选择。用requests.Session()而不是直接requests.get(),是因为 Session 会自动保存 cookie 并复用底层 TCP 连接,连续请求大量接口时性能会好很多。挂载HTTPAdapter(max_retries=3)是一个比较优雅的重试方案,它能在连接层自动处理一部分丢包问题。在业务层,我又加了一个 for 循环重试,主要应对 429 限流等需要等待的情况,用指数退避(2 ** attempt秒)来降低二次触发限流的概率。
5. 常见报错与稳定性优化速查
5.1 401、403、429……这些状态码到底在说什么
做 API 采集久了,你会对 HTTP 状态码产生一种天然的敏感。我整理了一张表格,把日常最容易遇到的几个状态码和对应的排查思路写在一起,你可以直接保存下来当速查表。
| 状态码 | 含义 | 最常见原因 | 排查方向 |
|---|---|---|---|
| 401 | 未认证 | token 缺失、过期、错误 | 检查 Authorization 头和 cookie,重新登录获取新 token |
| 403 | 禁止访问 | 请求头不全、IP 被限制、签名错误 | 补全 headers,验证签名算法,切换代理 IP |
| 404 | 接口不存在 | URL 写错、接口版本变更 | 回 Network 面板重新复制 URL,对比参数路径 |
| 429 | 请求过于频繁 | 单位时间请求次数超限 | 降低频率,加随机延时,写重试退避逻辑 |
| 500 | 服务器内部错误 | 请求参数格式错误、服务端临时故障 | 检查 JSON 参数格式,观察服务器是否恢复 |
| 502/504 | 网关错误 | 目标服务器过载、反爬网关拦截 | 退避重试,降低并发,必要时更换采集时段 |
其中 429 是 API 采集里最让人头疼的。很多站点不会明确告诉你限流阈值,只会默默吐出一个 429 然后把你挡在外面。遇到 429,我的经验是:第一时间停止当前循环,sleep 一段时间再试,至少等 10 到 30 秒。如果你是一次性任务,拖一拖也就过去了;如果你是长期定时任务,就得在代码里写更严谨的“分发节奏控制”,比如每请求一次固定间隔 1 到 2 秒,同时引入随机抖动(time.sleep(1 + random.random())),避免触发“定时器特征检测”。
5.2 为什么“浏览器里能跑,脚本里就报错”(UA 与 Cookies 的坑)
这个问题几乎每个新手都撞过:明明在浏览器里的同一个接口一打开就是数据,复制到 Python 里却报 401 或者 403。根本原因通常是“请求的语境”变了。浏览器里能跑,是因为浏览器自动携带了完整的请求头、正确的 UA、站点种下的 cookie,还有可能附带了 TLS 指纹;而这些在默认 Requests 里全都缺失。
最典型的坑是 cookie 失效。浏览器里的 cookie 会一直跟随页面更新,你复制出来那一刻是新鲜的,过几个小时可能就过期了。所以只要接口需要登录态,都建议用 Requests 的 Session 从头走一遍“登录 + 授权”流程,让代码自己维护 cookie,而不是长期硬编码某一个 cookie。另一个坑是部分站点对 UA 做了极严格校验,连 Chrome 的完整版本号都要匹配,所以别偷懒,直接复制浏览器的完整 UA 字符串,别用简写。
这里顺便提一个场景,很多朋友爬“王者战绩”类站点时,数据接口要求登录自己账号才能查。这种场景下,正确的做法是先登录,把登录后的 Authorization 存下来,再通过代码保持会话。如果验证码太烦人,也可以用 Playwright 只负责登录和拿 cookie,把最后的数据请求交给 Requests 去做。这个玩法很实用,既享受了 Playwright 处理验证码和复杂交互的能力,又享受了 Requests 轻量稳定的请求表现。
5.3 稳定性三板斧:重试、限频、会话复用
最后聊一聊长期跑任务的稳定性。代码写出来跑一次容易,难得是让它跑一个月不出问题。从我自己的经验看,稳定性主要靠三招:重试、限频、会话复用。
重试指的是每个请求都设置超时,并且在超时或返回 5xx 错误时自动重试。重试间隔不要固定,建议用指数退避,第一次等 1 秒,第二次等 2 秒,第三次等 4 秒,最多重试 3 次左右,避免无限循环拖垮系统。
限频指的是主动控制请求速率,不要一次性把某个接口打到密集请求。哪怕目标站点没有明显限流,也应该模拟“人类操作”的节奏,每次请求之间加 0.5 到 2 秒的随机延时。这个随机不是乱加的,而是为了不暴露机器的规律性,很多反爬系统正是靠分析“请求时间间隔过于规律”来判定机器人的。
会话复用指的是尽量用同一个requests.Session跑完整批任务,让 cookie 和连接池都能复用。你每 new 一个 Session 就等于重新建立一次 TCP 连接,对目标服务器来说就是一个新访问者,容易被高频访问策略盯上。用好一个 Session,保持连接活跃,配合稳定的请求头,整体稳定性会有质的提升。
我在实际项目里还习惯给脚本加一个“任务心跳表”,每次请求成功就把结果记录到本地日志,失败也记录失败原因。这样长期跑的时候,万一中途挂了,你能快速从日志里找到是哪个参数出了问题、哪段时间触发了限流,而不是一脸懵地从头排查。这套日志习惯,比我写过任何一个复杂算法都更值钱。
写到这里,关于“API 优先”这套思路就完整讲完了。我个人在做爬虫项目时最大的感受是:技术选型没有绝对的对错,但一定要懂权衡。Playwright 不是用来替代 Requests 的,Requests 也不是万能的。面对动态页面,先用 Network 手段把接口找清楚,能直接请求就走 Requests,遇到复杂登录和加密再让 Playwright 兜底,两条路配合着用,才能让采集任务既快又稳。如果你是从零开始学爬虫,建议你先打开浏览器开发者工具,拿一个自己常看的动态页面练练手,按文中的筛选流程找出两三个 API,再用 Requests 把它们调通。这个过程多走几遍,你对“数据到底从哪来”就会有完全不一样的理解。