×

某音开放平台关键词搜索接口实战:SHA256-RSA2048签名+分页防限流(附Python完整代码)

Ace Ace 发表于2026-04-28 15:58:44 浏览8 评论0

抢沙发发表评论

在某音电商选品、带货监控、关键词热度分析、商品库同步等场景中,官方关键词搜索接口(open.goods.search)是获取合规商品数据的核心通道。网上多数教程要么依赖爬虫逆向、存在封号风险,要么简化签名逻辑、忽略分页与限流细节,导致代码无法落地生产。本文基于某音开放平台官方规范,实现一套含SHA256-RSA2048标准签名、关键词语义优化、分页续爬、防风控调度的生产级方案,内容原创合规、无敏感信息,完全适配CSDN审核,代码可直接集成到电商相关系统。
一、接口核心定位与差异化亮点
某音关键词搜索接口区别于普通零售平台,核心适配短视频带货、直播选品场景,本文方案与网上通用教程的核心差异的在于:

  • 签名合规:完整实现官方要求的SHA256-RSA2048签名机制(非简化MD5),包含待签名串构造、私钥加密、Base64编码全流程,解决网上签名失败的高频痛点;

  • 防风控设计:内置分页游标调度、请求间隔控制、限流重试机制,适配平台限流规则,避免IP或账号封禁;

  • 语义优化:针对某音带货关键词(如“直播爆款 连衣裙”),实现语义拆分与标签提取,提升搜索结果匹配度;

  • 分页完整:支持自动续爬多页数据,处理has_more分页标识,解决网上教程“只获取单页数据”的缺陷。

适用场景:某音电商选品工具、关键词热度监控、直播带货货源筛选、商品数据同步系统开发。
二、接口基础规范(官方标准)
不同于网上简化版说明,本文整理官方完整规范,避免因参数或签名遗漏导致调用失败:

  • 请求地址:生产环境open.douyin.com/api/goo,测试环境sandbox.open.douyin.com

  • 请求方式:POST(JSON格式),严格禁止GET请求(易触发风控);

  • 鉴权方式:SHA256-RSA2048签名(需应用公钥、应用私钥、平台公钥),搭配access-token用户授权;

  • 必传参数:appid、keyword、cursor(分页游标)、count(每页数量)、timestamp、nonce_str、sign;

  • 分页规则:cursor初始值为0,响应返回has_more标识是否有下一页,下一页cursor取响应返回值;

  • 频率限制:默认限流,需根据应用权限调整请求间隔,避免触发401签名失败、429限流错误。

点击获取key和secret

三、完整生产级代码(Python原创封装)

import requests
import time
import json
import base64
import random
from Crypto.PublicKey import RSA
from Crypto.Signature import pkcs1_15
from Crypto.Hash import SHA256

class MouYinKeywordSearchClient:
    """某音关键词搜索接口客户端(生产级,SHA256-RSA2048签名+分页防限流)"""
    def __init__(self, appid, app_private_key, platform_public_key, access_token):
        self.appid = appid
        self.app_private_key = RSA.import_key(app_private_key)  # 应用私钥
        self.platform_public_key = RSA.import_key(platform_public_key)  # 平台公钥
        self.access_token = access_token
        self.base_url = "https://open.douyin.com/api/goods/search"
        self.timeout = 12
        self.retry_count = 2
        self.request_interval = 2  # 基础请求间隔,规避限流
        self.page_size = 20  # 每页数量

    def _generate_nonce_str(self):
        """生成请求随机串(签名必需,保证请求唯一性)"""
        return ''.join(random.sample('0123456789abcdefghijklmnopqrstuvwxyz', 16))

    def _generate_sign(self, method, uri, timestamp, nonce_str, body):
        """生成SHA256-RSA2048签名(官方标准流程,网上教程常简化)"""
        # 1. 构造待签名串(五行,每行以\n结束,含最后一行)
        sign_raw = f"{method}\n{uri}\n{timestamp}\n{nonce_str}\n{body}\n"
        # 2. SHA256哈希
        hash_obj = SHA256.new(sign_raw.encode("utf-8"))
        # 3. 应用私钥签名,Base64编码
        signature = pkcs1_15.new(self.app_private_key).sign(hash_obj)
        return base64.b64encode(signature).decode("utf-8")

    def _optimize_keyword(self, keyword):
        """某音带货关键词语义优化(差异化核心)"""
        # 拆分带货标签,提升匹配度(如“直播爆款 连衣裙”→“连衣裙 直播爆款 带货”)
        tags = {"爆款": "直播爆款", "带货": "抖音带货", "秒杀": "限时秒杀"}
        for short, full in tags.items():
            if short in keyword and full not in keyword:
                keyword = keyword.replace(short, full)
        # 过滤无效字符,避免搜索失真
        invalid_chars = ["@", "#", "$"]
        for char in invalid_chars:
            keyword = keyword.replace(char, "")
        return keyword

    def search(self, keyword, max_page=3):
        """关键词搜索(含分页续爬、语义优化、防风控)"""
        optimized_keyword = self._optimize_keyword(keyword)
        cursor = 0  # 初始分页游标
        all_items = []
        method = "POST"
        uri = "/api/goods/search"  # 签名用URI,去除域名

        while cursor is not None and len(all_items) // self.page_size < max_page:
            timestamp = str(int(time.time()))  # 10位秒级时间戳
            nonce_str = self._generate_nonce_str()
            # 构造请求体
            body = json.dumps({
                "appid": self.appid,
                "keyword": optimized_keyword,
                "cursor": cursor,
                "count": self.page_size
            }, ensure_ascii=False)
            # 生成签名
            sign = self._generate_sign(method, uri, timestamp, nonce_str, body)

            # 请求头(含签名信息)
            headers = {
                "Content-Type": "application/json;charset=UTF-8",
                "access-token": self.access_token,
                "Byte-Authorization": f"SHA256-RSA2048 appid=\"{self.appid}\",nonce_str=\"{nonce_str}\",timestamp=\"{timestamp}\",signature=\"{sign}\""
            }

            # 防风控+重试
            for attempt in range(self.retry_count + 1):
                try:
                    time.sleep(self.request_interval)
                    response = requests.post(
                        self.base_url,
                        data=body,
                        headers=headers,
                        timeout=self.timeout
                    )
                    response.raise_for_status()
                    result = response.json()

                    if result.get("code") == 0:
                        data = result.get("data", {})
                        items = data.get("items", [])
                        # 结构化处理,剔除冗余字段
                        parsed_items = [{
                            "product_id": item.get("product_id"),
                            "title": item.get("title"),
                            "price": item.get("price"),
                            "stock": item.get("stock"),
                            "main_image": item.get("main_image"),
                            "sales": item.get("sales"),  # 带货销量(核心字段)
                            "shop_name": item.get("shop_name")
                        } for item in items]
                        all_items.extend(parsed_items)
                        # 分页处理
                        cursor = data.get("cursor")
                        if not data.get("has_more"):
                            cursor = None
                        break
                    else:
                        return {"code": result.get("code", -1), "msg": result.get("msg", "接口调用失败")}
                except requests.exceptions.RequestException as e:
                    if attempt == self.retry_count:
                        return {"code": 500, "msg": f"请求异常:{str(e)}"}
                    # 限流重试,延长间隔
                    if "429" in str(e):
                        self.request_interval += 1
                    time.sleep(1)

        return {
            "code": 200,
            "msg": "success",
            "keyword": optimized_keyword,
            "total": len(all_items),
            "items": all_items
        }

# 调用示例
if __name__ == "__main__":
    # 替换为某音开放平台获取的真实信息
    APPID = "your_appid"
    APP_PRIVATE_KEY = "-----BEGIN RSA PRIVATE KEY-----\n你的应用私钥\n-----END RSA PRIVATE KEY-----"
    PLATFORM_PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----\n平台公钥\n-----END PUBLIC KEY-----"
    ACCESS_TOKEN = "your_access_token"

    client = MouYinKeywordSearchClient(APPID, APP_PRIVATE_KEY, PLATFORM_PUBLIC_KEY, ACCESS_TOKEN)
    # 某音带货关键词搜索示例
    search_result = client.search(keyword="直播爆款 连衣裙", max_page=2)
    print(json.dumps(search_result, ensure_ascii=False, indent=2))


四、核心避坑要点(网上教程极少提及)

  1. 签名避坑:待签名串必须严格遵循“五行格式”,每行以\n结束(含最后一行),否则签名验证失败(返回401错误);

  2. 私钥避坑:应用私钥需保留完整格式(含BEGIN/END标识),不可遗漏换行,否则无法完成加密;

  3. 分页避坑:cursor需从响应中获取下一页值,不可手动递增,否则会导致数据重复或获取失败;

  4. 限流避坑:高峰时段(直播高峰19-22点)需延长请求间隔至3-5秒,避免触发429限流;

  5. 权限避坑:需提前申请search相关权限,未授权会返回权限不足错误,需在开放平台完成权限申请。

群贤毕至

访客