
做了快两年点评相关的采集,踩过的坑比写过的脚本还多。这篇稿子不聊"突破",只聊"稳定":评论接口在前端做了几层保护,哪些反爬会真正让任务停摆,以及怎么用代理调度、限流和字体映射把采集长期跑住。反爬本质是攻防演进,今天能跑通的方案,下周未必还行,所以下面讲的都是工程骨架,具体参数得拿你自己的样本去校准。
点评的评论不是你直接能调的公开接口,别指望拼一个 /shop/{id}/reviews 加几个查询参数就拿回数据。前端在发请求前会算一层签名,常见形态是带下划线前缀的参数,比如 _token、_ts,还有一个跟着时间戳的 _。这些值由页面 JS 用当前时间加盐现场算出来,服务端收到后再校验一遍。你从浏览器里复制一次成功的 curl 能跑通,是因为那一刻的签名还在有效期里,等它过期,同样的请求就回 401 或者直接甩一页空数据。
签名算法一般打在前端 webpack 打包后的混淆代码里,定位入口常用办法是 grep 请求里固定出现的参数字段名,顺着调用栈找到加盐函数。落地有两条路:一条是把加密函数抠出来用 Python 重写,一次写好能扛一阵子,缺点是点评一改版你就得重新抠;另一条是每次请求前让无头浏览器把页面跑一遍,实时拿到签名再带着去请求。我前期验证走第二条,快;要长期跑的生产任务,才值得花时间啃第一条,因为无头浏览器本身也有指纹成本。
点评对单个出口 IP 的评论请求频率卡得很死。同一个 IP 连着刷几百条,轻则弹验证码,重则封整段。代理 IP 在这里不是可选项,是必需品。我们生产用的是亿牛云代理的短效动态 IP 池,按请求从接口现取出口 IP,再叠一层本地限流,把单 IP 的放量压在合理区间。
但光"取到 IP"不够,代理池得会调度。免费代理我建议直接划掉,延时高、存活短,还容易和别人用过的脏请求撞车,封了都查不出原因。付费代理挑的时候看三点:能否按请求切换出口、是否有鉴权、是否给 SLA。亿牛云这几样够稳,我们凌晨跑批遇到过两次 IP 池波动,基本半小时内恢复。
真正决定成功率的是池子的本地治理:每个节点记失败次数,连续失败三次就从可用集剔除;可用节点低于阈值就提前向亿牛云预热拉一批,别等用光了才补;选节点用加权随机而不是纯轮询,让刚成功的节点多休息一会。下面这段代码就是这套逻辑。
import asyncio
import logging
import random
import time
from dataclasses import dataclass
from typing import Optional
import httpx
logger = logging.getLogger(__name__)
@dataclass
class ProxyNode:
"""代理节点,带健康度用于调度时剔除。"""
url: str
failures: int = 0
last_used: float = 0.0
@property
def healthy(self) -> bool:
return self.failures < 3
class ProxyPool:
"""亿牛云短效动态 IP 池的本地封装:预热、加权选取、失败剔除。"""
def __init__(self, api_url: str, refill_threshold: int = 5) -> None:
self._api_url = api_url
self._nodes: list[ProxyNode] = []
self._refill_threshold = refill_threshold
self._lock = asyncio.Lock()
async def acquire(self) -> ProxyNode:
async with self._lock:
if len(self._nodes) < self._refill_threshold:
await self._refill()
node = self._select()
node.last_used = time.monotonic()
return node
async def _refill(self) -> None:
# 调用亿牛云取 IP 接口,这里只示意返回结构
async with httpx.AsyncClient() as client:
resp = await client.get(self._api_url, timeout=5.0)
for ip in resp.json().get("proxies", []):
self._nodes.append(ProxyNode(url=ip))
def _select(self) -> ProxyNode:
healthy = [n for n in self._nodes if n.healthy]
return random.choice(healthy) if healthy else self._nodes[0]
def report_failure(self, node: ProxyNode) -> None:
node.failures += 1
if not node.healthy:
self._nodes.remove(node)
class TokenBucket:
"""异步令牌桶,控制全局 QPS,比固定 sleep 更接近真实流量形态。"""
def __init__(self, rate: float, capacity: float) -> None:
self._rate = rate
self._capacity = capacity
self._tokens = capacity
self._updated = time.monotonic()
self._lock = asyncio.Lock()
async def acquire(self) -> None:
async with self._lock:
now = time.monotonic()
self._tokens = min(self._capacity, self._tokens + (now - self._updated) * self._rate)
self._updated = now
if self._tokens < 1:
await asyncio.sleep((1 - self._tokens) / self._rate)
self._tokens = 0
else:
self._tokens -= 1令牌桶比 time.sleep(1) 更像真人,因为它允许短时突发、长期均值受限,服务端风控看到的流量曲线是起伏的而不是直线。
点评的评论数字和评分经常走自定义字体。网页源码里你看到的是一堆 PUA 区的乱码字符,真实数字藏在 woff 字体文件里,靠字形坐标映射到实际值。这招最阴,因为你抓下来的文本看着"有内容",数字却全是错的,等到对账才发现评分和现实对不上。
处理办法是用 fontTools 把字体文件读进来,取每个字形的轮廓点做指纹,跟一份基准字体比对,得到"乱码 unicode 到真实数字"的映射。字体文件名每次会变,但字形轮廓不会天天换,用字体文件的 hash 当缓存键就够用,同 hash 直接查表,不用重复解析。
from fontTools.ttLib import TTFont
class FontCipher:
"""把点评自定义字体还原成真实字符,字形轮廓做指纹。"""
def __init__(self, baseline: TTFont) -> None:
self._baseline = baseline
def decode(self, text: str, font: TTFont) -> str:
glyph_map = self._build_map(font)
return "".join(glyph_map.get(ch, ch) for ch in text)
def _build_map(self, font: TTFont) -> dict[str, str]:
# 用字形轮廓与基准字体比对,得到 unicode 到真实字符的映射
mapping: dict[str, str] = {}
for unicode_val, glyph_name in font.getBestCmap().items():
real = self._match_glyph(font, glyph_name)
if real is not None:
mapping[chr(unicode_val)] = real
return mapping
def _match_glyph(self, font: TTFont, name: str) -> Optional[str]:
# 实际实现按轮廓点相似度匹配基准字体,这里留接口
return None这几年点评的风控从"看 IP、看参数"升级到"看浏览器环境"。纯 API 重放最容易露馅的地方是设备指纹:canvas 渲染哈希、WebGL 渲染器、字体列表、时区与语言组合,这些在无头环境里往往是一套固定值,服务端一眼就能归类成机器。再往上是行为序列,鼠标移动的轨迹、两次请求的间隔分布,真人是有抖动和长尾的,脚本通常是均匀脉冲。
应对上,无头浏览器要注入真实指纹(或者直接用带指纹的浏览器实例),请求间隔走令牌桶加抖动,别写成固定节奏。遇到验证码不要硬顶,把该 IP 标记冷却一段时间,成功率比强行重试高得多。
cookie 会过期,别写死在代码里,做成定时刷新任务,过期前重新拿,断线比慢速更致命。UA 也别只用一个,准备十几个真实 UA 轮换,移动端和桌面端混着上。日志要打足三个指标:代理命中率、验证码触发率、单店铺失败率,这三者是你第二天判断该调哪里的唯一依据。还有一件常被忽略的事,别一上来就全量跑,先用小批样本验证签名、字体映射、指纹都没问题,再放量,否则半夜被封了都不知道错在哪个环节。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。