亚马逊价格预警系统完整实现:Python + Scraper API + SQLite 全栈教程(2026)

本文适合有基础 Python 经验的亚马逊卖家运营开发者,从零搭建一套可生产使用的亚马逊价格预警系统。代码均经过测试,可直接运行。

前言:为什么不用 Keepa 等现成工具?

很多开发者在接到「帮公司搭价格监控」的需求时,第一反应是「用 Keepa API 不就行了」。但实际接触后会发现几个问题:

  1. Keepa 免费配额严重不足:免费账户每天约 100 次 API 请求,监控 500 个 ASIN 每小时轮询一次,日请求量是 12,000 次,根本不够用。Keepa 付费版按 token 计费,批量监控成本相当高。
  2. CamelCamelCamel 已停止 API:这个常被推荐的工具已不再提供第三方 API 接入。
  3. 数据时效性不足:现有工具大多以小时为单位更新,而竞品调价响应窗口往往在 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.pysave_price_snapshot 中增加汇率换算字段,统一换算为 USD 后存储,所有对比基于 USD 进行。

Q: 监控 ASIN 数量很多时如何提升效率?

A: 将 monitor_asin 替换为 concurrent.futures.ThreadPoolExecutor 并发调用,同时注意 API 速率限制,建议并发数不超过 10。

Q: API Key 从哪里申请?

A: 在 Pangolinfo 控制台注册后申请,文档中心有完整接口说明。


总结

这套亚马逊价格预警系统覆盖了从数据采集到通知推送的完整链路,关键设计决策包括:

  • 用第三方 Scraper API 替代自建爬虫,规避反爬维护成本
  • 分级预警 + 冷却期,避免通知疲劳
  • 异常时只记录日志不触发预警,保障数据质量
  • 模块化设计,便于单独替换任意层(如换成 PostgreSQL 或接入调价系统)

代码已可生产使用,根据实际场景调整阈值和通知渠道即可落地。


参考资料