亚马逊Listing流量分析系统实战:从数据采集到异常检测的完整实现

Pandas入门教程(二十二):股票数据分析实战——yfinance 获取数据、技术指标与收益率分析 把 Pandas 用在真实场景,才能体会它的价值。本文以茅台股票为例,用 yfinance 获取历史行情,完成缺失值处理与列筛选,绘制收盘价曲线,计算 SMA 移动平均线与 RSI 相对强弱指数,并通过日收益率、累计收益率和波动率完成完整的量化分析流程。 阅读详情

前言

作为一名从事电商数据分析多年的开发者,我经常遇到这样的场景:卖家的listing出单量突然翻倍,但完全不知道流量从哪来。亚马逊官方后台只提供笼统的Sessions数据,无法精准归因到具体的流量渠道。

本文将从技术角度,详细讲解如何构建一个完整的亚马逊listing流量分析系统,包括数据采集、存储、分析和异常检测的全流程实现。

技术栈:

  • Python 3.8+
  • Requests (HTTP请求)
  • Pandas (数据处理)
  • PostgreSQL (数据存储)
  • APScheduler (定时任务)
  • Matplotlib/Plotly (数据可视化)

源码地址:文末提供完整代码示例

在这里插入图片描述

一、需求分析与系统架构

1.1 业务需求

亚马逊卖家需要解决以下核心问题:

  1. 流量来源归因:区分自然搜索、付费广告、站外引流的流量占比
  2. 关键词排名监控:追踪核心关键词的实时排名变化
  3. 竞品动态监控:监控竞品的价格、库存、排名波动
  4. 异常检测告警:流量突增/突降时自动告警

1.2 系统架构设计

┌─────────────────────────────────────────────────────────┐
│                   数据采集层 (Data Collection)           │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐  │
│  │ 搜索结果采集  │  │ 产品详情采集  │  │ 榜单数据采集  │  │
│  └──────────────┘  └──────────────┘  └──────────────┘  │
│           Pangolinfo Scrape API / Amazon SP-API         │
└────────────────────────┬────────────────────────────────┘
                         │
┌────────────────────────▼────────────────────────────────┐
│                   数据存储层 (Data Storage)              │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐  │
│  │ PostgreSQL   │  │ Redis Cache  │  │ Time Series  │  │
│  │ (结构化数据)  │  │ (实时缓存)    │  │ DB (时序数据) │  │
│  └──────────────┘  └──────────────┘  └──────────────┘  │
└────────────────────────┬────────────────────────────────┘
                         │
┌────────────────────────▼────────────────────────────────┐
│                   数据分析层 (Data Analysis)             │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐  │
│  │ 排名变化分析  │  │ 流量归因算法  │  │ 异常检测算法  │  │
│  └──────────────┘  └──────────────┘  └──────────────┘  │
└────────────────────────┬────────────────────────────────┘
                         │
┌────────────────────────▼────────────────────────────────┐
│                   可视化层 (Visualization)               │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐  │
│  │ Web Dashboard│  │ 报表生成      │  │ 告警通知      │  │
│  │ (Flask/Django)│  │ (PDF/Excel)  │  │ (Email/Slack) │  │
│  └──────────────┘  └──────────────┘  └──────────────┘  │
└─────────────────────────────────────────────────────────┘

二、数据采集模块实现

2.1 核心数据采集类

import requests
import json
from datetime import datetime
from typing import List, Dict, Optional
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

class AmazonDataCollector:
    """
    亚马逊数据采集器
    使用Pangolinfo Scrape API进行数据采集
    """
    
    def __init__(self, api_key: str, base_url: str = "https://api.pangolinfo.com/v1"):
        self.api_key = api_key
        self.base_url = base_url
        self.session = requests.Session()
        self.session.headers.update({
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json"
        })
    
    def search_products(self, 
                       keyword: str, 
                       marketplace: str = "US",
                       page: int = 1) -> Dict:
        """
        采集搜索结果页数据
        
        Args:
            keyword: 搜索关键词
            marketplace: 市场代码 (US, UK, DE等)
            page: 页码
            
        Returns:
            包含搜索结果的字典
        """
        endpoint = f"{self.base_url}/scrape/amazon/search"
        
        payload = {
            "keyword": keyword,
            "marketplace": marketplace,
            "page": page,
            "include_sponsored": True,  # 包含广告位
            "include_organic": True     # 包含自然结果
        }
        
        try:
            response = self.session.post(endpoint, json=payload, timeout=30)
            response.raise_for_status()
            
            data = response.json()
            logger.info(f"成功采集关键词 '{keyword}' 的搜索结果,共 {len(data.get('results', []))} 条")
            
            return data
            
        except requests.exceptions.RequestException as e:
            logger.error(f"采集搜索结果失败: {e}")
            return {}
    
    def get_product_details(self, asin: str, marketplace: str = "US") -> Dict:
        """
        采集产品详情页数据
        
        Args:
            asin: 产品ASIN
            marketplace: 市场代码
            
        Returns:
            产品详情字典
        """
        endpoint = f"{self.base_url}/scrape/amazon/product"
        
        payload = {
            "asin": asin,
            "marketplace": marketplace,
            "include_reviews": False,  # 不包含评论(评论单独采集)
            "include_qa": False
        }
        
        try:
            response = self.session.post(endpoint, json=payload, timeout=30)
            response.raise_for_status()
            
            data = response.json()
            logger.info(f"成功采集产品 {asin} 的详情数据")
            
            return data
            
        except requests.exceptions.RequestException as e:
            logger.error(f"采集产品详情失败: {e}")
            return {}
    
    def get_bestsellers_rank(self, 
                            category: str, 
                            marketplace: str = "US") -> List[Dict]:
        """
        采集Best Sellers榜单数据
        
        Args:
            category: 类目ID或名称
            marketplace: 市场代码
            
        Returns:
            榜单产品列表
        """
        endpoint = f"{self.base_url}/scrape/amazon/bestsellers"
        
        payload = {
            "category": category,
            "marketplace": marketplace
        }
        
        try:
            response = self.session.post(endpoint, json=payload, timeout=30)
            response.raise_for_status()
            
            data = response.json()
            logger.info(f"成功采集类目 '{category}' 的榜单数据,共 {len(data.get('products', []))} 个产品")
            
            return data.get('products', [])
            
        except requests.exceptions.RequestException as e:
            logger.error(f"采集榜单数据失败: {e}")
            return []

2.2 关键词排名追踪

class KeywordRankTracker:
    """
    关键词排名追踪器
    """
    
    def __init__(self, collector: AmazonDataCollector):
        self.collector = collector
    
    def track_keyword_ranking(self, 
                             target_asin: str,
                             keywords: List[str],
                             marketplace: str = "US") -> List[Dict]:
        """
        追踪目标ASIN在多个关键词下的排名
        
        Args:
            target_asin: 目标产品ASIN
            keywords: 关键词列表
            marketplace: 市场代码
            
        Returns:
            排名数据列表
        """
        ranking_data = []
        
        for keyword in keywords:
            # 采集搜索结果
            search_results = self.collector.search_products(
                keyword=keyword,
                marketplace=marketplace
            )
            
            # 查找目标ASIN的排名
            organic_rank = self._find_product_rank(
                search_results.get('organic_results', []),
                target_asin
            )
            
            sponsored_rank = self._find_product_rank(
                search_results.get('sponsored_results', []),
                target_asin
            )
            
            ranking_data.append({
                'keyword': keyword,
                'asin': target_asin,
                'organic_rank': organic_rank,
                'sponsored_rank': sponsored_rank,
                'timestamp': datetime.now().isoformat(),
                'marketplace': marketplace
            })
            
            logger.info(f"关键词 '{keyword}': 自然排名={organic_rank}, 广告排名={sponsored_rank}")
        
        return ranking_data
    
    def _find_product_rank(self, results: List[Dict], target_asin: str) -> Optional[int]:
        """
        在搜索结果中查找产品排名
        
        Args:
            results: 搜索结果列表
            target_asin: 目标ASIN
            
        Returns:
            排名位置(从1开始),未找到返回None
        """
        for idx, product in enumerate(results):
            if product.get('asin') == target_asin:
                return idx + 1
        return None

2.3 竞品监控

class CompetitorMonitor:
    """
    竞品监控器
    """
    
    def __init__(self, collector: AmazonDataCollector):
        self.collector = collector
    
    def monitor_competitors(self, 
                           competitor_asins: List[str],
                           marketplace: str = "US") -> List[Dict]:
        """
        监控竞品的关键指标
        
        Args:
            competitor_asins: 竞品ASIN列表
            marketplace: 市场代码
            
        Returns:
            竞品数据列表
        """
        competitor_data = []
        
        for asin in competitor_asins:
            product_details = self.collector.get_product_details(
                asin=asin,
                marketplace=marketplace
            )
            
            if product_details:
                competitor_data.append({
                    'asin': asin,
                    'title': product_details.get('title'),
                    'price': product_details.get('price'),
                    'currency': product_details.get('currency'),
                    'rating': product_details.get('rating'),
                    'review_count': product_details.get('review_count'),
                    'in_stock': product_details.get('availability', {}).get('in_stock'),
                    'bsr': product_details.get('best_sellers_rank'),
                    'timestamp': datetime.now().isoformat(),
                    'marketplace': marketplace
                })
                
                logger.info(f"竞品 {asin}: 价格={product_details.get('price')}, 库存={product_details.get('availability', {}).get('in_stock')}")
        
        return competitor_data

三、数据存储模块

3.1 数据库设计

-- 关键词排名历史表
CREATE TABLE keyword_rankings (
    id SERIAL PRIMARY KEY,
    asin VARCHAR(20) NOT NULL,
    keyword VARCHAR(255) NOT NULL,
    organic_rank INTEGER,
    sponsored_rank INTEGER,
    marketplace VARCHAR(10) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_asin_keyword (asin, keyword),
    INDEX idx_created_at (created_at)
);

-- 竞品监控历史表
CREATE TABLE competitor_data (
    id SERIAL PRIMARY KEY,
    asin VARCHAR(20) NOT NULL,
    title TEXT,
    price DECIMAL(10, 2),
    currency VARCHAR(10),
    rating DECIMAL(3, 2),
    review_count INTEGER,
    in_stock BOOLEAN,
    bsr INTEGER,
    marketplace VARCHAR(10) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_asin (asin),
    INDEX idx_created_at (created_at)
);

-- 流量异常告警表
CREATE TABLE traffic_alerts (
    id SERIAL PRIMARY KEY,
    asin VARCHAR(20) NOT NULL,
    alert_type VARCHAR(50) NOT NULL,
    severity VARCHAR(20) NOT NULL,
    message TEXT,
    details JSONB,
    is_resolved BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    resolved_at TIMESTAMP,
    INDEX idx_asin (asin),
    INDEX idx_alert_type (alert_type),
    INDEX idx_is_resolved (is_resolved)
);

3.2 数据持久化

import psycopg2
from psycopg2.extras import execute_batch
from typing import List, Dict

class DataStorage:
    """
    数据存储管理器
    """
    
    def __init__(self, db_config: Dict):
        self.conn = psycopg2.connect(**db_config)
        self.cursor = self.conn.cursor()
    
    def save_keyword_rankings(self, rankings: List[Dict]) -> int:
        """
        批量保存关键词排名数据
        
        Args:
            rankings: 排名数据列表
            
        Returns:
            插入的记录数
        """
        sql = """
            INSERT INTO keyword_rankings 
            (asin, keyword, organic_rank, sponsored_rank, marketplace, created_at)
            VALUES (%(asin)s, %(keyword)s, %(organic_rank)s, %(sponsored_rank)s, 
                    %(marketplace)s, %(timestamp)s)
        """
        
        try:
            execute_batch(self.cursor, sql, rankings)
            self.conn.commit()
            logger.info(f"成功保存 {len(rankings)} 条排名数据")
            return len(rankings)
        except Exception as e:
            self.conn.rollback()
            logger.error(f"保存排名数据失败: {e}")
            return 0
    
    def save_competitor_data(self, competitors: List[Dict]) -> int:
        """
        批量保存竞品数据
        
        Args:
            competitors: 竞品数据列表
            
        Returns:
            插入的记录数
        """
        sql = """
            INSERT INTO competitor_data 
            (asin, title, price, currency, rating, review_count, 
             in_stock, bsr, marketplace, created_at)
            VALUES (%(asin)s, %(title)s, %(price)s, %(currency)s, %(rating)s,
                    %(review_count)s, %(in_stock)s, %(bsr)s, %(marketplace)s, %(timestamp)s)
        """
        
        try:
            execute_batch(self.cursor, sql, competitors)
            self.conn.commit()
            logger.info(f"成功保存 {len(competitors)} 条竞品数据")
            return len(competitors)
        except Exception as e:
            self.conn.rollback()
            logger.error(f"保存竞品数据失败: {e}")
            return 0
    
    def get_historical_rankings(self, 
                               asin: str, 
                               keyword: str, 
                               days: int = 30) -> List[Dict]:
        """
        获取历史排名数据
        
        Args:
            asin: 产品ASIN
            keyword: 关键词
            days: 查询天数
            
        Returns:
            历史排名列表
        """
        sql = """
            SELECT keyword, organic_rank, sponsored_rank, created_at
            FROM keyword_rankings
            WHERE asin = %s AND keyword = %s 
                  AND created_at >= NOW() - INTERVAL '%s days'
            ORDER BY created_at ASC
        """
        
        self.cursor.execute(sql, (asin, keyword, days))
        results = self.cursor.fetchall()
        
        return [
            {
                'keyword': row[0],
                'organic_rank': row[1],
                'sponsored_rank': row[2],
                'timestamp': row[3]
            }
            for row in results
        ]
    
    def close(self):
        """关闭数据库连接"""
        self.cursor.close()
        self.conn.close()

四、流量归因分析算法

4.1 流量来源归因

import pandas as pd
import numpy as np
from typing import Dict, List, Tuple

class TrafficAttributionAnalyzer:
    """
    流量归因分析器
    """
    
    def __init__(self, storage: DataStorage):
        self.storage = storage
    
    def analyze_traffic_source(self,
                               asin: str,
                               sales_change: float,
                               date_range: Tuple[str, str]) -> Dict:
        """
        分析流量来源变化
        
        Args:
            asin: 产品ASIN
            sales_change: 销量变化百分比
            date_range: 分析日期范围 (start_date, end_date)
            
        Returns:
            归因分析结果
        """
        # 1. 获取排名变化数据
        ranking_changes = self._analyze_ranking_changes(asin, date_range)
        
        # 2. 获取竞品变化数据
        competitor_changes = self._analyze_competitor_changes(asin, date_range)
        
        # 3. 计算各渠道贡献度
        attribution = self._calculate_attribution(
            sales_change,
            ranking_changes,
            competitor_changes
        )
        
        return attribution
    
    def _analyze_ranking_changes(self, 
                                 asin: str, 
                                 date_range: Tuple[str, str]) -> Dict:
        """
        分析排名变化
        """
        sql = """
            WITH ranked_data AS (
                SELECT 
                    keyword,
                    organic_rank,
                    created_at,
                    LAG(organic_rank) OVER (PARTITION BY keyword ORDER BY created_at) as prev_rank
                FROM keyword_rankings
                WHERE asin = %s 
                      AND created_at BETWEEN %s AND %s
            )
            SELECT 
                keyword,
                AVG(organic_rank) as avg_rank,
                AVG(prev_rank) as prev_avg_rank,
                COUNT(*) as data_points
            FROM ranked_data
            WHERE prev_rank IS NOT NULL
            GROUP BY keyword
        """
        
        self.storage.cursor.execute(sql, (asin, date_range[0], date_range[1]))
        results = self.storage.cursor.fetchall()
        
        ranking_changes = {}
        for row in results:
            keyword, avg_rank, prev_avg_rank, data_points = row
            if prev_avg_rank and avg_rank:
                change = prev_avg_rank - avg_rank  # 正值表示排名提升
                ranking_changes[keyword] = {
                    'current_rank': avg_rank,
                    'previous_rank': prev_avg_rank,
                    'change': change,
                    'improvement_pct': (change / prev_avg_rank * 100) if prev_avg_rank > 0 else 0
                }
        
        return ranking_changes
    
    def _analyze_competitor_changes(self, 
                                    asin: str, 
                                    date_range: Tuple[str, str]) -> List[Dict]:
        """
        分析竞品变化
        """
        sql = """
            SELECT 
                asin,
                AVG(CASE WHEN in_stock THEN 1 ELSE 0 END) as stock_rate,
                AVG(price) as avg_price,
                MIN(price) as min_price,
                MAX(price) as max_price
            FROM competitor_data
            WHERE created_at BETWEEN %s AND %s
            GROUP BY asin
        """
        
        self.storage.cursor.execute(sql, (date_range[0], date_range[1]))
        results = self.storage.cursor.fetchall()
        
        competitor_changes = []
        for row in results:
            comp_asin, stock_rate, avg_price, min_price, max_price = row
            
            # 检测断货
            if stock_rate < 0.5:  # 50%以上时间断货
                competitor_changes.append({
                    'asin': comp_asin,
                    'type': 'stockout',
                    'severity': 'high' if stock_rate < 0.2 else 'medium'
                })
            
            # 检测价格大幅波动
            if avg_price and max_price and min_price:
                price_volatility = (max_price - min_price) / avg_price
                if price_volatility > 0.2:  # 价格波动超过20%
                    competitor_changes.append({
                        'asin': comp_asin,
                        'type': 'price_change',
                        'volatility': price_volatility,
                        'severity': 'high' if price_volatility > 0.3 else 'medium'
                    })
        
        return competitor_changes
    
    def _calculate_attribution(self,
                               sales_change: float,
                               ranking_changes: Dict,
                               competitor_changes: List[Dict]) -> Dict:
        """
        计算流量归因
        """
        attribution = {
            'primary_source': None,
            'confidence': 0.0,
            'contributing_factors': [],
            'recommendations': []
        }
        
        # 评分系统
        scores = {
            'organic_ranking': 0,
            'competitor_impact': 0,
            'paid_advertising': 0,
            'external_traffic': 0
        }
        
        # 1. 评估排名变化的影响
        significant_ranking_improvements = [
            k for k, v in ranking_changes.items() 
            if v['change'] > 5  # 排名提升超过5位
        ]
        
        if significant_ranking_improvements:
            # 排名提升越多,有机流量贡献越大
            avg_improvement = np.mean([
                ranking_changes[k]['change'] 
                for k in significant_ranking_improvements
            ])
            scores['organic_ranking'] = min(avg_improvement * 10, 100)
            
            attribution['contributing_factors'].append({
                'factor': 'organic_ranking_improvement',
                'keywords': significant_ranking_improvements,
                'impact_score': scores['organic_ranking']
            })
        
        # 2. 评估竞品影响
        high_severity_competitor_issues = [
            c for c in competitor_changes 
            if c.get('severity') == 'high'
        ]
        
        if high_severity_competitor_issues:
            scores['competitor_impact'] = len(high_severity_competitor_issues) * 20
            
            attribution['contributing_factors'].append({
                'factor': 'competitor_issues',
                'details': high_severity_competitor_issues,
                'impact_score': scores['competitor_impact']
            })
        
        # 3. 确定主要来源
        max_score_source = max(scores, key=scores.get)
        attribution['primary_source'] = max_score_source
        attribution['confidence'] = min(scores[max_score_source] / 100, 0.95)
        
        # 4. 生成建议
        if max_score_source == 'organic_ranking':
            attribution['recommendations'].append(
                "排名提升是主要流量来源,建议增加广告预算巩固排名位置"
            )
        elif max_score_source == 'competitor_impact':
            attribution['recommendations'].append(
                "竞品出现问题导致流量转移,建议加快补货并优化listing以抓住机会"
            )
        
        return attribution

五、异常检测与告警

5.1 异常检测算法

from scipy import stats
from typing import Optional

class AnomalyDetector:
    """
    异常检测器
    使用统计方法检测流量异常
    """
    
    def __init__(self, storage: DataStorage, threshold: float = 2.0):
        """
        Args:
            storage: 数据存储对象
            threshold: Z-score阈值(默认2.0,即2个标准差)
        """
        self.storage = storage
        self.threshold = threshold
    
    def detect_ranking_anomaly(self, 
                               asin: str, 
                               keyword: str,
                               current_rank: int,
                               lookback_days: int = 30) -> Optional[Dict]:
        """
        检测排名异常
        
        Args:
            asin: 产品ASIN
            keyword: 关键词
            current_rank: 当前排名
            lookback_days: 回溯天数
            
        Returns:
            异常信息字典,无异常返回None
        """
        # 获取历史排名数据
        historical_data = self.storage.get_historical_rankings(
            asin, keyword, lookback_days
        )
        
        if len(historical_data) < 7:  # 数据不足
            return None
        
        # 提取排名值
        ranks = [d['organic_rank'] for d in historical_data if d['organic_rank']]
        
        if not ranks:
            return None
        
        # 计算统计指标
        mean_rank = np.mean(ranks)
        std_rank = np.std(ranks)
        
        if std_rank == 0:  # 排名完全稳定
            return None
        
        # 计算Z-score
        z_score = abs((current_rank - mean_rank) / std_rank)
        
        if z_score > self.threshold:
            # 检测到异常
            anomaly_type = 'ranking_drop' if current_rank > mean_rank else 'ranking_improvement'
            
            return {
                'type': anomaly_type,
                'keyword': keyword,
                'current_rank': current_rank,
                'historical_mean': mean_rank,
                'z_score': z_score,
                'severity': 'high' if z_score > 3.0 else 'medium',
                'message': f"关键词 '{keyword}' 排名异常: 当前{current_rank}位,历史平均{mean_rank:.1f}位"
            }
        
        return None
    
    def detect_competitor_anomaly(self, 
                                  competitor_asin: str,
                                  current_data: Dict,
                                  lookback_days: int = 30) -> List[Dict]:
        """
        检测竞品异常
        
        Args:
            competitor_asin: 竞品ASIN
            current_data: 当前竞品数据
            lookback_days: 回溯天数
            
        Returns:
            异常列表
        """
        anomalies = []
        
        # 检测断货
        if not current_data.get('in_stock'):
            anomalies.append({
                'type': 'competitor_stockout',
                'asin': competitor_asin,
                'severity': 'high',
                'message': f"竞品 {competitor_asin} 断货"
            })
        
        # 检测价格异常
        sql = """
            SELECT AVG(price) as avg_price, STDDEV(price) as std_price
            FROM competitor_data
            WHERE asin = %s 
                  AND created_at >= NOW() - INTERVAL '%s days'
                  AND price IS NOT NULL
        """
        
        self.storage.cursor.execute(sql, (competitor_asin, lookback_days))
        result = self.storage.cursor.fetchone()
        
        if result and result[0] and result[1]:
            avg_price, std_price = result
            current_price = current_data.get('price')
            
            if current_price and std_price > 0:
                price_z_score = abs((current_price - avg_price) / std_price)
                
                if price_z_score > self.threshold:
                    anomaly_type = 'price_drop' if current_price < avg_price else 'price_increase'
                    
                    anomalies.append({
                        'type': anomaly_type,
                        'asin': competitor_asin,
                        'current_price': current_price,
                        'historical_avg': avg_price,
                        'z_score': price_z_score,
                        'severity': 'high' if price_z_score > 3.0 else 'medium',
                        'message': f"竞品 {competitor_asin} 价格异常: 当前${current_price:.2f},历史平均${avg_price:.2f}"
                    })
        
        return anomalies

5.2 告警通知

import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import requests

class AlertNotifier:
    """
    告警通知器
    支持Email和Slack通知
    """
    
    def __init__(self, email_config: Dict = None, slack_webhook: str = None):
        self.email_config = email_config
        self.slack_webhook = slack_webhook
    
    def send_email_alert(self, subject: str, body: str, recipients: List[str]):
        """
        发送邮件告警
        """
        if not self.email_config:
            logger.warning("邮件配置未设置,跳过邮件发送")
            return
        
        msg = MIMEMultipart()
        msg['From'] = self.email_config['from']
        msg['To'] = ', '.join(recipients)
        msg['Subject'] = subject
        
        msg.attach(MIMEText(body, 'html'))
        
        try:
            with smtplib.SMTP(self.email_config['smtp_host'], self.email_config['smtp_port']) as server:
                server.starttls()
                server.login(self.email_config['username'], self.email_config['password'])
                server.send_message(msg)
            
            logger.info(f"成功发送邮件告警: {subject}")
        except Exception as e:
            logger.error(f"发送邮件失败: {e}")
    
    def send_slack_alert(self, message: str, severity: str = 'medium'):
        """
        发送Slack告警
        """
        if not self.slack_webhook:
            logger.warning("Slack Webhook未设置,跳过Slack通知")
            return
        
        # 根据严重程度设置颜色
        color_map = {
            'low': '#36a64f',      # 绿色
            'medium': '#ff9900',   # 橙色
            'high': '#ff0000'      # 红色
        }
        
        payload = {
            'attachments': [{
                'color': color_map.get(severity, '#808080'),
                'title': '亚马逊流量异常告警',
                'text': message,
                'footer': 'Amazon Traffic Monitor',
                'ts': int(datetime.now().timestamp())
            }]
        }
        
        try:
            response = requests.post(self.slack_webhook, json=payload)
            response.raise_for_status()
            logger.info("成功发送Slack告警")
        except Exception as e:
            logger.error(f"发送Slack告警失败: {e}")

六、定时任务调度

6.1 使用APScheduler实现定时采集

from apscheduler.schedulers.blocking import BlockingScheduler
from apscheduler.triggers.cron import CronTrigger

class MonitoringScheduler:
    """
    监控任务调度器
    """
    
    def __init__(self, 
                 collector: AmazonDataCollector,
                 storage: DataStorage,
                 detector: AnomalyDetector,
                 notifier: AlertNotifier):
        self.collector = collector
        self.storage = storage
        self.detector = detector
        self.notifier = notifier
        self.scheduler = BlockingScheduler()
    
    def setup_jobs(self, config: Dict):
        """
        设置定时任务
        
        Args:
            config: 配置字典,包含监控目标和调度时间
        """
        # 每天凌晨2点采集关键词排名
        self.scheduler.add_job(
            func=self.daily_ranking_job,
            trigger=CronTrigger(hour=2, minute=0),
            args=[config['target_asin'], config['keywords']],
            id='daily_ranking',
            name='每日关键词排名采集'
        )
        
        # 每6小时监控竞品
        self.scheduler.add_job(
            func=self.competitor_monitoring_job,
            trigger=CronTrigger(hour='*/6'),
            args=[config['competitor_asins']],
            id='competitor_monitoring',
            name='竞品监控'
        )
        
        # 每小时检测异常
        self.scheduler.add_job(
            func=self.anomaly_detection_job,
            trigger=CronTrigger(minute=0),
            args=[config['target_asin'], config['keywords']],
            id='anomaly_detection',
            name='异常检测'
        )
    
    def daily_ranking_job(self, target_asin: str, keywords: List[str]):
        """
        每日排名采集任务
        """
        logger.info("开始执行每日排名采集任务")
        
        tracker = KeywordRankTracker(self.collector)
        rankings = tracker.track_keyword_ranking(target_asin, keywords)
        
        # 保存到数据库
        self.storage.save_keyword_rankings(rankings)
        
        logger.info(f"每日排名采集完成,共采集 {len(rankings)} 条数据")
    
    def competitor_monitoring_job(self, competitor_asins: List[str]):
        """
        竞品监控任务
        """
        logger.info("开始执行竞品监控任务")
        
        monitor = CompetitorMonitor(self.collector)
        competitor_data = monitor.monitor_competitors(competitor_asins)
        
        # 保存到数据库
        self.storage.save_competitor_data(competitor_data)
        
        # 检测竞品异常
        for data in competitor_data:
            anomalies = self.detector.detect_competitor_anomaly(
                data['asin'], 
                data
            )
            
            for anomaly in anomalies:
                self._handle_anomaly(anomaly)
        
        logger.info(f"竞品监控完成,共监控 {len(competitor_data)} 个竞品")
    
    def anomaly_detection_job(self, target_asin: str, keywords: List[str]):
        """
        异常检测任务
        """
        logger.info("开始执行异常检测任务")
        
        # 获取最新排名数据
        tracker = KeywordRankTracker(self.collector)
        current_rankings = tracker.track_keyword_ranking(target_asin, keywords)
        
        # 检测排名异常
        for ranking in current_rankings:
            if ranking['organic_rank']:
                anomaly = self.detector.detect_ranking_anomaly(
                    target_asin,
                    ranking['keyword'],
                    ranking['organic_rank']
                )
                
                if anomaly:
                    self._handle_anomaly(anomaly)
        
        logger.info("异常检测完成")
    
    def _handle_anomaly(self, anomaly: Dict):
        """
        处理检测到的异常
        """
        # 记录到数据库
        sql = """
            INSERT INTO traffic_alerts 
            (asin, alert_type, severity, message, details)
            VALUES (%s, %s, %s, %s, %s)
        """
        
        self.storage.cursor.execute(sql, (
            anomaly.get('asin', 'unknown'),
            anomaly['type'],
            anomaly['severity'],
            anomaly['message'],
            json.dumps(anomaly)
        ))
        self.storage.conn.commit()
        
        # 发送告警通知
        if anomaly['severity'] == 'high':
            self.notifier.send_slack_alert(
                message=anomaly['message'],
                severity=anomaly['severity']
            )
    
    def start(self):
        """
        启动调度器
        """
        logger.info("监控调度器启动")
        self.scheduler.start()

七、数据可视化

7.1 使用Matplotlib生成趋势图

import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from datetime import datetime, timedelta

class DataVisualizer:
    """
    数据可视化器
    """
    
    def __init__(self, storage: DataStorage):
        self.storage = storage
        plt.rcParams['font.sans-serif'] = ['SimHei']  # 支持中文
        plt.rcParams['axes.unicode_minus'] = False
    
    def plot_ranking_trend(self, 
                          asin: str, 
                          keyword: str, 
                          days: int = 30,
                          save_path: str = None):
        """
        绘制排名趋势图
        """
        # 获取历史数据
        data = self.storage.get_historical_rankings(asin, keyword, days)
        
        if not data:
            logger.warning(f"没有找到 {keyword} 的历史数据")
            return
        
        # 准备数据
        dates = [d['timestamp'] for d in data]
        organic_ranks = [d['organic_rank'] if d['organic_rank'] else None for d in data]
        
        # 创建图表
        fig, ax = plt.subplots(figsize=(12, 6))
        
        ax.plot(dates, organic_ranks, marker='o', linestyle='-', linewidth=2, 
                markersize=4, label='自然排名')
        
        # 设置标题和标签
        ax.set_title(f'关键词 "{keyword}" 排名趋势 (ASIN: {asin})', fontsize=14, fontweight='bold')
        ax.set_xlabel('日期', fontsize=12)
        ax.set_ylabel('排名位置', fontsize=12)
        
        # 反转Y轴(排名越小越好)
        ax.invert_yaxis()
        
        # 格式化日期
        ax.xaxis.set_major_formatter(mdates.DateFormatter('%m-%d'))
        ax.xaxis.set_major_locator(mdates.DayLocator(interval=3))
        plt.xticks(rotation=45)
        
        # 添加网格
        ax.grid(True, alpha=0.3, linestyle='--')
        
        # 添加图例
        ax.legend(loc='best')
        
        plt.tight_layout()
        
        if save_path:
            plt.savefig(save_path, dpi=300, bbox_inches='tight')
            logger.info(f"排名趋势图已保存到 {save_path}")
        else:
            plt.show()
        
        plt.close()

八、完整使用示例

8.1 主程序

def main():
    """
    主程序入口
    """
    # 配置参数
    config = {
        'api_key': 'your_pangolinfo_api_key',
        'db_config': {
            'host': 'localhost',
            'port': 5432,
            'database': 'amazon_monitor',
            'user': 'postgres',
            'password': 'your_password'
        },
        'target_asin': 'B08XYZ1234',
        'keywords': [
            'silicone baking mat',
            'non-stick baking sheet',
            'reusable baking liner'
        ],
        'competitor_asins': [
            'B07ABC1234',
            'B09DEF5678',
            'B06GHI9012'
        ],
        'email_config': {
            'from': 'monitor@example.com',
            'smtp_host': 'smtp.gmail.com',
            'smtp_port': 587,
            'username': 'your_email@gmail.com',
            'password': 'your_app_password'
        },
        'slack_webhook': 'https://hooks.slack.com/services/YOUR/WEBHOOK/URL'
    }
    
    # 初始化组件
    collector = AmazonDataCollector(config['api_key'])
    storage = DataStorage(config['db_config'])
    detector = AnomalyDetector(storage, threshold=2.0)
    notifier = AlertNotifier(
        email_config=config['email_config'],
        slack_webhook=config['slack_webhook']
    )
    
    # 设置调度器
    scheduler = MonitoringScheduler(collector, storage, detector, notifier)
    scheduler.setup_jobs(config)
    
    # 启动监控
    try:
        scheduler.start()
    except (KeyboardInterrupt, SystemExit):
        logger.info("监控系统已停止")
    finally:
        storage.close()

if __name__ == '__main__':
    main()

九、性能优化建议

9.1 数据采集优化

  1. 并发请求:使用asyncio和aiohttp实现异步并发采集
  2. 请求限流:避免触发API速率限制
  3. 缓存机制:使用Redis缓存热点数据
  4. 增量更新:只采集变化的数据,减少API调用

9.2 数据库优化

  1. 索引优化:在常用查询字段上建立索引
  2. 分区表:按时间分区存储历史数据
  3. 定期归档:将旧数据归档到冷存储
  4. 连接池:使用数据库连接池提高性能

9.3 监控优化

  1. 采样策略:对于大量SKU,采用分批采样
  2. 优先级队列:重要产品优先监控
  3. 自适应频率:根据变化频率动态调整采集间隔

十、常见问题与解决方案

Q1: API调用频率限制怎么办?

A: Pangolinfo API支持较高的并发,但建议:

  • 使用请求队列控制并发数
  • 实现指数退避重试机制
  • 合理设置采集间隔

Q2: 数据库存储空间不足?

A: 实施数据生命周期管理:

  • 保留最近3个月的详细数据
  • 3-12个月的数据按天聚合
  • 12个月以上的数据归档或删除

Q3: 如何提高异常检测准确率?

A:

  • 增加历史数据积累时间(至少30天)
  • 调整Z-score阈值(根据业务特点)
  • 结合多个指标综合判断
  • 引入机器学习模型(如Isolation Forest)

十一、总结与展望

本文详细介绍了如何构建一个完整的亚马逊listing流量分析系统,涵盖了数据采集、存储、分析、异常检测和可视化的全流程实现。

核心要点:

  1. 使用Pangolinfo Scrape API获取准确的实时数据
  2. 建立时间序列数据库存储历史数据
  3. 实现统计学方法进行异常检测
  4. 通过流量归因算法定位流量来源
  5. 自动化监控和及时告警

下一步优化方向:

  • 引入机器学习模型提升预测准确性
  • 增加更多数据源(如Google Trends、社交媒体)
  • 开发Web Dashboard实现可视化管理
  • 集成A/B测试功能优化listing

作者简介:资深电商数据工程师,专注于亚马逊数据分析和自动化系统开发。

原创声明:本文为原创技术文章,转载请注明出处。

#亚马逊 #数据分析 #Python #API #流量监控 #电商技术

IPSec Over GRE和GRE Over IPSec技术 IPSec Over GRE 和 GRE Over IPSec 一. 技术介绍 IPsec     主要作用是对数据进行加密,因为他能提供所有有时候被单独用作实现加密的一种方法!IPsec建立的是一个逻辑隧道,并不是真正意义上的隧道!并且不能提供路由功能,因为IPsec不支持非ip流量,也不支持广播(组播)! GRE     (通用路由封装)能很好的提供一个真正意义上的点对点的隧道,GRE是一种三层VPN封装技术。GRE可以对某些网络层协议(如IPX、Apple Talk、IP等)的报文进行封装,使封装后 阅读详情

相关推荐

Synopsys工具链实战:如何用VCS、DC、ICC和Calibre跑通你的第一个数字IC设计项目?

本文详细介绍了如何使用Synopsys工具链(VCS、Design Compiler、ICC和Calibre)完成数字IC设计项目,从RTL代码到GDSII版图的全流程。涵盖环境配置、RTL设计验证、逻辑综合、物理实现及签核验证等关键步骤,帮助工程师掌握高效的数字IC设计流程。

weixin_42525005的博客 196

亚马逊Listing流量分析系统:从数据采集到异常检测的完整实现

在跨境电商运营中,流量数据是发现早期问题、优化运营决策的核心依据。亚马逊后台提供的Business Report仅能反映Session、转化率等总量指标,难以揭示流量结构变化,更无法支撑多店铺、多SKU场景下的高效监控。本文从数据采集、指标口径统一、数仓分层建模入手,系统讲解如何构建一套覆盖流量规模、质量、结构与节奏的Listing流量分析体系。在此基础上,介绍基于MAD鲁棒Z-Score、周同比及业务规则引擎的异常检测组合方案,并结合实际踩坑经验,展示如何通过报警分级、归因时间线与可视化看板,实现从数据监

weixin_30607125的博客 219

Matlab激活失败?手把手教你解决License checkout failed问题(附详细截图)

本文针对Matlab激活后出现“License checkout failed”错误的常见问题,提供了详细的解决方案。核心在于精准定位并正确填写Windows系统用户名,而非计算机名或账户全名。文章通过分步截图和命令指引,手把手教你使用`whoami`命令获取正确信息,并重新运行激活程序,从根本上解决因用户名填写错误导致的许可检查失败问题。

weixin_29062823的博客 570

AI全栈实战 | 3.9-01 爬虫入门:requests 加 BeautifulSoup 三分钟跑通,但 90% 的人栽在这 4 个“礼貌“问题上

⚠️:本文内容仅用于。请遵守目标站点的服务条款与相关法律法规,不得对目标服务造成负担,不得采集、传播个人隐私数据。技术是中性的,边界在你手里。爬虫的入门技术(requests 发请求 + BeautifulSoup 解析)三分钟就能学会。真正区分"会用爬虫"和"会做爬虫的人"的,是这篇我们跑通"请求→解析→存储"的最小闭环,然后重点把"合规与礼貌"这根弦给你绷上。

源码获取 | 运行部署 | 定制二开修改,如果回复不及时请耐心等待,请见谅。 399

Arxml文件可视化:当汽车软件架构遇见交互式设计工具

本文探讨了Arxml文件在汽车电子系统开发中的可视化技术,结合AUTOSAR架构标准,介绍了如何利用现代Web技术构建交互式设计工具。通过三维组件关系图、动态数据流展示和版本对比等功能,显著提升开发效率和架构评审质量,适用于MBD和ASPICE开发流程。

weixin_29050271的博客 427

5分钟搞定!用OpenCV实现全景图像柱面展开的保姆级教程(附完整代码)

本文提供了一份使用OpenCV实现全景图像柱面展开的详细教程。通过清晰的数学原理讲解和分步代码演示,指导读者快速掌握从等距柱状投影全景图到平面展开图的核心转换方法,并涵盖性能优化、参数调整及透视视图扩展等实用技巧,适合图像处理爱好者和开发者实践。

sony5的博客 572

从一段视频到可漫游的 4D 有声场景:思路拆解与最小复现手记

只需一段手机视频,你就能生成可"走进去"的 4D 有声场景:转头时狗叫声方向随之变化,走近时声音更清晰。本文拆解 Video-to-4DAVScene 的核心思路,手把手带你用几十行 Python 搭出最小管线,把视频中的运动物体变成带空间位置的声源,并附上三个真实踩坑的排障经验,代码可直接跑。🎧

helloagent的博客 232

【Flink】状态编程: 订单超时告警

文章目录一、基础概念二、Flink状态编程1、支持的状态类型2、Managed Keyed State六种类型基本API状态的生命周期3、Managed Operator State三、案例:订单延迟告警统计1、需求描述2、需求分析3、数据与模型4、详细实现 一、基础概念 在Flink架构体系中,有状态计算可以说是Flink非常重要的特性之一。 有状态计算是指: 在程序计算过程中,在Flink...

一只皮皮熊 2862

Python+Agent入门笔记(二):列表推导式、模块和异常,这些写 Agent 前得会

接上一篇《Python+Agent入门笔记(一)》。基础语法跑通之后,这篇聊几块日常写脚本绕不开的东西。还是按 JS/Go 开发者的视角写,尽量记录我实际卡过的地方。

qq_41241612的博客 430

35、使用 Geo 创建 GitLab 的分布式只读副本

本文详细介绍了如何使用GitLab Geo功能创建分布式只读副本,涵盖数据库主从配置、SSH密钥查找方式更改、通过Web UI添加从节点、激活哈希存储及状态检查等关键步骤。同时提供了常见问题解答和配置流程图,帮助用户快速部署并维护Geo集群,实现跨地域的高效数据同步与高可用架构。

chair的专栏 80

【AUTOSAR-COM】-2.3_通过PduGroup批量控制CAN Frame/Pdu的接收

-->如果实现类似UDS诊断 $28服务,控制所有APP CAN Rx Frame/Pdu/报文接收; -->自定义过滤CAN信号的接收。

「汽车电子助手」的博客 1989

【每天一个 Python 小案例30】Python 基础知识综合小项目:我的工具盒

作为系列收官,本篇把前 29 篇的知识点合成一个能直接用的命令行工具"我的工具盒":计算器(用 ast 解析表达式,不碰 eval)、单位换算与温度转换(一张换算表 + 特例分支)、记账与分类统计(Decimal + JSON 持久化)、以及用"字典当命令表"把菜单和业务逻辑解耦的主循环。文中对比了 eval 的安全隐患与 ast 白名单的做法,解释了 lambda 闭包在字典里的写法与晚绑定陷阱,并在最后给出整个系列的知识地图,把变量、数据结构、控制流、函数、类、异常、文件、标准库八块内容对应到具体篇目。

xinjiyaya的博客 419

告别调参难!YOLO超参数优化指南(网格搜索+贝叶斯优化实战)

YOLO超参数优化的本质,不是“试遍所有参数”,而是“用工具化方法,在场景约束内找到最优平衡”——网格搜索稳、贝叶斯优化快,两者结合(先粗搜再精调)是工业场景的最优解。先定范围:基于工业场景(小目标/边缘设备/多类别)缩小参数搜索范围,避免无效探索。先调核心:优先优化数据增强、学习率、输入尺寸,这些参数对精度影响最大。工具赋能:用网格搜索+贝叶斯优化替代人工试错,再用TensorBoard监控,让调参“有据可依”。

专注于Python爬虫开发,分享爬虫技巧、项目实战与反爬经验,使用Scrapy、BeautifulSoup等工具,解决数据抓取难题。 698

详解Python条件语句

在 Python 中,if...else 是一种条件语句,可以根据给定的条件执行不同的操作。这个语句通常用于控制程序的流程。

2601_96798737的博客 205

温度PID控制PLC程序

温度PID稳定控制程序,适用于欧姆龙PLC,对其他PLC也具有很大参考价值

C# WinForm 基于DirectShow.dll编写的程序,可边预览边保存视频,音频和图片

基于微软的DirectShow.dll的二次开发1.自动识别连接的摄像头跟麦克风且可以对其进行选择(默认选中第一个)2.可录制wmv和avi两种格式的视频并且都带有音频,avi可以录制的分辨率和帧数进行选择,wmv默认进行了淸晰度优化如不喜欢可去除代码中有相关提示3.可在预览点开之后进行拍照,也可边录制视频边拍照4.添加了视频和音频的压缩功能(酌情使用)这次二次开发发现C#对DirectShow的相关信息太少,这次做完也不敢藏私共享出来,还望大家指正

Python3 协程:异步编程的核心机制与实战

协程是解决I/O密集型任务等待瓶颈的高效机制,相比线程更轻量、无锁开销。从yield到async/await的演进,使异步编程语义清晰、易于理解。通过async/await可实现并发执行多个任务(如gather、create_task),显著提升网络请求、爬虫等场景性能。关键在于使用异步库(如aiohttp)替代同步库,避免阻塞事件循环。合理控制超时与并发数,才能发挥协程最大优势。

Lxn2zh的博客 159

文档:亲测可用解决vnc灰屏,黑屏,鼠标大黑叉情况

主要解决vnc-viewer灰屏,黑屏,鼠标大黑叉等现象,详细见文档。

python如何控制浏览器

Python 可以通过 Selenium、Playwright 等工具实现浏览器自动化,从打开网页到执行复杂交互操作均可脚本化完成。Selenium生态成熟

2601_96797912的博客 76

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

torch.compile 差异仅 1e-6,边界样本预测为何翻转

torch.compile 加速后输出与 eager 不一致,部分预测翻转。逐层对比与 graph break 分析定位到动态分支翻转根因。容差回归仅适用于无分支翻转场景,零容忍场景应禁用编译。

昨日种种,皆成今我。今日种种,皆成新我。 158

二进制木马免杀-Python 分离免杀与 Web 挂马

二进制木马免杀-Python 分离免杀与 Web 挂马

2503_94637398的博客 267

Python 100讲课程导航:第08篇《Python input 与类型转换:构建可靠的交互程序》

本文详解Python中input()的返回类型为字符串,强调必须通过int()、float()等函数进行类型转换。重点讲解了输入验证的重要性,结合try-except处理ValueError等异常,实现可靠的交互程序。通过购物预算计算器实战,演示如何防范非法输入、处理边界值,并推荐使用Decimal避免浮点精度问题,提升程序健壮性。

铭渊老黄 134

【Python】国家中小学智慧教育平台电子教材批量下载与 PDF 合成实战

成果:两本语文电子课本(一年级上册 118 页 / 23MB、三年级上册 130 页 / 30.3MB),合计 248 页、约 54MB,单本下载加合成 40~60 秒,PDF 页数校验全部通过。经验教训SPA 站点的数据接口优先从 bundle 里挖,比抓页面稳得多;资源链接要按 private/public 分组看待,只取平台公开部分;页数、目录名这类“可推断”的信息,永远用实际响应验证一次;受限环境里“能不能跑浏览器”要在方案定稿前确认。后续计划:这版 PDF 是图片版,没有文字层;

木头大左的博客 125

魔塔社区微调训练Qwen2-1.5B-Instruct模型并烧录到‌RK3568开发板教程

本文基于Ubuntu 22.04 + CUDA 13.0.3环境,使用LLaMA-Factory框架对Qwen2-1.5B-Instruct模型进行LoRA微调,训练儿童故事生成专用模型。通过配置qwen模板、制作alpaca格式数据集、执行sft训练并合并LoRA权重,最终将模型转换为GGUF格式,并量化为Q4_K_M以适配RK3568嵌入式平台。全程支持国产镜像加速下载与显存优化,实现高效轻量化部署。

浅清秋慕夏的博客 185

【回眸】DeepSeek V4.1 Flash 快速上手与实战

本文介绍如何高效本地部署开源大模型,通过现代化工具链实现分钟级环境搭建。聚焦显存优化、一键安装、代码调用与多轮对话实现,解决“显存不足”“连接超时”等痛点,提供参数调优与结构化输出技巧,助力开发者在离线场景下快速构建稳定、高效的本地推理系统。

啤酒鸭的物联网开发记录博客 222

智叟云集-手机证据抓取系统的工程结构与应用入口

本文系统阐述手机证据抓取系统的工程结构与应用入口,涵盖四大层面:工程目录划分(Android、Python服务、Web页面、测试与构建工具);Android组件通过Manifest声明可启动页面,主界面由MainActivity管理;Python服务通过run.py解析参数并启动FastAPI应用,支持远程访问;浏览器页面采用HTML/CSS/JS分离架构,静态资源通过路径关联,实现动态交互。各模块分工明确,协同构建可运行的采集系统。

2301_77178996的博客 231

Java Arrays.asList 常见坑详解|UnsupportedOperationException、数组联动、基本类型 bug

解析 Arrays.asList 常见开发陷阱,演示 UnsupportedOperationException、底层数组引用共享、基本类型数组问题,区分 Arrays 内部 ArrayList 与 java.util.ArrayList,给出数组转 List 的各种场景正确代码。

weixin_42617033的博客 450

利用python中matplotlib.pyplot库绘制图

plt.plot(x,y) # x,y是每个点的坐标,plot()把每个点按顺序画折线。plt.subplots(2,2)表示绘制2*2的子图布局,axs[0,0]表示在第0行第0列绘制子图...coordinates_i[:,0]表示获取每一行(每一个spot)的第0列(x坐标)coordinates_i[:,1]表示获取每一行(每一个spot)的第0列(y坐标)plot.subplot(2,2,1) 表示子图布局是2*2的,开始绘制第一个。也可以导入数据绘制。可以设置color属性,设置颜色。

qq_46147236的博客 102

【每天一个 Python 小案例29】Python 实现商品购物车

本篇实现一个购物车:商品目录、加入商品、修改数量、移除商品、查看小计、计算满减与 VIP 折扣、结算扣库存。核心设计是"目录与购物车分离"——目录存商品主数据,购物车只存 {商品编号: 数量};金额一律用 Decimal 并做 ROUND_HALF_UP 舍入。折扣部分讲清阶梯满减的匹配顺序(阈值从高到低、命中即停)与叠加顺序(先满减、后打折),并用一次完整结算演示 706 元如何变成 613.70 元。

xinjiyaya的博客 192
上一篇: 2026年Amazon Listing优化完全指南:COSMO算法与Rufus AI技术解析
下一篇: AI驱动的亚马逊Listing优化实战指南(2026版)
devnullcoffee
博客等级 码龄1年 165粉丝 107原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值