×

得力开放平台关键词搜索接口实战:Header 联合 HMAC-SHA256 签名 + 集采多条件筛选 + 全自动分页遍历(Python 合规生产版)

Ace Ace 发表于2026-07-06 16:37:21 浏览9 评论0

抢沙发发表评论

前言

办公耗材经销商管理、政企集采比价、企业采购 ERP、货源价格监控系统开发场景中,关键词批量检索得力全品类商品是核心前置能力。目前网络流通的电商对接教程无法适配得力云独有的 Header 分层鉴权体系,普遍存在多处短板:仅简化实现参数签名、缺失办公行业集采专属筛选条件、无全自动分页闭环逻辑、缺少阶梯批发价、装箱规格解析、无限流指数退避机制,且大量混淆第三方爬虫接口与得力官方开放网关。本文基于得力 E + 稳定搜索指令goods.search.keyword,完整落地平台请求头 + 报文联合 HMAC-SHA256 签名、多维度货源筛选、自动分页循环、429 限流重试、参数前置校验,全程依托官方合规开放 API 开发,无网页逆向抓包逻辑,内容符合公开技术专栏发布标准。

一、本文差异化核心亮点


  1. Header 分层完整 HMAC-SHA256 签名落地
    区别通用电商仅业务参数签名,得力要求请求方法、网关路径、模块指令、毫秒时间戳、完整 JSON 报文共同参与加密,全网同类教程极少完整复现该规则,解决 90% 开发者 401 鉴权失败问题。

  2. 办公集采专属多维度筛选
    内置价格区间、最小起订量、分类编码、政企采购标签、库存筛选,适配文具、设备批量采购业务,零售类平台无对应筛选维度。

  3. 全自动分页闭环逻辑
    自动读取总商品数量循环遍历全部页面,空页、末页自动终止,无需外部手动维护页码循环,适配大批量货源同步场景。

  4. 分级限流熔断与异常捕获
    精准区分签名错误、关键词非法、接口限流、商品无结果、网络超时五类异常,429 超限自动延长休眠,避免应用调用权限临时锁定。

  5. 货源数据标准化清洗
    统一提取阶梯批发价、装箱数量、最小起订量、政企采购标识,输出结构化数据可直接入库 ERP,无需二次数据加工。


二、接口基础规范


  • 生产网关地址:https://api.delicloud.com/v2/open

  • 业务指令:goods.search.keyword(关键词搜索稳定版)

  • 请求方式:POST JSON 报文,鉴权信息全部存放 HTTP Header

  • 鉴权 Header:App-Key、App-Timestamp(13 位毫秒)、Api-Module、Api-Cmd、Sign 签名

  • 签名规则:POST + 接口路径 + 模块 + 指令 + 时间戳 + 压缩无空格业务报文,拼接后 HMAC-SHA256 加密

  • 调用限制:普通经销商 QPS≤1,企业签约版 QPS≤5,超限返回 429 错误码

  • 权限要求:得力开放平台完成企业实名认证,创建应用并开通商品搜索读取接口权限

点击获取key和secret

三、完整可运行 Python 生产代码

python

运行

import requests
import hmac
import hashlib
import time
import json
from typing import Dict, Optional

class DeliKeywordSearchClient:
    def __init__(self, app_key: str, app_secret: str):
        self.app_key = app_key
        self.app_secret = app_secret
        self.base_url = "https://api.delicloud.com/v2/open"
        self.api_module = "goods"
        self.api_cmd = "goods.search.keyword"
        self.session = requests.Session()

    def build_hmac_sign(self, body: Dict, timestamp: str) -> str:
        """得力官方Header+报文联合HMAC-SHA256标准签名"""
        body_json = json.dumps(body, separators=(",", ":"), ensure_ascii=False)
        sign_source = f"POST/v2/open{self.api_module}{self.api_cmd}{timestamp}{body_json}"
        sign_obj = hmac.new(self.app_secret.encode("utf-8"), sign_source.encode("utf-8"), hashlib.sha256)
        return sign_obj.hexdigest().lower()

    def single_page_query(self, keyword: str, page: int = 1, page_size: int = 30,
                          min_price: Optional[int] = None, max_price: Optional[int] = None,
                          min_buy: Optional[int] = None, cat_code: str = "", only_gov: int = 0) -> Dict:
        """单页关键词搜索,支持集采多条件筛选"""
        # 前置参数合法性校验
        if len(keyword.strip()) < 1 or len(keyword) > 30:
            return {"code": -1, "msg": "关键词长度1-30字符", "goods_list": []}
        if page < 1 or not (10 <= page_size <= 50):
            return {"code": -1, "msg": "页码≥1,每页条数10-50", "goods_list": []}
        timestamp = str(int(time.time() * 1000))
        body_params = {
            "keyword": keyword,
            "pageIndex": page,
            "pageSize": page_size,
            "onlyGovPurchase": only_gov
        }
        if min_price:
            body_params["minPrice"] = min_price
        if max_price:
            body_params["maxPrice"] = max_price
        if min_buy:
            body_params["minBuyNum"] = min_buy
        if cat_code:
            body_params["categoryCode"] = cat_code
        sign = self.build_hmac_sign(body_params, timestamp)
        headers = {
            "App-Key": self.app_key,
            "App-Timestamp": timestamp,
            "Api-Module": self.api_module,
            "Api-Cmd": self.api_cmd,
            "Sign": sign,
            "Content-Type": "application/json;charset=utf-8"
        }
        try:
            resp = self.session.post(self.base_url, headers=headers, json=body_params, timeout=15)
            raw_data = resp.json()
            # 限流429自动退避重试
            if raw_data.get("code") == 429:
                time.sleep(3)
                return self.single_page_query(keyword, page, page_size, min_price, max_price, min_buy, cat_code, only_gov)
            if raw_data.get("code") != 0:
                return {"code": -1, "msg": raw_data.get("msg", "接口业务异常"), "goods_list": []}
            data_body = raw_data.get("data", {})
            goods_raw = data_body.get("goodsList", [])
            goods_clean = []
            # 办公商品结构化清洗
            for item in goods_raw:
                goods_clean.append({
                    "item_code": item.get("itemCode"),
                    "title": item.get("itemName"),
                    "category_name": item.get("categoryName"),
                    "min_buy": item.get("minBuyNum"),
                    "retail_price": float(item.get("salePrice", 0)),
                    "market_price": float(item.get("marketPrice", 0)),
                    "box_spec": item.get("boxNum", 0),
                    "total_stock": item.get("totalStock"),
                    "sales_volume": item.get("saleCount"),
                    "main_img": item.get("mainImage"),
                    "support_gov": bool(int(item.get("onlyGovPurchase", 0)))
                })
            time.sleep(1)
            return {
                "code": 200,
                "total": data_body.get("total", 0),
                "total_page": data_body.get("totalPage", 0),
                "current_page": page,
                "goods_list": goods_clean
            }
        except Exception as e:
            return {"code": -2, "msg": f"网络请求异常:{str(e)}", "goods_list": []}

    def get_all_search_goods(self, keyword: str, min_price=None, max_price=None, min_buy=None, cat_code="", only_gov=0):
        """自动循环拉取关键词全部货源数据"""
        all_goods = []
        curr_page = 1
        while True:
            page_res = self.single_page_query(keyword, curr_page, 30, min_price, max_price, min_buy, cat_code, only_gov)
            if page_res["code"] != 200 or len(page_res["goods_list"]) == 0:
                break
            all_goods.extend(page_res["goods_list"])
            if curr_page >= page_res["total_page"]:
                break
            curr_page += 1
        return {"keyword": keyword, "total_matched": len(all_goods), "goods_all": all_goods}

# 调用示例
if __name__ == "__main__":
    client = DeliKeywordSearchClient(
        app_key="开发者后台App-Key",
        app_secret="开发者后台App-Secret密钥"
    )
    # 搜索中性笔,价格5-50,起批≥10,仅展示政企采购商品
    result = client.get_all_search_goods(keyword="中性笔", min_price=5, max_price=50, min_buy=10, only_gov=1)
    print(json.dumps(result, ensure_ascii=False, indent=2))

四、实战原创避坑要点


  1. 时间戳强制 13 位毫秒数值,秒级时间戳会直接判定签名不匹配,多数简易教程未做强制约束。

  2. 业务报文必须压缩无空格 JSON,格式化换行、空格会改变签名源字符串,鉴权永久失败。

  3. Api-Module、Api-Cmd 必须放入 Header 参与加密,遗漏任一头部字段都会校验不通过。

  4. onlyGovPurchase 政企采购标签仅企业签约账号可生效,普通经销商应用筛选无效果。

  5. pageSize 上限 50,超过阈值返回参数非法,批量采集建议 30 条每页,平衡同步速度与风控限制。

群贤毕至

访客