
1. 搜狗搜索反爬机制的真实面貌做爬虫的人都有一个共识搜索引擎类站点是反爬最凶的阵地之一。搜狗搜索尤其如此它的反爬体系不是单一维度的而是多层叠加的。我前后用了大概三周时间反复测试、推翻、重建才把一套相对稳定的方案跑通。这篇文章不讲空泛的理论只讲我在实际操作中验证过的路径。先说清楚一个前提本文讨论的所有技术手段仅用于学习爬虫原理、研究反爬与反反爬的攻防逻辑。在实际项目中请务必遵守目标站点的robots协议和服务条款控制请求频率不对目标站点造成任何压力。这是做爬虫的基本职业操守也是让这套方案能长期稳定运行的前提。搜狗搜索的反爬验证码机制核心逻辑可以拆成三层来理解。第一层是请求频率检测。搜狗在服务端维护了一套基于IP的请求计数器当短时间内同一IP的请求量超过阈值就会触发验证。这个阈值不是固定的它会根据当前时段的整体流量动态调整。我实测下来白天高峰期可能请求几十次就触发凌晨低峰期能撑到上百次。第二层是行为特征分析。搜狗会检查请求头中的User-Agent、Referer、Accept-Language等字段是否完整且合理还会检测Cookie的携带情况。如果你的请求头看起来像是一个裸奔的requests请求即使频率不高也可能被直接拦截。第三层才是验证码挑战。当前两层检测判定你可疑之后搜狗会返回一个验证码页面要求你输入图形验证码或者进行滑块验证。验证通过后会给你一个临时的通行凭证通常是一个特定的Cookie在一定时间内可以正常访问。很多人一上来就想着怎么破解验证码其实方向错了。验证码是最后一道防线真正高效的策略是在前两层就把自己伪装成一个正常用户让验证码根本不触发。这就是标题里说的机智操作的核心思路——不是硬刚验证码而是绕过触发验证码的条件。2. 为什么常规的Scrapy配置一定会被拦2.1 默认请求头的致命缺陷Scrapy的默认配置在搜狗面前几乎是裸奔状态。默认的User-Agent是类似Scrapy/2.11 (https://scrapy.org)这样的字符串这等于直接告诉对方我是爬虫。搜狗的反爬系统识别到这种UA连频率检测都省了直接给你返回验证码页面。我最初测试的时候用默认配置请求搜狗搜索第一次请求就触发了验证码。后来把User-Agent换成正常浏览器的情况好了很多但请求十几次之后还是会被拦。这说明UA只是第一道门槛后面还有别的检测维度。2.2 Cookie缺失导致的会话断裂搜狗搜索在正常用户访问时会通过响应头下发一系列Cookie包括但不限于SUV、SNUID、ABTEST、IPLOC等。这些Cookie记录了你的会话状态、地理位置、浏览器指纹等信息。正常浏览器会自动携带这些Cookie进行后续请求而Scrapy默认是不维护Cookie的除非开启COOKIES_ENABLED。问题在于即使你开启了Cookie中间件Scrapy的Cookie管理方式和真实浏览器也有差异。比如搜狗会检查Cookie的过期时间、写入顺序、以及是否缺少某些应该存在的Cookie。一个只有两三个Cookie的请求和一个有十几个Cookie的请求在搜狗眼里是完全不同的。2.3 请求间隔过于规律Scrapy默认的下载延迟是0也就是说请求会以最快的速度连续发出。即使你设置了DOWNLOAD_DELAY如果是一个固定值比如固定延迟2秒这种机械式的规律性本身就是一个异常特征。真实用户的点击行为是有随机性的有时候快有时候慢有时候会停顿很久。我做过一个对比测试固定延迟1秒连续请求平均在第15次左右触发验证码而使用随机延迟0.5到3秒之间随机平均能撑到第40次以上。这个差距非常明显。2.4 缺少浏览器指纹相关的请求头除了User-Agent现代浏览器还会发送一系列请求头包括Accept、Accept-Encoding、Accept-Language、Connection、Upgrade-Insecure-Requests、Sec-Fetch-*系列等。这些请求头的组合构成了一个指纹搜狗会检查这个指纹是否合理。缺少关键请求头或者请求头之间的组合不符合常见浏览器的特征都会被标记为可疑。3. 绕过反爬的核心思路拆解3.1 整体策略模拟真实用户而非对抗验证码我在多次尝试之后确定了一个核心原则不要试图去破解验证码而是让验证码不出现。破解验证码是一条成本极高且不稳定的路——搜狗的验证码类型会不定期更换今天能用的识别方案明天可能就失效了。而模拟真实用户行为虽然前期配置麻烦一点但稳定性高得多。具体来说这套方案包含四个关键环节请求头伪装、Cookie预热、请求节奏控制、以及会话保持。下面逐个拆解。3.2 请求头伪装的完整清单一个真实的Chrome浏览器访问搜狗搜索时发送的请求头大概长这样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: text/html,application/xhtmlxml,application/xml;q0.9,image/avif,image/webp,image/apng,*/*;q0.8,application/signed-exchange;vb3;q0.7, Accept-Encoding: gzip, deflate, br, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Cache-Control: max-age0, Connection: keep-alive, Upgrade-Insecure-Requests: 1, Sec-Fetch-Dest: document, Sec-Fetch-Mode: navigate, Sec-Fetch-Site: none, Sec-Fetch-User: ?1, }这里有几个细节值得注意。Accept-Encoding中包含brBrotli压缩这是现代浏览器的标配如果你的请求头里没有这个搜狗可能会觉得你不像正常浏览器。Sec-Fetch-*系列请求头是Chrome 76之后引入的用来描述请求的上下文搜狗会检查这些字段是否存在以及值是否合理。另外User-Agent不要随便乱写。我建议选一个当前主流的Chrome版本并且在整个爬取过程中保持一致。频繁更换UA反而会引起怀疑。3.3 Cookie预热的正确姿势Cookie预热的意思是在正式爬取之前先用一个干净的会话去访问搜狗首页让服务端下发完整的Cookie集合然后再带着这些Cookie去请求搜索页面。具体操作是先用requests.Session()或者Scrapy的start_requests访问https://www.sogou.com/获取响应中的Set-Cookie字段把这些Cookie保存下来。然后再访问一次搜索页面看看是否正常返回结果。如果正常说明这个会话已经预热好了。这里有个关键点预热时使用的请求头必须和后续爬取时完全一致。如果预热时用的是Chrome的UA爬取时换成了Firefox的UA搜狗会发现同一个会话的浏览器指纹变了直接判定为异常。3.4 请求节奏的随机化处理前面提到固定延迟容易被识别。我的做法是在Scrapy的DOWNLOAD_DELAY基础上配合RANDOMIZE_DOWNLOAD_DELAY True让延迟在0.5倍到1.5倍之间随机浮动。但这还不够因为Scrapy的随机延迟范围太窄。更稳妥的方案是自定义一个下载中间件在每次请求之间插入一个服从某种分布的随机延迟。我实测下来使用指数分布均值为2秒的效果比较好既能保证速度又能模拟真实用户的思考时间。import random import time class RandomDelayMiddleware: def process_request(self, request, spider): delay random.expovariate(1.0 / 2.0) # 均值2秒的指数分布 delay min(delay, 8.0) # 上限8秒避免等待过久 time.sleep(delay)3.5 会话保持与Cookie更新搜狗在每次响应中都可能更新部分Cookie比如SNUID和SUV会定期刷新。如果你的爬虫不更新Cookie用着旧的Cookie继续请求很快就会被识别。Scrapy的CookiesMiddleware默认会处理Cookie的更新但前提是COOKIES_ENABLED True默认就是True。不过要注意Scrapy的Cookie中间件是基于cookiejar的它会把所有Cookie混在一起管理。如果你同时维护多个会话比如多个账号或多个IP需要手动指定cookiejar参数来隔离。4. Scrapy项目的完整配置与代码实现4.1 项目结构与核心文件我建议的项目结构是这样的sogou_spider/ ├── scrapy.cfg └── sogou_spider/ ├── __init__.py ├── items.py ├── middlewares.py ├── pipelines.py ├── settings.py └── spiders/ └── sogou_search.py核心逻辑集中在middlewares.py和spiders/sogou_search.py两个文件里。4.2 settings.py的关键配置项# settings.py BOT_NAME sogou_spider SPIDER_MODULES [sogou_spider.spiders] NEWSPIDER_MODULE sogou_spider.spiders # 遵守robots协议建议开启做有底线的爬虫 ROBOTSTXT_OBEY True # 并发控制搜狗对并发很敏感建议降到最低 CONCURRENT_REQUESTS 1 CONCURRENT_REQUESTS_PER_DOMAIN 1 CONCURRENT_REQUESTS_PER_IP 1 # 下载延迟 DOWNLOAD_DELAY 2 RANDOMIZE_DOWNLOAD_DELAY True # Cookie管理 COOKIES_ENABLED True COOKIES_DEBUG False # 重试设置 RETRY_ENABLED True RETRY_TIMES 3 RETRY_HTTP_CODES [500, 502, 503, 504, 408, 429] # 自动限速可选但建议开启 AUTOTHROTTLE_ENABLED True AUTOTHROTTLE_START_DELAY 2 AUTOTHROTTLE_MAX_DELAY 30 AUTOTHROTTLE_TARGET_CONCURRENCY 0.5 # 下载中间件 DOWNLOADER_MIDDLEWARES { sogou_spider.middlewares.RandomDelayMiddleware: 543, sogou_spider.middlewares.CookieWarmupMiddleware: 544, sogou_spider.middlewares.UserAgentMiddleware: 545, } # 默认请求头 DEFAULT_REQUEST_HEADERS { Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, }这里有几个配置项需要特别说明。CONCURRENT_REQUESTS设为1是刻意的搜狗对并发请求非常敏感同时发两个请求就可能触发验证。AUTOTHROTTLE_ENABLED开启后Scrapy会根据服务器的响应时间自动调整延迟这在应对搜狗这种动态反爬的站点时非常有用。4.3 自定义下载中间件的实现UserAgentMiddleware负责给每个请求加上完整的浏览器请求头# middlewares.py import random import time from scrapy import signals class UserAgentMiddleware: def __init__(self): self.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: text/html,application/xhtmlxml,application/xml;q0.9,image/avif,image/webp,image/apng,*/*;q0.8, Accept-Encoding: gzip, deflate, br, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Cache-Control: max-age0, Connection: keep-alive, Upgrade-Insecure-Requests: 1, Sec-Fetch-Dest: document, Sec-Fetch-Mode: navigate, Sec-Fetch-Site: none, Sec-Fetch-User: ?1, } def process_request(self, request, spider): for key, value in self.headers.items(): request.headers.setdefault(key, value) return NoneCookieWarmupMiddleware负责在爬虫启动时预热Cookieclass CookieWarmupMiddleware: def __init__(self): self.warmed_up False def process_request(self, request, spider): if not self.warmed_up and sogou.com in request.url: # 首次请求前先访问首页预热 import requests session requests.Session() session.headers.update({ 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: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, }) try: session.get(https://www.sogou.com/, timeout10) # 将预热得到的Cookie注入到Scrapy的请求中 for cookie in session.cookies: request.cookies[cookie.name] cookie.value self.warmed_up True except Exception as e: spider.logger.warning(fCookie预热失败: {e}) return NoneRandomDelayMiddleware负责在请求之间插入随机延迟class RandomDelayMiddleware: def process_request(self, request, spider): delay random.expovariate(1.0 / 2.0) delay min(delay, 8.0) time.sleep(delay) return None4.4 Spider主体的编写# spiders/sogou_search.py import scrapy from urllib.parse import quote class SogouSearchSpider(scrapy.Spider): name sogou_search allowed_domains [sogou.com] def __init__(self, keywordPython爬虫, *args, **kwargs): super().__init__(*args, **kwargs) self.keyword keyword def start_requests(self): # 先访问首页预热 yield scrapy.Request( urlhttps://www.sogou.com/, callbackself.parse_homepage, dont_filterTrue, ) def parse_homepage(self, response): # 首页访问成功后再发起搜索请求 search_url fhttps://www.sogou.com/web?query{quote(self.keyword)} yield scrapy.Request( urlsearch_url, callbackself.parse_search_results, dont_filterTrue, ) def parse_search_results(self, response): # 检查是否被重定向到验证码页面 if 验证码 in response.text or antispider in response.url: self.logger.warning(触发了验证码需要处理) return # 解析搜索结果 results response.css(div.vrwrap, div.rb) for result in results: title result.css(h3 a::text).get() link result.css(h3 a::attr(href)).get() snippet result.css(div.str-text-info::text).get() if title: yield { title: title.strip(), link: link, snippet: snippet.strip() if snippet else , }这个Spider的逻辑很清晰先访问首页预热Cookie再发起搜索请求解析结果。如果检测到验证码页面就记录日志并停止。5. 微信公众号文章搜索的适配要点5.1 搜狗微信搜索的入口差异搜狗微信搜索的入口是https://weixin.sogou.com/和主站www.sogou.com是分开的。虽然底层反爬机制类似但有几个关键差异需要注意。首先微信搜索的Cookie体系是独立的。你在主站预热的Cookie不能直接用于微信搜索需要单独预热。其次微信搜索对请求频率的限制更严格我实测下来连续请求5到8次就可能触发验证。第三微信搜索的验证码类型和主站不同主站主要是图形验证码微信搜索更多是滑块验证。5.2 微信搜索的Cookie预热流程def warmup_weixin_session(): session requests.Session() session.headers.update({ 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: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, }) # 先访问微信搜索首页 session.get(https://weixin.sogou.com/, timeout10) # 再访问一次搜索页面让Cookie完整 session.get(https://weixin.sogou.com/weixin?type2query测试, timeout10) return session预热完成后把session中的Cookie提取出来注入到Scrapy的请求中。注意微信搜索的Cookie中有一个SNUID字段特别重要它相当于你的身份证每次请求都会带上。5.3 微信文章列表的解析逻辑微信搜索的结果页面结构和主站不同文章列表通常在ul.news-list下面每条记录是一个li元素def parse_weixin_results(self, response): articles response.css(ul.news-list li) for article in articles: title article.css(h3 a::text).get() link article.css(h3 a::attr(href)).get() account article.css(a.account::text).get() summary article.css(p.txt-info::text).get() publish_time article.css(span.s2::text).get() yield { title: title.strip() if title else , link: link, account: account.strip() if account else , summary: summary.strip() if summary else , publish_time: publish_time, }微信搜索返回的文章链接是搜狗的跳转链接需要进一步请求才能拿到真实的微信文章URL。这个跳转链接的有效期很短通常只有几分钟所以拿到链接后要尽快处理。6. 实测中的意外情况与应对策略6.1 验证码页面的识别与处理即使做了充分的伪装长时间运行后仍然可能触发验证码。关键是要能及时识别并做出正确的处理。搜狗的验证码页面有几个特征URL中通常包含antispider或captcha页面标题包含验证码或请输入验证码页面内容中有一个img标签指向验证码图片。我的处理策略是一旦检测到验证码页面立即暂停当前会话记录触发时的请求参数和Cookie状态然后等待一段时间我设置的是15到30分钟后重新预热Cookie再继续。不要试图用同一个会话反复请求那样只会让情况更糟。6.2 IP被临时限制的恢复方法如果触发验证码的次数过多搜狗可能会对你的IP进行临时限制表现为所有请求都返回验证码页面即使换了新的Cookie也没用。这种情况下唯一的办法是等待。我实测下来临时限制通常持续30分钟到2小时不等。如果你有多个出口IP可用可以在检测到IP被限制后切换到备用IP。但要注意切换IP后需要重新预热Cookie因为Cookie是和IP绑定的。6.3 搜索结果为空的情况分析有时候请求返回的是200状态码但搜索结果为空。这种情况通常有几个原因一是关键词被搜狗判定为敏感词直接返回空结果二是Cookie不完整搜狗返回了一个简化版的页面三是请求被重定向到了一个中间页面。排查方法是把响应内容保存下来检查页面中是否有没有找到相关结果之类的提示或者检查是否有重定向。如果是Cookie问题重新预热通常能解决。6.4 长时间运行的稳定性维护如果要跑长时间的爬取任务建议加入以下机制定期重新预热Cookie每隔30到60分钟重新预热一次避免Cookie过期。请求计数与自动暂停每请求N次后主动暂停一段时间模拟用户的休息行为。异常日志记录把所有异常响应保存到文件方便事后分析。多会话轮换维护2到3个独立的会话轮流使用降低单个会话的请求密度。7. 几个容易踩的坑和我的经验总结第一个坑是过度依赖代理IP。很多人一遇到反爬就想着换IP但实际上搜狗的检测是多维度的换了IP但请求头、Cookie、行为特征没变照样会被拦。IP只是其中一个维度而且不是最重要的那个。第二个坑是忽略robots.txt。搜狗的robots.txt明确禁止了部分路径的爬取如果你不小心爬了这些路径不仅容易被封还可能涉及合规问题。建议在项目开始前先仔细阅读目标站点的robots.txt。第三个坑是请求头中的Accept-Encoding和实际解压方式不匹配。如果你在请求头里声明了支持br压缩但Scrapy没有安装brotli库响应内容会是一堆乱码。解决办法是安装brotli库或者在请求头中去掉br。第四个坑是Cookie的domain和path不匹配。手动注入Cookie时要注意Cookie的domain和path字段。如果domain设置错了Cookie不会被发送。建议直接用requests.Session管理Cookie然后整体注入。第五个坑是忽略了搜狗的动态渲染。部分搜索结果是通过JavaScript动态加载的直接用Scrapy请求拿不到完整内容。这种情况下需要考虑使用Splash或Playwright等工具进行渲染。不过搜狗的主要搜索结果还是服务端渲染的动态加载的主要是广告和推荐内容一般不影响核心数据的抓取。我在实际项目中的体会是做搜狗这类搜索引擎的爬虫技术方案只占三成七成在于耐心和细致。每一个请求头、每一个Cookie、每一次请求间隔都可能影响最终的稳定性。不要想着一步到位而是小步快跑每次调整一个变量观察效果逐步优化。这套方案我跑了大概两个月日均请求量控制在合理范围内触发验证码的概率低于5%对于学习和研究来说已经完全够用了。