ARTICLE DETAIL

资讯详情

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

动态站点抓取:用Requests直接拉取接口数据,不上Selenium更稳

动态站点抓取:用Requests直接拉取接口数据,不上Selenium更稳 上次实战里有人问过一个特别典型的问题“这个页面数据是JS加载出来的Requests直接抓HTML啥都没有那我是不是必须上Selenium了” 我当时的回答是先别急着开浏览器你要做的第一件事是回到接口。所谓动态站点数据根本不是藏在HTML里的而是后端通过一堆接口喂给前端JS再由JS渲染成你看到的页面。只要能识别出这些接口用 Requests 直接请求不仅逻辑干净抓取速度和稳定性还远胜于模拟浏览器。这篇文章写给那些刚学完Requests基础、想动手做点真东西的零基础读者也写给已经会用Selenium但觉得太笨重的朋友。我尽量把接口识别的套路、Requests重写的写法、以及踩过的坑一次性讲清楚代码都能直接抄看到最后你会发现动态站点的抓取没有想象中那么玄。1. 动态站点为什么值得“回到接口”1.1 页面是前后端分离的产物现在的Web站点尤其是一些内容型应用基本都采用了前后端分离的架构。后端服务负责提供数据把查询结果包装成JSON通过接口吐出去前端页面拿到JSON之后再用JS把数据组装成表格、卡片、列表渲染到浏览器里。说白了你在网页上看到的每一条记录十有八九都是从某个接口地址拉回来的浏览器里的界面只是个“显示器”。这就解释了一个新手常见的困惑用Requests去请求那个看起来十分正常的页面URL下载回来的HTML里全是JS脚本和空壳标签想要的产品名、价格、标题一个都搜不到。原因是HTML里根本没有数据数据在接口响应里。很多朋友在这个地方卡住然后就转去学Selenium觉得“既然要渲染那就让浏览器帮我渲染完再抓”。方向倒也不能说错但Selenium是把双刃剑。它能让你拿到完整的渲染结果也连带引入了浏览器驱动的安装问题、页面加载等待问题、无头模式的兼容性问题最麻烦的是它经常被反爬识别因为浏览器特征和正常用户访问有明显差异维护成本极高。而如果我们直接定位到背后的接口用Requests去请求拿到的就是纯净的JSON字符串解析简单响应极快而且天然绕开了“等待JS执行完成”这个复杂步骤这也就是标题里所说的“更稳”。1.2 Selenium 和 Requests 的取舍对比先放一个直观的对比表方便你理解为什么我更倾向于接口方案。下表是按我实际项目中的使用体验整理出来的合适的场景下Requests配合后文要说的接口识别方法优势非常明显。对比维度Selenium 方案Requests 直接请求接口响应速度需要等待浏览器启动、页面渲染通常数秒到数十秒毫秒级一个请求出去直接拿到JSON数据形态需要解析HTML结构数据混在标签中结构化JSON字段清晰直接用字典取值稳定性受浏览器版本、驱动、加载时间影响只要接口约定不变运行非常稳定反爬被识别风险浏览器指纹特征明显易被识别伪装请求头后与普通请求相似度更高学习曲线需要额外掌握WebDriver、等待策略等一个Session对象加一个请求方法就够了我第一回做这类站点时也老老实实开了Selenium结果遇到了一次反爬升级页面加载到一半弹出验证框驱动直接卡死重试三次才勉强抓到几十条数据。后来换成接口方案睡一觉起来数据早落库了。所以我的习惯逐渐变成能接口就接口Selenium只做兜底或者处理那些实在找不到接口的页面。2. 写代码前先把接口找出来2.1 打开开发者工具盯住 XHR / Fetch找到动态站点的数据接口不需要任何逆向功底浏览器自带的开发者工具就是最好的“透视图”。以Chrome为例按F12打开开发者工具切到Network网络面板然后注意面板上方有个过滤栏勾选Fetch/XHR这个操作能帮你把图片、CSS、字体等静态资源全部过滤掉。操作完再刷新一次页面你会看到网络请求列表里刷出来一批请求这些请求里就藏着数据的真实来源。它们通常有一个特点路径里带有api、ajax、data、list、search之类的关键词响应类型是JSON。逐个点开请求在Preview标签页里查看返回内容如果看到了页面上的数据比如书名、分类、价格恭喜你踏出最关键的一步了。这套方法我用了无数次了可以负责任地说绝大多数动态站点都逃不出这一步。哪怕是数据藏得很深的页面F12一开前端JS到底调用了什么接口一览无余。有学员问我有没有可能页面加载了但Network面板里什么都看不到有那通常是数据在WebSocket里推送的这种场景Requests方案确实不适用但比例不高先掌握常规接口分析的方法绝对不亏。2.2 从请求详情里完整抄下参数与请求头找到接口地址只是第一步真正决定你能不能复现请求的是对请求详情页的完整读取。继续在Network面板里点击该接口右侧会弹出详情面板需要关注几个TabHeaders请求头里除了Request URL也就是最终的接口地址还有Request Method一般是GET或POST以及一堆请求头字段。这些字段里User-Agent必须抄它就是你的“身份标识”后端经常用它判断你是不是真实浏览器Referer尽量带上它告诉服务端你是从哪个页面跳过来的没带Referer的请求在部分站点会被直接拒绝。Payload负载或Query String Parameters查询字符串参数里记录的是你传给接口的参数。GET请求通常参数拼在URL里的问号后面POST请求参数则在Payload里。这些参数一个都不能少少一个服务端就可能返回空数据或者报错。很多时候参数里还带着timestamp时间戳或者token令牌这类参数是站点做签名校验用的在第四章我会专门展开讲怎么处理。最后一个容易被忽略的入口是Copy as cURL功能。右键点击请求记录选择Copy——Copy as cURL(bash)复制出来的命令包含完整的URL、请求头、Cookie和参数。我在调试时经常用它来和Python代码做对照先在命令行跑一下这个curl命令能看到正常数据再用Requests按同样的内容重写一遍哪里不一致就改哪里效率非常高。3. 用 Requests 重写抓取逻辑3.1 先复现一次请求验证线路是否打通现在开始写代码。我强烈建议你不要上来就写完整爬虫而是先做一次最小化验证把刚才从Network面板里抄到的接口地址、请求头和参数用Requests原封不动发出去看看能不能拿到和浏览器里一样的数据。这一步能快速发现参数错误、请求头缺失、登录态失效等问题避免写一堆解析代码后才发现源头就不对。下面我用一个虚构的图书列表接口做演示这个接口模拟了一个典型动态站点页面渲染靠JS数据来自/api/books。你可以先照着结构换成自己遇到的真实地址import requests # 从Network面板抄到的接口地址 url https://example.com/api/books # 从请求详情里抄到的Headers headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36, Referer: https://example.com/books, Accept: application/json, text/plain, */*, } # GET请求参数通常直接拼在URL里 params { page: 1, size: 20, keyword: Python, } resp requests.get(url, headersheaders, paramsparams, timeout10) print(resp.status_code) # 200 表示服务器正常响应 print(resp.json()) # 直接把JSON转成Python字典这段代码跑通之后你会得到一个字典结构里面就是页面渲染所用的全部数据。这里我用了params参数而不是手动拼URLRequests会自动帮你把字典转成标准查询字符串还能处理一些特殊字符的转义比手拼字符串省心得多。3.2 解析 JSON 数据别再死磕 XPath上一章内容里不少朋友已经学了XPath用它去提取HTML中元素的text是很顺手的事情。可一旦进入动态站点的场景XPath就有些使不上劲了你要先在渲染后的HTML里定位容器标签再逐层找子元素页面结构稍微一调整路径就全断了。而接口返回的JSON呢数据是结构化的字段和数据天然对应用什么解析成本最低直接用response.json()转成Python字典然后按key取值即可。拿到JSON之后怎么找字段别靠猜先打印出来看。还是以上面的接口为例假设返回结构长这样{ code: 0, data: { total: 100, list: [ {id: 1, title: Python入门笔记, price: 59.0}, {id: 2, title: Requests实战手册, price: 49.5} ] } }那解析逻辑非常直白data resp.json() books data[data][list] for book in books: print(book[title], book[price])先通过data[data]进入嵌套字典再从list键下面拿到所有图书的数组循环里再取每条记录的字段。用生活类比的话JSON结构就像一个多层抽屉的柜子你只需要知道哪一层放着什么东西按标签一个个拉开就行。比起在HTML树里做“能爬就爬的逃生的XPath路径”体验好得不是一点半点。3.3 用 Session 保持登录状态和 Token很多接口看完了Headers发现还带着一长串Cookie或者Authorization字段。如果你每次请求都手动把这些值复制进代码第一次能用过一会儿Cookie过期代码就报废了。这时候你需要用requests.Session()来管理会话。Session对象的核心价值是“状态保持”登录后服务端下发的Cookie会被Session自动存储并在后续请求中自动携带。这比post之后手动拼接Cookie字符串优雅太多了。举个例子import requests session requests.Session() # 先登录拿到会话状态 login_data { username: your_name, password: your_password, } login_url https://example.com/api/login resp session.post(login_url, datalogin_data) print(resp.json()) # 后续请求自动带上Cookie books_url https://example.com/api/books resp session.get(books_url, headersheaders, timeout10) print(resp.status_code)登录成功之后服务端返回的Set-Cookie会被Session记住下一步请求/api/books时Session自动把Cookie放进请求头里跟浏览器登录后继续操作的行为是一致的。有些接口则是通过请求头里的Token鉴权那可以在Session上统一设置比如session.headers.update({Authorization: Bearer 你的token字符串})一旦在Session上更新了请求头后续所有通过该Session发出的请求都会自动带上这个头。我实际做项目时登录态基本都用Session管理特别是抓取需要分页获取大量数据时不用每条请求都重复处理Cookie代码会清爽很多。4. 接口加密参数与反爬识别的对策4.1 加密参数看得懂就重写看不懂就权衡上一章结尾提到有些接口的Query参数或Payload里会带着sign、token、timestamp这类字段。timestamp好理解就是时间戳常见是当前毫秒数sign则通常是把业务参数加上一个盐值或密钥用MD5、SHA1等算法算出来的签名。服务端收到请求后会按同样的规则重新算一遍签名如果和你传的不一样就直接判定为非法请求。碰到这种签名参数有没有办法走Requests方案有但完全取决于你能不能看懂签名算法。有的站点算法很简单就是把参数按字典序排序拼接成字符串再加个固定salt做MD5这种完全可以在Python里重写。难的是一上来就是一段几百行混淆过的JS让你逆向出真正的算法。这种我一般会权衡如果接口还有非加密版本可用或者数据量不大优先绕道如果确实有价值再考虑上JS逆向工具去执行那段JS但那已经是进阶路的深层问题。所以当你面对一个带加密参数的接口时我建议按下面的优先级来判断下一步先看有没有其他不需要签名的接口能拿到同样数据再尝试在Network面板里多翻几个请求确认签名是不是每次都在变化如果签名规则简单、可复现直接在Python里实现如果签名逻辑藏在重度混淆的JS里再评估要不要上Selenium而不是死磕Requests。任何时候都要记住回到接口是为了更稳而不是为了秀技术。如果接口这条路因为加密参数走不通换回渲染方案也完全不丢人。4.2 理解服务端的反爬视角既然要绕开反爬就得先了解反爬的开发者在想什么。前端防爬无外乎几个目标第一识别你“不是正常浏览器”第二限制你的请求速度第三让数据即使被拿到也需要额外破解成本。具体到实现上最常见的就是校验请求头、校验Referer、校验签名、限制IP频率以及在页面里植入各种检测脚本动态生成访问凭证。Requests本身的“马甲”非常简单默认的User-Agent是一段Python标识一出门就会被认出来。所以你不是被动挨打而是主动“穿着和浏览器一样的衣服”去请求。把Headers补成和浏览器一模一样把访问频率控制得接近人类操作速度配合Session维持一致性在绝大多数中低防站点上Requests方案依然能跑得又快又稳。4.3 别忽视验证码和动态Token这类硬门槛有必要提前说明的是回退接口也不是万能的。有些站点在请求链路的某个环节加上了行为验证比如滑块、点选、以及短时间内大量请求触发的动态验证码。这种验证码通常不是出现在接口里而是出现在请求中转时请求一频繁服务端就返回验证页让你必须通过验证才能继续拿数据。这类硬门槛下Requests不是不能处理而是处理成本高得离谱——除非你接打码平台或者本地跑识别模型否则基本要放弃纯代码方案。我的态度很明确不要为了“用Requests”而强行对抗高强度验证。真遇到这种情况划清边界先和业务方确认数据获取的合规方式远比在坑里死磕重要。毕竟爬虫的终极目标是拿数据不是跟网站对抗。5. 实战中常见的报错与排查清单5.1 “429 too many requests”不代表网络出错Requests方案跑起来之后遇到最多的一个报错大概就是429 Too Many Requests以及在重试机制下看到的exceeded retry limit, last status: 429 too many requests。我见过不少新手第一次碰到这个就直接判定网络问题换代理换IP折腾一下午其实是服务端在明确告诉你访问太频繁了。服务端限流的基本逻辑是时间窗口内计数超过阈值就拒绝服务。Requests默认是无限制地快速请求如果你在几秒内连续发了数百个请求触发限流几乎是必然的。这时候最直接的解决方式是人为控制频率也就是在每个请求之间加入延时import time for page in range(1, 11): resp session.get( https://example.com/api/books, params{page: page, size: 20}, headersheaders, timeout10, ) if resp.status_code 200: # 正常处理数据 pass else: print(fpage {page} failed: {resp.status_code}) time.sleep(2) # 每页之间停留2秒模拟人类操作节奏如果真的想做一个健壮的重试机制我推荐带退避的重试写法遇到429时等待更长的时间再重试而不是立刻重试。简单实现如下import time import requests def request_with_retry(session, url, max_retry3, **kwargs): for attempt in range(max_retry): resp session.get(url, **kwargs) if resp.status_code 200: return resp if resp.status_code 429 and attempt max_retry - 1: wait_time 2 ** (attempt 1) # 指数退避2秒、4秒、8秒 print(f触发限流等待{wait_time}秒后重试) time.sleep(wait_time) else: resp.raise_for_status() return resp这个函数的逻辑是第一轮请求如果429等2秒重试第二轮还429等4秒第三轮再429等8秒。这种策略能最大程度避免“频繁重试反而触发更严格封禁”的恶性循环。实测下来控制好频率以后绝大多数429都只是临时现象。5.2 返回的是 HTML 而不是 JSON还有一种情况比较迷惑人你请求的是接口地址但返回的内容是一整段HTML。这通常意味着服务端判断你的请求异常跳转到了验证页面或者错误提示页。排查方向有三个按可能性从高到低排列请求头不全尤其缺少User-Agent或者Referer服务端直接把你归为“非浏览器请求”缺少必要的Cookie接口要求先有页面访问记录、才能请求数据接口IP被限流或者进入了验证名单需要等待冷却或者更换网络出口。对应的处理办法也不难先把Copy as cURL里的所有请求头完整落到代码里再检查是否需要先用Session访问页面URL、让服务端下发初始Cookie最后才是考虑IP层面的问题。我见过九成“返回HTML”的问题用前两步就解决了。5.3 字段对不上解析时报KeyError接口返回正常状态码也是200但代码跑到一半报KeyError或者某些字段拿不到这说明接口返回结构和你预想的不一致。典型原因有两种一是接口更新了字段命名比如title改成了name二是某条数据本身就是空值对应字段被服务端省略了。解决方案不是加一堆if判断硬扛而是用.get()方法设置默认值。假设还是图书接口某本书可能没有price字段那取值时可以写成for book in books: title book.get(title, 未知标题) price book.get(price, 0)当字段缺失时程序不会因为KeyError崩溃而是用默认值顶上数据入库时再统一处理。另外我还有一个习惯是拿到接口响应后先保存一份原始JSON到本地比如raw_data.json出问题了用这份日志追溯结构变化非常方便。5.4 接口数据没变但页面变了这是一个特别容易忽略的坑接口还是那个接口但前端页面改版了。你看到页面布局、跳转逻辑、展示字段全变了于是以为代码要重写结果一查接口数据原封不动。这个经验很重要——页面是展示层接口是数据层两者是独立演化的。所以排查问题时优先确认接口返回是否正常再决定要不要动解析逻辑。维护爬虫时备份好接口返回样本就能快速定位到底是数据层变化还是展示层变化。5.5 常见问题速查表为了方便大家日常排查我把经常遇到的问题整理成一个速查表建议收藏现象报错或特征排查方向处理思路访问频繁被拒429 / exceeded retry limit请求频率过高加延时、指数退避重试接口返回HTML内容以!DOCTYPE html开头请求头缺失、Cookie不足补齐Headers、先用Session访问页面解析报错KeyError接口字段更新或空值省略用dict.get设置默认值登录态失效返回未登录提示或401Cookie过期、Token失效重新登录、使用Session维持会话接口找不到Network里请求全静态资源过滤选项没开对勾选Fetch/XHR后刷新请求返回空列表JSON正常但数据为空参数传错或签名校验不通过核对Payload参数、重算签名这个表格里的每一行都是我实际项目中遇到过并解决过的真实问题。我把它们单独拎出来讲是因为爬虫最大的风险不是写不出来而是跑一半突然崩了你不知道是反爬升级还是自己代码出错。有一套清晰的排查路数能帮你省下大量时间。爬虫这条路越往后走越会明白一个道理看清“数据从哪里来”比“怎么从页面里抠数据”重要得多。拿动态站点来说既然页面里的每一个字段都来自接口那我们干脆直接面对接口这反而是最省力也最稳定的方式。回退接口不是偷懒而是回归本质。跑了一两个实战项目后你再回头看上一章学的XPath、正则之类的内容会发现它们各有各的适用场景而Requests加接口的组合则是动态站点抓取场景里的常青方案。
返回列表