本文聚焦工程实现,从数据架构到完整代码,拆解如何用 Open Claw + Pangolinfo SERP API 构建具有实际价值的亚马逊广告监控系统。
TL;DR
亚马逊广告监控的工程核心:
- 用 Pangolinfo SERP API 实时抓取核心关键词的 SP 广告位(98% 覆盖率)
- 对比历史基准线,检测 Top3 入场/退场、第1位变化、价格大幅调整
- Open Claw LLM 对告警做业务语义解读
- 关键词分 A/B/C 三层,告警按优先级分级分渠道推送
数据入口:Pangolinfo Scrape API(支持 Amazon SERP,JSON 输出,分钟级实时)
一、为什么现有监控工具都不够用
广告后台的根本局限
广告账号后台给的是内视角数据——投放成本、曝光量、点击率——这些数据告诉你"自己做得怎么样",但完全不告诉你"竞品在干什么"。而竞品的动作,才是决定你需不需要调整出价策略的关键信号。
当你的 ACoS 突然恶化,你需要知道:是竞品新入场把竞价抬高了,还是你核心词的竞品忽然大幅降价导致你的点击量下降?这两种情况的应对策略截然相反。
Helium 10 等 SaaS 工具的缺陷
主流工具的竞品广告追踪数据通常有 24-48 小时延迟,且不开放 API 接口,无法集成进自己的自动化工作流。更关键的是,它们提供的是固定维度的预制报表,而不是你可以自由定义告警逻辑的数据管道。Open Claw 的价值正在于此:它让你把广告监控需求翻译成自然语言,由 Agent 决定调哪些工具、怎么分析、触发什么告警。
二、数据层:Pangolinfo SERP API 接入
核心 Tool 注册——这段代码把 Pangolinfo SERP 调用封装成 Open Claw 可用的工具:
# Open Claw MCP Tool:亚马逊 SERP 广告位实时查询
SERP_MONITOR_TOOL = {
"name": "get_amazon_ad_positions",
"description": (
"实时查询亚马逊搜索结果页面的 SP 广告位数据。"
"当用户询问某个关键词当前有哪些 ASIN 在投广告、位置如何时使用。"
"返回 Top of Search 广告位的 ASIN、品牌、价格、排名。"
"不适用于查询自然搜索排名(需使用其他 Tool)。"
),
"input_schema": {
"type": "object",
"properties": {
"keyword": {
"type": "string",
"description": "亚马逊搜索关键词,如 'wireless earbuds under $30'"
},
"marketplace": {
"type": "string",
"enum": ["US", "UK", "DE", "JP", "CA"],
"default": "US",
"description": "目标站点"
},
"top_n": {
"type": "integer",
"description": "返回前N个广告位,默认5",
"default": 5
}
},
"required": ["keyword"]
}
}
# 对应的工具实现函数
async def get_amazon_ad_positions(
keyword: str, marketplace: str = "US", top_n: int = 5
) -> dict:
"""Tool 实现:调用 Pangolinfo SERP API,返回结构化广告位数据"""
import aiohttp
headers = {"Authorization": f"Bearer {PANGOLINFO_API_KEY}"}
payload = {
"source": "amazon_search",
"query": keyword,
"marketplace": marketplace,
"page": 1,
"include_sponsored": True,
"include_organic": False,
"output_format": "json"
}
async with aiohttp.ClientSession() as session:
async with session.post(
"https://api.pangolinfo.com/v1/serp",
headers=headers, json=payload,
timeout=aiohttp.ClientTimeout(total=25)
) as resp:
data = await resp.json()
sponsored = data.get("sponsored_results", [])
top_ads = [
{
"rank": item.get("ad_rank"),
"asin": item.get("asin"),
"brand": item.get("brand", ""),
"price": item.get("price"),
"placement": item.get("ad_placement", "unknown")
}
for item in sorted(sponsored, key=lambda x: x.get("ad_rank", 999))
if "top" in item.get("ad_placement", "").lower()
][:top_n]
return {
"keyword": keyword,
"marketplace": marketplace,
"captured_at": datetime.utcnow().isoformat() + "Z",
"top_of_search_ads": top_ads,
"total_sponsored_count": len(sponsored)
}
三、变化检测:告警策略的工程实现
上面的数据采集是原材料,变化检测才是价值核心。以下是一个设计合理的告警规则引擎:
from dataclasses import dataclass
from typing import List, Set, Optional, Dict
from enum import Enum
class AlertTier(Enum):
CRITICAL = 1 # 立即通知,核心词+全新竞品
HIGH = 2 # 立即通知,重要变化
MEDIUM = 3 # 汇总日报,一般变化
INFO = 4 # 仅记录,不通知
@dataclass
class AdMonitorAlert:
keyword: str
tier: AlertTier
event: str
message: str
asin: str
captured_at: str
context: Optional[str] = None # LLM 生成的业务分析
class AdAlertEngine:
"""
亚马逊广告监控告警引擎
设计原则:宁可漏报不重要的,绝不淹没重要信号
"""
def __init__(self, keyword_priority: Dict[str, List[str]],
price_drop_pct: float = 12.0):
"""
keyword_priority: {"A": ["core kw1", ...], "B": [...], "C": [...]}
price_drop_pct: 触发价格告警的降幅阈值(百分比)
"""
self._tier_map: Dict[str, str] = {}
for tier, kws in keyword_priority.items():
for kw in kws:
self._tier_map[kw.lower()] = tier
self.price_threshold = price_drop_pct / 100
def _kw_tier(self, keyword: str) -> str:
return self._tier_map.get(keyword.lower(), "B")
def _resolve_alert_tier(self, base: AlertTier, keyword: str) -> AlertTier:
"""关键词优先级对告警级别的影响"""
kw_tier = self._kw_tier(keyword)
if kw_tier == "A" and base == AlertTier.HIGH:
return AlertTier.CRITICAL # A类词HIGH → 升级为CRITICAL
if kw_tier == "C":
return AlertTier.INFO # C类词 → 全部降为INFO
return base
def generate_alerts(
self,
current: dict, # get_amazon_ad_positions 返回值
baseline: dict # 上一次相同关键词的返回值
) -> List[AdMonitorAlert]:
"""对比生成告警列表"""
if baseline is None:
return [] # 首次建立基准线
kw = current["keyword"]
ts = current["captured_at"]
curr_ads = {ad["asin"]: ad for ad in current["top_of_search_ads"]}
base_ads = {ad["asin"]: ad for ad in baseline["top_of_search_ads"]}
curr_top3 = set(list(curr_ads.keys())[:3])
base_top3 = set(list(base_ads.keys())[:3])
curr_p1 = list(curr_ads.keys())[0] if curr_ads else None
base_p1 = list(base_ads.keys())[0] if base_ads else None
alerts = []
# === 检测1:第一位置变化 ===
if curr_p1 and curr_p1 != base_p1:
is_new = curr_p1 not in base_ads
base_tier = AlertTier.CRITICAL if is_new else AlertTier.HIGH
alerts.append(AdMonitorAlert(
keyword=kw, tier=self._resolve_alert_tier(base_tier, kw),
event="top1_change",
message=(
f"Top1广告位:{base_p1 or '无'} → {curr_p1}"
+ ("【全新竞品】" if is_new else "")
),
asin=curr_p1, captured_at=ts
))
# === 检测2:新竞品进入Top3 ===
for asin in curr_top3 - base_top3 - {curr_p1}:
rank = curr_ads[asin]["rank"]
alerts.append(AdMonitorAlert(
keyword=kw,
tier=self._resolve_alert_tier(AlertTier.HIGH, kw),
event="new_top3",
message=f"新竞品进入Top3 #{rank}:{asin}",
asin=asin, captured_at=ts
))
# === 检测3:Top3竞品消失 ===
for asin in base_top3 - curr_top3:
alerts.append(AdMonitorAlert(
keyword=kw,
tier=self._resolve_alert_tier(AlertTier.MEDIUM, kw),
event="top3_exit",
message=f"竞品 {asin} 退出Top3(可能缩减预算)",
asin=asin, captured_at=ts
))
# === 检测4:Top3内价格大降 ===
for asin, curr_ad in list(curr_ads.items())[:3]:
old_price = base_ads.get(asin, {}).get("price")
new_price = curr_ad.get("price")
if old_price and new_price and old_price > 0:
drop = (old_price - new_price) / old_price
if drop >= self.price_threshold:
alerts.append(AdMonitorAlert(
keyword=kw,
tier=self._resolve_alert_tier(AlertTier.HIGH, kw),
event="price_drop",
message=(
f"Top3竞品 {asin} 降价 {drop*100:.1f}%:"
f"${old_price:.2f}→${new_price:.2f}"
),
asin=asin, captured_at=ts
))
return alerts
四、完整调度器与Slack通知
import asyncio, json, sqlite3, aiohttp
from anthropic import Anthropic
class AdMonitorOrchestrator:
"""端到端的亚马逊广告监控调度器"""
def __init__(self, config: dict):
self.config = config
self.engine = AdAlertEngine(
keyword_priority=config["keyword_tiers"],
price_drop_pct=config.get("price_drop_pct", 12)
)
self.llm = Anthropic()
self.db = config["db_path"]
self._init_db()
def _init_db(self):
with sqlite3.connect(self.db) as c:
c.executescript("""
CREATE TABLE IF NOT EXISTS serp_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
keyword TEXT, marketplace TEXT,
captured_at TEXT, data_json TEXT
);
CREATE TABLE IF NOT EXISTS alert_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
keyword TEXT, asin TEXT, event TEXT,
tier TEXT, triggered_at TEXT, dispatched INTEGER DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_kw_ts
ON serp_history(keyword, marketplace, captured_at DESC);
""")
async def run_cycle(self, tier: str = "A", marketplace: str = "US"):
"""执行一个完整监控周期"""
keywords = self.config["keyword_tiers"].get(tier, [])
if not keywords:
return
# 批量采集(复用前面的 batch_fetch_serp)
snapshots = await batch_fetch_serp(keywords, marketplace)
all_alerts = []
for snap_data in snapshots:
# 持久化快照
with sqlite3.connect(self.db) as conn:
conn.execute(
"INSERT INTO serp_history (keyword, marketplace, captured_at, data_json)"
" VALUES (?, ?, ?, ?)",
(snap_data["keyword"], marketplace,
snap_data["captured_at"], json.dumps(snap_data))
)
# 加载历史基准线
row = conn.execute(
"SELECT data_json FROM serp_history"
" WHERE keyword = ? AND marketplace = ?"
" ORDER BY captured_at DESC LIMIT 1 OFFSET 1",
(snap_data["keyword"], marketplace)
).fetchone()
baseline = json.loads(row[0]) if row else None
alerts = self.engine.generate_alerts(snap_data, baseline)
# LLM 增强
for alert in alerts:
if alert.tier in (AlertTier.CRITICAL, AlertTier.HIGH):
alert.context = self._llm_analyze(alert, snap_data)
all_alerts.extend(alerts)
# 分发告警
await self._dispatch(all_alerts)
def _llm_analyze(self, alert: AdMonitorAlert, snap: dict) -> str:
top3_str = "\n".join([
f" #{ad['rank']}: {ad['asin']} - ${ad['price'] or 'N/A'}"
for ad in snap["top_of_search_ads"][:3]
])
resp = self.llm.messages.create(
model="claude-3-7-sonnet-20250219",
max_tokens=150,
messages=[{"role": "user", "content":
f"亚马逊广告监控告警:关键词「{alert.keyword}」\n"
f"事件:{alert.message}\n当前Top3:\n{top3_str}\n"
"请用80字以内说明可能原因和建议关注事项。直接输出,无需铺垫。"
}]
)
return resp.content[0].text
async def _dispatch(self, alerts: List[AdMonitorAlert]):
webhook = self.config.get("slack_webhook")
if not webhook:
return
to_send = [a for a in alerts
if a.tier in (AlertTier.CRITICAL, AlertTier.HIGH)]
async with aiohttp.ClientSession() as session:
for alert in to_send:
if self._is_duplicate(alert):
continue
emoji = "🚨" if alert.tier == AlertTier.CRITICAL else "⚠️"
msg = (
f"{emoji} *亚马逊广告监控* `{alert.tier.name}`\n"
f"词:`{alert.keyword}` | ASIN:`{alert.asin}`\n"
f"事件:{alert.message}"
+ (f"\n分析:_{alert.context}_" if alert.context else "")
)
await session.post(webhook, json={"text": msg})
self._mark_dispatched(alert)
def _is_duplicate(self, alert: AdMonitorAlert,
hours: int = 6) -> bool:
"""6小时内同类告警已发送则跳过"""
cutoff = (datetime.utcnow() - timedelta(hours=hours)).isoformat()
with sqlite3.connect(self.db) as conn:
return bool(conn.execute(
"SELECT 1 FROM alert_history WHERE keyword=? AND asin=?"
" AND event=? AND triggered_at>? AND dispatched=1 LIMIT 1",
(alert.keyword, alert.asin, alert.event, cutoff)
).fetchone())
def _mark_dispatched(self, alert: AdMonitorAlert):
with sqlite3.connect(self.db) as conn:
conn.execute(
"INSERT INTO alert_history (keyword, asin, event, tier,"
" triggered_at, dispatched) VALUES (?,?,?,?,?,1)",
(alert.keyword, alert.asin, alert.event,
alert.tier.name, alert.captured_at)
)
# 入口示例(配合 APScheduler 或 Celery 定时调用)
async def main():
config = {
"db_path": "/data/ad_monitor.db",
"slack_webhook": "https://hooks.slack.com/services/xxx",
"price_drop_pct": 12,
"keyword_tiers": {
"A": ["wireless earbuds", "bluetooth speaker"],
"B": ["earbuds under 30", "tws earbuds"],
"C": []
}
}
orch = AdMonitorOrchestrator(config)
await orch.run_cycle("A", "US")
if __name__ == "__main__":
asyncio.run(main())
五、性能与成本参考
| 指标 | 参考值 |
|---|---|
| 50 词 / 次采集时间(async) | 30-60 秒 |
| API 调用次数(50词 A+B类) | ~280 次/天 |
| SQLite → PostgreSQL 切换阈值 | 关键词 > 100 或频率 < 2h |
| 告警去重窗口 | 6 小时(同词+同ASIN+同事件) |
| LLM 调用成本(每次告警) | ~$0.001(claude-3-7-sonnet输入约200 token) |
总结
- 工具调用是核心:把 Pangolinfo SERP API 封装成 Open Claw Tool,描述清晰
- 异步并发是性能关键:asyncio + semaphore 控制并发数,避免超速
- 分层是可持续运营的保证:A/B/C 层频率不同,告警阈值不同
- 去重是信噪比的保障:同事件6小时静默,不产生告警疲劳