ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

动态站点爬虫最优解:识别接口并用Requests高效抓取

动态站点爬虫最优解:识别接口并用Requests高效抓取 1. 为什么“回到接口”是动态站点爬虫的最优解做爬虫的朋友应该都有过这种经历明明用 Requests 把页面 HTML 拉下来了结果翻遍源码连商品价格、用户评论、榜单数据的影子都没看到整个 HTML 里只有一堆script标签和空壳的div。看一眼网页源码少则几千行、多则几万行真正有价值的数据却不在里面。面对这种页面很多新手第一反应是上 Selenium、Playwright 这一类的自动化浏览器工具让浏览器去把 JavaScript 执行完再把渲染后的完整 DOM 拿回来。这条路确实能走通但我个人在实际项目中很少首选它。原因也很简单——用浏览器渲染一个页面的开销太大了。一个正常页面启动浏览器实例可能要几百毫秒甚至几秒再加上加载图片、字体、站点的各种第三方脚本单页平均要两三秒是家常便饭。如果你是爬一个列表页一页 20 条数据翻 50 页就是 1000 条数据光页面加载就是几分钟起步。而且浏览器实例也是内存消耗的“大户”一个 Chrome 页面动辄几百 MB 内存并发跑几个实例就可能把开发机搞瘫痪。动态站点的反爬检测也往往优先照顾“非浏览器环境”——请求频率稍微一高立刻弹出验证码或者要求登录Selenium 那种自带标记的自动化浏览器反而更容易被识别。真正高效且稳定的做法是标题里说的这个思路回到接口。动态站点的数据本质上是前端 JavaScript 向后端服务器发请求拿回来的这个请求我们叫它“接口”API。接口返回的往往是结构化的 JSON 数据字段清晰、层级明确拿到之后转换成 Python 字典或者列表就行根本不需要面对浩如烟海的 HTML 标签堆里去“大海捞针”。而且接口请求可以精准控制参数分页、筛选、排序都能在请求层面直接完成。对比之下接口爬虫不仅速度更快、稳定性更高代码量也少得多——这也是我在这篇文章里要带着大家从零开始走一遍的原因。这篇文章适合谁零基础刚学完 Requests 基本用法的读者或者已经会写最基础的静态页爬虫但还没搞明白动态站点该从哪里下手的朋友。我会分享我在实战中识别接口、分析接口、用 Requests 重写请求的完整流程同时讲清楚每一步的原理让大家不仅知道“怎么做”更明白“为什么这样做更稳”。2. 识别动态站点的数据接口识别接口是“回到接口”这条路的第一步也是最重要的一步。如果这一步走错了后面所有工作都白费。我在这个环节踩过的坑最多所以这里会把方法讲透。2.1 如何判断页面是不是动态渲染拿到一个目标站点先别急着自己猜直接动手按 F12 打开开发者工具或者右键菜单里的“检查”切到 Network网络面板刷新页面观察请求列表。这里有个很关键的判断手法看 HTML 文档请求的响应体里有没有你要的数据。具体操作是在 Network 面板的请求列表里找到文档类型的请求一般是以站点域名开头的那个Type 列显示为 document点击它在右侧的 Response响应标签页里看 HTML 源码。如果源码里搜不到你要的数据字段比如商品标题、价格、ID 这类关键词那么基本可以确定这是一个动态站点——数据是靠 JavaScript 运行时发异步请求才拿到的。另一种快速佐证方法是在页面上直接 CtrlU 查看网页源码注意不是检查元素。如果你在“检查”的 Elements 面板里能看到数据但网页源码里没有这就说明数据是 JS 动态插入的Elements 面板显示的是浏览器执行完 JS 之后的结果而网页源码是最初的响应体。这个区别极其重要很多新手在 Elements 里看到数据就以为静态能抓到结果用 Requests 拉下来才发现什么都没有白白浪费了时间。2.2 用 XHR 筛选直接锁定接口请求确认是动态站点之后打开 Network 面板刷新页面点击面板顶部的 Fetch/XHRChrome 新版叫 Fetch/XHR旧版叫 XHR筛选按钮。这个筛选器会帮你过滤掉图片、CSS、字体这些无关请求只保留页面里的异步网络请求——也就是数据接口的候选者。接下来的操作就是逐个点击这些请求在右侧的响应内容Response 或 Preview 标签里找你要的数据。请求数量多的时候不要烦躁这是正常现象。一个比较实用的技巧是边滚动页面边观察请求列表。比如你是要爬列表数据那就先把页面滚动到最底部触发出下一页或者加载更多这时你会看到新的请求冒出来这个请求十有八九就是数据接口。如果页面有筛选条件切换一下筛选条件同样观察有没有新请求出现带着“哪个参数触发了数据变化”的思维去排查接口很快就能定位到。定位到可疑请求之后我就会在响应内容里搜索一下数据字段比如“price”“name”“list”这些关键词。能搜到的话基本就是这个接口没跑了。2.3 判断接口质量的三个关键标准找到了一个能返回数据的请求不代表这个接口就适合直接拿来爬。我在实际项目中会从三个维度去评估一个接口的质量。返回格式的友好度优先选返回 JSON 的接口JSON 解析简单data[“key”] 一串下来就拿到值了。如果接口返回的是 HTML 片段甚至 XML能用但解析成本高一截需要谨慎考虑。参数的复杂度一个好接口的请求参数通常比较精简无非是页码、每页数量、排序方式这些。如果接口携带大量加密参数比如签名、token、动态时间戳那你就得评估一下逆向的成本值不值得放弃更方便的方式。数据的完整性检查返回的 JSON 里是否包含列表页需要展示的全部字段标题、链接、封面图、价格、发布时间等。有些列表接口只返回部分字段详情数据还得靠另一个详情接口这时候你就需要多收集一个接口对后续的数据拼接要有心理准备。这三个标准看起来很简单但做实战项目之前先花几分钟评估一下能帮你省下后面大量的调试时间。3. 分析接口的请求细节从 Copy as cURL 到参数拆解识别出接口只是起点真正要把接口用 Requests 复现出来还得拿到请求的完整细节。3.1 用复制 cURL 大法快速搭建 Requests 请求我最常用的手法是在 Network 面板的请求列表中右键点击目标请求选择Copy→Copy as cURL。Chrome 会把请求转成一段完整的 curl 命令带有请求的 URL、headers、请求体如果有的话。然后我习惯去一个在线转换工具把 curl 转成 Python Requests 代码你本地装了 curlconverter 命令行工具也可以pip 安装就能用或者如果你熟练的话直接照着 curl 里的信息手工写 Requests 代码。这个方法之所以效率高是因为它不会漏掉任何 header——很多人的请求被服务器拒绝原因就是漏了某些关键 header而从 cURL 出发基本可以避免这个问题。不过要提醒一句复制下来的 headers 往往有很多并不是必需的比如sec-ch-ua、sec-fetch-*这些浏览器环境相关的字段。你在后续调试里可以逐步精简只留下服务器真正校验的东西一般必留的是User-Agent和Referer。User-Agent 告诉服务器“我是一个什么浏览器”Referer 告诉服务器“我是哪个页面跳转过来的”很多站点凭这两个字段就能做初步过滤。3.2 为什么这些请求头一个都不能少这里我想单独展开讲讲 headers 里的几个关键字段因为这直接关系到你的请求能不能被服务器接受。先说User-Agent简称 UA。服务器的反爬第一步就是看 UA 是否来自正常浏览器的特征。Python 的 Requests 默认 UA 长这样python-requests/2.x.x服务器一眼就知道“你不是浏览器”很可能会直接拒绝或返回异常结果。所以务必要从浏览器的 Network 面板里复制一个完整的 UA 字符串或者自己维护一个 UA 列表。再说Referer。很多有防盗链机制的站点尤其是视频站、图床类站点会检查请求的 Referer 是不是本站的页面。如果 Referer 不合法接口就会返回 403。对于数据接口来说Referer 一般设成当前页面的地址就行。最后是Cookie。如果你的目标页面需要登录才能看到完整数据那 Cookie 就是绕不过去的。从 Network 面板里复制请求头中的 Cookie 字段直接塞进 Requests 的 headers 里是最快捷的方式但这种做法有有效期限制Cookie 过期后你又得手动换一次。正规项目里通常会配合登录接口来动态获取和更新 Cookie这个属于进阶话题这里先不展开但大家要记住复制粘贴 Cookie 只适合快速验证思路不适合长期稳定运行的爬虫。3.3 从复制到改造把调用链彻底理解透复制 cURL 也好转换代码也好都只是拿到一个“能用”的请求。要让请求稳定可维护我们必须搞清楚接口的调用链里发生了什么。我拿到一个目标接口之后通常会打开它的请求参数Payload 或 Params 标签页去看它提交了什么参数。常见的就是page页码列表接口的核心参数。limit或per_page每页数量有些站点限制了最大值比如最高只能填 60要根据实际观察调整。offset偏移量有些接口不用 page而用 offset/size 这套分页逻辑注意从 0 开始还是从 1 开始这是实战中最容易翻车的地方。sort、order排序方式。keyword搜索关键词如果是要做搜索爬虫这个参数就是核心。然后我还会关注返回 JSON 里的分页信息字段比如total总条目数、page_count总页数、has_more是否还有下一页。这些字段直接决定了爬虫循环得跑多少轮是控制请求次数、避免无谓请求的关键依据。理解清楚参数的逻辑之后你就能把一个“一次性请求”改造成“循环爬取所有页”的通用函数这也正是下一步要做的事。4. 用 Requests 重写接口请求从一行请求到一个健壮的爬虫函数这一步是整个教程的主体部分。我会带着大家把一个最简单的接口请求逐渐改造成一个能应对各种常见状况的稳定爬虫函数。4.1 第一版最朴素的接口请求长什么样假设我们找到了一个返回 JSON 的列表接口经过 cURL 转换和手动精简之后代码大概是这样的import requests url https://example.com/api/movie/list params { page: 1, limit: 20, } headers { 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, Referer: https://example.com/movie, } resp requests.get(url, paramsparams, headersheaders, timeout10) data resp.json() items data[data][list] for item in items: print(item[title], item[score])这段代码看着简单但它已经包含了四个关键要素接口地址、请求参数、请求头、超时设置。timeout10这个细节千万别省——没有超时设置的请求一旦服务器迟迟不响应你的程序就会一直挂在等 socket 响应的状态里爬虫批量跑起来的时候这种悬挂请求会像滚雪球一样越攒越多最后整个任务卡死。4.2 第二版把状态码和重定向逻辑纳入检查上面的代码有个问题如果接口返回了 404 或者 403代码仍会继续执行resp.json()然后抛出一个异常直接崩溃。我们写爬虫要假想各种意外情况都可能会发生所以推荐把响应检查前置。import requests def fetch_movie_list(page): url https://example.com/api/movie/list params {page: page, limit: 20} headers { User-Agent: Mozilla/5.0 ..., Referer: https://example.com/movie, } resp requests.get(url, paramsparams, headersheaders, timeout10) if resp.status_code ! 200: print(fpage {page} 请求失败状态码{resp.status_code}) return None data resp.json() return data[data][list]这里值得展开讲一下状态码的含义。200 是最理想的结果说明请求被正常处理并返回了数据。403 是服务器拒绝了请求常见原因包括 UA 被识别、Referer 校验不过、IP 被临时封禁。404 是接口路径不对很可能是 URL 写错了或者接口地址实际是拼接出来的动态路径。429 是请求频率太快触发了限流这时候最忌讳的操作是继续硬闯正确做法是停下来等一段时间后面“常见问题”章节会专门讲。还有一个很多人没注意到的问题重定向。有些接口尤其是用了负载均衡或 HTTPS 跳转的站点请求可能先返回 301/302Requests 默认会跟随重定向你最终拿到的是重定向之后的响应。绝大多数情况下这没问题但如果你发现打印出来的resp.url和你请求的 URL 不一样说明经历了重定向这时候要么优化参数比如把请求的 URL 改成最终的地址要么检查是不是被中间代理干预了。4.3 第三版加入分页循环与必要的数据清洗有了单页请求函数循环翻页就是水到渠成的事。但在设计循环时我要提醒一个新手非常容易犯的错误用 while True 死循环翻页直到返回空列表才停。这种做法在技术上是可行的但如果接口因为某个参数错误永远返回同样的数据你的循环就会从 1 跑到一百万次硬生生把 IP 请求到封禁。推荐做法是先通过返回的分页信息确定总页数老老实实地用for page in range(1, total_pages 1)遍历。import time import requests def fetch_all_movies(max_pages100): all_items [] for page in range(1, max_pages 1): items fetch_movie_list(page) if not items: break all_items.extend(items) # 控制请求频率 time.sleep(0.5) print(f第 {page} 页完成累计拿到 {len(all_items)} 条数据) return all_itemstime.sleep(0.5)这个细节千万不要删。很多站点不要求你快速返回数据反而更看重你是不是在“像人一样”地访问。间隔 0.5 秒是一个经验值实际速度可以根据站点的反爬强度来调整站点宽松就快一点严格就慢一点。拿到数据之后还有一个容易被忽略的步骤清洗。接口返回的 JSON 字段往往不是为爬虫准备的比如发布时间可能是时间戳格式如1700000000需要转换成可读的datetime对象有的字段带 HTML 标签像em热映/em需要正则剥离有的字段是 null可能会有不同含义无数据、未公布、不适用。在这个阶段把这些处理逻辑统一写好后面做数据分析的时候会省很大的力气。4.4 第四版异常重试与日志记录让爬虫活得更久爬虫跑起来以后真正消耗时间的是网络等待和异常处理而不是请求本身。网络是极不可靠的——DNS 抖动、服务器负载高、临时限流都会让你的请求偶发失败。一个稳健的爬虫必须对这些偶发问题有“自愈”能力。我常用的重试模式是这样的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_with_retry(): session requests.Session() retry Retry( total3, backoff_factor1, status_forcelist[500, 502, 503, 504], allowed_methods[GET], ) adapter HTTPAdapter(max_retriesretry) session.mount(https://, adapter) session.mount(http://, adapter) return session这里用了requests.Session它的好处是可以保持 TCP 连接复用多次请求同一个站点时不需要每次都重新握手建立连接能明显降低网络开销。Retry对象配置了最多重试 3 次、退避系数为 1 的指数退避策略——也就是说第一次失败后等 1 秒第二次失败后等 2 秒第三次等 4 秒。关键是status_forcelist只设置了 5xx 系列的服务器错误让程序不会在 4xx客户端错误上做无意义的重试。需要注意的是Requests 自带的重试机制对连接错误和 5xx 有效但对 429 状态码并不会自动处理。对于 429我一般会在业务代码里单独写等待逻辑这个放到“常见问题”一章专门讲。在重试之外日志记录也是保证爬虫可维护性的关键手段。我强烈建议不要用一堆看似整齐的print去记录过程而是用 Python 内置的logging模块。print 打出来的内容程序一关就没了而 logging 可以配置输出到文件file.log并附上时间戳、级别等元信息。等爬虫第二天早上跑崩了你打开日志文件一看“2024-06-01 03:22:15 - ERROR - page 87 请求失败”马上就能定位到问题发生的时刻和上下文而如果只有 print你只能在终端里翻屏找历史记录那叫一个痛苦。5. 实战案例用豆瓣 Top250 演示“回到接口”全流程理论讲了很多来一个完整的实战演练会更容易吸收。我选豆瓣电影 Top250 作为演示案例——它的动态加载机制非常典型接口地址清晰反爬门槛也比较适中非常适合练手。这里说明一下我们只演示技术流程用少量请求做学习验证爬虫项目请在遵守目标站点相关规则的前提下进行。打开豆瓣电影 Top250 页面按 F12 打开 Network 面板刷新页面你会看到页面的数据并不是在最初的 HTML 里而是在加载完成后由 JavaScript 发异步请求获取的。这个异步请求的接口地址形如https://movie.douban.com/j/chart/top_list?type5interval_id100:90actionstart0limit20观察这个 URL你就能猜出关键参数了——start是从第几条开始取limit是每页取多少条。再看下响应体是一份 JSON 数组每条记录包含了标题、评分、评分人数、排名等字段结构化程度极高。接下来我们按照前面讲的方法用 Requests 重写这个接口请求。import requests import time import logging logging.basicConfig(levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s) def fetch_top250(): base_url https://movie.douban.com/j/chart/top_list headers { 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, Referer: https://movie.douban.com/top250, } all_movies [] for start in range(0, 250, 20): params { type: 5, interval_id: 100:90, action: , start: start, limit: 20, } resp requests.get(base_url, headersheaders, paramsparams, timeout10) if resp.status_code ! 200: logging.error(fstart{start} 请求失败状态码 {resp.status_code}) break data resp.json() if not data: break all_movies.extend(data) logging.info(f已获取 {len(all_movies)} 条数据) time.sleep(1) return all_movies if __name__ __main__: movies fetch_top250() for m in movies[:5]: print(m[title], m[score])这段代码放在你自己的项目里稍加适配就能用在很多结构相似的动态站点列表页上。有个细节值得说明range(0, 250, 20)这个写法让 start 从 0 开始每轮增加 20直到拿到全部 250 条。这么做比 for 里直接累加更清晰也让接口的 offset 语义一目了然。还有一个贴心的小细节——我在这里的循环里写了if not data: break。有的接口超过数据末尾后不会报错而是返回空数组这时候如果你还在傻傻地往后发请求纯粹是在浪费资源并挑衅服务器的限流策略。空数据就是“该停了”的信号比任何逻辑判断都直接。整个案例跑一遍下来你会发现用接口的方式拿 250 条数据实际发送的请求只有 13 次左右而且每次请求加响应的时间都在几百毫秒以内。这个效率是任何浏览器渲染方案都无法比拟的。6. 常见问题与排查技巧实录从 429 到加密参数的对策实战过程从来不会一帆风顺。这篇文章涉及的场景里有一些重复率极高的问题我把自己踩过的坑和处理经验整理出来做成一个速查表大家对照着排查能省不少时间。6.1 状态码 429被限流了怎么办429 是“Too Many Requests”的意思服务器明确告诉你“你请求得太快了先冷静一会儿再来”。它和 403 的本质区别是403 是你的身份有问题UA 被识别、缺 Cookie 等而 429 是你的频率有问题。面对 429我见过最糟糕的处理方式是不停重试、加大并发结果触发更长的封锁周期。正确的应对策略是退避。我的做法是先停下来等 30-60 秒再尝试一个简单的探测请求比如请求首页不是数据接口。如果探测请求恢复了 200就从较小的频率重新开始爬比如每两秒一个请求。如果探测请求还是 429就把等待时间拉长到 5-10 分钟期间不要有任何对目标域名的请求。平时跑任务的时候给请求间隔留出余量不要卡着 1 秒一个的极限去跑。另外我记得有些服务端会在响应头里带上Retry-After字段告诉你具体需要等多少秒。这个字段是标准行为遇到 429 时可以先检查响应头里有没有它有的话按它说的等就对了。有一种特殊情况的 429 是“误伤”——因为你的出口 IP 是共享的网络出口比如公司内网、云服务器公共出口别的同事或同一个节点上的其他用户触发了限流你也跟着被牵连。这种情况下你无法从自身频率上解决问题只能等限流周期过去或者换一个出口环境。6.2 JSON 解析失败或返回内容不是 JSON 怎么办这是新手问得最多的问题之一。你用resp.json()解析时报错了说明响应体里的内容根本不是 JSON那它可能是什么最常见的一种情况是服务器返回的是一个 HTML 页面内容不是目标数据而是验证码页或“访问异常”提示页。你可以随便找个响应把内容打印出来print(resp.text[:500])看一眼如果是验证码页基本可以确定是触发了反爬。另一种常见情况是接口返回值被嵌套了一层 JSONP 包装比如callback({“data”: []})这时候不能直接用.json()得先从文本里把callback(和末尾的)剥掉再交给json.loads()解析。第三种情况是内容被压缩或者编码异常。Requests 一般会自动解码 gzip 和 deflate 压缩但如果你发现中文乱码可以检查一下resp.encoding设置为utf-8之后再访问resp.text或者直接用resp.content.decode(utf-8, errorsignore)。总结下来JSON 解析失败的排查顺序是先打印前 500 个字符看内容再判断是验证码页、JSONP 包装还是编码问题最后针对性地处理。不要一上来就去翻代码逻辑先看数据长什么样问题往往一眼就暴露了。6.3 参数中含有需要动态计算的加密值有些接口的请求参数里会包含sign、token、ts时间戳这类动态值这意味着你不能把参数写死而要在每次请求前动态生成。加密参数的来源一般有两种前端 JavaScript 动态生成网页里有一段 JS 代码根据时间和固定密钥算出一个签名拼在请求里。这种需要通过阅读混淆后的 JS 来还原算法属于典型的逆向工程。后端下发的临时 token先访问一次页面或一个初始化接口服务器返回一个 token后续请求带上它。这种相对友好只要把“先取 token再带 token 请求”的流程写进代码就行。我的建议是不要一看到加密参数就头大先静下心观察这次的签名是不是跟时间戳相关。如果签名的值在每次请求页面时都会变化但它和前一次页面里的某个值存在固定的对应关系那大概率是从初始化接口拿的问题就简化很多。如果确实遇到高强度混淆的算法那就要权衡一下投入产出比了——是自己逆向还是退一步找找有没有免签接口或者考虑降低频率只在少量必要页面使用。6.4 一份实用的问题速查表问题现象常见原因处理思路状态码 403UA 被识别 / Referer 校验不过 / Cookie 缺失补全 headers 里的 UA 和 Referer必要时带上登录后的 Cookie状态码 429请求频率过快停止重试按 30-60 秒退避检查 Retry-After 响应头状态码 500-504服务器端临时故障或负载过高配置指数退避重试最多重试 3-5 次后放弃该条请求JSON 解析报错返回了验证码页 / JSONP 包装 / 编码问题打印前 500 字符定位原因分别用 HTML 识别、剥 JSONP、调编码解决拿到的数据缺字段列表接口只返回摘要详情在另一个接口在 Network 面板里继续找详情接口做二次请求拼接接口参数是加密值前端签名 / 动态 token优先找初始化接口获取 token实在不行再考虑逆向 JS请求能通但数据不对参数类型错误或 offset 取值有误对照接口文档或抓包参数确认 start/page/offset 的起始值和语义程序卡住不往下走缺少 timeout 设置socket 悬挂等待为所有请求加上 timeout10并用 Session 复用连接遇到问题不要慌顺着表格里的思路一个一个排查。爬虫程序的调试本质就是“看反馈、调参数、看反馈”的循环绝大部分问题都能在这种循环里被解决。7. 从接口到工程的延伸Session、并发与数据落盘“回到接口”并不只是把单个请求写出来那么简单。当你的爬虫要爬的数据量变大就需要从“能用”走向“好用”。这一节分享几个我在实战里常用的工程化改造手法。7.1 Session 复用别让 TCP 握手拖慢你的爬虫前面提到过 Session 能复用 TCP 连接。这里展开说一下它的实际收益。当你用requests.get直接请求时每次都会走一遍完整的 HTTP 握手流程如果是 HTTPS 还要加上 TLS 握手。对于同一个站点的连续请求这种重复开销是非常可观的。而requests.Session()会把连接保存在连接池里后续请求直接复用已有连接速度提升非常明显。用起来也特别简单只需要在创建 Session 后把原来调用requests.get(...)的地方改成session.get(...)session requests.Session() session.headers.update({ User-Agent: Mozilla/5.0 ..., }) resp session.get(url, paramsparams, timeout10)Session 的另一个好处是它会自动帮你管理一部分 Cookie 状态。如果目标站点的接口需要持续的会话状态比如登录态Session 能保证同一会话内的请求始终带着上次响应的 Cookie。7.2 是否要上并发这是一个我经常被问到的问题“老师我用串行请求跑 1000 页太慢了能不能上并发”我的答案是可以但要谨慎。并发确实能大幅压缩爬取总时长比如 ThreadPoolExecutor 配合 5-10 个线程理论上就是 5-10 倍的速度提升。但并发也意味着短时间内请求密度骤增服务器那边限流策略的触发阈值很容易就被打到了结果就是 429 扑面而来。我的经验法则是对于需要登录、有较强反爬的站点尽量不要上并发保持串行加合理间隔对于公开数据、反爬宽松的站点可以用 3-5 个线程的轻并发并让每个线程在请求之间保持 0.5-1 秒的随机延迟。把最大并发数设定成“你愿意被永久封禁的风险等级”的正相关关系。说白了快不是目的稳才是。7.3 数据落盘JSON 文件还是数据库爬下来的数据总要存储。小批量学习项目直接写到 JSON 文件或者 CSV 就是最轻量的方案数据量上了几十万条以后建议落到 SQLite 或 MySQL。这里我推荐新手从 SQLite 起步——它不需要安装独立的数据库服务一个文件就是一个库Python 标准库sqlite3直接就能操作。每次请求完一页数据就增量写入数据库这样就算爬虫中途崩了已入库的数据也不会丢重启后还能接着断点续爬这是工程化爬虫的一个基本素养。8. 写在最后别把事情搞复杂回到接口本身就是最优解写这篇文章的初衷是想帮那些刚学会 Requests 基础、但面对动态站点不知道从何下手的读者找到一条更务实的路。我见过太多新手在动态站点面前第一反应是去学 Selenium学 Playwright然后在一堆“等待元素出现”“模拟点击”“处理弹窗”里消耗了大量时间。不是说这些工具不好而是杀鸡真的不用牛刀——动态站点的数据入口就是接口直接抓接口逻辑更简单、速度更快、稳定性更高。从我个人的实际体会来说“回到接口”的精髓不只是技术更是一种思维方式遇到看似复杂的问题先不要急着用更重的工具硬扛而是退一步把问题的本质看清。网页需要浏览器才能渲染是因为浏览器要执行 JS但 JS 执行完最终还是要和服务器对话。你绕开浏览器直接以服务器的“对话对象”身份出现反而更高效、更直接。最后再分享一个小技巧算是给这个系列的读者的额外礼物。如果你判断接口返回的 JSON 是规范的字段清晰、层级固定强烈建议你用Pydantic或者dataclass做一个数据模型层把接口返回的原始字典转换成即校验类型的结构化对象。这样做的好处是一旦接口字段变更这在实际项目中经常发生你的代码会立刻报错提示而不是等到数据写进数据库之后才发现整列都是空值。这种代码质量上的提前量能帮你在项目后期省下一大笔维护时间。希望这篇文章能帮你顺利跨过“动态站点爬虫”这道坎。如果大家在实操中遇到我这里没有覆盖到的问题也欢迎按自己的方式多试试——爬虫的乐趣本来就在那一次次的“排查-解决”循环里。
返回列表