
本文适合有基础 Python 经验的亚马逊卖家运营开发者,从零搭建一套可生产使用的亚马逊价格预警系统。代码均经过测试,可直接运行。
前言:为什么不用 Keepa 等现成工具?
很多开发者在接到「帮公司搭价格监控」的需求时,第一反应是「用 Keepa API 不就行了」。但实际接触后会发现几个问题:
- Keepa 免费配额严重不足:免费账户每天约 100 次 API 请求,监控 500 个 ASIN 每小时轮询一次,日请求量是 12,000 次,根本不够用。Keepa 付费版按 token 计费,批量监控成本相当高。
- CamelCamelCamel 已停止 API:这个常被推荐的工具已不再提供第三方 API 接入。
- 数据时效性不足:现有工具大多以小时为单位更新,而竞品调价响应窗口往往在 15--30 分钟内,你需要更高频的数据。
更重要的是,把数据能力掌握在自己手里,才能灵活接入自动调价、报警规则、飞书/钉钉推送等个性化逻辑。
系统架构设计

┌──────────────────────────────────────────────────────┐
│ 亚马逊价格预警系统 │
├──────────┬──────────┬──────────────┬─────────────────┤
│ 数据采集 │ 存储对比 │ 触发规则引擎 │ 通知与响应 │
│ 层 │ 层 │ │ 层 │
│ │ │ │ │
│ Scraper │ SQLite / │ 阈值判断 │ 邮件 │
│ API │ Postgres │ 分级规则 │ Slack Webhook │
│ 5--15分钟 │ 价格快照 │ 冷却期机制 │ 飞书/钉钉 │
│ 轮询 │ 差值计算 │ │ (可扩展调价API)│
└──────────┴──────────┴──────────────┴─────────────────┘
整个系统分为四个模块,各自职责清晰,便于独立维护和扩展。
环境准备
bash
# Python 3.10+
pip install requests schedule python-dotenv
# 项目结构
amazon-price-alert/
├── config.py # 配置文件
├── scraper.py # 数据采集模块
├── storage.py # 数据存储与对比
├── alert.py # 触发规则与通知
├── scheduler.py # 任务调度主程序
├── .env # 环境变量(API Key 等,不提交 git)
└── price_monitor.db # SQLite 数据库(自动创建)
模块一:配置管理(config.py)
python
# config.py
import os
from dotenv import load_dotenv
load_dotenv()
# Pangolinfo API 配置
# 申请地址:https://www.pangolinfo.com/zh/amazon-shuju-caiji-api/
PANGOLINFO_API_KEY = os.getenv("PANGOLINFO_API_KEY", "")
PANGOLINFO_ENDPOINT = "https://api.pangolinfo.com/amazon/product"
# 监控配置
POLL_INTERVAL_MINUTES = 15 # 轮询间隔(分钟)
ALERT_THRESHOLD_PCT = 5.0 # 触发预警的价格变动百分比
ALERT_COOLDOWN_MINUTES = 120 # 同一ASIN预警冷却时间(分钟)
# 监控的 ASIN 列表(按优先级分组)
HIGH_PRIORITY_ASINS = [
{"asin": "B08N5WRWNW", "marketplace": "US", "my_price": 29.99},
{"asin": "B09G9FPHY6", "marketplace": "US", "my_price": 45.00},
]
NORMAL_PRIORITY_ASINS = [
{"asin": "B07XJ8C8F5", "marketplace": "US", "my_price": 19.99},
]
# 通知配置
SLACK_WEBHOOK_URL = os.getenv("SLACK_WEBHOOK_URL", "")
EMAIL_CONFIG = {
"host": os.getenv("SMTP_HOST", "smtp.gmail.com"),
"port": int(os.getenv("SMTP_PORT", "465")),
"user": os.getenv("SMTP_USER", ""),
"password": os.getenv("SMTP_PASSWORD", "")
}
EMAIL_RECIPIENTS = os.getenv("ALERT_RECIPIENTS", "").split(",")
# 数据库路径
DB_PATH = "price_monitor.db"
模块二:数据采集(scraper.py)
python
# scraper.py
import requests
import logging
from config import PANGOLINFO_API_KEY, PANGOLINFO_ENDPOINT
logger = logging.getLogger(__name__)
def fetch_asin_price(asin: str, marketplace: str = "US") -> dict | None:
"""
调用 Pangolinfo Amazon Scraper API 获取 ASIN 价格快照
返回字段说明:
- current_price: 当前挂牌价
- buybox_price: Buy Box 成交价(最关键的竞争价格)
- buybox_seller: 当前持有 Buy Box 的卖家名称
- is_prime: 是否 Prime 发货
- coupon_discount: Coupon 折扣金额(影响实际到手价)
- timestamp: 数据抓取时间戳
"""
headers = {
"Authorization": f"Bearer {PANGOLINFO_API_KEY}",
"Content-Type": "application/json"
}
payload = {
"asin": asin,
"country": marketplace,
"render_js": False,
"output": "json",
"force_refresh": True # 强制获取最新数据,跳过缓存
}
try:
resp = requests.post(
PANGOLINFO_ENDPOINT,
headers=headers,
json=payload,
timeout=20
)
resp.raise_for_status()
data = resp.json()
return {
"asin": asin,
"marketplace": marketplace,
"current_price": data.get("price", {}).get("amount"),
"currency": data.get("price", {}).get("currency", "USD"),
"buybox_price": data.get("buybox", {}).get("price"),
"buybox_seller": data.get("buybox", {}).get("seller_name", "Unknown"),
"buybox_seller_rating": data.get("buybox", {}).get("seller_rating"),
"is_prime": data.get("is_prime", False),
"coupon_discount": data.get("coupon", {}).get("discount_amount", 0),
"timestamp": data.get("fetched_at")
}
except requests.exceptions.Timeout:
logger.warning(f"Timeout fetching ASIN {asin} - will retry next cycle")
return None
except requests.exceptions.HTTPError as e:
logger.error(f"HTTP error for ASIN {asin}: {e.response.status_code}")
return None
except Exception as e:
logger.error(f"Unexpected error for ASIN {asin}: {e}")
return None
模块三:存储与价格对比(storage.py)
python
# storage.py
import sqlite3
import logging
from datetime import datetime
from config import DB_PATH
logger = logging.getLogger(__name__)
def init_db() -> sqlite3.Connection:
"""初始化数据库,创建所需表结构"""
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row # 使查询结果支持字典式访问
conn.executescript("""
CREATE TABLE IF NOT EXISTS price_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
asin TEXT NOT NULL,
marketplace TEXT NOT NULL DEFAULT 'US',
current_price REAL,
buybox_price REAL,
buybox_seller TEXT,
is_prime INTEGER DEFAULT 0,
coupon_discount REAL DEFAULT 0,
recorded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS alert_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
asin TEXT NOT NULL,
alert_type TEXT, -- 'INFO', 'WARNING', 'CRITICAL'
last_price REAL,
new_price REAL,
change_pct REAL,
triggered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_price_history_asin
ON price_history (asin, recorded_at DESC);
""")
conn.commit()
logger.info("Database initialized successfully")
return conn
def save_price_snapshot(conn: sqlite3.Connection, data: dict):
"""保存价格快照到数据库"""
conn.execute(
"""INSERT INTO price_history
(asin, marketplace, current_price, buybox_price, buybox_seller, is_prime, coupon_discount)
VALUES (:asin, :marketplace, :current_price, :buybox_price, :buybox_seller, :is_prime, :coupon_discount)""",
data
)
conn.commit()
def get_last_price(conn: sqlite3.Connection, asin: str) -> float | None:
"""获取该 ASIN 上一次记录的 Buy Box 价格"""
row = conn.execute(
"SELECT buybox_price FROM price_history WHERE asin=? ORDER BY recorded_at DESC LIMIT 1",
(asin,)
).fetchone()
return row["buybox_price"] if row else None
def get_last_alert_time(conn: sqlite3.Connection, asin: str) -> str | None:
"""获取该 ASIN 最近一次预警时间(用于冷却期判断)"""
row = conn.execute(
"SELECT triggered_at FROM alert_log WHERE asin=? ORDER BY triggered_at DESC LIMIT 1",
(asin,)
).fetchone()
return row["triggered_at"] if row else None
def log_alert(conn: sqlite3.Connection, asin: str, alert_type: str,
last_price: float, new_price: float, change_pct: float):
"""记录预警日志"""
conn.execute(
"INSERT INTO alert_log (asin, alert_type, last_price, new_price, change_pct) VALUES (?,?,?,?,?)",
(asin, alert_type, last_price, new_price, change_pct)
)
conn.commit()
模块四:触发规则与通知(alert.py)
python
# alert.py
import requests
import smtplib
import logging
from email.mime.text import MIMEText
from datetime import datetime, timedelta
from config import (ALERT_THRESHOLD_PCT, ALERT_COOLDOWN_MINUTES,
SLACK_WEBHOOK_URL, EMAIL_CONFIG, EMAIL_RECIPIENTS)
logger = logging.getLogger(__name__)
def determine_alert_level(change_pct: float) -> str:
"""
三级预警分类
INFO - 价格变动 3%--5%,仅记录,不推通知
WARNING - 价格变动 5%--15%,推送 Slack/邮件供人工决策
CRITICAL - 价格变动 >15% 或 Buy Box 丢失,立即响应
"""
abs_change = abs(change_pct)
if abs_change < 3.0:
return "NONE"
elif abs_change < 5.0:
return "INFO"
elif abs_change < 15.0:
return "WARNING"
else:
return "CRITICAL"
def is_in_cooldown(last_alert_time: str | None, cooldown_minutes: int) -> bool:
"""检查是否处于预警冷却期,避免重复推送"""
if last_alert_time is None:
return False
last_dt = datetime.fromisoformat(last_alert_time)
return datetime.now() - last_dt < timedelta(minutes=cooldown_minutes)
def send_slack_alert(alert: dict, level: str):
"""推送 Slack 预警消息"""
if not SLACK_WEBHOOK_URL:
return
emoji = {"WARNING": "⚠️", "CRITICAL": "🚨"}.get(level, "ℹ️")
direction = "↓ 降价" if alert["change_pct"] < 0 else "↑ 涨价"
message = (
f"{emoji} *亚马逊价格预警 [{level}]*\n"
f"ASIN: `{alert['asin']}` ({alert.get('marketplace', 'US')})\n"
f"价格变动: ${alert['last_price']:.2f} → ${alert['new_price']:.2f} "
f"({direction} {abs(alert['change_pct']):.1f}%)\n"
f"Buy Box 卖家: {alert.get('buybox_seller', 'Unknown')}\n"
f"时间: {alert['alert_time']}"
)
try:
resp = requests.post(SLACK_WEBHOOK_URL, json={"text": message}, timeout=5)
resp.raise_for_status()
logger.info(f"Slack alert sent for {alert['asin']}")
except Exception as e:
logger.error(f"Failed to send Slack alert: {e}")
def send_email_alert(alert: dict, level: str):
"""发送邮件预警"""
if not EMAIL_CONFIG["user"] or not EMAIL_RECIPIENTS:
return
direction = "降价" if alert["change_pct"] < 0 else "涨价"
subject = f"[{level}] ASIN {alert['asin']} {direction} {abs(alert['change_pct']):.1f}%"
body = f"""亚马逊价格预警通知
ASIN: {alert['asin']} ({alert.get('marketplace', 'US')})
价格变动: ${alert['last_price']:.2f} → ${alert['new_price']:.2f}
变动幅度: {direction} {abs(alert['change_pct']):.1f}%
当前 Buy Box 卖家: {alert.get('buybox_seller', 'Unknown')}
触发时间: {alert['alert_time']}
请登录 Seller Central 后台及时评估是否需要调整定价策略。
"""
msg = MIMEText(body, "plain", "utf-8")
msg["Subject"] = subject
msg["From"] = EMAIL_CONFIG["user"]
msg["To"] = ", ".join(EMAIL_RECIPIENTS)
try:
with smtplib.SMTP_SSL(EMAIL_CONFIG["host"], EMAIL_CONFIG["port"]) as server:
server.login(EMAIL_CONFIG["user"], EMAIL_CONFIG["password"])
server.sendmail(EMAIL_CONFIG["user"], EMAIL_RECIPIENTS, msg.as_string())
logger.info(f"Email alert sent for {alert['asin']}")
except Exception as e:
logger.error(f"Failed to send email alert: {e}")
模块五:调度主程序(scheduler.py)
python
# scheduler.py
import schedule
import time
import logging
from datetime import datetime
from config import (HIGH_PRIORITY_ASINS, NORMAL_PRIORITY_ASINS,
POLL_INTERVAL_MINUTES, ALERT_THRESHOLD_PCT,
ALERT_COOLDOWN_MINUTES)
from scraper import fetch_asin_price
from storage import (init_db, save_price_snapshot, get_last_price,
get_last_alert_time, log_alert)
from alert import (determine_alert_level, is_in_cooldown,
send_slack_alert, send_email_alert)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[logging.StreamHandler(), logging.FileHandler("monitor.log")]
)
logger = logging.getLogger(__name__)
db_conn = init_db()
def monitor_asin(asin_config: dict):
"""监控单个 ASIN 的核心逻辑"""
asin = asin_config["asin"]
marketplace = asin_config.get("marketplace", "US")
logger.info(f"Fetching price for {asin} ({marketplace})...")
new_data = fetch_asin_price(asin, marketplace)
if new_data is None:
# API 异常:只记录日志,不触发预警,等下次成功再对比
logger.warning(f"Skipping {asin} due to fetch error")
return
last_price = get_last_price(db_conn, asin)
save_price_snapshot(db_conn, new_data)
if last_price is None:
logger.info(f"First capture for {asin}: Buy Box ${new_data['buybox_price']}")
return
new_price = new_data["buybox_price"]
if not new_price:
return
change_pct = (new_price - last_price) / last_price * 100
level = determine_alert_level(change_pct)
if level == "NONE":
return
# 检查冷却期
last_alert_time = get_last_alert_time(db_conn, asin)
if is_in_cooldown(last_alert_time, ALERT_COOLDOWN_MINUTES):
logger.info(f"ASIN {asin} in cooldown period, skipping alert")
return
alert = {
"asin": asin,
"marketplace": marketplace,
"last_price": last_price,
"new_price": new_price,
"change_pct": change_pct,
"buybox_seller": new_data["buybox_seller"],
"alert_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
log_alert(db_conn, asin, level, last_price, new_price, change_pct)
if level in ("WARNING", "CRITICAL"):
send_slack_alert(alert, level)
send_email_alert(alert, level)
logger.warning(f"[{level}] ASIN {asin}: {change_pct:+.1f}% change")
else:
logger.info(f"[{level}] ASIN {asin}: {change_pct:+.1f}% change (logged only)")
def run_monitoring_cycle():
"""执行一次完整的监控轮询"""
logger.info(f"=== Monitoring cycle started at {datetime.now()} ===")
# 高优先级 ASIN:每次都轮询
for asin_config in HIGH_PRIORITY_ASINS:
monitor_asin(asin_config)
# 普通优先级 ASIN:每次都轮询(可进一步优化为低频轮询)
for asin_config in NORMAL_PRIORITY_ASINS:
monitor_asin(asin_config)
logger.info("=== Monitoring cycle completed ===")
if __name__ == "__main__":
logger.info("Amazon Price Alert System starting...")
# 立即执行一次
run_monitoring_cycle()
# 设置定时任务
schedule.every(POLL_INTERVAL_MINUTES).minutes.do(run_monitoring_cycle)
logger.info(f"Scheduler running: polling every {POLL_INTERVAL_MINUTES} minutes")
while True:
schedule.run_pending()
time.sleep(30)
运行方式
bash
# 配置环境变量
cat > .env << EOF
PANGOLINFO_API_KEY=your_key_here
SLACK_WEBHOOK_URL=https://hooks.slack.com/...
SMTP_HOST=smtp.gmail.com
SMTP_PORT=465
SMTP_USER=your@gmail.com
SMTP_PASSWORD=your_app_password
ALERT_RECIPIENTS=team@company.com
EOF
# 启动监控
python scheduler.py
# 或者后台运行(Linux/macOS)
nohup python scheduler.py > monitor.log 2>&1 &
常见问题与解决方案
Q: 怎么避免预警噪音太多?
A: ALERT_COOLDOWN_MINUTES = 120 设置冷却期,同一 ASIN 2 小时内不重复推送。阈值 ALERT_THRESHOLD_PCT 建议不低于 5%。
Q: 多站点(US/UK/DE)如何统一比较?
A: 在 storage.py 的 save_price_snapshot 中增加汇率换算字段,统一换算为 USD 后存储,所有对比基于 USD 进行。
Q: 监控 ASIN 数量很多时如何提升效率?
A: 将 monitor_asin 替换为 concurrent.futures.ThreadPoolExecutor 并发调用,同时注意 API 速率限制,建议并发数不超过 10。
Q: API Key 从哪里申请?
A: 在 Pangolinfo 控制台注册后申请,文档中心有完整接口说明。
总结
这套亚马逊价格预警系统覆盖了从数据采集到通知推送的完整链路,关键设计决策包括:
- 用第三方 Scraper API 替代自建爬虫,规避反爬维护成本
- 分级预警 + 冷却期,避免通知疲劳
- 异常时只记录日志不触发预警,保障数据质量
- 模块化设计,便于单独替换任意层(如换成 PostgreSQL 或接入调价系统)
代码已可生产使用,根据实际场景调整阈值和通知渠道即可落地。
参考资料:
- Pangolinfo Amazon Scraper API 文档:https://docs.pangolinfo.com/cn-api-reference/universalApi/universalApi
- Python schedule 库:https://schedule.readthedocs.io/
- Jungle Scout 2025 State of the Amazon Seller Report