
Tags: #Python #Amazon #爬虫 #API #选品
前言
亚马逊 Movers and Shakers(MnS)榜单每小时更新一次,记录各品类内 BSR 排名涨幅最大的商品。这份数据对跨境电商选品决策的价值极高,但要做到系统化、自动化地采集,有几道技术门槛需要跨越。
本文记录了我们从自建爬虫到接入 Pangolinfo Scrape API 的完整技术演进过程,包含核心代码实现和生产环境中遇到的真实问题。
TL;DR:
- 亚马逊 MnS 页面反爬机制强,自建爬虫稳定性差
- Pangolinfo Scrape API 提供 MnS 解析模板,直接输出结构化 JSON
- 本文提供完整可运行的 Python 采集 + 预警代码
- 覆盖接入方式、字段说明、性能优化和常见报错解法
技术背景:MnS 页面的反爬挑战
亚马逊 Movers and Shakers 数据在 https://www.amazon.com/gp/movers-and-shakers/{category_id} 路径下,DOM 结构复杂且存在动态渲染。自建 requests + BeautifulSoup 方案面临以下问题:
- 速率限制严格:同一 IP 高频访问会触发 503/CAPTCHA,普通住宅 IP 的安全频率约为每 10 分钟 1 次
- User-Agent 检测:亚马逊会检测 UA 字符串,需要定期更新浏览器指纹
- Cookie/Session 状态依赖:部分字段(如用户地区相关的价格)需要维持有效 Session
- HTML 结构变动频繁:MnS 页面在 A/B 测试期间结构变化快,解析规则需要频繁维护
我们早期自建的爬虫方案在全品类规模下的可用率只有约 73%,丢数据问题在高流量节点(Prime Day 前后)尤为严重。
解决方案:接入 Pangolinfo Scrape API
Pangolinfo Scrape API 针对亚马逊主要页面类型提供了预处理好的解析模板,MnS 模板输出的标准 JSON 包含以下核心字段:
json
{
"asin": "B09XK3K3T3",
"title": "PRODUCT TITLE HERE",
"current_rank": 47,
"previous_rank": 1847,
"rank_gain_pct": 3826.0,
"rank_gain_absolute": 1800,
"category": "Kitchen & Dining",
"category_id": "1055398",
"price": 24.99,
"rating": 4.3,
"review_count": 128,
"image_url": "https://...",
"listing_url": "https://www.amazon.com/dp/B09XK3K3T3",
"badge": "Best Seller",
"timestamp": "2026-04-22T08:00:00Z"
}
完整实现代码
1. 基础采集模块
python
"""
amazon_mns_collector.py
亚马逊 Movers and Shakers 数据采集模块
依赖: requests, python-dotenv
"""
import os
import requests
import time
import logging
from typing import List, Dict, Optional
from datetime import datetime
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s"
)
logger = logging.getLogger(__name__)
PANGOLINFO_API_KEY = os.environ.get("PANGOLINFO_API_KEY", "")
API_ENDPOINT = "https://api.pangolinfo.com/scrape"
# 亚马逊品类 ID 映射(部分常用品类)
CATEGORY_MAP = {
"1055398": "Kitchen & Dining",
"3375251": "Sports & Outdoors",
"1063498": "Home Storage",
"172282": "Electronics",
"284507": "Tools & Home Improvement",
"2619533011": "Health & Household",
}
def fetch_mns_page(
category_id: str,
locale: str = "us",
retry: int = 3,
retry_delay: float = 5.0
) -> Optional[List[Dict]]:
"""
采集指定品类的 MnS 榜单数据
Args:
category_id: 亚马逊品类节点 ID
locale: 站点区域(us/uk/de/jp/...)
retry: 失败重试次数
retry_delay: 重试间隔(秒)
Returns:
商品列表(含涨幅、ASIN 等字段),失败时返回 None
"""
if not PANGOLINFO_API_KEY:
raise ValueError("环境变量 PANGOLINFO_API_KEY 未设置")
payload = {
"url": f"https://www.amazon.{'co.uk' if locale == 'uk' else locale if locale != 'us' else 'com'}"
f"/gp/movers-and-shakers/{category_id}",
"parse_type": "movers_shakers",
"output_format": "json",
"locale": locale
}
headers = {
"Authorization": f"Bearer {PANGOLINFO_API_KEY}",
"Content-Type": "application/json"
}
for attempt in range(1, retry + 1):
try:
resp = requests.post(
API_ENDPOINT, json=payload, headers=headers, timeout=45
)
resp.raise_for_status()
data = resp.json()
items = data.get("items", [])
logger.info(
f"品类 {category_id} ({CATEGORY_MAP.get(category_id, 'Unknown')}): "
f"采集到 {len(items)} 条数据"
)
return items
except requests.HTTPError as e:
logger.warning(f"HTTP 错误 (尝试 {attempt}/{retry}): {e.response.status_code}")
if e.response.status_code == 429:
# 速率限制:等待更长时间
time.sleep(retry_delay * 3)
elif e.response.status_code >= 500:
time.sleep(retry_delay)
else:
break # 4xx 非 429:不重试
except requests.Timeout:
logger.warning(f"请求超时 (尝试 {attempt}/{retry})")
time.sleep(retry_delay)
except Exception as e:
logger.error(f"未知错误: {e}")
break
return None
2. 预警过滤与评分模块
python
"""
mns_alert_engine.py
涨幅预警与商业价值评分引擎
"""
from typing import List, Dict, Tuple
SPIKE_THRESHOLD = 1000.0 # 涨幅 > 1000% 触发预警
REVIEW_CAP = 300 # 评论数 < 300 视为切入窗口开放
MIN_RATING = 3.8 # 评分低于此值忽略(质量风险高)
def score_opportunity(item: Dict) -> float:
"""
对 MnS 涨幅商品打分(0-100分)
评分维度:涨幅强度 + 竞争壁垒低(评论少)+ 评分良好
"""
gain = item.get("rank_gain_pct", 0)
reviews = item.get("review_count", 9999)
rating = item.get("rating", 0)
# 涨幅分(最高 50 分)
gain_score = min(50, gain / 200)
# 竞争空间分(评论越少越高,最高 30 分)
if reviews < 50:
competition_score = 30
elif reviews < 150:
competition_score = 20
elif reviews < 300:
competition_score = 10
else:
competition_score = 0
# 评分质量分(最高 20 分)
rating_score = max(0, (rating - 3.0) / 2.0 * 20) if rating >= 3.0 else 0
return round(gain_score + competition_score + rating_score, 1)
def filter_and_rank_alerts(items: List[Dict]) -> List[Tuple[float, Dict]]:
"""
过滤预警商品并按机会分排序
返回: [(score, item), ...] 降序排列
"""
results = []
for item in items:
if item.get("rank_gain_pct", 0) < SPIKE_THRESHOLD:
continue
if item.get("rating", 0) < MIN_RATING and item.get("review_count", 0) > 20:
continue
score = score_opportunity(item)
results.append((score, item))
return sorted(results, key=lambda x: x[0], reverse=True)
3. 主运行入口
python
"""
main.py
MnS 监控主程序 - 每 30 分钟扫描一次所有目标品类
"""
import time
from datetime import datetime
from amazon_mns_collector import fetch_mns_page, CATEGORY_MAP
from mns_alert_engine import filter_and_rank_alerts
SCAN_INTERVAL = 1800 # 30 分钟
def format_alert_message(score: float, item: dict) -> str:
return (
f"⚡ [{score:.0f}分] ASIN: {item['asin']}\n"
f" 涨幅: +{item.get('rank_gain_pct', 0):.0f}% | "
f"当前 BSR: #{item.get('current_rank', '?')} | "
f"评论: {item.get('review_count', '?')} | "
f"评分: {item.get('rating', '?')}\n"
f" {item.get('title', '')[:50]}"
)
def main():
print(f"[{datetime.utcnow().strftime('%Y-%m-%d %H:%M UTC')}] "
f"MnS 监控启动,覆盖 {len(CATEGORY_MAP)} 个品类")
while True:
for cat_id, cat_name in CATEGORY_MAP.items():
items = fetch_mns_page(cat_id)
if not items:
print(f" ⚠️ {cat_name}: 无数据")
continue
alerts = filter_and_rank_alerts(items)
print(f" {cat_name}: {len(items)} 条 | "
f"{len(alerts)} 个预警")
for score, item in alerts[:3]: # 每品类最多输出前 3 名预警
print(format_alert_message(score, item))
time.sleep(2) # 品类间间隔
print(f"\n 下次扫描时间: {SCAN_INTERVAL // 60} 分钟后\n")
time.sleep(SCAN_INTERVAL)
if __name__ == "__main__":
main()
常见问题与解决方案
Q1: 返回 items 为空列表,但没有报错?
A: 通常是因为 category_id 格式有误或该品类暂无 MnS 数据(极小的子品类)。验证方法:直接在浏览器打开对应 URL,确认页面有内容后再调试 API 调用。
Q2: 某些品类的 review_count 字段返回 null?
A: 新品或评论数极少的商品(< 5条)亚马逊在 MnS 页面不展示评论数,null 是正常值,处理时需要做 or 0 的空值保护。
Q3: 如何支持非美国站点(如英国 Amazon UK)?
A: 修改 locale 参数为 uk,URL 中的域名会相应调整为 amazon.co.uk。Pangolinfo API 支持 us/uk/de/jp/ca 等主流站点。
Q4: 每次调用都采集 100 条 MnS 商品,想只拿前 20,有没有办法在 API 层面过滤?
A: 当前 API 返回完整的 Top 100,过滤逻辑需要在客户端实现。可以在 filter_and_rank_alerts 中加 items = items[:20] 切片。
性能优化建议
- 异步并发采集 :多品类采集可以用
asyncio+aiohttp重写,从顺序 60 秒减少到 10 秒以内(50 个品类场景) - 增量更新:用 Redis 缓存上次采集结果,只处理 ASIN 变动的部分,降低下游系统写入压力
- 预警去重:同一 ASIN 在连续 3 次扫描中都触发预警,只发送一次通知,避免飞书 / Slack 轰炸
参考资料
如果你在对接过程中遇到具体问题,欢迎在评论区留言,或者私信交流。