做电商数据的人,应该对Shein(希音)不陌生。这家快时尚品牌这几年的增长速度有多夸张,行业里都有目共睹。而围绕希音的“web采集”需求,这两年也越来越多——有人想做价格监测,有人想跟踪新品上架节奏,有人想分析品类分布,还有人想给自家的选品系统喂数据。无论目的是什么,本质都是同一件事:把希音网站上的商品信息、价格变动、评价内容等公开数据,用程序化的方式持续地、结构化地抓取下来。
这篇文章就把我实际做过的希音web采集项目完整拆开讲一遍。从最开始的方案选型,到请求构造、数据解析、存储设计,再到跑起来之后遇到的各种坑,都会覆盖到。如果你正准备做类似的电商数据采集,或者已经在做了但遇到瓶颈,这篇应该能帮你省掉不少试错时间。
1. 项目整体思路:为什么绕不开Web采集这条路
很多人第一反应是找官方API。但希音并没有面向第三方开放商品数据接口,市面上能看到的数据服务商,要么是自己在持续采集,要么是走了某个灰色渠道。对于绝大多数需要数据的团队或个人来说,web采集是唯一现实可行的手段。
1.1 核心需求拆解:采集希音到底要拿什么数据
动手之前,先把需求定义清楚。不同业务方向,采集的侧重点完全不同。
最常见的几类需求是这样的:
- 价格监控与调价策略:需要采集商品详情页的售价、划线价、折扣率,按SKU维度记录价格变化历史。这类需求要求采集频率高、字段精确,对价格字段的实时性要求很严格。
- 新品上新跟踪:需要采集新品列表、上新时间、首图、销量趋势。这类需求更关注列表页和分类页,需要能识别“哪些是最近7天上架的”。
- 竞品品类分析:需要采集某一大类下的商品结构、价格带分布、属性分布。这类需求对单品的细节要求不高,但需要覆盖全,要能分页拉取完整列表。
- 评价与口碑分析:需要采集商品评论内容、评分分布、评论时间。这类需求对翻页和去重的要求很高,评论多时容易产生大量重复请求。
我做的这个项目,主打的是综合型采集,以价格监测为切入点,兼顾新品跟踪。所以核心目标就从“把商品页抓下来”变成了“把商品主键、SKU信息、价格体系、上下架状态这四类字段稳定地拿回来”。
需要明确一点:采集的目标不是把整个网站镜像下来。存储成本、带宽成本、解析成本都摆在那里,采集前先画清楚字段边界,能帮你省掉后面大量的麻烦。
1.2 方案选型:自建采集框架还是用现成爬虫工具
确定要采以后,下一个问题就是用什么实现。我见过有人用现成的图形化采集工具(比如后羿采集器、八爪鱼这类),也有人用Python自己写。两种方式各有各的适用场景。
图形化采集器的优势是上手快,不需要写代码,配置选择器之后就能跑。劣势也很明显:网站前端一改版,配置就得全部重新做;复杂的分页和动态加载处理起来很吃力;而且并发控制和反爬策略基本没有可调空间。
自己写采集程序,前期投入大一点,但后面所有环节都可控。请求频率、重试机制、代理切换、数据落库方式、增量更新的判断逻辑,全部按项目需求定制。对于一个长期要跑的数据采集项目,我强烈建议用代码实现。语言就选Python,生态最成熟,requests、parsel、pandas这些库随手就能用,后期如果要做数据分析也不用换语言。
框架层面,我没用Scrapy,直接上了requests + parsel + sqlite的组合。原因很简单:这个项目的目标站点相对单一,不需要Scrapy那种分布式调度能力;requests的代码路径更直接,调试起来心智负担低。如果你要采几十个网站,Scrapy有价值,但单一站点用轻量方案效率更高。
2. 采集环节的技术拆解:从请求构造到数据落库
这一部分是整个项目的核心。我会按实际工作流的顺序,把每个环节的做法和思考讲清楚。
2.1 页面分析与请求定位:搞清数据到底藏在哪
打开希音的官网,随便点进一个商品详情页,会看到页面结构非常复杂。图片懒加载、首屏直出、滚动动态加载、脚本动态渲染——看起来数据好像是前端Ajax请求动态拉取的对吧?其实不全是。
这里有个关键发现:希音的商品详情页,很多核心数据是直接内嵌在首屏HTML里的。具体来说,在页面的<script>标签里,有一段JSON数据,里面包含了商品的名称、价格、SKU属性、图片地址等信息。这意味着,只要拿到详情页的HTML源码,不需要执行任何JavaScript,就能解析出绝大部分关键字段。
这个发现对采集方案的影响是决定性的。它意味着:
- 不需要渲染JavaScript(也就省了Selenium、Playwright这类重型工具)
- 不需要处理接口签名和加密参数(这类动态请求通常会有token之类的校验)
- 直接对静态HTML做解析就能拿到结构化数据
所以,真正要做的工作被拆成了两步:第一步,定位到包含数据的那段JSON在HTML中的位置;第二步,写一个稳健的解析逻辑把它提取出来。
实际操作中,可以在浏览器里打开开发者工具,选Sources或者Elements面板,按关键词(比如价格字段、商品ID)搜索页面源码,很快就能定位到那段脚本。说白了就是找到数据所在的锚点,然后用正则或者parsel提取即可。
2.2 请求构造:Header、Cookie与基础参数处理
拿到页面之后,要构造出能正常返回200的请求。这一步有几个细节需要注意。
首先,请求头必须模拟真实浏览器的行为。UA(User-Agent)是基础中的基础,但光有UA还不够。我实测下来,以下请求头字段对希音站点的响应率有明显影响:
| Header字段 | 推荐值 | 说明 |
|---|---|---|
| User-Agent | Chrome最新版UA | 不可缺失 |
| Accept | text/html,application/xhtml+xml,... | 让服务端知道你要的是HTML |
| Accept-Language | zh-CN,zh;q=0.9,en;q=0.8 | 影响页面返回的语言版本 |
| Accept-Encoding | gzip, deflate, br | 服务端会返回压缩内容,需让requests自动解压 |
| Connection | keep-alive | 保持长连接,减少握手开销 |
| Referer | 对应分类页URL | 部分页面会校验来源页 |
Cookie的问题要单独聊一下。希音的商城分很多区域站点(美国站、欧洲站、中东站等),不同站点的语言、货币、商品价格体系都是独立的。首次请求时,服务端会种下区域识别相关的Cookie,后续请求带上这些Cookie才能返回正确区域的内容。
实践下来,最稳的方式是:用requests.Session()保持会话,先手动请求一次首页拿到初始Cookie,再带上这个Session去请求详情页。这样避免了自己手工拼接Cookie串的麻烦,也能让服务端认为你是连续操作的正常用户。
一个容易踩的误区:不要以为Cookie字符串固定不变就写死在代码里。希音的Cookie中有几个关键值是有时效性的,过期之后再用老Cookie请求,会被直接拒绝或强制跳转到验证页面。用Session自动管理Cookie,是最省心的方案。
2.3 数据解析:从HTML中稳定提取JSON数据
拿到HTML源码之后,解析策略非常关键。我采用的方法是:先定位包含数据的大块script片段,再从片段中抽离JSON,最后用json库解析成字典。
定位脚本片段的锚点,我在项目里用的是window.__PRELOADED_STATE__这个变量名(不同的站点版本变量名可能不同,需要自行确认)。经验是优先搜索类似__INITIAL_STATE__、__PRELOADED_STATE__、window.__data这些常见命名,哪个存在用哪个。
这里有个关键点:这段脚本里的JSON并不是严格的JSON格式。因为JavaScript的对象字面量允许key不带引号(虽然在现代代码中很少这么写),而且可能包含undefined这样的非JSON值。所以直接用json.loads解析大概率会失败。
我的处理方式是这样的:
- 先用正则把
window.__PRELOADED_STATE__ = {...};这段整体提取出来 - 用
demjson3或者json5这类宽松JSON解析库去解析,而不是直接用标准json库 - 解析失败时,用
html.unescape处理掉HTML实体(比如"、\u003d这类转义)
解析成功之后,数据是一个多层的嵌套字典。商品详情页里,核心字段通常藏在类似product、goods这样的子节点下。需要做的就是从嵌套结构中找到正确的路径,然后一层层取出来。
举个例子,价格字段在我当时采集的版本里路径大概是data.product.price.salePrice这样的结构,但不同站点版本有差异。笔者的建议是:第一次解析时,把整个JSON结构递归打印出来,人工查看一遍字段路径,然后再写提取逻辑。不要凭感觉猜路径,因为猜错的代价是后面要反复修。
2.4 数据清洗与字段映射:原始数据不能直接入库
从JSON里提取出来的数据,是网站前端的原始数据结构,直接入库是有问题的。原因有三个:字段命名不规范(前端喜欢用简写)、格式不统一(有的价格是字符串有的已经是数字)、存在大量冗余(很多渲染用的配置字段对分析完全没用)。
所以我在提取之后,加了一层清洗和映射逻辑。核心工作包括:
- 把前端字段名统一映射成自己业务定义的字段名(比如
prodId->product_id) - 把价格字段统一转换成
Decimal类型,避免浮点精度问题 - 把时间字段统一成
datetime格式,方便后续按时间排序、计算 - 把图片URL里的尺寸参数替换掉(希音的图片URL通常会带
_thumbnail之类的后缀,需要换成原图参数) - 把SKU列表展开成一行一行的SKU记录,而不是塞在一个字段里
这层清洗逻辑的价值在于,原始采集程序只需要管“抓取和解析”,业务数据层只需要管“消费和分析”,中间的转换规则独立维护,任一侧变更时另一侧不受影响。
3. 实操过程复盘:一次完整的采集任务是怎么跑通的
这一节我按实际项目的操作顺序,把从零到有、从单页面到批量跑通的过程完整复盘一遍。你跟着这个流程走一遍,基本就能跑通自己的采集。
3.1 环境准备:依赖清单与工程结构
我的Python环境是3.10版本。第三方库用得不多,核心就这几个:
pip install requests parsel json5 pandas openpyxlrequests:发HTTP请求parsel:解析HTML,底层是lxmljson5:解析宽松JSON(处理前端变量赋值时的非严格JSON)pandas + openpyxl:数据清洗和导出Excel用
工程结构上,我按功能模块拆分了文件,而不是把所有代码放在一个脚本里:
shein_crawler/ ├── config.py # 配置:目标URL、请求头、频率控制参数 ├── fetcher.py # 请求模块:负责发送请求、重试、错误处理 ├── parser.py # 解析模块:定位script块,提取JSON ├── cleaner.py # 清洗模块:字段映射、类型转换、去重 ├── storage.py # 存储模块:负责写入sqlite ├── scheduler.py # 调度模块:控制采集频率和增量更新 └── main.py # 入口:编排整个采集流程模块化的好处后面会体现得很明显。比如希音网站改版导致解析逻辑需要调整时,只需要改parser.py,其他模块完全不用动。如果你把全部逻辑都写在一个脚本里,改一处就要整体回归测试。
3.2 请求模块的代码实现:带重试和频率控制的请求封装
请求模块是整个采集程序的基石。我封装了一个带重试机制和限速控制的请求函数,核心逻辑如下:
import time import random import requests from requests.adapters import HTTPAdapter class Fetcher: def __init__(self, session=None, max_retries=3): self.session = session or requests.Session() # 挂载HTTP适配器,设置连接池大小和重试策略 adapter = HTTPAdapter(pool_connections=10, pool_maxsize=10, max_retries=max_retries) self.session.mount('https://', adapter) self.session.mount('http://', adapter) # 默认请求头 self.session.headers.update({ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', }) def get_html(self, url, params=None): for attempt in range(3): try: resp = self.session.get(url, params=params, timeout=15) if resp.status_code == 200: return resp.text elif resp.status_code in (403, 429): # 被限流或拒绝,等待后重试 wait_time = 10 * (attempt + 1) + random.uniform(1, 3) time.sleep(wait_time) continue else: resp.raise_for_status() except requests.RequestException as e: if attempt == 2: raise time.sleep(2 * (attempt + 1)) return None这里有几个设计点值得说明:
重试策略。网络请求不是每次都成功,超时、连接重置、服务端5xx都时有发生。重试次数设为3次,每次重试的等待时间递增,避免刚失败完立刻又去请求导致更大的压力。
频率控制。这个是重点。我见过很多人写采集程序,for循环里直接调请求,几百个页面几秒钟就发完了,结果IP很快被封。我项目里的节奏是:每两个请求之间至少随机间隔2到5秒。看起来好像很慢,但稳定性和持久性远比瞬时速度重要。一次性把IP打没,后面要等很久才能解封。
随机间隔的意义在于,固定间隔(比如每3秒一次)其实也是可以被规则识别的。用随机数把间隔打散,让请求时间序列更接近人为操作。
3.3 解析模块的代码实现:定位脚本块并提取JSON
解析模块的代码,核心是提取script块里的大JSON。这里的关键是要处理“非严格JSON”的问题。
import re import json5 from parsel import Selector class Parser: # 定义变量名到JSON的匹配模式 PATTERN_JSON = re.compile(r'window\.__PRELOADED_STATE__\s*=\s*(\{.*?\});', re.S) def parse_product(self, html): sel = Selector(text=html) scripts = sel.xpath('//script/text()').getall() json_text = None for script in scripts: # 优先找包含核心数据的script块 if '__PRELOADED_STATE__' in script: m = self.PATTERN_JSON.search(script) if m: json_text = m.group(1) break if not json_text: return None # 处理HTML实体转义 json_text = bytes(json_text, 'utf-8').decode('unicode_escape') # 用json5解析非严格JSON data = json5.loads(json_text) return data有几点要特别注意:
正则匹配的惰性匹配问题。上面示例里用(\{.*?\});是非贪婪模式,如果这段JSON里本身就包含};这种字符串,就会提前截断。实际项目中我一般不用正则直接匹配整个JSON,而是用字符串查找的方式定位起始位置和结束位置:起始位置是变量名后的第一个{,结束位置是与之配对的最后一个}。要写一个简单的括号计数器来配平。这样更稳。
decode的顺序。如果先做json5.loads再做unicode_escape,可能会出问题。先解码转义再解析JSON,顺序不能乱。
前端变量名的稳定性。希音的代码在改版时,有时候变量名会变。如果某天解析突然返回None,优先去页面源码里搜一下window.开头的变量,看看有没有改名。这个检查可以写成日志告警,方便第一时间发现。
3.4 数据存储与增量更新:SQLite起步,避免全量重复抓取
存储方案我用的是SQLite。项目数据量在几十万量级以内时,SQLite完全够用,还免去了部署维护数据库服务的负担。
表结构设计上,按业务需要拆了两个核心表:
CREATE TABLE products ( product_id TEXT PRIMARY KEY, title TEXT, category TEXT, sale_price DECIMAL(10,2), original_price DECIMAL(10,2), discount_rate DECIMAL(5,2), product_url TEXT, first_seen_at DATETIME, last_seen_at DATETIME, is_active INTEGER DEFAULT 1 ); CREATE TABLE price_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, product_id TEXT NOT NULL, sale_price DECIMAL(10,2), original_price DECIMAL(10,2), check_at DATETIME NOT NULL, UNIQUE(product_id, check_at) );增量更新逻辑是这样的:
products表存商品当前状态,product_id为主键,每次采集时INSERT OR REPLACEprice_history表记录的是每次价格检查的快照,用于追溯价格变化历史- 每次采集前,先从
products表里读出当前商品的product_id集合,再跟本次采集到的product_id做差集。差集里的商品置为is_active = 0,表示已下架。这样就能追踪上下架状态变化。
这个设计看起来简单,但实际用起来很有价值。比如做价格监控时,想知道某个商品最近一个月涨过几次价,直接查price_history表即可,不需要去第三方平台买数据。
4. 高频问题与排查记录:实测中踩过的坑
跑采集项目的时间一长,各种奇奇怪怪的问题都会冒出来。这一节把我在项目里实际遇到的高频问题整理成清单,并附上排查思路和解决方案。
4.1 频繁请求导致IP被限流怎么办
这个恐怕是做采集都会遇到的第一个坎。
现象是:程序跑着跑着,突然连续出现403状态码,或者页面返回的内容变成了一段验证JavaScript的代码(要求浏览器环境验证)。这说明服务端已经检测到你来自同一IP的访问频率异常。
我的处理方式分三层:
第一层:调低请求频率。把间隔从最短1秒调整到5秒以上,观察是否恢复。多数情况下,单纯降低频率就能让问题消失,前提是IP还没被拉黑。如果已经返回验证码,说明IP已经被标记了,要等待一段时间自然解封。
第二层:更换IP出口。如果业务对采集量有较高要求,单IP不够用,就需要引入代理IP池。这里要说明的是,今天的商用代理服务质量参差不齐,很多便宜的代理IP本身就属于高风险IP段,用上之后反而更容易触发风控。我的经验是:优先选住宅代理或者高质量数据中心代理,频率控制在每个IP每分钟不超过10次请求。
第三层:行为模拟。在请求序列中模拟人类操作的特征,比如:访问详情页之前先访问这个商品的列表页或分类页;请求间隔用随机分布;每天的访问时间段控制在北京时间白天到晚上。这些行为序列的模拟,能在一定程度上降低风控识别的概率。
但我必须提醒一句:任何绕开风控的手段都有合规边界。采集的初衷如果是价格监测、学术研究这类合理用途,就尽量保持低姿态;如果业务本身就对数据实时性要求极高,且需要绕过各种防护,那就需要认真评估法律风险。
4.2 解析出来的价格字段偶尔为空或异常
运行一段时间后会发现,极少数商品的价格字段解析出来是null或者0。去页面源码一看,发现这些商品要么是“已售罄”,要么是“即将上架”,页面里根本没有渲染价格节点。
这种情况要做的不是修解析逻辑,而是处理边界场景:
- 售罄商品:价格可能还在,也可能被替换成“补货提醒”之类的文本。采集时如果解析不到价格,就记录为null,同时标记商品状态为
out_of_stock - 预售商品:价格存在但可能显示的是预售价,需要在字段里增加一个
price_type标识 - 多规格商品:不同SKU的价格不同,页面展示的是价格区间(比如“$12.99 - $15.99”)。解析时如果遇到区间价格,需要把它拆成
min_price和max_price两个字段,而不是直接存字符串
这种字段级的数据质量细节,决定了你后面做分析时要不要花时间清数。我在项目里专门加了一层数据校验规则,凡是不符合预设类型和范围的数据,统一丢弃并写入错误日志,而不是让它混入主数据表。
4.3 站点改版导致解析逻辑失效怎么办
希音的站点更新频率不算低,前端布局一改,最脆弱的环节就是CSS选择器和JSON路径。
我遇到过一次典型情况:某天例行任务跑完后,检查日志发现成功解析率为0。排查后发现,页面里的变量名从__PRELOADED_STATE__改成了__NEXT_DATA__。改一行正则的匹配规则就恢复了。
这个事件给我的启发是:
- 一定要做采集成功率监控。每天任务跑完后,统计当天成功解析的商品数与预期数量的比值,低于阈值就告警
- 解析逻辑要与HTML解耦。不要在产品代码里到处散落着CSS选择器,而是收敛到专门的解析配置模块里
- 定期人工抽检页面源码。每周抽几个商品页,人工查看页面结构是否发生变化。这个动作花不了多少时间,但能让你在站点改版的早期就发现问题,而不是等到大量数据缺失才察觉
4.4 采集频率越来越慢,如何提升整体吞吐量
出现这种情况通常是因为增量更新逻辑没做好。很多初学者的做法是:每次全量抓一遍所有商品页。商品数量少时没问题,但当商品数量达到几十万级别后,全量抓取的耗时会从几十分钟逐渐变成几天,最终不可持续。
解决思路是分层调度:
- 高优先级商品(比如自己关注的竞品,或最近有调价的商品):每1~2小时抓一次
- 中优先级商品(正常在售商品):每6~12小时抓一次
- 低优先级商品(下架商品或长期无变化的商品):每24~48小时抓一次
这种分层的核心价值是:在有限的请求预算内,把采集资源集中在最有信息增量的事件上。价格变动通常发生在一段时间的集中调价期,新品上新集中在某几个时间点,这些都是高优先级的采集时机,不需要均匀地扫全量。
5. 合规与风险控制:采集项目的底线思考
做数据采集,绕不开合规这个话题。我现在做采集项目,第一步不是写代码,而是先把合规边界画清楚。
5.1 明确数据使用边界
希音网站上的商品数据,本质上是公开可浏览的信息,任何人都能在浏览器里看到。但“公开可见”不等于“可以随意使用”。用这些数据做价格监测、市场分析这类研究性用途,风险相对可控;但如果是大规模抓取后用于商业转售、用于训练竞对模型、或者在自营平台上直接搬运他人商品信息,法律风险就会显著上升。
在做项目之前,建议先想清楚这三个问题:
- 采集的数据是用于内部决策分析,还是对外提供数据服务?
- 采集的频率是否会影响目标网站的正常运营?
- 采集的数据是否包含个人信息(比如用户昵称、评论者的ID)?
第三个问题尤其敏感。个人信息有专门的法律保护体系,采集包含个人信息的数据需要格外谨慎。如果业务确实需要评论数据,建议做脱敏处理,只保留内容文本和评级,不保留用户标识。
5.2 技术上的风险控制策略
技术上,我习惯遵循几条底线:
- 单IP请求频率严格控制在人类浏览水平(每秒不超过1次)
- 避开业务高峰时段大批量采集(减少对目标站点的负载影响)
- 不尝试绕过登录、验证码等主动防护机制(这类行为性质完全不同)
- 不对目标网站发起超出正常浏览行为之外的请求花样(比如并发测试、接口探测等)
这几点做下来,采集项目能在相当长的时间里稳定运行,同时又不会给目标站点带来明显的负担。数据采集这个事,追求的不是“跑得多快”,而是“跑得多稳、能跑多久”。慢一点、稳一点,细水长流地持续更新,比短时间暴力抓取然后被封IP要高效得多。
5.3 数据质量的持续保障机制
最后再分享一个我认为每个采集项目都必须有的设计:数据质量看板。不需要很复杂,一个简单的统计脚本就够了。每天自动汇总以下指标:
| 指标 | 正常范围 | 触发预警的条件 |
|---|---|---|
| 商品详情页成功率 | 98%以上 | 低于95% |
| 新增商品数量 | 每日50~500不等 | 为0或异常突增10倍以上 |
| 价格变动商品数 | 每日10~200不等 | 为0或异常突增 |
| 下架商品数量 | 每日0~500不等 | 连续3天为0 |
| 解析空字段率 | 低于2% | 超过5% |
这些指标一旦有异动,第一时间去看日志定位问题,而不是等数据积累到不可收拾再返工。采集项目的日常维护,80%的工作量其实都是在处理“数据质量变差”这件事,剩下20%才是写新代码。
我实际跑了一段时间后最大的感受是:单次采集成功并不难,难的是让这个采集程序在一个月、一个季度、甚至更长时间里保持稳定输出。做到这一点的关键不在代码多高效,而在于你对这个站点的变化有没有感知、对数据质量有没有监控、对异常情况有没有预案。
如果你正准备做希音的web采集,建议先小范围地跑通一条商品链路的完整流程,确认字段解析稳定、存储逻辑无误之后,再逐步扩大商品覆盖范围。一上来就想全量抓,大概率会在早期被各种边界问题拖住进度。稳扎稳打,先跑通再跑宽,这个节奏对大多数采集项目来说都是最省力的。