商品价格又双叒叕被套路了?这6个Java爬虫技巧帮你追踪所有电商价格历史!

商品价格又双叒叕被套路了?这6个Java爬虫技巧帮你追踪所有电商价格历史!

作为一名后端开发,我见过太多电商价格的"套路现场":

  • 某购物节前商品价格先涨再降,"优惠"后比平时还贵100块
  • 某知名品牌手机"限时特价"999元,实际上一个月前就是这个价
  • 某电商平台"史上最低价"的笔记本,查了价格历史发现半年前更便宜

电商价格套路深如海,作为程序员的我们总不能坐以待毙吧?今天就教大家用Java写一个商品价格追踪系统,让所有价格猫腻无所遁形!

一、商品价格追踪系统到底是个啥?为啥这么有用?

商品价格追踪系统的核心就是:定期爬取电商网站的商品价格,记录价格变化历史,分析价格趋势

为啥要做价格追踪?

  • 省钱神器:知道商品的真实价格波动,避免被虚假促销套路
  • 最佳时机:找到商品的价格低点,在最便宜的时候下手
  • 商业价值:做电商数据分析,了解市场价格趋势
  • 技术提升:爬虫、数据存储、前端展示,全栈技能get

二、Java爬虫构建价格追踪系统的6个核心技巧

技巧1:选对爬虫框架,事半功倍

别再用原始的HttpURLConnection了,现在有更好的选择!

推荐方案:Selenium + ChromeDriver(应对反爬) + Jsoup(解析HTML)

@Component
public class WebDriverManager {
    
    private WebDriver driver;
    
    @PostConstruct
    public void initDriver() {
        // 配置Chrome选项
        ChromeOptions options = new ChromeOptions();
        options.addArguments("--headless"); // 无头模式
        options.addArguments("--no-sandbox");
        options.addArguments("--disable-dev-shm-usage");
        options.addArguments("--disable-gpu");
        options.addArguments("--window-size=1920,1080");
        
        // 设置User-Agent,模拟真实浏览器
        options.addArguments("--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
        
        // 禁用图片加载,提升速度
        Map<String, Object> prefs = new HashMap<>();
        prefs.put("profile.managed_default_content_settings.images", 2);
        options.setExperimentalOption("prefs", prefs);
        
        this.driver = new ChromeDriver(options);
        
        // 设置隐式等待
        driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));
    }
    
    public WebDriver getDriver() {
        return driver;
    }
    
    @PreDestroy
    public void quitDriver() {
        if (driver != null) {
            driver.quit();
        }
    }
}

技巧2:智能解析商品信息,应对页面变化

不同电商网站的页面结构千差万别,要做好适配。

@Component
public class ProductInfoExtractor {
    
    /**
     * 京东商品信息提取
     */
    public ProductInfo extractJDProduct(String url) {
        WebDriver driver = webDriverManager.getDriver();
        
        try {
            driver.get(url);
            
            // 等待价格元素加载
            WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(15));
            WebElement priceElement = wait.until(
                ExpectedConditions.presenceOfElementLocated(
                    By.cssSelector(".price-now, .p-price .price")
                )
            );
            
            // 提取商品信息
            String title = driver.findElement(By.cssSelector(".sku-name")).getText();
            String priceText = priceElement.getText().replaceAll("[^\\d.]", "");
            BigDecimal price = new BigDecimal(priceText);
            String imageUrl = driver.findElement(By.cssSelector(".spec-img")).getAttribute("src");
            
            return ProductInfo.builder()
                .title(title)
                .price(price)
                .imageUrl(imageUrl)
                .platform("京东")
                .url(url)
                .crawlTime(LocalDateTime.now())
                .build();
                
        } catch (Exception e) {
            log.error("京东商品信息提取失败: {}", url, e);
            throw new CrawlException("商品信息提取失败", e);
        }
    }
    
    /**
     * 淘宝商品信息提取
     */
    public ProductInfo extractTaobaoProduct(String url) {
        WebDriver driver = webDriverManager.getDriver();
        
        try {
            driver.get(url);
            
            // 淘宝需要特殊处理,可能有登录弹窗
            handleTaobaoPopup(driver);
            
            // 等待价格加载
            WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(20));
            WebElement priceElement = wait.until(
                ExpectedConditions.presenceOfElementLocated(
                    By.cssSelector(".notranslate, .tm-price-cur")
                )
            );
            
            String title = driver.findElement(By.cssSelector(".tb-main-title")).getText();
            String priceText = priceElement.getText().replaceAll("[^\\d.]", "");
            BigDecimal price = new BigDecimal(priceText);
            
            return ProductInfo.builder()
                .title(title)
                .price(price)
                .platform("淘宝")
                .url(url)
                .crawlTime(LocalDateTime.now())
                .build();
                
        } catch (Exception e) {
            log.error("淘宝商品信息提取失败: {}", url, e);
            throw new CrawlException("商品信息提取失败", e);
        }
    }
    
    /**
     * 通用商品信息提取(自动识别平台)
     */
    public ProductInfo extractProductInfo(String url) {
        if (url.contains("jd.com")) {
            return extractJDProduct(url);
        } else if (url.contains("taobao.com") || url.contains("tmall.com")) {
            return extractTaobaoProduct(url);
        } else if (url.contains("amazon.cn")) {
            return extractAmazonProduct(url);
        } else {
            throw new UnsupportedOperationException("暂不支持该电商平台: " + url);
        }
    }
}

技巧3:反爬虫策略,让你的爬虫更稳定

电商网站的反爬虫手段越来越厉害,我们要见招拆招。

@Component
public class AntiCrawlStrategy {
    
    private final Random random = new Random();
    private final List<String> userAgents = Arrays.asList(
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
        "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
        "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36"
    );
    
    /**
     * 随机延迟,模拟人工操作
     */
    public void randomDelay() {
        try {
            int delay = 2000 + random.nextInt(3000); // 2-5秒随机延迟
            Thread.sleep(delay);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
    
    /**
     * 随机切换User-Agent
     */
    public void switchUserAgent(WebDriver driver) {
        String userAgent = userAgents.get(random.nextInt(userAgents.size()));
        ((JavascriptExecutor) driver).executeScript(
            "Object.defineProperty(navigator, 'userAgent', {get: () => '" + userAgent + "'})"
        );
    }
    
    /**
     * 模拟鼠标移动
     */
    public void simulateHumanBehavior(WebDriver driver) {
        Actions actions = new Actions(driver);
        
        // 随机滚动页面
        JavascriptExecutor js = (JavascriptExecutor) driver;
        js.executeScript("window.scrollTo(0, " + random.nextInt(1000) + ")");
        
        randomDelay();
        
        // 随机移动鼠标
        actions.moveByOffset(random.nextInt(100), random.nextInt(100)).perform();
    }
    
    /**
     * 处理验证码(简单的图片验证码识别)
     */
    public boolean handleCaptcha(WebDriver driver) {
        try {
            // 检查是否有验证码
            List<WebElement> captchaElements = driver.findElements(
                By.cssSelector(".captcha, .verify-code, .security-code")
            );
            
            if (!captchaElements.isEmpty()) {
                log.warn("检测到验证码,尝试处理...");
                
                // 这里可以集成第三方验证码识别服务
                // 或者使用机器学习模型识别
                // 暂时返回false,需要人工处理
                return false;
            }
            
            return true;
        } catch (Exception e) {
            log.error("验证码处理失败", e);
            return false;
        }
    }
}

技巧4:数据存储和历史追踪,记录每一次价格变化

价格历史数据是核心,存储设计要合理。

@Entity
@Table(name = "product_price_history")
public class ProductPriceHistory {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = "product_id")
    private Long productId;
    
    @Column(name = "price", precision = 10, scale = 2)
    private BigDecimal price;
    
    @Column(name = "original_price", precision = 10, scale = 2)
    private BigDecimal originalPrice; // 原价
    
    @Column(name = "discount_info")
    private String discountInfo; // 促销信息
    
    @Column(name = "stock_status")
    private String stockStatus; // 库存状态
    
    @Column(name = "crawl_time")
    private LocalDateTime crawlTime;
    
    @Column(name = "platform")
    private String platform;
    
    // getters and setters...
}

@Service
public class ProductPriceService {
    
    @Autowired
    private ProductPriceHistoryRepository priceHistoryRepository;
    
    /**
     * 保存价格历史
     */
    public void savePriceHistory(ProductInfo productInfo) {
        // 检查是否是新的价格变化
        ProductPriceHistory lastRecord = priceHistoryRepository
            .findTopByProductIdOrderByCrawlTimeDesc(productInfo.getProductId());
        
        if (lastRecord == null || !lastRecord.getPrice().equals(productInfo.getPrice())) {
            // 价格有变化,保存新记录
            ProductPriceHistory history = new ProductPriceHistory();
            history.setProductId(productInfo.getProductId());
            history.setPrice(productInfo.getPrice());
            history.setOriginalPrice(productInfo.getOriginalPrice());
            history.setDiscountInfo(productInfo.getDiscountInfo());
            history.setStockStatus(productInfo.getStockStatus());
            history.setCrawlTime(productInfo.getCrawlTime());
            history.setPlatform(productInfo.getPlatform());
            
            priceHistoryRepository.save(history);
            
            // 如果价格下降,发送通知
            if (lastRecord != null && productInfo.getPrice().compareTo(lastRecord.getPrice()) < 0) {
                sendPriceDropNotification(productInfo, lastRecord.getPrice());
            }
            
            log.info("价格变化记录已保存: 商品ID={}, 新价格={}, 旧价格={}", 
                productInfo.getProductId(), 
                productInfo.getPrice(), 
                lastRecord != null ? lastRecord.getPrice() : "无"
            );
        }
    }
    
    /**
     * 获取价格历史趋势
     */
    public List<PriceTrendDTO> getPriceTrend(Long productId, int days) {
        LocalDateTime startTime = LocalDateTime.now().minusDays(days);
        
        List<ProductPriceHistory> histories = priceHistoryRepository
            .findByProductIdAndCrawlTimeAfterOrderByCrawlTime(productId, startTime);
        
        return histories.stream()
            .map(history -> PriceTrendDTO.builder()
                .price(history.getPrice())
                .crawlTime(history.getCrawlTime())
                .discountInfo(history.getDiscountInfo())
                .build())
            .collect(Collectors.toList());
    }
    
    /**
     * 分析价格趋势
     */
    public PriceAnalysisResult analyzePriceTrend(Long productId) {
        List<ProductPriceHistory> histories = priceHistoryRepository
            .findByProductIdOrderByCrawlTime(productId);
        
        if (histories.size() < 2) {
            return PriceAnalysisResult.builder()
                .trend("数据不足")
                .build();
        }
        
        // 计算价格统计信息
        List<BigDecimal> prices = histories.stream()
            .map(ProductPriceHistory::getPrice)
            .collect(Collectors.toList());
        
        BigDecimal minPrice = prices.stream().min(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
        BigDecimal maxPrice = prices.stream().max(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
        BigDecimal avgPrice = prices.stream()
            .reduce(BigDecimal.ZERO, BigDecimal::add)
            .divide(BigDecimal.valueOf(prices.size()), 2, RoundingMode.HALF_UP);
        
        BigDecimal currentPrice = prices.get(prices.size() - 1);
        BigDecimal previousPrice = prices.get(prices.size() - 2);
        
        String trend = currentPrice.compareTo(previousPrice) > 0 ? "上涨" : 
                      currentPrice.compareTo(previousPrice) < 0 ? "下降" : "稳定";
        
        return PriceAnalysisResult.builder()
            .minPrice(minPrice)
            .maxPrice(maxPrice)
            .avgPrice(avgPrice)
            .currentPrice(currentPrice)
            .trend(trend)
            .priceDropPercentage(calculatePriceDropPercentage(currentPrice, maxPrice))
            .build();
    }
}

技巧5:定时任务和监控,全自动价格追踪

系统要能自动运行,定时抓取价格数据。

@Service
public class ProductCrawlScheduler {
    
    @Autowired
    private ProductInfoExtractor productInfoExtractor;
    
    @Autowired
    private ProductPriceService productPriceService;
    
    @Autowired
    private ProductRepository productRepository;
    
    /**
     * 定时抓取所有商品价格(每30分钟执行一次)
     */
    @Scheduled(fixedRate = 1800000) // 30分钟
    public void crawlAllProducts() {
        log.info("开始定时抓取商品价格...");
        
        List<Product> products = productRepository.findByStatusOrderByPriority("ACTIVE");
        
        for (Product product : products) {
            try {
                crawlSingleProduct(product);
                
                // 随机延迟,避免请求过于频繁
                Thread.sleep(5000 + new Random().nextInt(10000));
                
            } catch (Exception e) {
                log.error("抓取商品价格失败: productId={}, url={}", 
                    product.getId(), product.getUrl(), e);
            }
        }
        
        log.info("定时抓取完成,共处理{}个商品", products.size());
    }
    
    /**
     * 抓取单个商品价格
     */
    private void crawlSingleProduct(Product product) {
        try {
            ProductInfo productInfo = productInfoExtractor.extractProductInfo(product.getUrl());
            productInfo.setProductId(product.getId());
            
            // 保存价格历史
            productPriceService.savePriceHistory(productInfo);
            
            // 更新商品基本信息
            updateProductInfo(product, productInfo);
            
        } catch (Exception e) {
            // 记录失败次数
            product.setFailCount(product.getFailCount() + 1);
            if (product.getFailCount() >= 5) {
                product.setStatus("FAILED");
                log.warn("商品连续抓取失败5次,已暂停: productId={}", product.getId());
            }
            productRepository.save(product);
            throw e;
        }
    }
    
    /**
     * 高优先级商品快速抓取(每5分钟执行一次)
     */
    @Scheduled(fixedRate = 300000) // 5分钟
    public void crawlHighPriorityProducts() {
        List<Product> highPriorityProducts = productRepository
            .findByStatusAndPriorityGreaterThanOrderByPriority("ACTIVE", 8);
        
        for (Product product : highPriorityProducts) {
            try {
                crawlSingleProduct(product);
                Thread.sleep(2000); // 高优先级商品间隔时间短一些
            } catch (Exception e) {
                log.error("高优先级商品抓取失败: productId={}", product.getId(), e);
            }
        }
    }
    
    /**
     * 清理过期数据(每天凌晨2点执行)
     */
    @Scheduled(cron = "0 0 2 * * ?")
    public void cleanupOldData() {
        LocalDateTime cutoffTime = LocalDateTime.now().minusDays(90); // 保留90天数据
        
        int deletedCount = priceHistoryRepository.deleteByCreateTimeBefore(cutoffTime);
        log.info("清理了{}条过期价格记录", deletedCount);
    }
}

技巧6:价格提醒和数据分析,让数据产生价值

光抓取数据还不够,要能及时提醒用户价格变化。

@Service
public class PriceAlertService {
    
    @Autowired
    private WechatMessageService wechatMessageService;
    
    @Autowired
    private EmailService emailService;
    
    /**
     * 发送价格下降通知
     */
    public void sendPriceDropNotification(ProductInfo productInfo, BigDecimal oldPrice) {
        BigDecimal dropAmount = oldPrice.subtract(productInfo.getPrice());
        BigDecimal dropPercentage = dropAmount.divide(oldPrice, 4, RoundingMode.HALF_UP)
            .multiply(BigDecimal.valueOf(100));
        
        // 构建消息内容
        String message = String.format(
            "💰 价格下降提醒\n\n" +
            "商品:%s\n" +
            "原价:¥%s\n" +
            "现价:¥%s\n" +
            "下降:¥%s (%.1f%%)\n" +
            "平台:%s\n\n" +
            "快去看看吧!",
            productInfo.getTitle(),
            oldPrice,
            productInfo.getPrice(),
            dropAmount,
            dropPercentage,
            productInfo.getPlatform()
        );
        
        // 发送微信通知
        wechatMessageService.sendTextMessage("price_alert_user", message, "app_agent_id");
        
        // 发送邮件通知(如果价格下降超过10%)
        if (dropPercentage.compareTo(BigDecimal.valueOf(10)) >= 0) {
            sendEmailAlert(productInfo, oldPrice, dropAmount, dropPercentage);
        }
    }
    
    /**
     * 价格趋势分析和预测
     */
    public PricePredictionResult predictPriceTrend(Long productId) {
        List<ProductPriceHistory> histories = priceHistoryRepository
            .findByProductIdOrderByCrawlTime(productId);
        
        if (histories.size() < 10) {
            return PricePredictionResult.builder()
                .prediction("数据不足,无法预测")
                .confidence(0.0)
                .build();
        }
        
        // 简单的趋势分析(实际项目中可以用更复杂的算法)
        List<BigDecimal> recentPrices = histories.stream()
            .skip(Math.max(0, histories.size() - 10))
            .map(ProductPriceHistory::getPrice)
            .collect(Collectors.toList());
        
        // 计算移动平均线
        BigDecimal ma5 = calculateMovingAverage(recentPrices, 5);
        BigDecimal ma10 = calculateMovingAverage(recentPrices, 10);
        BigDecimal currentPrice = recentPrices.get(recentPrices.size() - 1);
        
        String prediction;
        double confidence;
        
        if (currentPrice.compareTo(ma5) < 0 && ma5.compareTo(ma10) < 0) {
            prediction = "价格可能继续下降,建议再等等";
            confidence = 0.7;
        } else if (currentPrice.compareTo(ma5) > 0 && ma5.compareTo(ma10) > 0) {
            prediction = "价格可能上涨,建议尽快购买";
            confidence = 0.6;
        } else {
            prediction = "价格相对稳定,可以考虑购买";
            confidence = 0.5;
        }
        
        return PricePredictionResult.builder()
            .prediction(prediction)
            .confidence(confidence)
            .ma5(ma5)
            .ma10(ma10)
            .build();
    }
}

三、实战案例:从0到1搭建商品价格追踪系统

下面分享我之前搭建的一个完整的商品价格追踪系统案例。

系统架构设计

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   前端展示层     │    │   API网关层     │    │   爬虫服务层    │
│   React + ECharts│───▶│   Spring Gateway │───▶│   Selenium     │
└─────────────────┘    └─────────────────┘    └─────────────────┘
                                                        │
┌─────────────────┐    ┌─────────────────┐             │
│   通知服务层     │    │   数据服务层     │             │
│   微信/邮件通知  │◀───│   Spring Boot   │◀────────────┘
└─────────────────┘    └─────────────────┘
                                │
                       ┌─────────────────┐
                       │   数据存储层     │
                       │ MySQL + Redis   │
                       └─────────────────┘

功能特性

  1. 多平台支持:京东、淘宝、天猫、亚马逊等主流电商
  2. 智能监控:自动检测价格变化,支持自定义监控频率
  3. 价格预警:价格下降自动通知,支持微信和邮件
  4. 数据分析:价格趋势图表,最低价提醒,购买建议
  5. 反爬虫对抗:模拟真实用户行为,突破各种限制

关键技术指标

  • 监控商品数量:支持10万+商品同时监控
  • 数据更新频率:高优先级商品5分钟,普通商品30分钟
  • 价格变化检测:99%准确率,1分钟内发送通知
  • 系统稳定性:7×24小时运行,可用率99.5%

四、6个避坑指南,90%的人都踩过!

1. 请求频率控制坑

问题:请求过于频繁被网站封IP
解决方案

  • 设置随机延迟,模拟人工操作
  • 使用代理IP池轮换
  • 分时段抓取,避开高峰期

2. 页面结构变化坑

问题:网站改版导致元素定位失效
解决方案

  • 使用多种选择器备选方案
  • 定期检查和更新爬虫规则
  • 实现自动重试和降级机制

3. 反爬虫验证码坑

问题:遇到验证码无法自动处理
解决方案

  • 集成第三方验证码识别服务
  • 使用机器学习模型识别
  • 人工介入处理复杂验证码

4. 数据存储性能坑

问题:大量历史数据导致查询缓慢
解决方案

  • 建立合适的数据库索引
  • 定期清理过期数据
  • 使用Redis缓存热点数据

5. 内存泄漏坑

问题:WebDriver长时间运行导致内存溢出
解决方案

  • 及时关闭WebDriver实例
  • 定期重启爬虫进程
  • 监控内存使用情况

6. 价格解析错误坑

问题:特殊价格格式导致解析失败
解决方案

  • 使用正则表达式严格过滤
  • 多重校验价格合理性
  • 记录异常价格便于排查

五、5个监控指标,确保系统稳定运行

1. 爬取成功率

  • 目标值:>95%
  • 告警阈值:<90%

2. 价格变化检测准确率

  • 目标值:>99%
  • 告警阈值:<95%

3. 系统响应时间

  • 目标值:<2秒
  • 告警阈值:>5秒

4. 数据更新及时性

  • 目标值:延迟<5分钟
  • 告警阈值:延迟>15分钟

5. 资源使用率

  • CPU:<70%
  • 内存:<80%
  • 磁盘:<85%

六、总结:商品价格追踪系统的4个关键点

  1. 稳定性第一:做好反爬虫对抗,确保长期稳定运行
  2. 数据质量:准确解析商品信息,及时发现价格变化
  3. 用户体验:及时通知,直观展示,实用的购买建议
  4. 系统扩展性:支持多平台,易于添加新的电商网站

这套商品价格追踪系统我已经稳定运行1年多,追踪了5000多个商品,帮我省了不少钱。记住:技术的价值在于解决实际问题,用程序员的方式对抗商家的套路,这就是我们的优势!

如果你也想搭建自己的价格追踪系统,欢迎关注公众号服务端技术精选,一起交流爬虫技术和避坑经验!


标题:商品价格又双叒叕被套路了?这6个Java爬虫技巧帮你追踪所有电商价格历史!
作者:jiangyi
地址:http://jiangyi.space/articles/2025/12/21/1766304297476.html

    0 评论
avatar