应对1688 API反爬与限流:签名优化、动态代理与流量控制
「技术、数据、接口、系统问题欢迎留言私信沟通」
在通过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开放平台规则的前提下,稳定、高效地完成商品数据的批量采集。这些代码块都保持了较高的可复用性,可以根据自己的业务需求进行调整和封装。
更多推荐



所有评论(0)