前言

亚马逊SP广告投放有一个被普遍忽视的信息不对称问题:你能看到自己的广告位表现数据(还有12-24小时延迟),但完全看不到竞品在同一搜索页面上的广告布局——他们在Top of Search出现的频率、什么时段加大投入、大促前后如何调整策略。
本文提供一套从数据采集到自动告警的完整实现方案,核心依赖:
- Pangolinfo Amazon Scraper API:98% SP广告位采集成功率(行业最高)
- Python + schedule:定时任务自动化
- 飞书Webhook:告警推送
TL;DR:每小时采集核心关键词的搜索页面 → 解析TOS/RoS广告位分布 → 与历史数据对比 → 竞品TOS份额超阈值则推送告警 → 每日生成日报
一、为什么需要实时监控广告位?
1.1 广告后台数据的三大结构性问题
问题一:12-24小时延迟
Amazon Advertising Console的Placement Report有固定的数据延迟——这是系统架构决定的,不可绕过。在Prime Day等高烈度竞价时段,CPC可以在几小时内飙升30-50%,而这个信号你只能在次日的报告里看到。
问题二:只有自己的数据,看不到竞品
Placement Report展示的是你账户内广告的位置表现,完全不涉及竞品在同一关键词下的广告布局。这就像打牌只看自己的手牌,不知道对手出了什么。
问题三:无法批量追踪多关键词的动态变化
大多数广告SaaS工具的关键词追踪配额有限,刷新频率也无法达到分钟级或小时级。
1.2 技术挑战:为什么广告位采集成功率普遍不高?
亚马逊的广告模块不是随页面HTML一起加载的——它通过独立的JavaScript异步请求注入,注入时机还受浏览器环境的行为特征影响。
普通Python爬虫(requests + BeautifulSoup)在亚马逊搜索页面上根本看不到广告位,因为解析HTML时广告还没有加载。需要一个完整的JS渲染环境,还要通过亚马逊的行为信任评分检测。这是为什么大多数自建爬虫在广告位采集上成功率不到50%的核心原因。
Pangolinfo的Amazon Scraper API针对这个问题专门优化了渲染等待策略和请求工程,SP广告位采集成功率达到98%,在每日30M+次调用的生产规模下持续验证。
二、系统架构设计
┌─────────────────────────────────────────────────────────────┐
│ 定时调度层(Cron / schedule) │
│ 每小时触发核心关键词广告位采集 │
└────────────────────────┬────────────────────────────────────┘
│
┌────────────────────────▼────────────────────────────────────┐
│ 数据采集层(Pangolinfo Scraper API) │
│ 实时采集亚马逊SERP → JS渲染 → 提取SP/SB/SBV广告位数据 │
└────────────────────────┬────────────────────────────────────┘
│
┌────────────────────────▼────────────────────────────────────┐
│ 数据解析层(Python) │
│ TOS/RoS分类 → 竞品品牌识别 → TOS份额计算 → 历史数据对比 │
└────────────────────────┬────────────────────────────────────┘
│
┌────────────────────────▼────────────────────────────────────┐
│ 告警与报告层(飞书/Slack Webhook) │
│ 竞品TOS份额>阈值 → 即时告警 | 每日生成广告位日报 │
└─────────────────────────────────────────────────────────────┘
三、完整代码实现
3.1 核心数据采集模块
#!/usr/bin/env python3
"""
亚马逊搜索页面广告位置实时监控系统
依赖:requests, schedule, json, sqlite3(可选,用于历史数据存储)
"""
import requests
import json
import sqlite3
import schedule
import time
import logging
from datetime import datetime
from dataclasses import dataclass, asdict
from typing import List, Optional, Dict
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
@dataclass
class AdPosition:
"""单个广告位数据结构"""
position: int # 页面位置序号
placement_type: str # top_of_search / rest_of_search / pdp
asin: str
ad_type: str # SP / SB / SBV / SBA
brand_name: str
title: str
price: str
rating: float
review_count: int
is_prime: bool
@dataclass
class SerpAdAnalysis:
"""单次SERP广告位分析结果"""
keyword: str
timestamp: str
tos_ads: List[AdPosition]
ros_ads: List[AdPosition]
tos_count: int
ros_count: int
total_ad_count: int
ad_density_pct: float
brands_in_tos: List[str]
competitor_tos_count: int
competitor_tos_share_pct: float
class AmazonAdPositionMonitor:
"""亚马逊搜索页面广告位置实时监控器"""
BASE_URL = "https://api.pangolinfo.com/v1"
def __init__(
self,
api_key: str,
target_brands: List[str],
db_path: str = "ad_positions.db"
):
self.api_key = api_key
self.target_brands = [b.lower() for b in target_brands]
self.session = requests.Session()
self.session.headers.update({"Authorization": f"Bearer {api_key}"})
self.db_path = db_path
self._init_db()
def _init_db(self):
"""初始化SQLite数据库(存储历史数据用于趋势对比)"""
conn = sqlite3.connect(self.db_path)
conn.execute("""
CREATE TABLE IF NOT EXISTS ad_position_snapshots (
id INTEGER PRIMARY KEY AUTOINCREMENT,
keyword TEXT NOT NULL,
timestamp TEXT NOT NULL,
tos_count INTEGER,
ros_count INTEGER,
ad_density_pct REAL,
competitor_tos_count INTEGER,
competitor_tos_share_pct REAL,
brands_in_tos TEXT, -- JSON array
raw_data TEXT -- Full JSON snapshot
)
""")
conn.commit()
conn.close()
def fetch_serp(
self,
keyword: str,
zip_code: str = "10001", # 纽约,避免数据中心IP地区偏差
marketplace: str = "amazon.com"
) -> dict:
"""
采集亚马逊关键词搜索页面
render=True 开启JS渲染(广告位必须的)
extract_ads=True 专门提取广告位数据
"""
url = f"https://www.{marketplace}/s?k={keyword.replace(' ', '+')}&ref=nb_sb_noss"
payload = {
"url": url,
"render": True,
"extract": "structured",
"extract_ads": True,
"country": "US",
"zip_code": zip_code
}
for attempt in range(3):
try:
resp = self.session.post(
f"{self.BASE_URL}/amazon/search",
json=payload,
timeout=45
)
resp.raise_for_status()
return resp.json()
except requests.RequestException as e:
logger.warning(f"采集失败(第{attempt+1}次): {e}")
if attempt < 2:
time.sleep(2 ** attempt)
return {}
def parse_ad_positions(
self,
serp_data: dict,
keyword: str
) -> SerpAdAnalysis:
"""解析SERP数据,提取广告位分布信息"""
raw_ads = serp_data.get("ads", [])
all_results = serp_data.get("results", [])
tos_ads = []
ros_ads = []
for ad in raw_ads:
pos = AdPosition(
position=ad.get("position", 0),
placement_type=ad.get("placement", "rest_of_search"),
asin=ad.get("asin", ""),
ad_type=ad.get("ad_type", "SP"),
brand_name=ad.get("brand_name", ""),
title=ad.get("title", "")[:80],
price=ad.get("price", ""),
rating=float(ad.get("rating", 0)),
review_count=int(ad.get("review_count", 0)),
is_prime=ad.get("is_prime", False)
)
if ad.get("placement") == "top_of_search" or ad.get("position", 99) <= 4:
tos_ads.append(pos)
else:
ros_ads.append(pos)
# 竞品TOS分析
competitor_tos = [
ad for ad in tos_ads
if any(brand in ad.brand_name.lower() for brand in self.target_brands)
]
brands_in_tos = list({ad.brand_name for ad in tos_ads if ad.brand_name})
return SerpAdAnalysis(
keyword=keyword,
timestamp=datetime.now().isoformat(),
tos_ads=tos_ads,
ros_ads=ros_ads,
tos_count=len(tos_ads),
ros_count=len(ros_ads),
total_ad_count=len(raw_ads),
ad_density_pct=round(len(raw_ads) / max(len(all_results), 1) * 100, 1),
brands_in_tos=brands_in_tos,
competitor_tos_count=len(competitor_tos),
competitor_tos_share_pct=round(
len(competitor_tos) / max(len(tos_ads), 1) * 100, 1
)
)
def save_snapshot(self, analysis: SerpAdAnalysis):
"""保存分析结果到数据库"""
conn = sqlite3.connect(self.db_path)
conn.execute("""
INSERT INTO ad_position_snapshots
(keyword, timestamp, tos_count, ros_count, ad_density_pct,
competitor_tos_count, competitor_tos_share_pct, brands_in_tos, raw_data)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
analysis.keyword,
analysis.timestamp,
analysis.tos_count,
analysis.ros_count,
analysis.ad_density_pct,
analysis.competitor_tos_count,
analysis.competitor_tos_share_pct,
json.dumps(analysis.brands_in_tos, ensure_ascii=False),
json.dumps(asdict(analysis), ensure_ascii=False)
))
conn.commit()
conn.close()
def get_trend(self, keyword: str, hours: int = 24) -> List[dict]:
"""获取关键词广告位历史趋势数据"""
conn = sqlite3.connect(self.db_path)
rows = conn.execute("""
SELECT timestamp, tos_count, competitor_tos_share_pct, ad_density_pct
FROM ad_position_snapshots
WHERE keyword = ?
AND timestamp > datetime('now', ? || ' hours')
ORDER BY timestamp
""", (keyword, f"-{hours}")).fetchall()
conn.close()
return [
{
"timestamp": r[0],
"tos_count": r[1],
"competitor_tos_share_pct": r[2],
"ad_density_pct": r[3]
}
for r in rows
]
def run_batch(self, keywords: List[str], zip_code: str = "10001") -> List[SerpAdAnalysis]:
"""批量监控关键词广告位"""
results = []
for keyword in keywords:
logger.info(f"采集关键词:{keyword}")
try:
serp_data = self.fetch_serp(keyword, zip_code)
if not serp_data:
logger.warning(f"空数据:{keyword}")
continue
analysis = self.parse_ad_positions(serp_data, keyword)
self.save_snapshot(analysis)
results.append(analysis)
emoji = "🔴" if analysis.competitor_tos_share_pct > 50 else (
"🟡" if analysis.competitor_tos_share_pct > 25 else "🟢"
)
logger.info(
f"{emoji} {keyword}: TOS {analysis.tos_count}位 | "
f"竞品TOS占比 {analysis.competitor_tos_share_pct}%"
)
except Exception as e:
logger.error(f"❌ {keyword} 处理失败: {e}")
time.sleep(0.5)
return results
# ──────────────────────────────────────────────────────────
# 告警模块
# ──────────────────────────────────────────────────────────
class AlertManager:
"""告警管理器(飞书/Slack)"""
def __init__(self, feishu_webhook: str = None, slack_webhook: str = None):
self.feishu_webhook = feishu_webhook
self.slack_webhook = slack_webhook
def check_and_alert(
self,
analyses: List[SerpAdAnalysis],
tos_share_threshold: float = 50.0 # 竞品TOS占比超过此值触发告警
):
"""检查是否触发告警条件"""
alerts = [
a for a in analyses
if a.competitor_tos_share_pct >= tos_share_threshold
]
if not alerts:
return
msg = f"⚠️ **亚马逊广告位告警** - {datetime.now().strftime('%Y-%m-%d %H:%M')}\n\n"
for a in alerts:
msg += f"🔴 **{a.keyword}**\n"
msg += f" 竞品TOS占比:{a.competitor_tos_share_pct}%(阈值:{tos_share_threshold}%)\n"
msg += f" TOS出现的品牌:{', '.join(a.brands_in_tos[:5])}\n\n"
if self.feishu_webhook:
requests.post(self.feishu_webhook, json={
"msg_type": "text",
"content": {"text": msg}
})
logger.warning(f"告警已发送,涉及关键词:{[a.keyword for a in alerts]}")
def send_daily_report(self, analyses: List[SerpAdAnalysis]):
"""发送每日广告位日报"""
date_str = datetime.now().strftime("%Y-%m-%d")
report = f"📊 **亚马逊广告位日报 - {date_str}**\n\n"
# 按竞品TOS占比从高到低排序
sorted_analyses = sorted(analyses, key=lambda a: a.competitor_tos_share_pct, reverse=True)
for a in sorted_analyses[:10]: # 只展示前10个关键词
status = "🔴" if a.competitor_tos_share_pct > 50 else (
"🟡" if a.competitor_tos_share_pct > 25 else "🟢"
)
report += f"{status} {a.keyword}: 竞品TOS {a.competitor_tos_share_pct}% | 广告密度 {a.ad_density_pct}%\n"
if self.feishu_webhook:
requests.post(self.feishu_webhook, json={
"msg_type": "text",
"content": {"text": report}
})
# ──────────────────────────────────────────────────────────
# 主程序:定时任务配置
# ──────────────────────────────────────────────────────────
if __name__ == "__main__":
# 配置
PANGOLINFO_API_KEY = "your_api_key_here"
FEISHU_WEBHOOK = "https://open.feishu.cn/open-apis/bot/v2/hook/your_token"
KEYWORDS = [
"home storage organizer",
"yoga mat thick non slip",
"dog treats grain free",
"portable charger 20000mah",
# 添加你的核心关键词...
]
COMPETITOR_BRANDS = ["BrandA", "CompetitorX", "TargetSeller"]
# 初始化
monitor = AmazonAdPositionMonitor(
api_key=PANGOLINFO_API_KEY,
target_brands=COMPETITOR_BRANDS
)
alert_manager = AlertManager(feishu_webhook=FEISHU_WEBHOOK)
def hourly_job():
"""每小时运行一次的广告位监控任务"""
logger.info("⏰ 开始广告位监控任务...")
results = monitor.run_batch(KEYWORDS)
alert_manager.check_and_alert(results, tos_share_threshold=50.0)
logger.info(f"✅ 完成,共分析 {len(results)} 个关键词")
def daily_report_job():
"""每日凌晨2点发送日报"""
results = monitor.run_batch(KEYWORDS)
alert_manager.send_daily_report(results)
# 定时任务配置
schedule.every().hour.at(":00").do(hourly_job)
schedule.every().day.at("02:00").do(daily_report_job)
logger.info("🚀 亚马逊广告位监控系统已启动")
logger.info(f"📊 监控 {len(KEYWORDS)} 个关键词,竞品品牌:{COMPETITOR_BRANDS}")
# 启动时立即运行一次
hourly_job()
while True:
schedule.run_pending()
time.sleep(60)
四、AI Agent集成:Amazon Data MCP配置
对于Claude/Cursor用户,接入MCP后可以用自然语言触发广告位分析:
{
"mcpServers": {
"pangolinfo-amazon": {
"command": "npx",
"args": ["-y", "@pangolinfo/amazon-data-mcp"],
"env": {
"PANGOLINFO_API_KEY": "your_api_key_here"
}
}
}
}
配置后对AI说:“分析’yoga mat’这个词现在的TOS有哪些竞品”——Agent实时采集,自动输出分析报告。
五、性能指标参考
| 指标 | 数值 |
|---|---|
| SP广告位采集成功率 | 98%(行业最高) |
| 平均单次采集耗时 | 10-15秒(含JS渲染) |
| 推荐监控频率 | 每小时(旺季可调至每30分钟) |
| 建议关键词数量 | 50-200个(单次批量) |
| 数据历史保留 | SQLite本地存储,可无限期 |
29



被折叠的 条评论
为什么被折叠?



