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

一、接口基础说明

点击获取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货源比价、供应商监控、选品报表
相关推荐
瑞克-桑切斯1 小时前
7、MySQL 慢查询排查与优化实战指南
java·服务器·数据库
Dream-Y.ocean1 小时前
鸿蒙PC平台宅匠 3D 室内设计适配实战:基于 Electron 壳方案的 Three.js 双渲染引擎与参数化户型编辑器开发
javascript·3d·harmonyos
Csvn2 小时前
性能监控
前端
Highcharts.js2 小时前
Highcharts Gantt使用符号的自定义数据标签开发甘特图示例
javascript·甘特图·highcharts·图表开发·实战代码·响应式图表
Csvn3 小时前
构建工具
前端
vibecode3 小时前
网站服务器失去响应怎么解决?我把连接超时和读超时这两段计时拆开跑了一遍
运维·服务器
FRing033 小时前
vue2干货总结3(computed计算属性、watch监听器)
开发语言·前端·vue.js·vue2
迪康Defender4 小时前
终端供应链安全:应用白名单与软件正版化检测落地|终端安全治理实践
运维·服务器·网络·安全·web安全·自动化·运维开发
沫璃染墨5 小时前
从零入门计算机网络系列(十二):HTTP长连接与会话管理详解——从Connection到Cookie与Session
linux·服务器·c++·tcp/ip·ubuntu·http·交互
kyriewen5 小时前
Skills 榜前 10 我全装了:只有 3 个能直接用
前端·javascript·程序员