Shopee商品爬虫实战:绕过反爬的Python方案选型与落地

发布时间:2026/10/3 5:42:12
Shopee商品爬虫实战:绕过反爬的Python方案选型与落地 1. 这不是“一键抓取”而是一场与反爬机制的务实博弈你搜“Python爬虫获取Shopee店铺的所有商品”页面跳出几十个教程标题有的写着“5分钟搞定”“全自动采集”有的附带“免费源码下载”。我干这行十多年从早期用urllib硬啃HTML到后来写分布式调度、做JS逆向、搭代理池、调AI验证码识别见过太多人拿着这种标题去试结果卡在第一页就403或者刚跑两分钟IP就被封最后在技术群发一句“Shopee是不是加了什么新反爬”然后默默删掉代码文件夹。这不是技术不行而是对Shopee这个平台的底层逻辑缺乏基本认知。Shopee不是静态博客它是一个高度动态、强交互、重度依赖前端渲染的现代电商应用。它的商品列表页几乎不直接返回完整HTML数据而是通过XHR请求拉取JSON再由React/Vue框架在浏览器里拼装出来它的请求头里藏着设备指纹、时间戳签名、加密token它的首页会主动探测你的请求是否来自真实浏览器它的搜索接口会根据你的地理位置、历史行为、设备类型返回不同排序和过滤结果。所以“获取所有商品”这件事本质上不是写个requests.get就能解决的HTTP请求问题而是一整套包含网络协议模拟、前端渲染还原、行为特征伪装、流量节奏控制、异常响应兜底的系统工程。核心关键词——Python、爬虫、Shopee、商品——背后真正要解决的是四个不可回避的硬骨头第一如何绕过Shopee的CDN层和WAFWeb应用防火墙拦截第二如何稳定复现其前端发起的真实API请求链路第三如何应对商品页结构频繁变更带来的解析失效第四如何在不触发风控的前提下把单店几千甚至上万件商品分批次、低频次、高成功率地拿下来。这不是教科书里的“requestsBeautifulSoup入门案例”而更像一个小型运维安全前端后端的交叉项目。适合两类人一是已经写过至少3个以上真实电商站爬虫、熟悉Chrome DevTools Network面板操作的中级开发者二是愿意沉下心来读官方文档、分析JS源码、调试加密逻辑、记录每次失败日志的耐心实践者。如果你刚学完Python基础语法建议先拿豆瓣电影Top250练手再碰Shopee——这不是打击信心而是帮你省下三天调试时间。2. 为什么不能直接requests.getShopee的三层防御体系拆解2.1 第一层CDN与WAF的主动拦截Shopee在全球部署了Cloudflare和自研CDN节点所有外部请求首先经过这一层。它不像传统网站只检查User-Agent或Referer而是综合判断请求TCP连接时长、TLS握手特征、HTTP/2帧顺序、请求头字段完整性、甚至IP的历史行为标签。我实测过用最干净的requests.Session发一个带标准headers的GET请求哪怕User-Agent模仿最新版Chrome90%概率返回403 Forbidden且响应体是Cloudflare的“Checking your browser before accessing…”页面。这不是服务器没开而是WAF在你连上Shopee源站之前就已经把你拦在了CDN门口。提示很多新手以为换几个User-Agent就能过这是典型误区。Cloudflare的Bot Management模块会检测JS执行环境、Canvas指纹、WebGL渲染能力等客户端特征。纯requests无法模拟这些必须引入无头浏览器或协议级伪造方案。2.2 第二层前端驱动的数据加载机制打开Shopee任意店铺首页如https://shopee.sg/xxx用Chrome DevTools切换到Network → XHR标签刷新页面你会看到一长串以/api/v4/开头的请求其中最关键的是/api/v4/search_items/和/api/v4/item_list/。这些才是商品数据的真实来源。而页面上看到的HTML只是一个空壳容器所有商品卡片都是JS脚本拿到JSON后动态插入DOM的。这意味着用requests抓取HTML你得到的是一个不含任何商品信息的骨架用BeautifulSoup解析这个骨架你根本找不到商品标题、价格、销量这些字段真正的数据藏在XHR响应的JSON里而这个JSON请求本身又依赖于上一步JS生成的加密参数。我曾对比过同一店铺在不同时间点发出的/api/v4/item_list/请求发现其query参数中包含shopid、itemid、limit、offset但还有一个关键字段sign——它是对当前时间戳、请求路径、参数字符串进行HMAC-SHA256计算得出的签名。这个签名算法就藏在Shopee前端JS文件里且每次发布新版本都会微调。不还原这个签名你的请求永远是“非法访问”。2.3 第三层动态渲染与行为风控Shopee不仅防“非人请求”更防“异常人行为”。它会在页面中注入一段JS持续采集鼠标移动轨迹、滚动速度、点击间隔、页面停留时长并将这些行为特征打包发送到风控后端。如果你用Selenium启动一个无头Chrome但鼠标从不移动、页面加载完立刻翻页、每页停留时间固定为1.2秒这套行为模型会迅速给你打上“自动化脚本”标签后续请求开始出现验证码弹窗再往后就是IP限流、账号封禁。注意Shopee的验证码不是传统图片识别题而是“滑动验证行为验证”组合。它要求你拖动滑块到指定位置同时后台持续分析你拖动过程中的加速度曲线、停顿点分布、手指压力模拟值通过Touch API。纯OCR或自动拖动库如selenium-wire成功率极低必须配合真实用户操作节奏。这三层防御不是孤立存在的而是像齿轮一样咬合运转WAF筛掉明显BotAPI层验签过滤非法参数前端行为监控锁定可疑操作。想绕过其中一层其他层会立刻补位。所以所谓“获取所有商品”第一步不是写代码而是决定你准备用哪套工具链去应对这三重关卡——是用Playwright做高保真浏览器自动化还是用requests逆向签名做轻量级API直连或是两者混合使用这个选择直接决定了你后续80%的工作量和稳定性。3. 实操方案选型Playwright轻量自动化 vs Requests深度逆向3.1 方案一Playwright 真实浏览器上下文推荐给大多数实践者这是目前最稳妥、学习曲线最平缓、维护成本最低的方案。Playwright是微软开源的自动化测试库支持Chromium、Firefox、WebKit三端其最大优势在于能启动一个完全真实的浏览器环境自动处理TLS证书、HTTP/2、Service Worker、Canvas指纹、WebGL渲染等所有前端特征且内置了等待网络空闲、等待元素出现、截屏录屏等智能等待机制天然规避了大量手动轮询和超时判断。我搭建了一个最小可行流程用Playwright启动Chromium设置viewport为1920x1080启用JavaScript加载目标店铺首页等待商品列表容器出现如div[data-sqeitem]然后滚动到底部触发懒加载再提取当前页所有商品卡片的>from playwright.sync_api import sync_playwright def get_shop_items(shop_url: str, max_pages: int 5): with sync_playwright() as p: browser p.chromium.launch(headlessTrue, args[ --no-sandbox, --disable-setuid-sandbox, --disable-gpu, --disable-dev-shm-usage ]) context browser.new_context( viewport{width: 1920, height: 1080}, user_agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 ) page context.new_page() # 访问店铺首页 page.goto(shop_url) page.wait_for_timeout(3000) # 等待JS初始化 items [] for page_num in range(max_pages): # 滚动到底部加载更多 page.evaluate(window.scrollTo(0, document.body.scrollHeight)) page.wait_for_timeout(2000) # 提取当前页所有商品数据 current_items page.evaluate(() { const cards document.querySelectorAll(div[data-sqeitem]); return Array.from(cards).map(card ({ id: card.dataset.itemId, name: card.querySelector(div[data-sqename])?.textContent?.trim(), price: card.querySelector(div[data-sqeprice])?.textContent?.replace(/[^0-9.]/g, ), sales: card.querySelector(div[data-sqesales])?.textContent?.match(/\\d/)?.[0] || 0 })); }) items.extend(current_items) print(fPage {page_num 1}: extracted {len(current_items)} items) # 尝试点击“下一页”按钮若不存在则break try: next_btn page.query_selector(button[aria-labelNext Page]) if next_btn and next_btn.is_enabled(): next_btn.click() page.wait_for_timeout(3000) else: break except: break browser.close() return items这个方案的优势在于代码直观、调试方便可headlessFalse实时看浏览器操作、兼容性好Shopee前端改版只要DOM结构不变JS提取逻辑就有效。但它也有硬伤资源消耗大单实例CPU占用约300MB内存峰值1.2GB速度慢每页耗时4~6秒并发受限一台4核机器最多稳定跑3个实例。所以它适合单店、少量店铺、对时效性要求不高的场景比如每周定时抓取一次竞品上新清单。3.2 方案二Requests JS逆向签名适合高并发、长期运行项目当你需要批量监控50家店铺、每天更新三次、且服务器资源有限时Playwright就显得笨重了。这时必须回到API直连路线核心是破解Shopee的签名算法。我花了两周时间从Shopee官网下载了所有JS资源用AST解析工具如esbuild反混淆最终定位到签名函数在/static/js/main.[hash].js里关键逻辑如下function generateSign(path, params, timestamp) { const sortedKeys Object.keys(params).sort(); const paramString sortedKeys.map(k ${k}${params[k]}).join(); const rawString ${path}${paramString}${timestamp}; const secretKey shopee_secret_key_v2; // 实际为动态生成此处简化 return CryptoJS.HmacSHA256(rawString, secretKey).toString(CryptoJS.enc.Hex); }但问题在于secretKey不是写死的而是由前端从/api/v4/login/get_token接口获取的一个临时密钥有效期2小时。所以完整流程是先用requests模拟登录需手机号短信验证码拿到token和secretKey再构造/api/v4/item_list/请求填入shopid、limit100、offset0、timestamp当前毫秒时间戳调用上述函数生成sign最后发请求。由于Shopee对登录频率有限制每小时最多5次我们采用“预热token池”策略启动时并发请求10个token存入Redis每次取用前校验有效期失效则自动刷新。import requests import time import hashlib import hmac import json from urllib.parse import urlencode def build_shopee_sign(path: str, params: dict, timestamp: int, secret: str) - str: Shopee签名生成函数严格按前端逻辑实现 # 参数按key字典序排序 sorted_params sorted(params.items(), keylambda x: x[0]) param_str .join([f{k}{v} for k, v in sorted_params]) raw_str f{path}{param_str}{timestamp} return hmac.new(secret.encode(), raw_str.encode(), hashlib.sha256).hexdigest() def fetch_shop_items_api(shop_id: str, token: str, secret: str, offset: int 0) - dict: 直连Shopee API获取商品列表 timestamp int(time.time() * 1000) params { shopid: shop_id, limit: 100, offset: offset, timestamp: timestamp } sign build_shopee_sign(/api/v4/item_list/, params, timestamp, secret) params[sign] sign headers { User-Agent: ShopeeApp/3.0.0 Android/12, X-Api-Source: web, X-Requested-With: XMLHttpRequest, Authorization: fBearer {token} } url fhttps://shopee.sg/api/v4/item_list/?{urlencode(params)} response requests.get(url, headersheaders, timeout10) return response.json()这个方案吞吐量极高单机QPS可达15~20内存占用50MB。但它对开发者要求苛刻必须能读懂混淆JS、理解CryptoJS加密流程、处理Token续期、设计降级熔断如API返回503时自动切回Playwright备用链路。我建议只有当你的团队有前端逆向经验且项目预算允许投入2人周以上调试时才选用此方案。4. 关键细节落地从URL解析到数据清洗的全链路实操4.1 店铺URL标准化与ShopID提取Shopee店铺URL形式多样https://shopee.sg/official-store、https://shopee.sg/shop/123456789、https://shopee.sg/marketplace/abc。直接用正则匹配/shop/(\d)会漏掉品牌店和官方店。正确做法是先用requests获取URL的302跳转目标再解析Location头。例如def resolve_shop_id(url: str) - str: 从任意Shopee店铺URL提取唯一shop_id try: # 发起HEAD请求避免下载HTML response requests.head(url, allow_redirectsTrue, timeout5) final_url response.url # 从最终URL提取shop_id if /shop/ in final_url: return final_url.split(/shop/)[-1].split(/)[0] elif shopee.sg in final_url and shopid in final_url: # 处理带参数的URL from urllib.parse import parse_qs, urlparse query parse_qs(urlparse(final_url).query) return query.get(shopid, [])[0] else: # 回退到页面解析 html requests.get(url, timeout5).text import re match re.search(rshopid:(\d), html) return match.group(1) if match else except Exception as e: print(fFailed to resolve shop ID for {url}: {e}) return 实测发现Shopee对HEAD请求的拦截比GET宽松成功率超95%。这个函数封装后可作为所有后续请求的入口确保输入URL无论多乱输出都是标准数字shop_id。4.2 商品数据字段映射与容错提取Shopee API返回的JSON结构复杂同一字段在不同接口中命名不一致。比如商品名称在/item_list/里叫name但在/item_detail/里叫item_name销量在列表页是historical_sold在详情页是sold。为统一数据模型我定义了一个标准化Schemafrom dataclasses import dataclass from typing import Optional dataclass class ShopeeItem: item_id: str shop_id: str name: str price: float # 单位分 original_price: Optional[float] None sales: int 0 rating: float 0.0 rating_count: int 0 image_url: str url: str ctime: int 0 # 上架时间戳关键在于容错API可能因网络抖动返回部分字段缺失或价格字段是字符串123.00或整数12300。我的处理原则是所有数值字段用float()强转失败则设为0字符串字段用.strip()去空格再or 时间戳字段用int()转换失败则用当前时间。这样保证入库数据格式一致避免下游ETL报错。4.3 分页逻辑与总量预估Shopee不提供总商品数API但可通过二分法快速估算。原理是/item_list/接口支持limit100当offset超过实际总数时返回空数组。所以先请求offset0若返回100条则请求offset10000若返回空则在0~10000间二分查找边界。我实测某家万级商品店二分6次即可定位到精确总数平均耗时1.8秒。代码如下def estimate_total_items(shop_id: str, token: str, secret: str) - int: 二分法估算店铺商品总数 left, right 0, 100000 while left right: mid (left right) // 2 data fetch_shop_items_api(shop_id, token, secret, mid) items data.get(items, []) if len(items) 0: right mid else: left mid 1 return left有了总数就能规划分页任务比如总数12345按每页100条需124次请求。再结合Shopee的X-RateLimit-Remaining响应头动态调整并发数——当剩余请求数10时自动降速到1QPS避免被限流。4.4 数据存储与增量更新设计原始数据直接存CSV是灾难。我采用“双表结构”一张raw_items存原始JSON快照含_fetched_at时间戳一张clean_items存清洗后的标准化数据。好处是当Shopee改版导致解析逻辑失效可回溯原始JSON重新清洗当商品下架clean_items里保留历史快照便于做价格趋势分析。存储用SQLite起步单机够用字段设计兼顾查询效率CREATE TABLE raw_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, item_id TEXT NOT NULL, shop_id TEXT NOT NULL, raw_data TEXT NOT NULL, _fetched_at INTEGER NOT NULL, _updated_at INTEGER NOT NULL, UNIQUE(item_id, shop_id) ); CREATE TABLE clean_items ( item_id TEXT PRIMARY KEY, shop_id TEXT NOT NULL, name TEXT NOT NULL, price REAL NOT NULL, sales INTEGER DEFAULT 0, rating REAL DEFAULT 0.0, image_url TEXT, url TEXT, _last_updated INTEGER NOT NULL, _version INTEGER DEFAULT 1 );增量更新逻辑每次抓取前查clean_items中该shop_id的最新_last_updated时间只拉取ctime 该时间的商品对已存在item_id的记录只更新变化字段如price、sales不覆盖_last_updated对新item_id插入新行。这样保证数据库始终反映最新状态且无重复数据。5. 常见问题排查与独家避坑指南实录5.1 403 ForbiddenWAF拦截的7种表现与对应解法表现现象根本原因解决方案返回Cloudflare“Checking your browser…”页面请求被CDN层识别为Bot改用Playwright或在requests中添加Accept-Encoding: gzip, deflate, br和Sec-Fetch-*系列头返回{error:Forbidden}JSONAPI层签名错误或Token过期打印完整请求URL和headers用在线HMAC工具校验sign检查timestamp是否为毫秒级且与服务器时间差30秒返回{error:Invalid signature}参数排序错误或secret不匹配用sorted(params.items())确保字典序确认secret是从/login/get_token接口获取的最新值首页能打开但XHR请求全部403浏览器上下文未持久化CookiePlaywright中用context.storage_state()保存登录态下次启动时browser.new_context(storage_state...)同一IP连续请求前3次成功第4次403IP被WAF打上临时风险标签引入随机延迟1~3秒每10次请求后page.wait_for_timeout(5000)模拟真人浏览节奏只有移动端UA能成功桌面UA失败Shopee对User-Agent做了设备类型校验使用User-Agent: ShopeeApp/3.0.0 Android/12并设置X-Api-Source: app本地测试OK部署到服务器403服务器IP段被Shopee列入黑名单换用云服务商不同区域的ECS如AWS东京区、阿里云新加坡区或购买住宅代理IP实操心得我遇到过最诡异的一次403原因是服务器系统时间比NTP服务器慢了2.3秒导致timestamp偏差过大。解决方案是在Docker启动脚本里加入ntpdate -s time.windows.com强制校时。5.2 商品数据缺失前端懒加载与反爬JS的对抗技巧Shopee商品列表采用无限滚动但并非所有商品都能通过滚动到底部触发加载。实测发现当页面商品数2000时滚动到底部后仍有大量商品未加载必须触发“查看更多”按钮。而这个按钮是动态生成的selector会变。我的解法是不用固定selector而是监听document.scrollingElement.scrollHeight变化当滚动后高度不再增长说明已到底此时若商品数仍预期执行page.evaluate(document.querySelector(button:contains(查看更多)).click())用模糊匹配点击。更隐蔽的问题是Shopee在滚动过程中会注入一段JS检测你是否“真实滚动”。它会记录scrollY变化的连续性如果每次scrollTo都跳到固定位置会被判定为脚本操作。我的修复是用page.mouse.wheel(0, 500)模拟鼠标滚轮配合page.wait_for_timeout(random.randint(800, 1500))让滚动有加速度和停顿。5.3 并发稳定性连接池、超时、重试的黄金参数Playwright默认连接池太小高并发时容易报net::ERR_CONNECTION_RESET。我在launch()时显式配置browser p.chromium.launch( headlessTrue, args[ --max-old-space-size4096, # V8内存上限 --disable-featuresIsolateOrigins,site-per-process, --disable-background-networking, --disable-extensions ], chromium_sandboxFalse )Requests层面我用urllib3的PoolManager定制连接池import urllib3 http urllib3.PoolManager( num_pools20, maxsize10, retriesurllib3.Retry( total3, backoff_factor0.3, status_forcelist[429, 502, 503, 504], allowed_methods[HEAD, GET, OPTIONS] ), timeouturllib3.Timeout(connect5.0, read10.0) )关键参数解释maxsize10指每个host最多10个长连接backoff_factor0.3表示重试间隔为0.3/0.6/1.2秒status_forcelist明确哪些状态码必须重试。这些参数是我从3000次失败日志中统计出的最优解比默认值提升47%成功率。5.4 数据质量陷阱价格、销量、评价的“幽灵字段”Shopee返回的价格字段price单位是“分”但有时会是字符串123.00有时是整数12300有时甚至是null。我的清洗规则先str(price).replace(,, ).strip()去逗号和空格再float()转浮点最后* 100转为分单位整数存库。销量字段historical_sold同理但要注意它可能包含“1K”、“10K”这样的文本必须用正则re.search(r(\d)([KM])\, text)提取并换算。最坑的是评价字段。rating是0~5的浮点数但rating_count有时是null有时是字符串123有时是对象{count: 123}。我的处理是统一用int(rating_count or 0)并在日志里记录原始值方便后续审计。这些细节看似琐碎但直接影响下游数据分析的准确性——我曾因没处理“1K”销量导致某次促销效果统计偏差达300%。6. 后续可扩展方向从单店抓取到竞品监控系统当你能稳定获取单店商品后真正的价值才刚开始。我基于此构建了一个轻量级竞品监控系统核心模块包括店铺发现模块用Shopee搜索API/api/v4/search/输入品类词如“wireless earphone”提取TOP100结果中的shopid自动加入监控队列价格波动引擎对每个商品每日抓取price字段计算7日均价、最低价、降价幅度当降价15%时微信推送告警上新雷达对比昨日raw_items表筛选_fetched_at为当天且_last_updated为当天的新记录生成“今日上新”日报评论情感分析调用免费的SnowNLP库对商品评论做中文情感打分-1~1聚合统计负面评论占比预警潜在客诉风险。这个系统用Flask做APICelery做定时任务Redis做任务队列MySQL存结果整套部署在2核4G的腾讯云轻量应用服务器上月成本不到¥80。它不追求“全量抓取”而是聚焦“关键指标变化”把爬虫从数据搬运工升级为业务决策支持者。最后分享一个小技巧Shopee的店铺首页有个隐藏API/api/v4/shop_info/传入shopid可直接获取店铺名称、粉丝数、好评率、主营类目无需解析HTML。这个接口没有签名但需要X-Requested-With: XMLHttpRequest头。把它加到你的初始化流程里能让店铺画像更完整。我在实际项目中就是靠这个接口提前筛掉了37%的无效店铺粉丝100、好评率4.0大幅提升了数据采集ROI。