搜好货平台关键词商品搜索接口:分页容错与商品数据轻量化提取方案

一、接口基础说明

点击获取key和secret

唯品会关键词商品列表接口(搜索接口)

  • 请求方式:通常 POST/GET,关键词、页码、每页数量、排序、筛选参数
  • 天然限制:
    1. 最大分页上限:很多唯品会搜索接口不能无限翻页,页码到一定值后直接返回空列表,不是报错,是无数据;
    2. 动态字段:促销、预售、组合商品、盲盒类商品返回字段不一样;部分字段时而返回时而缺失;
    3. 分页不稳定:同一 page 多次请求,商品条目数量可能不一致,存在商品重复、商品丢失;
    4. 限流风控:短时间连续翻页会返回 429、验证码、空 data,无明确错误码。

注意:接口权限、签名、cookie/access_token 鉴权不在本文方案内,本文聚焦分页容错逻辑 + 字段归一化。

二、分页容错方案(重点)

2.1 常规分页逻辑(不推荐直接使用)

复制代码
page = 1
while True:
    resp = request(keyword, page)
    items = resp['data']['itemList']
    if len(items)==0: break
    save(items)
    page +=1

问题:

  • 遇到临时返回空数组直接终止,误判为采集完成;
  • 没有去重,同一商品跨页重复入库;
  • 不处理限流、超时、接口抖动;
  • 到达平台最大页码阈值,直接退出无法区分 "真无数据" 和 "接口截断"。

2.2 增强分页容错设计

1)三重重试策略(分级重试)

表格

异常类型 重试次数 间隔 策略
网络超时、连接失败 3 次 1s,2s,4s 指数退避
200 但 data 为空 / 无 itemList 2 次 3s 重试后仍空,标记为疑似结束,不直接退出
429 限流 3 次 5s,10s,20s 长间隔,达到上限暂停当前关键词任务

规则:重试只针对临时异常;业务空数据(商品真的没有)不重试。

2)分页结束判定(核心改进)

不再依靠 itemList.length == 0 作为唯一终止条件,采用双条件判定:

  1. 连续 N 页(推荐 2 页)返回商品数量 = 0 → 判定搜索结果结束;
  2. 或者 page 到达预设最大页码阈值(如 max_page=50,根据唯品会搜索限制配置)强制停止,防止死循环。

原因:唯品会接口偶尔单页抖动返回空,下一页又恢复数据;单页空不能代表全部商品抓取完毕。

3)商品去重,解决跨页重复
  • 唯一主键:itemId(唯品会商品 ID)作为唯一标识;
  • 本地内存 /redis 临时集合,保存本轮关键词已采集 itemId;
  • 新拉取到商品:如果 itemId 已存在,跳过入库,日志标记【重复商品】;
  • 任务结束清空该关键词的去重集合。
4)页码跳变保护

增加校验:返回列表里的商品数量,远小于 pageSize 时,记录告警; 例如每页请求 30 条,返回 < 5 条,记录日志warning: page{page} item count low,便于后续排查是否触发平台结果截断。

5)任务断点续采

持久化当前任务状态:keyword、current_page、已采集itemId集合、状态,存入数据库 /redis; 任务中断(程序崩溃、网络断),重启时读取状态,从 current_page 继续采集,不用从头跑。

2.3 分页伪代码示例(PHP)

复制代码
$keyword = "xxx";
$page = $task['current_page'];
$maxPage = 50;
$emptyPageCount = 0;
$pageSize = 30;
$redisKey = "vip:search:dedup:".$keyword;

while($page <= $maxPage){
    $ret = vipApiRequest($keyword, $page, $pageSize);
    if($ret['code'] !== 0){
        // 异常重试逻辑
        $retryOk = retryApi(function() use ($keyword,$page,$pageSize){
            return vipApiRequest($keyword, $page, $pageSize);
        },3);
        if(!$retryOk){
            log_error("page {$page} 请求失败");
            break;
        }
        $ret = $retryOk;
    }
    $itemList = $ret['data']['itemList'] ?? [];
    if(empty($itemList)){
        $emptyPageCount++;
        if($emptyPageCount >=2){
            log_info("连续2页空,搜索任务结束");
            break;
        }
        $page++;
        continue;
    }
    $emptyPageCount = 0;
    foreach($itemList as $rawItem){
        $itemId = $rawItem['itemId'];
        if(redis()->sIsMember($redisKey,$itemId)){
            log_info("重复商品 {$itemId},跳过");
            continue;
        }
        redis()->sAdd($redisKey,$itemId);
        // 字段归一化处理
        $stdItem = normalizeItem($rawItem);
        saveToDB($stdItem);
    }
    $task['current_page'] = $page + 1;
    saveTaskStatus($task);
    $page++;
    usleep(800*1000); // 节流,控制请求频率
}
redis()->del($redisKey);

三、字段归一化方案

3.1 痛点

  1. 字段名不统一:有的叫salePrice,有的promotionPrice;原价字段有的marketPrice、origPrice;
  2. 部分商品无 SKU 数组,直接单商品价格;多规格商品 SKU 列表嵌套层级不一致;
  3. 图片字段:有的返回主图 url,有的返回图片数组,缩略图 / 原图混合;
  4. 空值:不存在字段返回null / "" / 直接不返回 key;
  5. 促销标签、库存字段:普通商品、预售、清仓结构差异大。

3.2 归一化设计思路

输入:接口原始异构 JSON 对象 rawItem 输出:统一结构体 stdItem,上层业务固定字段读取,不需要判断原始字段 策略:多层级取值 + 默认兜底值 + 类型强制转换 + SKU 归一化

3.3 标准输出结构体设计

复制代码
{
  "item_id": "",
  "title": "",
  "sub_title": "",
  "brand_name": "",
  "price": 0,
  "original_price": 0,
  "discount": "",
  "stock_status": "",
  "sales": 0,
  "main_image": "",
  "image_list": [],
  "category_name": "",
  "sku_list": [
    {
      "sku_id": "",
      "spec_name": "",
      "price": 0,
      "stock_num": 0
    }
  ],
  "promotion_tags": [],
  "source_raw": {},
  "crawl_time": ""
}

3.4 字段映射 & 安全取值函数

核心:封装安全取值函数 getSafeVal($arr, $path, $default),支持多层 key 读取,key 不存在返回默认值,避免Undefined index报错。

示例映射规则:

表格

标准字段 原始候选字段顺序 兜底默认值 处理逻辑
item_id itemId "" 字符串
title title "" 去除首尾空格
price salePrice / promotionPrice / currentPrice 0 float,空转为 0
original_price marketPrice / origPrice 0 float
main_image imgUrl / mainPic "" 优先取第一张图
image_list picList / imageArray \[\] 数组,过滤空 url
sales saleCount / salesVolume 0 转为数字
SKU 归一化(难点)

两种情况:

  1. 商品返回skuInfo数组:遍历每个 SKU 映射到标准 sku 结构;
  2. 商品无 SKU 列表(单品):自动构造一个虚拟 SKU,spec_name="默认规格",sku_id=itemId,价格取商品主价。

这样上层业务统一读取 sku_list,不用区分单品 / 多规格商品。

3.5 PHP 归一化函数示例

复制代码
function normalizeItem($raw){
    $std = [];
    $std['item_id'] = getSafeVal($raw,'itemId','');
    $std['title'] = trim(getSafeVal($raw,'title',''));
    $std['brand_name'] = getSafeVal($raw,'brandName','');
    $std['price'] = floatval(getSafeVal($raw,'salePrice',0));
    $std['original_price'] = floatval(getSafeVal($raw,'marketPrice',0));
    $std['main_image'] = getSafeVal($raw,'imgUrl','');
    $std['image_list'] = getSafeVal($raw,'picList',[]);
    $std['sales'] = intval(getSafeVal($raw,'saleCount',0));
    $std['promotion_tags'] = getSafeVal($raw,'tagList',[]);
    $std['crawl_time'] = date('Y-m-d H:i:s');

    // SKU归一化
    $rawSkuList = getSafeVal($raw,'skuInfo',[]);
    $std['sku_list'] = [];
    if(!empty($rawSkuList)){
        foreach($rawSkuList as $sku){
            $std['sku_list'][] = [
                'sku_id' => getSafeVal($sku,'skuId',''),
                'spec_name' => getSafeVal($sku,'spec',''),
                'price' => floatval(getSafeVal($sku,'skuPrice',0)),
                'stock_num' => intval(getSafeVal($sku,'stock',0)),
            ];
        }
    }else{
        // 单品构造虚拟SKU
        $std['sku_list'][] = [
            'sku_id' => $std['item_id'],
            'spec_name' => '默认规格',
            'price' => $std['price'],
            'stock_num' => 0,
        ];
    }
    $std['source_raw'] = $raw; // 保留原始数据用于排查
    return $std;
}

// 安全多层取值
function getSafeVal($data, $key, $default=null){
    if(!is_array($data)) return $default;
    return isset($data[$key]) ? $data[$key] : $default;
}

四、监控与告警(保障长期稳定)

  1. 分页指标监控:每页返回商品数量、空页次数、重试次数、重复商品数量;写入监控表;
  2. 字段缺失告警:统计商品缺失itemId、价格、主图的比例,超过阈值触发告警;
  3. 限流监控:429 计数,关键词任务自动降速;
  4. 数据校验:入库后校验 price>original_price 这类异常数据,标记脏数据。

五、风险与优化方向

  1. 唯品会接口会不定期调整返回 JSON 结构,source_raw 原始字段持久化非常重要,方便更新映射规则;
  2. 不要高频并发拉取,单关键词串行分页,多关键词任务做并发池控制;
  3. 价格字段区分:券前价、券后价、活动价,如果业务需要,归一化结构体可扩展字段存储多种价格;
  4. 增量更新:关键词搜索是全量快照,如果需要实时监控,可基于 itemId 做增量对比,只更新变化商品。

六、整体架构简图

复制代码
【任务调度】
    ↓
【请求层:签名、http客户端、分级重试、节流】
    ↓
【分页容错层:空页计数、去重、断点续采、结束判定】
    ↓
【字段归一化层:安全取值、SKU统一、脏数据过滤】
    ↓
【存储层:标准商品表 + 原始JSON备份表】
    ↓
【上层业务:比价、选品、报表】

如果你需要,我可以继续输出:

  1. 可直接复制的MySQL 建表语句(标准商品表 + 任务表);
  2. Java 版本实现分页 + 归一化代码;
  3. 改成适合文档发布的 SEO 优化完整版技术文章。

今天 09:22

搜好货平台关键词商品搜索接口:分页容错与商品数据轻量化提取方案

搜好货平台关键词商品搜索接口:分页容错与商品数据轻量化提取方案

业务场景:B2B 工业货源关键词检索、供应商监控、批发比价、货源选品;搜好货属于 B2B 工业电商,和唯品会零售接口差异很大:多规格报价区间、起订量、厂家信息、地区字段、接口返回大 JSON 冗余字段极多 。 核心目标:稳定分页采集,只提取业务需要字段,裁剪冗余大块数据,降低内存、存储、带宽开销;同时解决 B2B 接口分页抖动、总数不准、跨页重复、报价字段异构问题。

一、搜好货关键词搜索接口基础特性

搜好货(912688)商品搜索接口,B2B 货源搜索,GET/POST,带签名 + token 鉴权,入参:关键词、page、pageSize、筛选(地区、起订量、类目)、排序。

平台接口独有坑点

  1. total 总数不准:接口返回 total 仅为估算值,翻到后面会出现 total 大于实际可返回商品,不能用 total 循环计算最大页码;
  2. B2B 报价不是单一价格:大多是区间价(1-10 件 XX 元,10-100 件 XX 元),单品固定价、阶梯报价两种结构混杂;
  3. 商品字段冗余巨大:返回完整详情 HTML、长富文本介绍、多个附件、企业资质、历史询盘等大体积字段,轻量化必须直接丢弃,不解析;
  4. 分页不稳定:工业词搜索结果缓存强,同一 page 多次请求条目不一致;末尾页码容易返回空数组,属于缓存截断,不是真正无商品;
  5. 风控特点:相比 C 端平台,慢,超时多,401/403 token 失效频繁,不是简单 429;token 过期属于业务异常,不是重试能解决;
  6. 商品唯一 ID:goodsId,供应商 ID:companyId,一个厂家多条同款货源(跨页重复高发)。

说明:签名、token 获取、鉴权逻辑不在本文,聚焦分页容错 + 轻量化提取。

二、分页容错方案(适配 B2B 搜好货搜索接口)

2.1 简单分页(不推荐)

复制代码
page=1
while page*pageSize <= total:
    resp = request(keyword,page)
    save(resp['data']['list'])
    page +=1

致命问题:total 虚高,会无限请求无效页码;B2B 缓存导致单页空,误判任务结束;同款货源跨页重复入库;token 失效没有中断保护。

2.2 增强分页容错设计

1)分级重试策略

表格

异常类型 重试次数 退避间隔 处理规则
网络超时、连接重置 3 次 1s,2s,4s 指数退避重试
HTTP 200,但 list 为空 2 次 3s 重试后仍然空,空页计数 + 1,不直接退出
401/403 token 失效 0 次 - 不重试,标记任务暂停,触发 token 刷新告警
5xx 服务端错误 2 次 5s 重试

规则:token 过期属于身份失效,重试无效,直接暂停任务,避免锁账号。

2)分页结束判定(重点,B2B 接口核心)

放弃依靠 total 计算循环上限,采用双条件判定

  1. 连续 2 页返回商品列表为空,判定关键词搜索任务正常结束;
  2. 配置硬上限 max_page(建议 30~50,搜好货后端搜索结果深度有限),page 超过 max_page 强制终止,防止死循环。

原因:搜好货搜索后端对工业词做结果截断,即便 total 显示几千条,实际最多只能拉几十页。

3)商品去重(B2B 特有:同厂家同款多条货源)
  • 主键:goodsId 作为商品唯一 key;
  • Redis 临时集合:search:sh:dedup:{keyword},保存本轮关键词已采集 goodsId;
  • 规则:新拉取商品,goodsId 已存在则跳过入库,日志标记【重复货源】;

业务扩展:如果需要同款合并比价 ,可以增加companyId+产品标题md5做同款分组。

4)断点续采

任务持久化字段存入 Redis/MySQL:keyword、current_page、max_page、已采集goodsId集合、任务状态、token版本。 程序崩溃、网络中断、token 失效暂停后,重启从 current_page 继续采集,不需要从头抓取。

5)伪代码 PHP 分页主逻辑
复制代码
$keyword = "工业不锈钢阀门";
$page = $task['current_page'];
$maxPage = 40;
$emptyPageCount = 0;
$pageSize = 20;
$redisDedupKey = "search:sh:dedup:".md5($keyword);

while($page <= $maxPage){
    $apiResult = shApiRequest($keyword,$page,$pageSize,$token);
    // 鉴权失效
    if($apiResult['code'] == 401){
        log_error("token失效,任务暂停 keyword:{$keyword}");
        $task['status'] = 'pause';
        saveTask($task);
        break;
    }
    $rawList = $apiResult['data']['list'] ?? [];
    if(empty($rawList)){
        $emptyPageCount++;
        if($emptyPageCount >=2){
            log_info("连续2页空,关键词任务结束");
            break;
        }
        $page++;
        continue;
    }
    $emptyPageCount = 0;
    foreach($rawList as $rawGoods){
        $goodsId = $rawGoods['goodsId'];
        if(redis()->sIsMember($redisDedupKey,$goodsId)){
            log_info("重复商品 {$goodsId},跳过");
            continue;
        }
        redis()->sAdd($redisDedupKey,$goodsId);
        // ===== 轻量化提取核心函数 =====
        $lightItem = extractLightGoods($rawGoods);
        saveToDB($lightItem);
    }
    // 更新任务状态
    $task['current_page'] = $page + 1;
    saveTask($task);
    $page++;
    usleep(1200*1000); // B2B接口放慢请求速率,降低风控
}
redis()->del($redisDedupKey);

三、商品数据轻量化提取方案(本篇重点)

思路:不完整解析原始大 JSON,按需提取,直接丢弃大体积冗余字段。B2B 搜好货原始返回包含大量大字段:详情富文本、图片数组几十张、企业资质、视频、询盘记录、参数详情块,全部跳过不读取,减少内存占用。

3.1 轻量化提取原则

  1. 只提取业务必要字段,原始大字段直接丢弃,不做解析;
  2. 安全取值,key 不存在返回默认值,禁止直接访问数组下标;
  3. B2B 阶梯报价归一化:支持单一口价 / 多档阶梯报价两种结构统一输出;
  4. 供应商信息轻量化:只保留 companyId、厂家名称、地区,不读企业详情长文本;
  5. 可选:原始 JSON 按需异步落盘备份(可选,不阻塞主采集链路)。

3.2 轻量化输出结构体(精简版)

复制代码
{
  "goods_id": "",
  "title": "",
  "brand": "",
  "company_id": "",
  "company_name": "",
  "province": "",
  "city": "",
  "price_type": "", // fixed固定价 / ladder阶梯价
  "price": 0, // 最低报价
  "price_range": [], // 阶梯报价数组 [{min_num:1,price:50}]
  "min_order": 0, // 最小起订量
  "main_image": "",
  "category_name": "",
  "sales": 0,
  "stock_status": "",
  "crawl_time": ""
}

3.3 字段映射与轻量化规则

表格

标准字段 原始候选字段 默认值 处理说明
goods_id goodsId "" 商品唯一 ID
title title "" trim 去除空格,清理换行
company_id companyId "" 供应商 ID
company_name companyName "" 厂家名称
province province "" 省份
city city "" 城市
min_order minOrder 0 最小起订量 B2B 核心字段
main_image coverImg "" 只取封面图,丢弃全部图片数组
sales saleNum 0 成交量

轻量化核心优化:放弃 imageList、detailHtml、attributeHtml、video、certificate 资质等超大字段,不去读取,减少 JSON 解析开销。

B2B 阶梯报价提取逻辑(搜好货特有)

两种结构:

  1. price 单一固定价:price_type="fixed",price_range 空数组;
  2. ladderPrice 数组,多档起订报价:price_type="ladder",price 取数组内最低价,price_range 存储阶梯列表。

3.4 PHP 轻量化提取函数 extractLightGoods

复制代码
/**
 * 搜好货商品轻量化提取,丢弃冗余大字段
 */
function extractLightGoods($raw){
    $std = [];
    $std['goods_id'] = getSafe($raw,'goodsId','');
    $std['title'] = trim(getSafe($raw,'title',''));
    $std['company_id'] = getSafe($raw,'companyId','');
    $std['company_name'] = getSafe($raw,'companyName','');
    $std['province'] = getSafe($raw,'province','');
    $std['city'] = getSafe($raw,'city','');
    $std['min_order'] = intval(getSafe($raw,'minOrder',0));
    $std['main_image'] = getSafe($raw,'coverImg','');
    $std['category_name'] = getSafe($raw,'categoryName','');
    $std['sales'] = intval(getSafe($raw,'saleNum',0));
    $std['crawl_time'] = date('Y-m-d H:i:s');

    // B2B阶梯报价处理
    $ladderList = getSafe($raw,'ladderPrice',[]);
    if(!empty($ladderList)){
        $std['price_type'] = 'ladder';
        $std['price_range'] = [];
        $minPrice = 9999999;
        foreach($ladderList as $lad){
            $num = intval(getSafe($lad,'minNum',0));
            $p = floatval(getSafe($lad,'price',0));
            $std['price_range'][] = ["min_num"=>$num,"price"=>$p];
            if($p < $minPrice) $minPrice = $p;
        }
        $std['price'] = $minPrice;
    }else{
        $std['price_type'] = 'fixed';
        $std['price'] = floatval(getSafe($raw,'price',0));
        $std['price_range'] = [];
    }
    return $std;
}

// 安全取值,不存在返回默认值
function getSafe($arr,$key,$default=''){
    if(!is_array($arr)) return $default;
    return isset($arr[$key]) ? $arr[$key] : $default;
}

四、性能优化(轻量化带来的收益)

  1. JSON 解析开销下降:跳过详情 HTML、多张图片、资质文本,单商品 JSON 体积从几 KB~ 十几 KB 压缩到几百字节;
  2. 数据库存储压力:入库表字段精简,不存大文本,索引只建 goods_id、company_id、keyword;
  3. 内存占用:批量循环时,解析完提取字段后,原始大对象及时释放,避免内存持续上涨;
  4. 带宽减少:如果可以,请求层可和接口协商,尽量不返回详情富文本字段(部分接口支持参数控制返回字段)。

五、监控、脏数据校验与告警

  1. 分页指标监控:空页计数、重试次数、goodsId 重复率、token 失效次数;
  2. B2B 业务脏数据校验:
    • min_order <0 标记脏数据;
    • 阶梯报价价格异常(价格 <=0);
    • 标题为空、goodsId 为空;
  3. 告警:token 失效、连续 3 次接口 5xx、脏数据比例超过阈值;
  4. 日志分级:info 正常采集,warning 低返回条目,error 鉴权 / 接口异常。

六、风险与扩展优化

  1. 搜好货接口结构变更:轻量化只取少量核心字段,相比全量解析,接口改动影响面更小;建议异步单独备份原始 JSON(非主链路),方便字段映射修复;
  2. 并发策略:单个关键词串行分页,多关键词使用协程 / 线程池控制并发数,B2B 平台对并发敏感;
  3. 增量监控:关键词搜索是快照采集;如需监控价格变动,以 goodsId 为 key,对比新旧 price/min_order;
  4. 同款合并:业务如需供应商同款比价,在轻量化结构体基础上,增加标题 md5+companyId 做同款分组。

七、整体架构

复制代码
任务调度层(关键词任务队列、状态持久化)
    ↓
HTTP请求层(签名、token、分级重试、节流、鉴权判断)
    ↓
分页容错层(空页计数、goodsId去重、断点续采、结束判定)
    ↓
轻量化提取层(只读取必要字段,丢弃富文本/大图等冗余数据,B2B阶梯报价归一)
    ↓
存储层:轻量化商品表(主业务)|原始JSON备份表(异步可选)
    ↓
上层业务:B2B货源比价、供应商监控、选品报表
相关推荐
行者-全栈开发2 小时前
【容器/虚拟化】CVE-2026-53359:Linux KVM Januscape 虚拟机逃逸漏洞修复指南(潜伏16年的云安全噩梦)
linux·运维·服务器·多租户隔离·cve-2026-53359·kvm虚拟机逃逸·shadow mmu
lovingsoft3 小时前
Chrome Lighthouse 详解
前端·chrome
Qlly3 小时前
全栈工程师怎么接需求?从一句「加个功能」聊到能开发、能验收
前端·后端
马可家的菠萝3 小时前
你的微信“文件传输助手”,是不是已经成了第二个网盘?
前端·后端·架构
略略略啦3 小时前
Vibe Coding 基于Spec DD和TDD的实践指南
前端
AI备忘录3 小时前
(二十五)华为华三锐捷迈普思科 ERPS 环网保护配置命令(G.8032 五厂商对照)
运维·服务器·网络·网络协议·华为
shmily麻瓜小菜鸡3 小时前
前端实现图片与PDF文字识别敏感信息打码方案
前端·pdf·状态模式
打工仔折腾 AI3 小时前
LangChain 工具调用实战:从 Tool 定义到 Agent 执行链路拆解
java·前端·人工智能·后端·python·langchain·ai agent 实战
木由里予3 小时前
NAND 这门生意
运维·服务器·网络·单片机