「技术、数据、接口、系统问题欢迎留言私信沟通」

在通过1688开放平台API批量采集商品数据时,开发者经常会遇到两类障碍:反爬拦截和调用限流。反爬的表现包括签名反复验证失败、IP被临时封禁甚至应用权限被冻结;限流则主要体现在接口每秒请求数(QPS)和单日总调用量的硬性限制上。这些机制本意是保障平台稳定,但也给合规的数据采集带来了不少麻烦。

下面结合一些实际处理经验,从签名规范、IP代理、请求频率控制和调用量分配几个方面,分享一套相对完整的优化方案。文中会附上经过封装的Python代码,可以直接拿来参考或改动。


一、反爬与限流的典型表现

先梳理一下常见的反爬和限流现象,方便后面有针对性地解决。

1. 反爬拦截的常见形式

  • 签名无效:即使按照官方规则拼接参数并生成sign,接口仍然返回“sign无效”。常见原因是参数排序、URL编码细节出错,或者本地时间与1688服务器时间偏差太大,导致签名验证失败。

  • IP临时封禁:短时间内从同一个IP发出大量请求后,接口开始返回“请求来源异常”或者直接拒绝连接。IP被拉入黑名单,通常封禁几小时到一天不等。

  • 账号/应用权限冻结:频繁调用某些敏感接口(比如批量获取商品详情),或者使用明显伪造的item_id,可能导致应用临时被停用,需要联系平台解封。

2. 限流的主要规则

  • QPS限制:个人应用一般限制在10 QPS,企业应用可以申请到20~50 QPS。一旦超过阈值,接口会返回error_code=429(请求过于频繁)。

  • 单日调用量限制:以alibaba.item.get为例,个人应用通常每天只能调用1000次,企业应用可到1万次。达到上限后当天无法再继续调用。

  • 接口关联叠加:如果同时调用多个关联接口(例如先查商品详情,再查价格、库存),这些调用量会累加计算,更容易触发单日上限。


二、反爬应对策略

反爬的核心,是让自己的请求行为尽可能接近“正常访问”,同时严格保证参数的正确性。

1. 规范签名生成与参数编码

1688对签名验证的精度很高,参数顺序、特殊字符转码、时间戳都必须完全符合要求。下面这个函数处理了常见的签名“坑”,能解决绝大多数签名无效的问题。

import hashlib
import time
from urllib.parse import urlencode, quote

def build_sign(params, app_secret):
    """
    严格按照1688规则生成签名
    :param params: 请求参数字典,不含sign
    :param app_secret: 应用的App Secret
    :return: 32位小写MD5签名
    """
    # 1. 按参数名的ASCII码升序排序
    sorted_items = sorted(params.items(), key=lambda x: x[0])

    # 2. 对每个值进行URL编码,处理中文、空格等字符。
    #    注意1688的签名要求:部分符号不编码,用safe参数指定
    encoded_parts = []
    for k, v in sorted_items:
        encoded_value = quote(str(v), safe='~!*()')
        encoded_parts.append(f"{k}={encoded_value}")

    # 3. 用&拼接参数串,末尾直接追加App Secret
    sign_string = "&".join(encoded_parts) + app_secret

    # 4. 计算MD5,返回小写结果
    return hashlib.md5(sign_string.encode("utf-8")).hexdigest().lower()


def fetch_item_detail(app_key, app_secret, item_id):
    """
    获取单件商品详情,内置签名和时间戳处理
    """
    # 构造请求参数,时间戳使用本地格式化时间,避免时区偏差
    params = {
        "app_key": app_key,
        "method": "alibaba.item.get",
        "timestamp": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
        "format": "json",
        "v": "2.0",
        "item_id": item_id,
        # 以下两个参数模拟官方SDK的常见字段,降低被识别为异常流量的概率
        "partner_id": "apidoc",
        "sdk_version": "python/3.9"
    }

    # 生成签名并加入参数
    params["sign"] = build_sign(params, app_secret)

    api_url = "https://gw.open.1688.com/openapi/param2/1/com.alibaba.product/alibaba.item.get"
    full_url = f"{api_url}?{urlencode(params, quote_via=quote)}"

    try:
        resp = requests.get(full_url, timeout=15)
        data = resp.json()
        if data.get("error_response"):
            print(f"接口返回错误:{data['error_response']['msg']}")
        return data
    except Exception as e:
        print(f"请求异常:{e}")
        return None

优化要点:

  • 显式传入partner_id和sdk_version,让请求头看起来像是从官方SDK发出的;

  • 使用quote函数并设置safe='~!*()',避免对特殊符号过度编码;

  • 时间戳直接取自time.localtime(),防止因时区设置导致与服务器时间不一致。

2. 动态IP池与代理轮换

如果需要大量采集(例如单日几千次),只靠单一IP几乎必然会被封。比较可行的办法是维护一个付费的高匿代理IP池,每次请求随机选取代理,并在代理失效时自动切换。

import random
import requests

# 代理池示例,实际使用时应定期更新或对接代理服务API
PROXY_POOL = [
    "http://112.111.12.113:8080",
    "https://123.124.125.126:443",
    "http://101.102.103.104:9090"
]

def get_random_proxy():
    return random.choice(PROXY_POOL)


def fetch_with_proxy(app_key, app_secret, item_id):
    """
    带代理轮换的商品详情请求,失败时自动更换代理并重试
    """
    params = {
        "app_key": app_key,
        "method": "alibaba.item.get",
        "timestamp": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
        "format": "json",
        "v": "2.0",
        "item_id": item_id
    }
    params["sign"] = build_sign(params, app_secret)
    api_url = "https://gw.open.1688.com/openapi/param2/1/com.alibaba.product/alibaba.item.get"
    full_url = f"{api_url}?{urlencode(params, quote_via=quote)}"

    # 最多尝试3次,每次使用不同代理
    for attempt in range(3):
        proxy = get_random_proxy()
        proxies = {"http": proxy, "https": proxy}
        try:
            resp = requests.get(full_url, proxies=proxies, timeout=15)
            data = resp.json()

            # 如果返回信息提示IP被限制,就换一个代理重试
            if data.get("error_response") and "IP" in data["error_response"]["msg"]:
                print(f"代理{proxy}受限,尝试下一个")
                continue

            return data
        except Exception as e:
            print(f"代理请求异常:{e},重试中")
            continue

    print("所有代理尝试均失败")
    return None

使用建议:

  • 不要用免费代理,速度和稳定性很难支撑连续采集,而且容易被平台识别;

  • 代理池规模最好与请求并发量匹配,比如持续10 QPS的场景,池中至少保持20个以上可用IP;

  • 可以配合一个定时任务,每隔一段时间从代理服务商API拉取最新IP,淘汰失效的。


三、限流应对策略

限流问题主要靠控制请求节奏和合理分配每日调用量来解决。

1. 基于令牌桶的QPS控制

直接在循环里加固定sleep不够灵活,用令牌桶可以更精细地贴合QPS上限。下面这个控制器会动态计算是否需要等待。

import time
from collections import deque

class QPSController:
    """QPS控制器,确保每秒实际请求数不超过设定值"""
    def __init__(self, max_qps):
        self.max_qps = max_qps
        self.request_times = deque()

    def wait(self):
        """在发起请求前调用,如果当前秒内请求数已满,自动阻塞"""
        now = time.time()
        # 清除1秒前的记录
        while self.request_times and now - self.request_times[0] > 1:
            self.request_times.popleft()

        # 如果这一秒内已经达到上限,休眠直到下一周期开始
        if len(self.request_times) >= self.max_qps:
            sleep_time = 1 - (now - self.request_times[0])
            time.sleep(sleep_time)

        self.request_times.append(time.time())

将它嵌入批量请求流程:

def batch_fetch_items(app_key, app_secret, item_ids, max_qps=10):
    qps_ctrl = QPSController(max_qps)
    results = []

    for item_id in item_ids:
        qps_ctrl.wait()                     # 保证不超QPS
        data = fetch_with_proxy(app_key, app_secret, item_id)
        if data:
            data["request_time"] = time.time()   # 记录请求时间,方便后续统计
            results.append(data)
        print(f"已处理商品 {item_id},当前秒内请求数:{len(qps_ctrl.request_times)}")

    return results

2. 错峰分配每日调用量

如果当天需要采集的商品数量远超单日上限(如个人应用上限1000次),比较好的做法是把任务拆到不同时段,或者用多个应用账号分担。下面是一个按小时平均分配剩余调用量的方案。

import datetime

def calc_hourly_limit(daily_limit, used_calls, current_hour):
    """
    计算当前小时还可以发起多少次请求
    :param daily_limit: 单日总上限
    :param used_calls: 今天已经使用的次数(需外部持久化)
    :param current_hour: 当前小时数 (0-23)
    """
    remaining_hours = 24 - current_hour
    remaining_calls = daily_limit - used_calls
    if remaining_hours <= 0:
        return 0
    # 预留20%余量,避免后期因估算偏差超限
    return int(remaining_calls / remaining_hours * 0.8)


def scheduled_batch_fetch(app_key, app_secret, item_ids, daily_limit=1000):
    """
    按小时分批采集,避免单日总量用尽
    注意:used_calls需要在实际项目中存入文件或数据库,这里仅做演示
    """
    used_calls = 0          # 应从持久化存储中读取
    results = []
    hour_call_count = {}    # 记录每个小时已经请求的次数

    for item_id in item_ids:
        now = datetime.datetime.now()
        hour = now.hour

        # 初始化当前小时计数器
        if hour not in hour_call_count:
            hour_call_count[hour] = 0

        # 当前小时允许的最大次数
        max_this_hour = calc_hourly_limit(daily_limit, used_calls, hour)

        # 如果当前小时已经用尽配额,等待到下一小时
        while hour_call_count[hour] >= max_this_hour:
            next_hour = (hour + 1) % 24
            wait_seconds = (next_hour - hour) * 3600 - now.minute * 60 - now.second
            if wait_seconds <= 0:
                wait_seconds += 24 * 3600
            print(f"当前小时配额已满,休眠 {wait_seconds} 秒")
            time.sleep(wait_seconds)
            now = datetime.datetime.now()
            hour = now.hour
            if hour not in hour_call_count:
                hour_call_count[hour] = 0
            # 重新计算可用次数(因为小时变了,配额可能变化)
            max_this_hour = calc_hourly_limit(daily_limit, used_calls, hour)

        # QPS控制 + 请求
        qps_ctrl.wait()
        data = fetch_with_proxy(app_key, app_secret, item_id)
        if data:
            data["request_time"] = time.time()
            results.append(data)
            used_calls += 1
            hour_call_count[hour] += 1

        # 单日总量用完,终止循环
        if used_calls >= daily_limit:
            print(f"已达到单日上限 {daily_limit} 次,停止采集")
            break

    return results

实际应用中需要注意:

  • 今日已用次数(used_calls)需要落盘存储(如数据库或Redis),避免重启程序后从零开始;

  • 如果采集任务跨天,需要在零点重置计数器;

  • 当采集规模很大时,可以申请多个应用账号,将商品列表分片,并行运行多个采集进程,每个进程消耗一个账号的配额。


通过上面几个方面的组合优化,基本可以在遵守1688开放平台规则的前提下,稳定、高效地完成商品数据的批量采集。这些代码块都保持了较高的可复用性,可以根据自己的业务需求进行调整和封装。

更多推荐