做二手生意的人,或者搞市场调研的人,多半都动过这样的念头:把平台上在售的商品信息批量抓下来,分析一下价格走势、上新节奏和热门品类。标题里的“咸鱼”按下不表,先把它当成一个专做C2C交易的二手闲置平台,全文统一叫它目标平台或二手闲置平台。这类程序的需求其实很清晰:定时抓取商品标题、价格、成色、发布地区、发布时间,存进自己的数据库,再做筛选和报表。可标题里那句“100%成功”着实把人带偏了,真实世界没有100%的成功率,但我们可以通过一套设计,把成功率从“碰运气”拉高到“工程化、可持续”。这篇文章就围绕一个目标平台的商品数据抓取程序,把方案设计、字段规划、具体代码、踩坑排查和合规边界一次讲完,适合刚接触爬虫的开发者,也适合想把自己内部采集脚本工程化的朋友。
1. 先把“100%成功”这个话掰开揉碎
1.1 任何爬虫项目都没有“一次成功”这回事
我见过不少项目需求,开头动不动就写“全自动”“100%成功”“稳定不封”,等到真跑起来才发现,这些词只能当愿望,不能当验收标准。商品数据抓取程序面对的是一个动态系统:页面结构会改版、接口参数会加签名、风控策略会升级、商品本身会下架、价格会调整,甚至用户所在地不同,看到的商品列表都不一样。只要任何一个环节发生变化,你昨天还跑得好好的脚本,今天就可能全部扑空。
所以我做的第一件事永远是转移预期:不保证100%成功,但保证每一类失败都有日志、有重试、有兜底。把“100%成功”翻译成工程语言,其实是“足够高的成功率 + 失败自动恢复 + 长时间稳定运行”。这个标准反而更能落地。真正成熟的采集程序,不是不出错,而是出错之后能自己爬起来,并且天亮之后你能从日志里定位到错在哪。
1.2 先回答三个问题:抓什么、干什么、怎么存
很多新手上来就问“用什么框架”,我反而会先问“你到底要分析什么”。同样是抓商品数据,需求不同,设计完全不同。比如有人想做选品分析,那他需要商品ID、标题、价格、销量、成色、地区、上架时间,还要保留历史价格;有人只想做价格提醒,那只需盯住少数几个商品ID,定时拉取价格,成本极低;还有人想监控同行上新,那就得高频扫描某个卖家的在售列表,这和选品全库抓取是两种任务。
我常建议用一个表格先把字段定下来。字段不是越多越好,够用就行。一般来说,商品数据抓取程序至少要包含这几类:商品唯一标识(ID)、标题、当前价格、原价或参考价、运费、成色/使用程度、所在地、发布时间、商品状态(在售/已下架)、关键图片URL。如果要做卖家维度分析,再加一个脱敏后的卖家标识,但不要碰手机号、IM号、聊天内容这些个人隐私字段。采集边界一旦划清楚,后面写解析代码、建表、清洗都会顺很多。
1.3 动手前,先做合规自查
这里的合规不是套话,是真的能帮你少惹麻烦。我自己接过一个模拟项目X,需求是监控某二手平台某类目下的价格分布。第一版脚本写得很快,但上线前我停下来查了三件事:目标平台是不是有官方开放接口、网站robots协议怎么写的、公开的商品数据里有没有个人信息。结果发现平台有面向开发者的开放数据通道,虽然接口覆盖的商品字段比页面少一些,但胜在稳定合法。最终方案改成了“官方接口为主、页面采集为辅”,既满足分析需要,又避开了大部分风控风险。
如果你确实只能做页面采集,那就记住几个原则:只采集公开可见的信息,绝不绕过登录权限,不批量下载图片,不把采集频率压到影响平台正常服务的程度,不把数据用于二次售卖或恶意营销。个人数据保护方面也要留个心眼,涉及个人昵称、信用分等字段,能不用就不用,不得不用就做脱敏。合规不是项目的阻碍,它其实是让项目能长期跑下去的护栏。
2. 数据字段和链路设计:别一上来就写爬虫
2.1 商品数据字段怎么定
我建议你动手敲代码之前,先在纸上把字段清单画出来。下面是基于常见二手交易场景整理的一份字段参考表,你可以按需增删。
| 字段 | 示例值 | 用途 | 注意事项 |
|---|---|---|---|
| 商品ID | 6382... | 去重和关联详情 | 要选稳定的唯一标识,不要用标题做ID |
| 标题 | 99新 大疆无人机 | 品类判断、关键词搜索 | 注意清洗换行和空格 |
| 当前价格 | 2599.00 | 价格分析 | 保留两位小数字符串转浮点数 |
| 原价/参考价 | 3999.00 | 折扣分析 | 没有时填NULL,别强制填0 |
| 运费 | 0.00 | 实际购买成本 | 有的商品卖家包邮,有的到付 |
| 成色 | 99新 | 二手商品核心特征 | 可能是枚举文本,统一映射 |
| 所在地 | 广东 深圳 | 地域分析 | 可能需要从省市区字符串里拆分 |
| 发布时间 | 2026-01-18 14:30 | 上新监测 | 注意时区 |
| 商品状态 | 在售/已下架/已售出 | 生命周期管理 | 每次采集都更新 |
| 图片URL | https://... | 留存缩略图 | 仅存URL,不主动下载 |
| 卖家标识 | masked_uid_123 | 卖家维度分析 | 必须脱敏,不能是原始ID |
| 抓取时间 | 2026-01-18 15:00 | 历史版本比对 | 每条记录都要有时间戳 |
这个表不是为了好看,而是为了告诉你每一份数据将来有什么用。比如“发布时间”这一项,很多二手商品是短期发布的,它能帮你看清楚平台每天真实上新在几点,也能辅助判断一个卖家是不是在频繁刷屏。
2.2 数据入口怎么选:列表页、详情页还是接口
目标平台给爬虫的信息入口通常有三个:搜索结果页、类目列表页、商品详情页。列表页请求数量少,一个页面能拿几十个商品的基础字段,适合做全量扫描,比如“这个平台现在一共有多少台某品牌的相机在卖”。详情页信息完整,但一个页面只有一件商品,适合对重点商品做精细追踪,比如你要盯住某个卖家的某台机器什么时候降价。
我自己的做法是分层采集:第一层用列表页抓“候选商品”,只拿商品ID、标题、价格、缩略图这几项;第二层再根据商品ID进详情页,补齐描述、成色、卖家信息。这样做的好处是,列表页变动频率高但单次成本低,即使列表页解析出错,也不会丢掉已经抓到的商品详情。如果平台有移动端或H5的JSON接口,数据质量普遍比解析HTML更稳定,可以优先评估,但要注意接口鉴权字段是否允许公开调用。总之一句话:能用公开接口就别碰页面解析,能用页面解析就别碰流量轰炸。
2.3 全量采集和增量更新怎么搭
一套商品数据抓取程序,最忌每次都是全量重来。全量抓一遍可能要几万次请求,既慢又容易被限流。更好的做法是把任务拆成“首轮全量 + 每日增量 + 定时详情刷新”。首轮全量用于把数据库从零填起来,之后每天的增量任务只抓“新发布的商品”,而已经入库的商品按各自生命周期单独调度。
增量更新的核心是“上次抓取时间”和“商品状态”两个字段。比如凌晨2点定时跑增量,只抓发布时间在24小时内的商品;而那些在售中的老商品,每6小时做一次价格刷新就行。这个设计能把请求量降一个数量级,也让数据可靠性大幅提高。我曾经把一个每天请求几十万次的脚本,改成这种分层调度后,日请求量降到三万以内,数据完整度反而提升了,因为没那么容易被频控了。
3. 实操:从请求到入库的完整过程
3.1 请求层的基本代码
不管用什么框架,请求层都要具备四样东西:超时设置、重试机制、退避策略、请求头管理。下面这段示例用的是Python的requests库,逻辑很简单,但已经能扛住大多数日常场景。
import time import requests HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/126.0 Safari/537.36" } def fetch_html(url, max_retry=3): for attempt in range(max_retry): try: resp = requests.get(url, headers=HEADERS, timeout=8) if resp.status_code == 200: return resp.text if resp.status_code in {403, 429}: # 遇到风控,先停下来降低频率 wait_time = 5 * (attempt + 1) time.sleep(wait_time) continue except requests.RequestException: time.sleep(2) return None这套代码看起来普通,但有两个容易被忽略的点。第一个是timeout必须设置,不设置的话一个卡死的连接可能拖垮整个调度任务。第二个是403、429不能立即重试,要按指数退避等待,等待时间可以加上随机抖动,比如1.5s、3s、6s,避免所有请求整齐划一地同时重试,反而触发更大规模的封禁。
3.2 页面解析和字段提取
请求拿到HTML之后,解析层最常用的还是CSS选择器或XPath。我习惯用parsel库,因为它的语法和Scrapy同源,将来迁移到异步爬虫时改动很小。
from parsel import Selector def parse_list_page(html): sel = Selector(text=html) items = [] for card in sel.css("div.item-card"): item_id = card.xpath("@data-id").get() title = card.css("a.title::text").get() price = card.css("span.price::text").get() location = card.css("span.location::text").get() items.append({ "item_id": item_id, "title": title.strip() if title else "", "price": parse_price(price), "location": location.strip() if location else "" }) return items这里我强调一个坑:不要直接用text()裸提取,因为前端模板改版后,原来的span.price可能变成div.price,你应该把“CSS选择器”和“字段名”做成一张配置表存起来,页面改版时只改配置不改代码。另外,价格字段常常会同“¥”或“包邮”混在一起,直接转float会报错,得先做一层清洗,比如把非数字字符去掉再转。
3.3 去重和入库
入库环节我推荐先建好数据库表,再把唯一索引加上去。商品ID就是天然的去重键,如果同一ID重复抓取,应该用INSERT ON CONFLICT或UPDATE而不是插入新行。
CREATE TABLE product_snapshot ( id INTEGER PRIMARY KEY AUTOINCREMENT, item_id TEXT NOT NULL UNIQUE, title TEXT, price REAL, location TEXT, status TEXT DEFAULT 'on_sale', first_seen_at DATETIME, last_seen_at DATETIME, UNIQUE(item_id, status) );去重不要只用item_id,还要加上状态维度。同一件商品今天在售,明天可能已售出,后天价格又变了。如果只留一条最新快照,历史变化就全丢了;如果每抓一次都插一条新纪录,数据表会膨胀得非常快。折中的方案是维护两张表:一张是“商品主表”,记录商品当前状态;一张是“价格历史表”,记录每次抓取时的价格和状态变化。分析价格走势时查历史表,做实时监控时查主表。
3.4 并发、限速和可观测性
很多人一提到提高效率就上多线程,这其实是把双刃剑。并发能提高抓取速度,但对目标平台来说,你就是一堆来自同一IP的密集请求,很容易触发风控。我在实际项目中会先用单线程跑一遍,计算出单次请求的耗时和成功率,再决定开多少并发。一般单IP下,每秒2到5个请求已经算比较克制了。
如果你要抓的数据量大,可以在代码里加一个简单的限流器,比如信号量加Interval控制。更重要的是一定要有日志和统计面板,每完成100个请求就打印一次成功率、平均耗时、失败状态码分布。没有这些指标,你会连“100%成功”到底怎么定义都说不清楚。我自己踩过最深的坑就是日志太糙,半夜程序挂了,第二天看日志只看到一堆空白,只能从头排查。
4. 常见问题和排查实录
4.1 403、429、连接超时怎么处理
这是我被问最多的一类问题。“昨天还能跑,今天全403”是常态。403通常是风控识别到了异常请求,429是明确的限流信号,连接超时可能是网络波动,也可能是程序并发开太大。我的处理套路是:先停掉所有任务,手动用浏览器访问一次目标页面,如果浏览器正常,说明对方没有封IP,问题出在请求头上;如果浏览器也访问不了,说明当前网络环境已受限,需要等一段时间再继续。
请求头那里,User-Agent是最基础的一层,别用默认的Python-requests,但也别完全伪造一个假的浏览器UA就万事大吉。有些平台会校验TLS指纹和浏览器行为,这类反爬手段不是改UA能解决的。我的建议很实际:遇到这种情况不要硬刚,降低频率、错峰采集、优先使用官方数据通道,比研究绕过方案稳妥得多,也安全得多。
4.2 商品数据是动态加载的怎么办
很多现代页面,商品列表不是写在HTML里的,而是浏览器先加载JS,再通过接口渲染。你用requests直接拿HTML,拿到的基本就是个空壳。这时候有两个思路:一是找到页面背后的JSON接口,直接请求接口,处理速度比解析HTML快得多;二是用无头浏览器自动化工具模拟浏览器渲染。
我优先推荐找JSON接口,虽然有时会遇到加密参数,但别去搞逆向破解,那属于高风险操作。更好的办法是看看接口是否包含公开的appkey或签名逻辑,如果没有公开权限,就踏踏实实用浏览器自动化。无头浏览器效率低,也有被识别出来的风险,但处理少量重点商品没问题。另外要强调一点:如果页面出现验证码,立刻停止程序。验证码是平台保护自己的一种方式,绕过它既不明智也有法律风险,换一个时间段再跑,往往就恢复了。
4.3 价格、库存、状态数据对不齐
二手商品数据最有意思的就是它的不稳定性。同一件商品,早上和晚上价格可能不一样;你上午抓到“在售”,下午可能已经卖掉了。如果程序只看当前页面,很容易把“已售出”误判成“抓取失败”。我遇到的典型场景是:商品详情页里已经没有价格了,但JSON接口里还会返回一个历史价格,这时候如果不做判断,就可能把一条失效数据当正常数据入库。
解决方法是在字段里增加status和captured_at两个字段,每次抓取都更新。对于状态异常的商品,不直接删除,而是标记为removed。这样你在做价格趋势分析时,可以按时间范围过滤“状态=在售”的数据;在做商品热度分析时,则可以统计哪些商品在上架后几小时内就变成了removed。数据不一致不是bug,它是商品真实生命周期的体现,用模型去承接它就好。
4.4 解析出来的字段乱码或缺失
字段缺失是家常便饭。有的卖家不填成色,有的商品没有原价,有的标题里全是表情符号和特殊字符。解析时最常见的报错是编码问题和类型转换问题。页面声明是UTF-8,但部分数据里混入了GBK编码,可能导致标题乱码。统一在解析后做一层清洗:先按文本规范化为UTF-8,再去除控制字符和多余的空白。
至于缺失字段,我建议给每个字段都设计一个“不可用”的默认值,比如空字符串、NULL或者-1。这样后续做统计时可以用IS NOT NULL过滤掉缺失项。千万不要在清洗时把空值填成0,否则价格分析时会多出一堆“0元商品”的假数据,这个坑我见过不止一次。
5. 合规边界和后续玩法
5.1 优先找官方口子
说句实在话,现在做正规的数据分析,第一选择绝对不是爬页面,而是检查目标平台有没有开放API、开放数据或官方合作渠道。以某二手平台为例,曾经开放过面向垂直行业的数据服务,比如废旧电子产品回收估价、二手行情指数,这些官方数据比你去抓几万条页面再自己算准确得多。即便是为了学习,也可以先把爬虫跑通在本地小规模环境里,只抓少量公开数据,不用于任何商业场景。
合规不是一句空泛的口号,而是每个实际决策里的成本计算。官方接口可能字段不够全,但有授权、稳定、不被封;页面采集看着自由,但你需要持续维护,还要承担风控和法律风险。小项目可以图省事,涉及商业决策的项目,我强烈建议先做合规评估。
5.2 数据落地之后怎么用
抓数据只是开始,真正有价值的是数据应用。这里举几个我见过的落地场景。第一个是二手商品价格带分析,把某类目的商品按价格区间分组,算出分布比例,你能看出哪个价位段竞争最激烈。第二个是地域供给分析,观察哪些城市的货源多、价格低,对做闲置回收的团队有直接参考价值。第三个是“秒下架”监控,很多热销商品上架后几小时就没了,这类商品往往代表真实需求很强,监控它们的标题和关键词,可以做选品参考。
这些应用都不需要特别复杂的算法,关键是要有持续稳定的数据源。程序能不能跑一个月不断档,比它单次跑得多快更重要。所以我在设计时会把数据库表结构、字段含义、采集日志全部写成文档,这样三个月以后回来看代码,还能知道每个字段是怎么来的。
5.3 我的两个小建议
第一,先小规模跑通再上量。我第一次做类似项目时,直接写了个50线程的并发脚本,跑完十分钟发现数据库里一半数据是重复的,浪费了大量带宽。正确做法是先抓100条数据写出完整链路,确认解析、清洗、入库都没问题,再把任务挂到定时调度上。第二,做好失败留痕。每次请求的URL、状态码、报错信息都要记录下来,这不仅是排查问题的依据,也是你评估“成功率”的唯一可信来源。
最后再分享一个技巧:给程序加一个“熔断开关”,当连续失败超过50条时自动暂停一小时,并且发一条通知到手机。这个开关拯救过我很多次半夜的睡眠。商品数据抓取程序说到底不是靠蛮力,而是靠对失败的管理。你把失败管理好了,成功率自然就会稳定在那种让人看了想说“100%”的水平。