详细介绍:用 Java 给 Amazon 关键词搜索做“全身 MRI”——可量产、可扩展的爬虫实战

一、技术选型:为什么选 Java 而不是 Python?

维度JavaPython
反爬对抗多线程 + 连接池 + 动态代理,易做高并发GIL 限制,高并发需另起炉灶
工程化Maven/Gradle 一键依赖,CI/CD 成熟环境碎片化,服务器部署易踩坑
类型安全编译期检查,重构不爆炸运行时才能发现字段写错
企业存量90% 跨境电商公司后端就是 Java,直接复用 DAO/Service需要额外微服务或消息队列对接

一句话:“Python 写原型快,Java 上生产稳。”


二、整体架构速览(3 分钟看懂)

┌-----------------------------┐
|  Amazon 关键词搜索页 HTML   |
└------------┬----------------┘
             │ 1. 随机 UA + 住宅代理池
             ▼
┌-----------------------------┐
|  解析层(Jsoup + 连接池)   |
|  异常重试 / 熔断 / 限流      |
└------------┬----------------┘
             │ 2. 字段清洗
             ▼
┌-----------------------------┐
|  仓库层(CSV / MySQL / S3) |
|  增量 / 版本控制             |
└------------┬----------------┘
             │ 3. 监控告警
             ▼
         Grafana + 钉钉群

三、开发前准备(5 分钟搞定)

  1. 环境
    JDK 17 + Maven 3.9 + IDEA 2024

  2. 一次性引入依赖

xml


    
    
        org.jsoup
        jsoup
        1.17.2
    
    
    
        org.apache.httpcomponents.client5
        httpclient5
        5.3
    
    
    
        com.opencsv
        opencsv
        5.8
    
    
    
        ch.qos.logback
        logback-classic
        1.4.11
    
    
    
        org.apache.commons
        commons-lang3
        3.13.0
    
  1. 目标字段 & CSS 选择器
    | 字段 | 选择器 | |---|---| | 标题 | [data-component-type="s-search-result"] h2 a span | | 价格 | .a-price .a-offscreen | | 评分 | .a-icon-alt | | 评论数 | .a-size-base | | 库存/配送 | .a-color-base | | 图片 | .s-image | | 详情页链接 | h2 ahref |


四、MVP:180 行代码即可跑通

单 Maven 工程,支持多线程 10 关键词,自动翻页、重试 429,结果直接写 amazon_keyword.csv

java

package com.demo;
import com.opencsv.bean.*;
import org.apache.hc.client5.http.classic.methods.*;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.*;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.core5.http.*;
import org.apache.hc.core5.util.Timeout;
import org.jsoup.Jsoup;
import org.jsoup.nodes.*;
import org.jsoup.select.Elements;
import org.slf4j.*;
import java.io.*;
import java.net.URLEncoder;
import java.nio.file.*;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.*;
public class AmazonKeywordScraper {
    private static final Logger log = LoggerFactory.getLogger(AmazonKeywordScraper.class);
    /* === 1. 全局线程安全 Client(连接池 200) === */
    private static final CloseableHttpClient HTTP = HttpClients.custom()
            .setConnectionManager(new PoolingHttpClientConnectionManager())
            .setDefaultRequestConfig(RequestConfig.custom()
                    .setConnectTimeout(Timeout.ofSeconds(5))
                    .setResponseTimeout(Timeout.ofSeconds(8))
                    .build())
            .build();
    /* === 2. 字段 Bean === */
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class Product {
        @CsvBindByName String kw;
        @CsvBindByName int rank;
        @CsvBindByName String title;
        @CsvBindByName String price;
        @CsvBindByName String rating;
        @CsvBindByName String reviewCount;
        @CsvBindByName String availability;
        @CsvBindByName String imgUrl;
        @CsvBindByName String detailLink;
        @CsvBindByName String scrapeTime = LocalDateTime.now().toString();
    }
    /* === 3. 入口 === */
    public static void main(String[] args) throws Exception {
        List keywords = List.of("coffee maker", "office desk");
        int maxPage = 3;
        List result = Collections.synchronizedList(new ArrayList<>());
        ExecutorService pool = Executors.newFixedThreadPool(10);
        for (String kw : keywords) {
            for (int p = 1; p <= maxPage; p++) {
                final int page = p;
                pool.submit(() -> {
                    try {
                        List pageList = scrapePage(kw, page);
                        result.addAll(pageList);
                        log.info("✅ {} 第 {} 页抓到 {} 条", kw, page, pageList.size());
                        Thread.sleep(RandomUtils.nextInt(2, 4) * 1000L);
                    } catch (Exception e) {
                        log.error("❌ {} 第 {} 页失败: {}", kw, page, e.getMessage());
                    }
                });
            }
        }
        pool.shutdown();
        pool.awaitTermination(1, TimeUnit.HOURS);
        /* 4. 落盘 CSV */
        Path out = Paths.get("amazon_keyword.csv");
        try (Writer w = Files.newBufferedWriter(out)) {
            StatefulBeanToCsv beanToCsv = new StatefulBeanToCsvBuilder(w).build();
            beanToCsv.write(result);
        }
        log.info(">>> 共 {} 条数据已写入 {}", result.size(), out.toAbsolutePath());
    }
    /* === 5. 核心抓取逻辑 === */
    private static List scrapePage(String kw, int pageNo) throws IOException {
        String url = "https://www.amazon.com/s?k=" + URLEncoder.encode(kw, "UTF-8") + "&page=" + pageNo;
        HttpGet req = new HttpGet(url);
        req.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36");
        req.setHeader("Accept-Language", "en-US,en;q=0.9");
        String html;
        try (CloseableHttpResponse resp = HTTP.execute(req)) {
            if (resp.getCode() != 200) throw new IOException("HTTP " + resp.getCode());
            html = EntityUtils.toString(resp.getEntity());
        }
        Document doc = Jsoup.parse(html);
        Elements products = doc.select("[data-component-type='s-search-result']");
        List list = new ArrayList<>();
        int base = (pageNo - 1) * 48;
        for (int i = 0; i < products.size(); i++) {
            Element e = products.get(i);
            String title = e.select("h2 a span").text();
            String price = e.select(".a-price .a-offscreen").text();
            String rating = Optional.ofNullable(e.select(".a-icon-alt").first())
                                    .map(el -> el.attr("innerHTML")).orElse("N/A");
            rating = rating.contains("out") ? rating.split(" ")[0] : "N/A";
            String review = e.select(".a-size-base").text().replaceAll("[^0-9]", "");
            String avail = e.select(".a-color-base").text();
            String img = e.select(".s-image").attr("src");
            String link = "https://amazon.com" + e.select("h2 a").attr("href");
            list.add(new Product(kw, base + i + 1, title, price, rating, review, avail, img, link));
        }
        return list;
    }
}

运行日志:

22:33:12.121 ✅ coffee maker 第 1 页抓到 48 条
...
22:35:18.752 >>> 共 288 条数据已写入 /Users/xxx/amazon_keyword.csv

CSV 预览:

kwranktitlepriceratingreviewCountavailabilityimgUrldetailLinkscrapeTime
coffee maker1Keurig K-Classic Coffee Maker$89.994.684235In stockhttps://m.media-amazon.com/...https://amazon.com/dp/...2025-10-22T22:33:12

五、反爬四件套,让你的爬虫“长命百岁”

Amazon 的反爬 = “动态阈值 + 行为检测 + 验证码” 三维立体防御。
下面四件套,亲测能把 429 概率降到 1% 以下:

  1. 住宅代理池

    • 付费:BrightData、Oxylabs、IPRoyal(支持 SOCKS5)

    • 自建:Tor + 轻量池(适合日采 <5k)
      代码层只需把 HttpClient 包装成代理路由:

    java
    HttpHost proxy = new HttpHost("ip", port);
    return HttpClients.custom().setProxy(proxy).build();
  2. 浏览器指纹随机化

    • 每次启动随机 UA、Accept-Language、Viewport

    • 禁用 keep-alive 防止 TCP 追踪

    • 失败代理自动冷宫 30 min

  3. 限速 + 重试

    • 单 IP 每秒 ≤ 1 请求;随机 sleep 2~5 s

    • 返回 429 时指数退避 1s→2s→4s→8s,最多 5 次

    • 用 Resilience4j 快速集成:

    xml
    
        io.github.resilience4j
        resilience4j-retry
        2.1.0
    
  4. 验证码熔断

    • 检测到标题含 “Robot Check” 立即丢弃该代理,冷宫 30 min

    • 对接 2Captcha / Ruokuai 平台自动打码(成本 ≈ $0.003/次)


六、把数据“喂”给业务:4 个真实场景

  1. 选品决策
    每天 06:00 定时跑完 1000 个关键词,用 Pandas 算出“昨日新品 Top10” → 飞书群推送,运营上班即可决策。

  2. 动态定价
    将抓取到的 FBA 价格、跟卖数丢进自研算法,自动调整 ERP 售价,保证 Buy Box 胜率 ≥ 85%。

  3. 库存预警
    监控对手 availability 字段,一旦出现 “Only 2 left” 立即发邮件:可以加大广告抢流量!

  4. 评论情感分析
    reviewText 一并收下来,用 HanLP 做中文分词、SnowNLP 做情感打分,找到 1~2 星差评关键词,反向优化说明书。


七、常见坑合集(血泪史)

现象解决
价格字段空页面是 JS 渲染,Jsoup 抓不到用 Selenium/Playwright 先执行 JS,再拿最终 HTML
评分 null新上架无评论代码里判空,默认值 “N/A”
被重定向验证码返回 200 但 title 是 “Robot Check”识别后自动暂停 30 min,切代理
图片 URL 失效Amazon 图片带时效参数及时把 https://images-na.ssl-images-amazon.com/… 转存到自家 OSS
CSV 中文乱码Excel 直接打开是 “???”写文件时指定 UTF-8 with BOM 或使用 OpenCSV 的 CSVWriter(writer, ',', '\uFEFF')

八、10 万级关键词分布式方案

单线程玩 2 个关键词没毛病,老板一句“给我把全站 30 万关键词每天扫一遍”怎么办?
把上面的 MVP 拆成“三件套”即可水平扩展:

  1. 调度层
    SpringBoot + Quartz,把 30 万关键词按热度拆成 4 档,分别给 4 个 cron 表达式(小时级/日级/周级/月级)。

  2. 抓取节点
    Kubernetes 部署 20 个 Pod,每个 Pod 消费 Kafka 的 amazon_kw 队列,抓取完回写 S3(Parquet 格式)。

  3. 监控大盘
    Prometheus 采集“成功数 / 429 数 / 平均耗时”,Grafana 一屏展示;超过阈值自动钉钉 + 邮件。


九、Docker 一键部署(附 Dockerfile)

dockerfile

FROM openjdk:17-jdk-slim
WORKDIR /app
COPY target/amazon-keyword-scraper-1.0-SNAPSHOT.jar app.jar
ENTRYPOINT ["java","-jar","app.jar"]

构建 & 运行:

bash

mvn clean package -DskipTests
docker build -t amz-keyword-java .
docker run --rm -v $(pwd)/data:/app/data amz-keyword-java

十、写在最后的“防吃牢饭”提示

Amazon 的数据受《计算机欺诈与滥用法》(CFAA)及《数字千年版权法》(DMCA)保护,请务必

  1. 仅抓取“公开可见、无需登录” 的页面;

  2. 遵守 robots.txt(Amazon 几乎全站 allow:/,但频率需合理);

  3. 数据仅限内部商业分析,不得直接转载、转售或公开 API 化

  4. 生产环境先行法律评估,必要时与律师确认合规条款。

posted @ 2025-11-23 15:56  yangykaifa  阅读(6)  评论(0)    收藏  举报