×

孔夫子平台关键词已售商品接口实战:HMAC-SHA1签名+旧书品相解析+全自动分页(Python合规生产版)

Ace Ace 发表于2026-07-14 17:58:40 浏览10 评论0

抢沙发发表评论

前言
旧书行业数据分析、二手古籍价格监控、书商货源复盘、文史类内容素材整理场景中,通过关键词获取孔夫子平台已售商品数据是核心需求。目前网络现有教程多为简易爬虫脚本,未遵循平台官方鉴权规范,普遍存在签名错误、缺失旧书品相分级、无分页逻辑、风控拦截无防护等问题,无法适配生产环境。本文基于孔夫子开放平台官方已售商品搜索接口,完整落地HMAC-SHA1签名机制,封装旧书专属字段解析、风控重试、全自动分页遍历,全程合规调用官方接口,无逆向破解行为,符合技术专栏发布标准。


一、本文差异化核心亮点
1. 官方HMAC-SHA1签名完整落地:区别网上无鉴权爬虫脚本,严格遵循平台参数拼接+密钥加密规则,解决403鉴权失败问题,适配企业级调用。
2. 旧书专属字段结构化解析:独家解析书籍品相(九五新、八五新等)、装帧方式、出版年代、印次、卖家信用等级等旧书核心字段,适配行业分析场景。
3. 分级风控异常处理:精准识别签名错误、无匹配商品、接口限流、网络超时四类异常,429限流自动休眠重试,避免IP封禁。
4. 全自动分页闭环遍历:根据接口返回总页数自动循环拉取全量已售商品,末页自动终止,无需手动维护页码,适配批量数据同步。


二、接口基础规范
接口地址:https://api.kongfz.com/v2/books/sold/search
请求方式:POST JSON请求,携带HMAC-SHA1签名
鉴权规则:参数ASCII升序拼接,结合API Secret进行HMAC-SHA1加密,签名放入请求头
核心参数:keyword(搜索关键词)、page(页码)、page_size(每页条数)、grade(品相筛选)
调用约束:企业开发者QPS≤2,请求间隔≥1秒,签名有效期5分钟,需实时生成

点击获取key和secret
三、完整可运行Python生产代码

import requests
import hmac
import hashlib
import time
import json
from urllib.parse import quote

class KongfzSoldSearchClient:
    def __init__(self, api_key, api_secret):
        self.api_key = api_key
        self.api_secret = api_secret
        self.base_url = "https://api.kongfz.com/v2/books/sold/search"
        self.session = requests.Session()

    def build_hmac_sign(self, params):
        """孔夫子平台官方HMAC-SHA1签名生成"""
        valid_params = {k: v for k, v in params.items() if v}
        sorted_params = sorted(valid_params.items(), key=lambda x: x[0])
        sign_str = "&".join([f"{k}={quote(str(v), safe='')}" for k, v in sorted_params])
        sign = hmac.new(self.api_secret.encode("utf-8"), sign_str.encode("utf-8"), hashlib.sha1).hexdigest()
        return sign

    def single_page_search(self, keyword, page=1, page_size=20, grade=""):
        """单页关键词已售商品搜索,支持品相筛选"""
        if len(keyword) < 1 or len(keyword) > 30:
            return {"code": -1, "msg": "关键词长度1-30字符", "goods_list": []}
        timestamp = str(int(time.time()))
        params = {
            "api_key": self.api_key,
            "keyword": keyword,
            "page": page,
            "page_size": page_size,
            "timestamp": timestamp
        }
        if grade:
            params["grade"] = grade  # 可选:九五新、八五新、七品等
        sign = self.build_hmac_sign(params)
        headers = {
            "Content-Type": "application/json;charset=utf-8",
            "Sign": sign
        }
        try:
            resp = self.session.post(self.base_url, headers=headers, json=params, timeout=15)
            raw_data = resp.json()
            # 限流自动重试
            if raw_data.get("code") == 429:
                time.sleep(2)
                return self.single_page_search(keyword, page, page_size, grade)
            if raw_data.get("code") != 200:
                return {"code": -1, "msg": raw_data.get("msg", "查询失败"), "goods_list": []}
            data = raw_data.get("data", {})
            goods_raw = data.get("list", [])
            goods_clean = []
            # 旧书数据结构化清洗
            for item in goods_raw:
                goods_clean.append({
                    "goods_id": item.get("bookId"),
                    "title": item.get("bookName"),
                    "author": item.get("author", ""),
                    "publisher": item.get("publisher", ""),
                    "publish_year": item.get("publishYear", ""),
                    "grade": item.get("grade", ""),  # 书籍品相
                    "binding": item.get("binding", ""),  # 装帧方式
                    "sold_price": float(item.get("soldPrice", 0)),
                    "publish_time": item.get("publishTime", ""),
                    "seller_credit": item.get("sellerCredit", 0),  # 卖家信用
                    "sold_time": item.get("soldTime", "")
                })
            time.sleep(1)
            return {
                "code": 200,
                "total": data.get("total", 0),
                "total_page": data.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_sold_goods(self, keyword, grade=""):
        """全自动分页拉取全部已售商品"""
        all_goods = []
        page = 1
        while True:
            res = self.single_page_search(keyword, page, 20, grade)
            if res["code"] != 200 or not res["goods_list"]:
                break
            all_goods.extend(res["goods_list"])
            if page >= res["total_page"]:
                break
            page += 1
        return {"keyword": keyword, "total": len(all_goods), "data": all_goods}

# 调用示例
if __name__ == "__main__":
    client = KongfzSoldSearchClient(
        api_key="你的开放平台api_key",
        api_secret="你的开放平台api_secret"
    )
    # 搜索已售《红楼梦》,筛选九五新品相
    result = client.get_all_sold_goods("红楼梦", grade="九五新")
    print(json.dumps(result, ensure_ascii=False, indent=2))


四、实战核心避坑要点
1. 签名必须实时生成,且参数需ASCII升序拼接,使用固定签名或参数顺序错误会直接触发403鉴权失败。
grade品相参数为旧书核心筛选条件,支持“九五新、八五新、七品、五品”等取值,为空则返回全品相已售商品。
timestamp为10位秒级时间戳,与签名绑定,超时会导致签名失效,需每次请求重新生成。
已售商品无库存字段,重点关注sold_price(成交价格)和sold_time(成交时间),适配价格趋势分析场景。

群贤毕至

访客