1. 从一次失败的抓取说起为什么转转这么难爬那天下午我正试图帮一个做二手手机市场分析的朋友抓取转转平台上的商品数据。按照常规思路我熟练地打开浏览器开发者工具找到商品列表页的请求复制下cURL命令准备在Python脚本里用requests库复现。一切看起来都很顺利直到我运行脚本后返回的HTML里只有一行孤零零的“正在检测访问环境...”或者干脆是一个空白页。刷新几次页面甚至能看到浏览器里正常显示的商品列表但用脚本请求就是不行。这感觉就像有一堵无形的墙你知道数据就在那里但就是拿不到。这就是典型的反爬虫机制在起作用而转转作为国内头部的二手交易平台其反爬体系经过多年迭代已经相当成熟和复杂。它不再是简单地检查User-Agent或者封禁高频IP这种“一刀切”的初级手段。转转的反爬更像一个多层次的防御系统从网络层、协议层到应用层都设置了关卡。对于数据采集者来说这不再是一场简单的“请求与响应”游戏而是一场涉及浏览器行为模拟、JavaScript逆向、网络协议理解的“攻防战”。这场攻防的核心已经远远超出了“写个Python脚本发请求”的范畴。它要求我们理解现代Web应用是如何工作的特别是前端JavaScript如何与后端API交互以及平台如何区分“真人用户”和“自动化脚本”。接下来我将结合实战经验拆解转转反爬的几个关键层面并分享一套相对可行的应对思路。请注意所有讨论均基于技术学习与研究目的旨在理解Web安全机制请务必遵守目标网站的robots.txt协议及相关法律法规。2. 第一道防线动态令牌与请求签名当你打开转转的商品页面时看似简单的页面背后浏览器已经执行了大量的JavaScript代码。其中最关键的一环就是生成每次请求都必须携带的“令牌”和“签名”。这是转转反爬最基础也最有效的一环。2.1 核心原理为什么需要签名想象一下你去银行取钱光有银行卡类似URL不行还得输入密码类似签名。服务器通过这个“密码”来验证请求的合法性。转转的API请求中通常会包含以下几个关键参数_token: 一个有时效性的令牌可能来源于Cookie或初次请求的响应。sign或_sign: 对请求参数如时间戳、设备信息、特定密钥按照一定算法计算出的哈希值。timestamp: 当前时间戳用于防止重放攻击即拦截请求后重复发送。这些参数不是前端写死的而是由页面加载的JavaScript代码动态计算生成的。如果你直接用Python的requests发送一个“干净”的请求缺少这些参数或者参数值不正确服务器会立刻识别为非法请求返回验证失败。2.2 实战观察抓包分析关键参数我们以抓取某个商品详情页为例。在浏览器以Chrome为例中打开开发者工具F12切换到Network网络面板然后刷新页面。筛选XHR/Fetch请求在商品页加载过程中你会看到大量请求。过滤出XHR或Fetch类型的请求这些通常是获取数据的API接口。定位数据接口寻找包含商品信息如价格、标题、描述的请求。其URL可能包含/api/item/detail、/goods/v1等字样。检查请求头Headers和负载Payload请求头重点关注Cookie、User-Agent以及一些自定义的Header如X-Requested-With、X-Sign等。查询参数Query String Parameters或负载Payload在Payload标签页的view source或Query String Parameters部分仔细查找包含token、sign、_t、timestamp等字段。一个简化后的请求示例如下GET https://api.zhuanzhuan.com/goods/v1/detail?itemId123456789_tokenabc123def456_signsha1_hash_value_t1640995200000这里的_token、_sign、_t就是需要攻克的难点。2.3 应对策略一逆向JavaScript计算逻辑这是最根本但也最复杂的方法。目标是找到生成sign等参数的JavaScript函数并用Python重写它。搜索关键代码在开发者工具的Sources源代码面板使用CtrlShiftF进行全局搜索。可以尝试搜索关键词如sign、_sign、token、encrypt、encode等。下断点调试在疑似生成签名的函数处下断点然后重新触发请求如滚动页面加载更多。当代码执行到断点时观察函数的输入参数和返回值理清其计算逻辑。代码还原与移植将JavaScript的计算逻辑用Python实现。这通常涉及MD5、SHA1、HMAC等哈希算法以及可能存在的Base64编码、AES加密等。你需要用到Python的hashlib、hmac、base64、Crypto等库。注意平台会不定期更新加密算法和密钥这意味着你逆向出来的代码可能在一段时间后就失效了需要重新分析。这是一个持续的对抗过程。2.4 应对策略二自动化浏览器驱动当JavaScript逆向过于复杂或变动频繁时使用自动化浏览器工具是更直接的选择。这类工具可以控制一个真实的浏览器如Chrome来加载页面、执行JavaScript然后直接从浏览器内存中获取渲染后的数据或拦截网络请求。Selenium老牌自动化测试工具支持多种浏览器。你可以用它打开转转页面等待页面加载完成然后通过driver.page_source获取完整HTML或用driver.execute_script()执行JavaScript来获取数据。from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC options webdriver.ChromeOptions() # 添加一些选项避免被检测为自动化工具虽不完全可靠 options.add_argument(--disable-blink-featuresAutomationControlled) options.add_experimental_option(excludeSwitches, [enable-automation]) options.add_experimental_option(useAutomationExtension, False) driver webdriver.Chrome(optionsoptions) driver.get(https://m.zhuanzhuan.com/...) try: # 等待某个页面元素加载出来确保页面JS已执行完毕 element WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.CLASS_NAME, goods-title)) ) # 获取商品标题 title driver.find_element(By.CLASS_NAME, goods-title).text print(title) finally: driver.quit()Playwright / Puppeteer较新的浏览器自动化库比Selenium更强大API更现代对现代Web应用单页应用SPA支持更好能更自然地模拟用户行为。它们可以直接拦截和修改网络请求更容易获取API返回的JSON数据。优缺点对比方法优点缺点逆向JS效率高资源消耗小速度快适合大规模采集。技术门槛高需要逆向能力随网站更新需维护代码。自动化浏览器绕过前端加密直接获取渲染结果模拟真人行为更逼真。速度慢资源消耗大内存、CPU容易被检测需额外反反爬措施。在实际项目中我通常会采用混合策略对于核心的、变动不频繁的签名算法进行逆向用于构造关键API请求对于复杂的、动态加载的内容或者用于获取初始令牌(token)的环节使用自动化浏览器作为辅助。3. 第二道防线行为指纹与环境检测即使你成功模拟了请求参数服务器依然可能拒绝你。因为它还在检查你的“行为指纹”和“访问环境”。这就像保安不仅查你的门禁卡还观察你走路的样子、说话的语气是不是像这里的住户。3.1 常见的检测维度HTTP头信息User-Agent: 是否是一个真实、常见浏览器的标识是否与你的其他头信息矛盾例如移动端UA却带着桌面端的CookieAccept-Language,Accept-Encoding,Connection等标准头是否齐全且符合浏览器惯例Sec-*系列头如Sec-CH-UA这些是浏览器自动发送的客户端提示Client Hints用于标识浏览器和平台脚本很难完美模拟。Cookie管理与会话状态真实的浏览会产生一系列Cookie它们之间有生命周期和关联性。脚本直接生成或使用一个孤立的Cookie很容易被识别。需要模拟完整的会话流程访问首页 - 获取初始Cookie - 携带Cookie访问后续页面。TLS/SSL指纹JA3指纹客户端在与服务器建立HTTPS连接时会发送一个“Client Hello”报文其中包含了加密套件列表、扩展列表等信息。这个组合对于每种浏览器或HTTP库如requests,curl几乎是唯一的称为JA3指纹。Python的requests库有自己独特的JA3指纹与Chrome/Firefox不同。一些高级的反爬系统会检测这个指纹。浏览器环境与API当使用自动化浏览器时虽然解决了JS执行问题但浏览器会暴露一些自动化特征。例如navigator.webdriver属性为true。浏览器通过CDPChrome DevTools Protocol驱动会存在一些特有的变量或函数。平台JS可能会检测window,document,navigator对象下的各种属性如插件列表、屏幕分辨率、时区、字体列表等来判断是否是真实的用户环境。3.2 实战应对如何伪装得更像“真人”完善请求头使用真实的浏览器User-Agent字符串。可以从https://www.useragentstring.com/等网站获取。复制浏览器中完整请求的所有Headers特别是Accept、Accept-Language、Referer上一个页面的来源对防爬很重要等。headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Accept: application/json, text/plain, */*, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Referer: https://m.zhuanzhuan.com/, Sec-Fetch-Dest: empty, Sec-Fetch-Mode: cors, Sec-Fetch-Site: same-origin, # ... 其他必要的Header }管理Cookie会话使用requests.Session()对象它会自动处理Cookie的存储和发送模拟浏览器的会话行为。import requests session requests.Session() # 第一次访问获取初始Cookie session.get(https://m.zhuanzhuan.com) # 后续请求会自动携带Cookie response session.get(https://api.zhuanzhuan.com/...)应对自动化浏览器检测Selenium可以通过execute_cdp_cmd执行Chrome DevTools Protocol命令来覆盖navigator.webdriver属性。driver.execute_cdp_cmd(Page.addScriptToEvaluateOnNewDocument, { source: Object.defineProperty(navigator, webdriver, { get: () undefined }); })Playwright在启动上下文时可以使用add_init_script方法注入脚本达到类似效果。使用stealth插件对于Selenium有第三方库如selenium-stealth可以尝试隐藏更多自动化特征。处理TLS指纹进阶这是比较棘手的一环。普通requests库难以修改。可以尝试以下方案使用curl_cffi库它模拟了真实浏览器的TLS指纹。使用pyhttpx或tls_client等专门处理TLS的库。终极方案是使用中间人代理将请求转发给一个真实浏览器内核如通过playwright或selenium控制的浏览器来发送但这会极大增加复杂度和资源消耗。个人心得环境检测是一场“道高一尺魔高一丈”的较量。没有一劳永逸的方案。我的策略是“最小必要伪装”即优先解决最可能触发拦截的明显问题如残缺的Headers、异常的Cookie再根据遇到的错误响应逐步调整。过度复杂的伪装有时会引入新的不稳定因素。4. 第三道防线请求频率、模式与验证码当你突破了参数签名和环境检测以为可以高枕无忧时平台还有最后几道闸门速率限制、行为模式分析和验证码。4.1 频率限制Rate Limiting这是最直接的防御。服务器会监控单个IP或用户会话在单位时间内的请求次数。如果超过阈值就会暂时或永久封禁。表现请求返回429 Too Many Requests状态码或者返回错误信息提示“操作过于频繁”。应对策略降低请求频率在请求间加入随机延时模拟人类阅读和点击的间隔。可以使用time.sleep(random.uniform(1, 3))。使用代理IP池这是应对IP封锁的核心手段。你需要一个稳定的代理IP来源注意合规性并在代码中轮换使用不同的IP。import random proxies_list [ {http: http://ip1:port, https: http://ip1:port}, {http: http://ip2:port, https: http://ip2:port}, # ... ] proxy random.choice(proxies_list) response requests.get(url, headersheaders, proxiesproxy, timeout10)设置超时与重试网络请求可能失败需要健壮的重试机制但要配合频率控制避免失败重试导致请求雪崩。from tenacity import retry, stop_after_attempt, wait_random_exponential retry(stopstop_after_attempt(3), waitwait_random_exponential(multiplier1, max10)) def make_request(url): # ... 发起请求 if response.status_code ! 200: raise Exception(fRequest failed: {response.status_code}) return response4.2 行为模式分析平台会分析你的请求序列是否符合人类行为。例如点击流正常人不会以恒定毫秒间隔点击链接。你的请求时间间隔是否完全均匀浏览深度正常人不会只访问API接口而不加载任何CSS、图片、字体等静态资源。鼠标移动与滚动在Web端可以通过JavaScript监听鼠标移动、滚动事件。纯API请求缺乏这些交互信号。应对策略随机化等待时间不仅是请求间隔包括模拟“浏览-点击-浏览”的不同阶段等待时间也应不同。模拟完整页面访问即使你只需要API数据也可以先让自动化浏览器访问一下落地页执行一些滚动操作然后再触发API请求。这在使用自动化浏览器方案时是顺带完成的。引入“噪音”请求偶尔请求一些不重要的静态资源或页面干扰行为分析模型。但这要谨慎会增加服务器负担和自身流量。4.3 验证码CAPTCHA当系统高度怀疑你是爬虫时最后的杀手锏就是验证码例如滑块拼图、点选文字、算术题等。应对策略规避触发通过良好的行为模拟和频率控制尽量避免触发验证码。这是上策。人工打码对于小规模、低频的需求遇到验证码时手动处理。验证码识别服务接入第三方打码平台如超级鹰、图鉴等通过API发送验证码图片获取识别结果。这需要额外成本且识别率并非100%。自动化破解不推荐对于简单滑块验证可以通过图像识别计算滑块缺口位置然后用自动化工具模拟拖动。但这涉及更复杂的图像处理和轨迹模拟且平台会不断升级验证码机制维护成本极高。踩坑实录我曾在一个项目中过于追求速度将请求间隔设得很短且固定很快IP就被封了。后来改为(基础间隔 随机浮动)的模式并加入了“忙时等待更长”模拟白天用户多系统响应慢的逻辑稳定性大大提升。记住“慢就是快”在爬虫领域很多时候是真理。5. 工程化实践构建一个健壮的爬虫系统理解了攻防技术点后我们需要将其整合到一个可维护、可扩展的系统中。一个用于应对转转这类强反爬网站的爬虫不应该是一个简单的脚本而是一个小型的系统。5.1 系统架构设计一个健壮的爬虫系统通常包含以下模块调度中心 (Scheduler) | v 任务队列 (Task Queue) - 爬虫节点 (Crawler Node 1) | | | v | [请求构造器] - [签名生成器] - [HTTP客户端] - [代理中间件] - [请求] | | | | v v | [解析器] - [响应处理器] - [响应] | | v v 去重与过滤 ----------- 数据存储器 (Data Storage)调度中心负责任务的生成、分发和优先级管理。例如根据商品ID生成抓取任务。任务队列使用Redis、RabbitMQ等消息队列解耦调度器和爬虫节点实现异步处理和负载均衡。爬虫节点请求构造器根据任务类型组装URL、Headers、Payload。集成签名生成逻辑。HTTP客户端封装请求发送逻辑集成代理切换、重试、异常处理。代理中间件管理代理IP池包括IP的获取、验证、评分和轮换。响应处理器检查响应状态码、内容处理反爬挑战如重定向到验证码页面。解析器从HTML或JSON中提取结构化数据。数据存储器将解析后的数据存入数据库如MySQL、MongoDB或文件。去重与过滤防止重复抓取过滤无效数据。5.2 关键代码模块示例1. 代理IP池管理# proxy_pool.py import random import time import requests from typing import List, Dict class ProxyPool: def __init__(self, proxy_sources: List[str]): self.proxies [] self.bad_proxies set() self.proxy_sources proxy_sources self._refresh() def _refresh(self): 从多个源获取并验证代理IP new_proxies [] for source in self.proxy_sources: try: resp requests.get(source, timeout10) ips resp.text.strip().split(\n) for ip in ips: proxy {http: fhttp://{ip}, https: fhttp://{ip}} if self._validate_proxy(proxy): new_proxies.append(proxy) except: continue self.proxies new_proxies def _validate_proxy(self, proxy: Dict) - bool: 快速验证代理是否可用 try: # 用一个快速、稳定的测试地址 resp requests.get(http://httpbin.org/ip, proxiesproxy, timeout5) return resp.status_code 200 and origin in resp.json() except: return False def get_proxy(self) - Dict: 获取一个随机可用的代理如果池子空了则刷新 if not self.proxies: self._refresh() if not self.proxies: raise Exception(No available proxies.) proxy random.choice(self.proxies) # 简单标记实际应用中应有更复杂的评分和熔断机制 self.proxies.remove(proxy) return proxy def report_bad(self, proxy: Dict): 报告一个坏掉的代理 self.bad_proxies.add(tuple(proxy.items())) # 可以设置一个定时任务定期清理bad_proxies并重新验证 # 使用 pool ProxyPool([http://proxy-source-1.com/list, http://proxy-source-2.com/list]) proxy pool.get_proxy()2. 带重试和代理切换的请求客户端# http_client.py import requests from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type from .proxy_pool import ProxyPool class ZhuanzhuanClient: def __init__(self, proxy_pool: ProxyPool): self.session requests.Session() self.proxy_pool proxy_pool # 初始化Session的Headers self.session.headers.update({ User-Agent: ..., Accept: ..., # ... }) def _make_signed_request(self, url, paramsNone): 内部方法构造带签名的请求参数 # 这里应集成之前逆向得到的签名生成逻辑 import time timestamp int(time.time() * 1000) # 假设我们从某个地方获取了token token self._get_token() # 假设的签名函数 sign self._generate_sign(params, timestamp, token) params[_t] timestamp params[_token] token params[_sign] sign return params retry( stopstop_after_attempt(5), waitwait_exponential(multiplier1, min2, max10), retryretry_if_exception_type((requests.ConnectionError, requests.Timeout, requests.HTTPError)) ) def fetch(self, url, max_proxy_retry3): 发送请求支持代理重试 for i in range(max_proxy_retry): proxy self.proxy_pool.get_proxy() try: # 如果是API请求先签名 # signed_params self._make_signed_request(url, params) # response self.session.get(url, paramssigned_params, proxiesproxy, timeout15) response self.session.get(url, proxiesproxy, timeout15) response.raise_for_status() # 非200状态码抛出HTTPError # 检查响应内容是否包含反爬提示 if 访问过于频繁 in response.text or 验证码 in response.text: self.proxy_pool.report_bad(proxy) raise requests.HTTPError(Anti-spider triggered.) return response except (requests.ConnectionError, requests.Timeout) as e: print(fProxy {proxy} failed with error: {e}. Retrying with new proxy...) self.proxy_pool.report_bad(proxy) continue except requests.HTTPError as e: if e.response.status_code 429: print(Rate limited. Sleeping...) time.sleep(60) # 被封禁等待更长时间 self.proxy_pool.report_bad(proxy) raise # 重新抛出触发重试 else: # 其他HTTP错误可能不是代理问题直接抛出 raise raise Exception(fFailed after {max_proxy_retry} proxy retries for url: {url}) # 使用 proxy_pool ProxyPool(...) client ZhuanzhuanClient(proxy_pool) try: html client.fetch(https://m.zhuanzhuan.com/goods/123).text except Exception as e: print(fFetch failed: {e})5.3 监控与告警系统跑起来不代表万事大吉。必须建立监控成功率监控统计每日/每小时请求成功率成功率骤降可能是反爬策略升级。代理IP健康度监控代理池中可用IP的数量和平均响应时间。数据质量监控检查抓取到的数据字段是否为空、格式是否正确。日志记录详细的日志是排查问题的唯一依据。记录每个请求的URL、使用的代理、响应状态码、耗时、以及遇到的异常。当成功率低于阈值或频繁出现特定反爬提示时应触发告警如邮件、钉钉机器人通知开发者及时介入分析。6. 法律、伦理与可持续性技术讨论之外我们必须正视爬虫的法律与伦理边界。转转平台的数据是其核心资产过度的、恶意的爬取会损害其正常运营。遵守robots.txt首先检查https://www.zhuanzhuan.com/robots.txt。它规定了哪些目录允许或禁止爬取。虽然这不是法律文件但它是网站所有者意愿的明确表达遵守它是基本的行业规范。控制爬取速度将请求频率控制在远低于人类操作的极限之下避免对目标服务器造成显著负载。这既是技术策略也是道德要求。尊重数据版权与用户隐私抓取的商品信息可能涉及用户发布的图片、文本。切勿抓取个人隐私信息如电话号码、聊天记录并谨慎使用抓取的数据避免用于商业侵权或个人骚扰。明确爬取目的用于个人学习、学术研究或市场分析少量、非商业通常风险较低。但用于大规模商业复制、建立竞争性平台或进行恶意刷量则可能构成不正当竞争或侵犯计算机信息系统安全面临法律风险。在实际操作中我始终坚持“最小必要”原则只抓取项目必需的最少数据字段将爬虫运行时间安排在网站流量较低的时段如深夜一旦发现访问困难首先检查自己的行为是否过于激进并主动增加延迟或暂停任务。这场“攻防战”没有永远的胜利者。平台会持续升级防御而爬虫技术也在不断进化。作为开发者更重要的是理解其背后的技术原理和设计思想这不仅能帮助你写出更健壮的爬虫也能让你从防御者的角度思考如何更好地保护自己的Web应用。保持学习保持敬畏在技术的边界内合理探索这才是长久之道。