首页 / 资讯中心 / 文章详情

data-id数据指纹与游标式翻页:解决爬虫分页偏移

data-id数据指纹与游标式翻页:解决爬虫分页偏移 ★ FEATURED ARTICLE
1. 为什么说data-id是爬虫设计的“金线”先聊一个很实际的场景。你接到一个采集需求目标是一个带列表页的站点列表里每条数据长得差不多名称、缩略图、发布时间都能抓到但真正能唯一定位一条记录的标识往往不在肉眼看到的文本里而是藏在HTML标签的属性里。这种把业务主键塞进标签属性的做法行话叫属性数据埋点。最常见的就是data-id、data-key、data-sid这类自定义属性。站点前端JavaScript要靠它绑定事件、定位DOM节点后端对接时也要靠它回传状态所以它是整个页面逻辑里天然的“数据锚点”。我做爬虫这么多年一个很深的体会是文本字段再全都不如一个稳定可靠的ID来得金贵。文本会变格式会调字段可能缺失但data-id只要后端写入时逻辑不变它就一直是那条数据的身份证。而且它藏在DOM属性里比你从URL里猜参数、从文本里掐数字要精准得多绕开了不少正则表达式的脆弱环节。不过光会提取data-id还不够还会遇到一个比提取更隐蔽的坑——分页偏移。分页偏移的核心问题是你按页码或偏移量翻列表翻着翻着数据突然少了一段或者出现重复甚至越翻越乱。原因通常是列表页不是单纯按offset切的背后有排序算法在动态调整或者有推荐位、置顶位在插入“非分页序列”的数据。你以为是稳定的线性分页实际上每请求一次后端返回的集合都在悄悄变化。把data-id提取和分页偏移这两个点组合起来就是一套比较完整的精准爬虫设计思路用页面里埋的属性ID做数据指纹和去重基准用游标式翻页替代死板的页码offset翻页用幂等写入兜底重复请求带来的脏数据。这篇文章就按这个路线把我实际用下来比较顺手的方案完整拆一遍包括代码、参数取舍、坑点复盘适合正在做站点数据采集、列表页解析、或者想把自己的爬虫脚本升级得更稳的人参考。2. 数据埋点和分页偏移的底层逻辑拆解2.1 属性数据埋点是怎么工作的属性数据埋点本质上是在HTML标签上挂自定义属性供前端逻辑识别。标准做法是用>div classitem-card>session requests.Session() seen_ids load_seen_ids() # 从数据库或文件加载历史ID集合 cursor None # 游标替代页码 while True: resp fetch_list_page(session, cursor) page_docs extract_items(resp.text) new_docs [doc for doc in page_docs if doc[item_id] not in seen_ids] if not new_docs: break write_to_storage(new_docs) seen_ids.update([doc[item_id] for doc in new_docs]) cursor extract_next_cursor(resp.text) if not cursor: break sleep(interval)这套结构的核心变化是把“按页码翻”变成“按游标循环”。无论后端怎么偏移每次翻页都用服务端返回的下一个游标作为请求参数从机制上保证不会跳页。3.2 解析方式选择用对象化解析而不是正则硬切>from lxml import html def extract_items(html_text): tree html.fromstring(html_text) cards tree.cssselect(div.item-card) result [] for card in cards: item_id card.get(data-item-id) title card.cssselect(h3.item-title) if item_id is None: continue result.append({ item_id: item_id.strip(), title: title[0].text_content().strip() if title else , # ... 其他字段 }) return result3.3 游标管理的两种模式游标管理是实现精准分页的关键具体分两种模式。第一种是服务端显式返回游标。响应体里带nextCursor字段或者下一页的URL里带一个?cursorxxxx参数。这种最简单直接把游标透传给下一次请求即可。第二种是服务端只给ID序列需要自己推导游标。比如列表返回了30条数据全都带data-id下一页请求参数是?after上一页最后一条的id那游标就是当前页data-id列表的最后一个值。第二种模式要求对data-id的排序语义有预估如果列表按id倒序排列游标取当前页最小id如果按正序排列游标取当前页最大id。判断方法很简单看第一页和后续页id变化趋势写个小日志跑一轮就清楚了。我实际项目里遇到过一种恶心情况列表id乱序按插入时间排序但id不是严格递增。这时候用“最后一条id”做游标会丢数据稳妥做法是记录整页id集合下次翻页时带上限量或时间参数用“窗口推进”代替“单点游标”。3.4 幂等写入即使分析了也要防御架构上可以尽量避免重复抓取但网络请求是脆弱的你的脚本可能在某一次解析失败后重试导致同一页被反复处理。所以落库层必须做幂等。幂等的意思很简单同一条数据的id重复写入不会产生脏数据。实现方式是建表时给item_id加唯一约束写入用INSERT ... ON DUPLICATE KEY UPDATE或者“先查再插”。前一秒抓到的和上一轮抓到的最终落在表里的都是同一行。CREATE TABLE items ( item_id VARCHAR(64) PRIMARY KEY, title VARCHAR(255), price DECIMAL(10,2), crawled_at DATETIME );def write_to_storage(docs): for doc in docs: cursor.execute( INSERT INTO items (item_id, title, price, crawled_at) VALUES (%s, %s, %s, NOW()) ON DUPLICATE KEY UPDATE titleVALUES(title), priceVALUES(price) , (doc[item_id], doc[title], doc[price]) )这能兜住两类问题重复翻页抓到的相同id以及因为分页偏移造成的重复数据。只要id进不去重复内容就进不去。4. 实操全过程从页面分析到稳定跑数4.1 第一步页面异常分析和字段规划动手写代码前先花20分钟手工分析目标页面的DOM结构。打开浏览器开发者工具找到列表容器节点确认每个数据项的根节点class在根节点上找到data-id属性再看关键展示字段对应的选择器。把所有要抓的字段列成一张表标注好类型、是否必需、默认值。这一步很影响后续效率因为字段规划越清楚解析代码越少返工。我给一个快速上手的字段规划模板字段名来源属性/节点类型备注item_iddiv数据节点的data-idstring前导零不能丢保持字符串titleh3.titlestring用text_content()取完整文本pricespan.pricedecimal原样保留两位小数create_timetime.pubstringISO格式或时间戳按需转换4.2 第二步请求头与请求频率的工程化处理请求这块有两条硬经验。第一请求头不要照抄浏览器的全套字段很多站点反爬校验的就是Header顺序和缺失特征。但也不能不带头正常做法是带上User-Agent、Accept、Accept-Language这几个基本字段保持和常规浏览器一致。第二频率控制必须随机化。固定间隔1秒的请求模式比固定5秒更容易被识别。用一个小抖动函数让间隔在2到5秒之间随机浮动代码层面很简单import time import random def gentle_sleep(): time.sleep(random.uniform(2.0, 5.0))如果数据量很大建议加一个简单的令牌桶限速控制每秒请求数不超过阈值避免因为自己请求过快导致IP被临时限制。4.3 第三步完整采集循环的落地代码把前面几个模块组装起来就是一个可以直接跑起来的最小实现import requests import random import time from lxml import html import mysql.connector HEADERS { User-Agent: Mozilla/5.0 (compatible;>MAX_PAGES 1000 if page_no MAX_PAGES: break校验方面采集结束后统计一下数据总条数、唯一id数、最大最小id区间。如果数据量比自己预估少了30%大概率有隐藏的过滤条件没有处理到位这时候回头看页面是否有“只看有货”“只看特价”这类隐形筛选。5. 常见问题与排查技巧实录5.1 问题速查表现象最可能的原因排查与解决办法越翻页数据越少插入型广告位占用了分页槽位改按下一页URL的显式参数翻页或者从流量日志反推每页真实条数存在大量重复id排序动态变化导致跨页重复上指纹去重配合幂等写入同时考虑改游标模式>python -c import requests from parse_script import extract_items resp requests.get(https://api.example.com/items?limit3, headersHEADERS) docs extract_items(resp.text) print(len(docs)) print(docs[:5]) 如果result长度是3说明解析逻辑和页面结构匹配如果是0或者报错多半是选择器层级不对回到浏览器开发者工具重新确认class名。另外一个技巧是直接在浏览器控制台跑document.querySelectorAll(div.item-card)数一下条数再对比脚本输出。浏览器解析出来的DOM结构和爬虫用lxml解析出来的不完全一样但节点数量和data-id取值应该完全一致这是定位解析问题的分界线。5.4 关于增量采集的落地建议最后说增量采集。全量采集跑完不是终点业务上通常需要每天更新。增量设计有两个思路。思路一是“断点续抓”以已采集的数据id作为截止条件翻到第一个全部存在于库里的页就停。思路二是“时间增量”如果列表页支持按时间筛选直接带updated_from昨天0点的参数只抓当天更新的数据。我一般优先用思路二因为更省流量而且不太依赖游标模式的稳定性。但前提是站点列表页支持时间过滤参数不支持的话退回思路一配合幂等写入效果也够用。增量任务还要注意处理“下架数据”。有些站点更新后会把部分商品下架列表页不再返回业务上可能是库存清空也可能是彻底删除。建议单独做一张状态表每天记录当日可见的id集合和昨天的集合做差集差集部分标记为下架。这张表不直接影响采集主流程但对业务对账非常有帮助。结尾这套基于data-id提取加游标分页的方案我在好几个采集项目里都跑过最深的感受是爬虫设计里真正难的不是发请求和解析HTML而是把“数据指纹”和“翻页机制”这两个地基打牢。指纹不稳去重和增量都无从谈起翻页机制没看透采集量越大脏数据越多。最后再分享一个容易被忽略的小技巧写完采集脚本后在数据库里跑一条简单的统计SQL按id长度分布、数据更新时间分布、来源页号分组各看一眼。有时候数据量的异常不在代码报错里而在分布形态上这些例行检查能救你于无声无息的数据污染。希望这篇文章能帮你少踩几个我踩过的坑。
阅读完成 · 觉得有帮助?
咨询建站