×

淘宝开放平台关键词搜索商品接口深度实战:从分页爬取到数据去重(附全场景可运行代码)

Ace Ace 发表于2026-03-04 17:28:20 浏览27 评论0

抢沙发发表评论

一、差异化背景:跳出“单次调用”的浅层陷阱

网上常规教程仅演示“传入关键词返回一页商品”,但实际业务中会面临:分页逻辑混乱导致数据漏取、重复调用返回相同数据、搜索结果字段解析不完整、接口限流触发失败等问题。

本文基于淘宝开放平台最新版SDK(top-sdk-java 4.3.0),聚焦关键词搜索接口的企业级落地:从接口授权、多条件精准搜索、分页自动爬取、核心字段去重、异常容错到数据本地化,覆盖从“单次调用”到“批量获取有效数据”的全链路,适配2025年淘宝接口规则变更,解决常规教程的核心痛点。
二、前置准备(合规且可落地)

    开放平台授权:
        登录淘宝开放平台(https://open.taobao.com/),创建应用并完成实名认证(个人/企业);
        申请“taobao.items.search”接口权限(注意:该接口有严格的QPS和每日配额限制,个人开发者约500次/日,企业可申请扩容);
        获取应用的AppKey、AppSecret,记录生产环境网关地址:https://eco.taobao.com/router/rest。

    Maven依赖配置(避开网上非官方低版本SDK):

<!-- 淘宝开放平台官方SDK -->
<dependency>
    <groupId>com.taobao.api</groupId>
    <artifactId>top-sdk-java</artifactId>
    <version>4.3.0</version>
</dependency>
<!-- JSON解析(阿里fastjson适配淘宝返回格式) -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>2.0.32</version>
</dependency>
<!-- 日志依赖(排查接口调用问题) -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>2.0.9</version>
</dependency>
<!-- 工具类(处理分页、去重、数据格式化) -->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.22</version>
</dependency>
<!-- 集合去重/缓存(用于数据去重) -->
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>32.1.3-jre</version>
</dependency>

在这里插入图片描述

点击获取key和secret

三、核心代码实现(全链路可运行)

1. 接口调用工具类(含分页、限流容错、多条件搜索)

区别于常规代码:支持多条件筛选(价格区间/销量排序)、自动分页、基于商品ID的重复校验、限流友好的重试机制:

import com.taobao.api.DefaultTaobaoClient;
import com.taobao.api.TaobaoClient;
import com.taobao.api.request.ItemsSearchRequest;
import com.taobao.api.response.ItemsSearchResponse;
import com.taobao.api.ApiException;
import com.taobao.api.domain.Item;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import cn.hutool.core.util.StrUtil;
import com.google.common.collect.Sets;

import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * 淘宝关键词搜索商品接口工具类(含分页、去重、多条件筛选)
 * 核心差异:1. 自动分页爬取 2. 商品ID去重 3. 多条件精准搜索 4. 限流适配重试
 */
public class TaobaoItemSearchApiUtil {
    private static final Logger logger = LoggerFactory.getLogger(TaobaoItemSearchApiUtil.class);

    // 替换为自己的配置
    private static final String APP_KEY = "你的AppKey";
    private static final String APP_SECRET = "你的AppSecret";
    // 生产网关(沙箱环境:https://gw.api.tbsandbox.com/router/rest)
    private static final String GATEWAY_URL = "https://eco.taobao.com/router/rest";

    // 容错/分页配置
    private static final int MAX_RETRY = 3;        // 最大重试次数
    private static final long RETRY_INTERVAL = 3;  // 重试间隔(秒)
    private static final int PAGE_SIZE = 40;       // 每页条数(最大40条,淘宝接口限制)
    private static final int MAX_PAGE = 10;        // 最大爬取页数(避免无限分页)
    private static final int TIMEOUT = 5000;       // 接口超时(毫秒)

    // 去重缓存(存储已爬取的商品ID,避免重复)
    private static final Set<String> ITEM_ID_CACHE = Sets.newConcurrentHashSet();

    /**
     * 单页搜索商品
     * @param keyword 搜索关键词
     * @param pageNo 页码(从1开始)
     * @param minPrice 最低价格(可选,null则不限制)
     * @param maxPrice 最高价格(可选,null则不限制)
     * @param sort 排序方式(sale_desc:销量降序,price_asc:价格升序,默认综合排序)
     * @return 单页商品列表
     */
    private static List<Item> searchSinglePage(String keyword, Integer pageNo, 
                                              BigDecimal minPrice, BigDecimal maxPrice, String sort) {
        // 1. 参数校验
        if (StrUtil.isBlank(keyword) || pageNo < 1) {
            logger.error("参数错误:关键词不能为空,页码需≥1");
            return null;
        }

        // 2. 初始化客户端
        TaobaoClient client = new DefaultTaobaoClient(GATEWAY_URL, APP_KEY, APP_SECRET);
        client.setConnectTimeout(TIMEOUT);
        client.setReadTimeout(TIMEOUT);

        // 3. 构建请求(多条件筛选)
        ItemsSearchRequest request = new ItemsSearchRequest();
        request.setQ(keyword);                  // 核心关键词
        request.setPageNo(pageNo);              // 页码
        request.setPageSize(PAGE_SIZE);         // 每页条数
        if (minPrice != null) {
            request.setStartPrice(minPrice);    // 价格下限
        }
        if (maxPrice != null) {
            request.setEndPrice(maxPrice);      // 价格上限
        }
        if (StrUtil.isNotBlank(sort)) {
            request.setSort(sort);              // 排序方式
        }
        // 指定返回字段(减少数据冗余)
        request.setFields("num_iid,title,price,org_price,sales,stock,shop_name,category_id,location");

        // 4. 带重试的调用逻辑
        int retryCount = 0;
        while (retryCount < MAX_RETRY) {
            try {
                ItemsSearchResponse response = client.execute(request);
                if (response.isSuccess()) {
                    List<Item> itemList = response.getItems();
                    logger.info("关键词【{}】第{}页调用成功,返回{}条商品", keyword, pageNo, 
                                itemList == null ? 0 : itemList.size());
                    return itemList;
                } else {
                    String errCode = response.getErrorCode();
                    String errMsg = response.getMsg();
                    logger.error("关键词【{}】第{}页调用失败:错误码{},信息{}", keyword, pageNo, errCode, errMsg);

                    // 限流/系统异常重试,业务异常直接返回
                    if ("isv.api-call-limit-exceeded".equals(errCode) || "sys.service-unavailable".equals(errCode)) {
                        retryCount++;
                        if (retryCount < MAX_RETRY) {
                            logger.info("触发限流/系统异常,第{}次重试(间隔{}秒)", retryCount, RETRY_INTERVAL);
                            TimeUnit.SECONDS.sleep(RETRY_INTERVAL);
                        }
                    } else {
                        break; // 业务异常无需重试
                    }
                }
            } catch (ApiException e) {
                logger.error("关键词【{}】第{}页接口异常:错误码{},信息{}", 
                            keyword, pageNo, e.getErrCode(), e.getErrMsg());
                retryCount++;
                if (retryCount < MAX_RETRY) {
                    try {
                        TimeUnit.SECONDS.sleep(RETRY_INTERVAL);
                    } catch (InterruptedException ie) {
                        Thread.currentThread().interrupt();
                        break;
                    }
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                logger.error("重试间隔线程中断", e);
                break;
            }
        }
        logger.error("关键词【{}】第{}页重试{}次仍失败", keyword, pageNo, MAX_RETRY);
        return null;
    }

    /**
     * 批量分页搜索商品(自动去重)
     * @param keyword 搜索关键词
     * @param minPrice 最低价格(可选)
     * @param maxPrice 最高价格(可选)
     * @param sort 排序方式(可选)
     * @return 去重后的商品列表
     */
    public static List<Item> batchSearch(String keyword, BigDecimal minPrice, 
                                        BigDecimal maxPrice, String sort) {
        // 清空历史去重缓存(新搜索任务重置)
        ITEM_ID_CACHE.clear();
        List<Item> finalItemList = Lists.newArrayList();

        // 循环分页爬取
        for (int pageNo = 1; pageNo <= MAX_PAGE; pageNo++) {
            List<Item> pageItemList = searchSinglePage(keyword, pageNo, minPrice, maxPrice, sort);
            if (pageItemList == null || pageItemList.isEmpty()) {
                logger.info("关键词【{}】第{}页无数据,终止分页爬取", keyword, pageNo);
                break; // 无数据则终止分页
            }

            // 数据去重(基于商品ID)
            for (Item item : pageItemList) {
                String itemId = item.getNumIid();
                if (StrUtil.isNotBlank(itemId) && !ITEM_ID_CACHE.contains(itemId)) {
                    ITEM_ID_CACHE.add(itemId);
                    finalItemList.add(item);
                }
            }

            // 延迟(避免触发限流)
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                logger.error("分页延迟中断", e);
                break;
            }
        }

        logger.info("关键词【{}】分页爬取完成,去重后共获取{}条商品", keyword, finalItemList.size());
        return finalItemList;
    }

    // 测试入口
    public static void main(String[] args) {
        // 搜索“手机壳”,价格10-50元,按销量降序,自动分页爬取
        List<Item> itemList = batchSearch("手机壳", new BigDecimal("10"), 
                                         new BigDecimal("50"), "sale_desc");
        if (itemList != null) {
            itemList.forEach(item -> {
                logger.info("商品ID:{},标题:{},价格:{},销量:{}",
                            item.getNumIid(), item.getTitle(), item.getPrice(), item.getSales());
            });
        }
    }
}

2. 搜索结果结构化解析(业务级落地)

常规教程仅打印原始数据,本文将返回的商品列表解析为业务可用的POJO,重点处理销量、价格等核心字段的              格式化              

import com.taobao.api.domain.Item;
import com.alibaba.fastjson.JSON;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;

import java.math.BigDecimal;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 搜索结果结构化解析工具
 * 核心差异:将原始接口数据转为业务可直接使用的结构化对象
 */
// 业务级商品搜索结果POJO
class BusinessSearchItem {
    private String itemId;          // 商品ID
    private String title;           // 商品标题(脱敏处理,避免特殊字符)
    private BigDecimal price;       // 售价
    private BigDecimal originalPrice; // 原价
    private Integer sales;          // 销量(淘宝接口返回的是累计销量)
    private Integer stock;          // 库存
    private String shopName;        // 店铺名称
    private String categoryId;      // 类目ID
    private String location;        // 发货地

    // 省略getter/setter
    @Override
    public String toString() {
        return JSON.toJSONString(this, true);
    }
}

/**
 * 解析工具类
 */
public class SearchResultParser {
    /**
     * 解析原始商品列表为业务对象列表
     * @param rawItemList 接口返回的原始商品列表
     * @return 业务级商品列表
     */
    public static List<BusinessSearchItem> parse(List<Item> rawItemList) {
        if (rawItemList == null || rawItemList.isEmpty()) {
            return null;
        }

        // 流式解析+格式化
        return rawItemList.stream()
                .filter(rawItem -> rawItem != null && StrUtil.isNotBlank(rawItem.getNumIid()))
                .map(rawItem -> {
                    BusinessSearchItem businessItem = new BusinessSearchItem();
                    // 基础字段赋值
                    businessItem.setItemId(rawItem.getNumIid());
                    // 标题脱敏(去除特殊字符、换行符)
                    businessItem.setTitle(StrUtil.cleanBlank(rawItem.getTitle()).replaceAll("[^\\u4e00-\\u9fa5a-zA-Z0-9]", ""));
                    // 价格格式化(避免字符串转数字异常)
                    businessItem.setPrice(NumberUtil.toBigDecimal(rawItem.getPrice()));
                    businessItem.setOriginalPrice(NumberUtil.toBigDecimal(rawItem.getOrgPrice()));
                    // 销量/库存格式化(空值处理)
                    businessItem.setSales(rawItem.getSales() == null ? 0 : rawItem.getSales());
                    businessItem.setStock(rawItem.getStock() == null ? 0 : rawItem.getStock());
                    // 其他字段
                    businessItem.setShopName(rawItem.getShopName());
                    businessItem.setCategoryId(rawItem.getCid());
                    businessItem.setLocation(rawItem.getLocation());
                    return businessItem;
                })
                .collect(Collectors.toList());
    }

    // 测试入口
    public static void main(String[] args) {
        // 批量搜索商品
        List<Item> rawItemList = TaobaoItemSearchApiUtil.batchSearch("手机壳", 
                                                                    new BigDecimal("10"), 
                                                                    new BigDecimal("50"), 
                                                                    "sale_desc");
        // 解析为业务对象
        List<BusinessSearchItem> businessItemList = parse(rawItemList);
        if (businessItemList != null) {
            logger.info("解析后业务数据:\n{}", JSON.toJSONString(businessItemList, true));
        }
    }
}


3. 搜索结果本地化存储(落地最后一步)

将解析后的业务数据保存到本地JSON文件(可扩展为MySQL/ElasticSearch),按关键词分目录存储,便于后续分析:

import com.alibaba.fastjson.JSON;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.date.DateUtil;

import java.util.List;

/**
 * 搜索结果本地化工具
 */
public class SearchResultPersistenceUtil {
    /**
     * 保存业务级搜索结果到本地
     * @param businessItemList 业务级商品列表
     * @param keyword 搜索关键词(用于创建目录)
     */
    public static void saveToLocal(List<BusinessSearchItem> businessItemList, String keyword) {
        if (businessItemList == null || businessItemList.isEmpty() || StrUtil.isBlank(keyword)) {
            logger.error("保存失败:数据为空或关键词无效");
            return;
        }

        // 1. 构建存储目录(关键词+日期,避免文件覆盖)
        String dateStr = DateUtil.format(DateUtil.date(), "yyyyMMdd");
        String basePath = String.format("./taobao_search_result/%s/%s/", keyword, dateStr);
        FileUtil.mkdir(basePath);

        // 2. 构建文件名(时间戳命名)
        String timeStr = DateUtil.format(DateUtil.date(), "HHmmss");
        String filePath = basePath + "search_result_" + timeStr + ".json";

        // 3. 保存JSON(格式化输出)
        String jsonStr = JSON.toJSONString(businessItemList, true);
        FileUtil.writeUtf8String(jsonStr, filePath);

        logger.info("关键词【{}】搜索结果已保存到:{},共{}条数据", 
                    keyword, filePath, businessItemList.size());
    }

    // 测试入口
    public static void main(String[] args) {
        // 1. 批量搜索
        List<Item> rawItemList = TaobaoItemSearchApiUtil.batchSearch("手机壳", 
                                                                    new BigDecimal("10"), 
                                                                    new BigDecimal("50"), 
                                                                    "sale_desc");
        // 2. 解析
        List<BusinessSearchItem> businessItemList = SearchResultParser.parse(rawItemList);
        // 3. 保存
        if (businessItemList != null) {
            saveToLocal(businessItemList, "手机壳");
        }
    }
}

四、关键技术要点(差异化核心)

    分页逻辑优化:淘宝搜索接口每页最大返回40条,且存在“空页”情况(页码过大返回无数据),代码中加入“空页终止”和“最大页数限制”,避免无效调用;
    数据去重机制:基于商品ID(num_iid)构建并发安全的去重缓存,解决分页调用中可能出现的商品重复问题;
    多条件精准搜索:支持价格区间筛选、销量/价格排序,覆盖“精准选品”的业务场景(常规教程仅支持关键词);
    字段格式化处理:对标题做脱敏去特殊字符、对价格/销量做空值处理,避免后续数据处理时出现异常;
    限流适配策略:区分“限流异常(可重试)”和“业务异常(不重试)”,且分页间加入1秒延迟,降低触发限流的概率。


群贤毕至

访客