1. 项目概述:为什么拦截请求头是爬虫进阶的必修课
最近在折腾一个数据采集项目时,又遇到了那个老生常谈的问题:目标网站的反爬机制。这次的情况有点特殊,对方不是简单的验证码或者IP限制,而是通过检测请求头中的一些特定字段,比如User-Agent、Referer,甚至是Accept-Language来识别和拦截自动化脚本。用传统的requests库或者Selenium直接发起请求,几乎立刻就会被“请喝茶”。就在我准备放弃,考虑更复杂的代理池和请求指纹伪装方案时,我想起了Playwright这个现代浏览器自动化工具中的一个强大功能——Route拦截。这个功能,可以说是为这类“请求头检测”型反爬策略量身定制的解决方案。
简单来说,Playwright的Route功能允许我们在浏览器页面发起任何网络请求(无论是导航、加载资源还是API调用)之前,将其“拦截”下来。在请求真正发送到服务器之前,我们有机会查看并修改这个请求的所有细节,包括最重要的请求头(Headers)。这意味着,我们可以把脚本发出的、带有明显自动化工具特征的请求头,在最后一刻“偷梁换柱”,替换成与真实浏览器访问时一模一样的、毫无破绽的请求头。整个过程对目标服务器是透明的,它收到的就是一个看起来完全正常的请求。
这不仅仅是绕过反爬,更是一种“精准伪装”。它解决的核心痛点是:自动化工具(包括Playwright自身)在发起请求时,其默认的请求头集合与真实人类用户通过浏览器访问时存在差异。这些差异就是反爬系统最直接的指纹。通过Route拦截修改,我们能够抹平这些差异,极大地降低被识别为机器人的概率。这个技巧尤其适合那些对请求头完整性、顺序、甚至大小写都有严格校验的中高级反爬网站。接下来,我就结合实战,拆解如何一步步利用Playwright的Route功能,构建一个稳健的、能绕过常见请求头检测的爬虫。
2. 核心思路拆解:Route如何成为请求的“中间人”
在深入代码之前,我们必须先理解Playwright中Route的工作原理。你可以把它想象成网络请求流上的一个“检查站”或“中间人”。当你在Page对象上设置了一个路由(route)时,你就为所有匹配特定条件的网络请求安装了一个监听器。
2.1 Route拦截的基本流程
整个拦截修改的流程,可以概括为以下四个步骤:
- 定义拦截条件:你告诉
Playwright,你想拦截什么样的请求。这通常通过一个URL模式(字符串或正则表达式)来实现,比如拦截所有请求*/*,或者只拦截特定API接口**/api/data*。 - 提供处理函数:当有请求命中你设置的拦截条件时,
Playwright不会立即将其发送出去,而是暂停这个请求,并调用你预先提供的处理函数(handler或callback)。 - 在函数中操作请求:在这个处理函数里,你会收到一个
Route对象。这个对象代表了被拦截的请求。你可以通过它来:- 读取请求信息:获取请求的URL、方法(GET/POST)、请求头、POST数据等。
- 修改请求:这是最关键的一步。你可以构造一个全新的请求头字典,替换掉原来的请求头。
- 决定请求命运:修改完成后,你必须明确告诉这个请求接下来该怎么做。通常是调用
route.continue()让修改后的请求继续发送;或者调用route.fulfill()直接返回一个自定义的响应,而不再访问真实服务器。
- 请求继续或终止:根据你的指令,被修改后的请求会继续其旅程,或者由你提供的模拟响应直接返回给页面。
这个机制的精妙之处在于,它是在浏览器引擎内部发生的。修改请求头的操作发生在网络栈的底层,其效果远比在高级语言层(如Python的requests库)直接设置headers参数要彻底和真实。因为浏览器自身的一些内部元数据(metadata)也会被一并处理,使得发出的请求在TCP/IP层面都更接近原生浏览器行为。
2.2 为何选择Route而非简单设置Headers?
你可能会问,Playwright的Page对象本身不就可以通过set_extra_http_headers方法来设置全局请求头吗?为什么还要大费周章地用Route?
这里有一个本质区别和几个关键优势:
set_extra_http_headers是“添加”而非“替换”:这个方法是在浏览器默认请求头的基础上,额外添加或覆盖你指定的头字段。但浏览器的默认请求头集(特别是Playwright控制的浏览器)可能本身就不完整,或者带有某些自动化特征(例如User-Agent中可能包含HeadlessChrome)。Route则允许你进行“全量替换”,你可以提供一个完全由你掌控的、与真实浏览器100%一致的请求头字典。- 处理动态请求头:有些网站在页面加载后,会通过JavaScript动态发起Ajax请求,并为这些请求添加特定的令牌(Token)或签名(Signature)到请求头中。这些头字段无法通过预先设置的方式添加,因为它们依赖于页面上下文。而
Route拦截发生在请求发出的瞬间,你可以在这个时间点,从页面上下文中(例如从localStorage或全局变量)读取这些动态值,并注入到请求头中。 - 精细化的控制:你可以针对不同类型的请求(图片、CSS、XHR)应用不同的请求头策略。例如,只对数据API接口进行深度伪装,而对静态资源请求则放行或使用简单伪装,以此提升效率。
- 修复或移除异常头:极少数情况下,浏览器或
Playwright可能会产生一些非标准或易被识别的请求头。通过Route,你可以检查并移除这些“问题头”,确保请求的纯净性。
注意:
Route功能非常强大,但也会带来一定的性能开销,因为每个匹配的请求都需要经过你的JavaScript处理函数。在实战中,拦截条件要尽可能精确,避免使用*/*这样的全拦截模式,除非确实需要对所有请求进行修改。
3. 实战演练:一步步构建请求头拦截爬虫
理论讲得再多,不如一行代码。我们以一个需要模拟真实Chrome浏览器访问的网站为例,展示完整的实现过程。假设我们要爬取的目标网站会严格检查User-Agent,Accept-Language,Accept-Encoding,Referer等头信息。
3.1 环境准备与基础爬虫搭建
首先,确保你已经安装了Playwright和对应的浏览器。如果你还没安装,可以通过以下命令完成:
pip install playwright playwright install chromium我们先写一个最基础的、会被反爬拦截的Playwright脚本:
import asyncio from playwright.async_api import async_playwright async def basic_crawl(): async with async_playwright() as p: # 启动浏览器,默认是无头模式 browser = await p.chromium.launch(headless=False) # 为了演示,先关闭无头模式 page = await browser.new_page() # 尝试访问目标网站 target_url = 'https://example.com/data-page' try: response = await page.goto(target_url, wait_until='networkidle') print(f"状态码: {response.status}") # 尝试获取页面内容 content = await page.content() print(f"页面标题: {await page.title()}") except Exception as e: print(f"访问失败: {e}") finally: await browser.close() asyncio.run(basic_crawl())运行这个脚本,你很可能会得到一个非200的状态码(如403、429),或者页面内容被重定向到一个验证页面。这就是反爬机制在起作用。
3.2 实现Route拦截与请求头替换
现在,我们来引入Route拦截。核心是在创建page对象后,加载页面(goto)之前,设置路由规则。
import asyncio from playwright.async_api import async_playwright, Route # 定义一个精心准备的、与真实Chrome浏览器一致的请求头字典 REALISTIC_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/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', 'Accept-Encoding': 'gzip, deflate, br', 'Connection': 'keep-alive', 'Upgrade-Insecure-Requests': '1', 'Sec-Fetch-Dest': 'document', 'Sec-Fetch-Mode': 'navigate', 'Sec-Fetch-Site': 'none', 'Sec-Fetch-User': '?1', 'Cache-Control': 'max-age=0', } async def intercept_and_modify(route: Route): """路由拦截处理函数""" # 获取当前被拦截的请求 request = route.request # 打印原始请求头(调试用) # print(f"拦截到请求: {request.url}") # print(f"原始头信息: {request.headers}") # 关键步骤:复制原始请求头,并用我们的真实头字典覆盖它 # 这里选择“覆盖”而非“全量替换”,是为了保留一些请求特有的头,如Content-Type(对于POST请求) headers = dict(request.headers) headers.update(REALISTIC_HEADERS) # 用我们的伪装头更新字典 # 继续请求,并传入修改后的请求头 await route.continue_(headers=headers) async def advanced_crawl_with_route(): async with async_playwright() as p: browser = await p.chromium.launch(headless=False) page = await browser.new_page() # !!!核心步骤:在页面发起任何请求之前,先设置路由拦截规则 # 这里使用 ‘**’ 通配符,表示拦截该页面域下的所有请求 # 你也可以更精确,例如 ‘**/api/**’ 只拦截API请求 await page.route('**', intercept_and_modify) target_url = 'https://example.com/data-page' try: # 现在,通过这个page发起的任何请求,都会先经过intercept_and_modify函数的处理 response = await page.goto(target_url, wait_until='networkidle') print(f"状态码: {response.status}") if response.ok: print("成功绕过初步反爬!") # 可以开始你的数据提取逻辑了 # await page.screenshot(path='success.png') # data = await page.evaluate('() => document.body.innerText') except Exception as e: print(f"访问失败: {e}") finally: await browser.close() asyncio.run(advanced_crawl_with_route())代码解读与实操心得:
REALISTIC_HEADERS字典:这是伪装的核心。里面的每一个键值对都应该是从真实浏览器(如Chrome)的开发者工具“网络”选项卡中复制过来的。特别注意User-Agent、Sec-Fetch-*系列头,这些是现代浏览器和反爬系统重点关注的指纹信息。Accept-Encoding告诉服务器浏览器支持哪些压缩格式,这也必须匹配。intercept_and_modify函数:这是路由处理器。它接收一个Route对象。我们通过route.request获取到被拦截的请求对象。这里我采用了dict(request.headers)复制原头信息,再用update方法合并伪装头的策略。这样做的好处是保留了原始请求中可能存在的、我们未提供的特殊头字段(例如POST请求的Content-Type)。page.route(‘**’, handler):这一行是注册拦截器。**是一个通配符模式,匹配所有URL。在爬虫初期调试时,这样设置很方便,可以观察所有请求。但在生产环境中,强烈建议替换为更具体的模式,比如**/api/data/**或**/*.json,只拦截数据请求,避免对图片、样式表等静态资源的无谓拦截,这会显著影响页面加载速度。route.continue_(headers=headers):这是放行请求的指令,同时将我们修改后的headers字典传递进去。continue_方法会使用这些新的头信息重新组装请求并发送。
运行这个改进后的脚本,你会发现成功率大大提升。页面能够正常加载,状态码也变成了200。
3.3 高级技巧:动态、差异化的请求头管理
上面的例子是“一刀切”地为所有请求设置相同的头。但在更复杂的场景下,我们需要更精细的控制。
场景一:为不同域名的请求设置不同的RefererReferer头表示请求的来源页面。对于直接从地址栏输入的导航请求,Referer通常为空或不存在。但对于页面内链接跳转或Ajax请求,一个合理的Referer能大大增加真实性。
async def smart_intercept(route: Route): request = route.request headers = dict(request.headers) headers.update(REALISTIC_HEADERS_BASE) # 基础头 # 动态设置Referer # 如果请求不是页面导航(即不是最初的goto),且来自当前页面域名 if request.url.startswith('https://target-site.com') and page.url: # 将当前页面URL作为Referer headers['Referer'] = page.url # 针对特定API,添加额外的认证头(假设令牌存储在页面变量中) if '/api/secure/' in request.url: # 通过evaluate从页面上下文中获取令牌 token = await page.evaluate('() => window.APP_TOKEN') if token: headers['Authorization'] = f'Bearer {token}' await route.continue_(headers=headers)场景二:随机化请求头,避免行为模式单一长期使用同一套请求头,即使它再真实,也可能因为“过于一致”而被关联分析。我们可以准备一个头信息池,每次拦截时随机选取。
import random USER_AGENT_POOL = [ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ... Chrome/120.0.0.0', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 ... Version/16.0 Safari/605.1.15', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 ... Chrome/119.0.0.0', ] ACCEPT_LANGUAGE_POOL = ['zh-CN,zh;q=0.9', 'en-US,en;q=0.8,zh;q=0.6', 'zh-TW,zh;q=0.9,en;q=0.8'] async def random_headers_intercept(route: Route): headers = dict(route.request.headers) # 随机化关键头 headers['User-Agent'] = random.choice(USER_AGENT_POOL) headers['Accept-Language'] = random.choice(ACCEPT_LANGUAGE_POOL) # 更新其他固定头 headers.update({ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Encoding': 'gzip, deflate, br', }) await route.continue_(headers=headers) # 使用时,可以针对不同类型的请求应用不同的拦截策略 async def setup_routes(page): # 对HTML页面请求,使用随机头策略 await page.route('**/*.html', random_headers_intercept) # 对数据API请求,使用更固定但完整的伪装头 await page.route('**/api/**', intercept_and_modify) # 对静态资源,可以完全不拦截或只做简单处理 # await page.route('**/*.{css,js,png,jpg,jpeg,gif,ico}', lambda route: route.continue_())实操心得:动态化和随机化是应对高级反爬系统的有效手段,但切忌过度。过于频繁地更换
User-Agent,可能导致同一会话(Session)内的行为矛盾,反而露出马脚。一个稳妥的策略是:每个爬虫实例(或每个浏览器上下文BrowserContext)使用一套固定的、完整的真实头,而在不同的实例间进行差异化。同时,Referer的逻辑一定要符合浏览器的真实导航逻辑。
4. 深入原理:Playwright Route与浏览器网络栈的协作
要真正用好Route,不能只停留在API调用层面,还需要理解它在浏览器网络栈中的位置,这有助于我们排查一些诡异的问题。
当你在Playwright中调用page.route()时,底层发生的是:
- CDP协议通信:
Playwright通过 Chrome DevTools Protocol 向浏览器内核发送指令,注册一个“请求拦截”事件监听器。 - 浏览器内核拦截:浏览器(以Chromium为例)的网络模块收到指令后,会在请求生命周期的早期(在发送到网络之前)将其挂起。
- 上下文隔离与通信:被挂起的请求信息(URL、方法、头等)通过CDP传回给
Playwright的客户端(你的Python脚本)。你的处理函数在Node.js/Python环境中执行。 - 决策与反馈:你的处理函数决定如何处置这个请求(
continue_或fulfill),并将决策(包括修改后的头)通过CDP传回浏览器。 - 请求继续:浏览器网络模块根据收到的指令,要么使用新头继续发送请求,要么直接构造一个模拟响应返回给渲染进程。
这个过程带来了一个非常重要的限制:你的拦截处理函数是异步的,并且运行在Playwright的主控端。这意味着,如果你的处理函数执行太慢(比如进行了耗时的网络IO或复杂计算),会阻塞整个页面的网络请求,导致页面加载卡顿甚至超时。
因此,在拦截函数中:
- 避免同步阻塞操作。
- 避免进行额外的网络请求(除非你知道自己在做什么)。
- 逻辑应尽可能轻量,快速做出决策。
这也是为什么建议使用精确的URL模式进行拦截,而不是拦截所有请求。拦截一个图片请求并快速放行,对性能影响微乎其微;但如果拦截了上百个图片和CSS文件,每个都执行一段复杂的逻辑,累积的延迟就会非常可观。
5. 避坑指南与常见问题排查
在实际使用中,你肯定会遇到各种各样的问题。下面是我踩过的一些坑和对应的解决方案。
5.1 问题一:拦截后页面加载不全或样式错乱
现象:使用了**全局拦截后,页面虽然能打开,但CSS样式没加载,图片显示破碎,或者JavaScript报错。
原因:你的请求头修改策略可能破坏了某些关键请求。例如,你为所有请求都强制添加了Accept: text/html,但浏览器请求CSS文件时,正确的Accept头应该是text/css,*/*;q=0.1。服务器看到错误的Accept头,可能返回错误的内容或不支持的内容类型。
解决方案:
- 精细化拦截:立即将全局拦截
**改为只拦截你的目标数据请求,例如**/api/**或**/*.json。让静态资源请求走浏览器默认流程。 - 条件化修改:如果必须全局拦截,则在处理函数中判断请求资源类型,差异化设置头信息。
async def conditional_intercept(route: Route): request = route.request headers = dict(request.headers) # 根据URL后缀或请求头中的Accept信息判断资源类型 if request.url.endswith('.css'): # CSS资源,恢复正确的Accept头 headers['Accept'] = 'text/css,*/*;q=0.1' elif request.url.endswith('.js'): # JS资源 headers['Accept'] = '*/*' elif 'image' in headers.get('accept', ''): # 图片资源,保持原样或设置为image/webp,image/apng,*/* pass else: # 默认情况(如HTML, XHR),应用我们的伪装头 headers.update(REALISTIC_HEADERS) await route.continue_(headers=headers)5.2 问题二:修改了请求头,但依然被识别
现象:已经精心伪造了所有常见的请求头,但网站仍然返回反爬页面或验证码。
排查思路:
- 检查“隐形”指纹:除了标准的HTTP头,浏览器还有大量其他指纹,如WebGL、Canvas、AudioContext、字体列表、屏幕分辨率、时区、语言偏好(navigator.languages)等。这些可以通过
Playwright的page.add_init_script或启动浏览器时的上下文参数进行模拟。context = await browser.new_context( viewport={'width': 1920, 'height': 1080}, locale='zh-CN', timezone_id='Asia/Shanghai', user_agent=REALISTIC_HEADERS['User-Agent'] # 这里也可以统一设置 ) page = await context.new_page() - 检查Cookie和Session:对方可能通过Cookie或本地存储来跟踪会话。确保你的爬虫在多次请求间维持了合理的会话状态。使用
BrowserContext可以天然隔离会话。 - 检查请求顺序和时序:人类操作有随机延迟,而脚本请求往往过于规律。在关键操作(如点击、翻页)之间使用
page.wait_for_timeout(random.uniform(1000, 3000))添加随机等待。 - 使用“有头”模式调试:在开发阶段,设置
headless=False,亲眼观察浏览器的行为,并用开发者工具的网络面板对比你的请求和真实浏览器请求的每一个细节,包括头字段的顺序(有些反爬会检查这个)、大小写。 - 验证Route是否生效:在拦截函数中打印出修改前后的请求头,确认你的修改确实被应用了。
5.3 问题三:Route拦截导致性能下降
现象:爬虫速度变慢,CPU或内存使用率升高。
优化方案:
- 缩小拦截范围:这是最有效的优化。从
**改为**/api/data/**。 - 避免在拦截函数中执行昂贵操作:不要在里面读文件、做网络请求、进行复杂字符串处理。
- 复用BrowserContext和Page:不要为每个任务都创建新的浏览器实例。一个
BrowserContext可以创建多个Page,路由设置在Page或Context级别都行。设置在Context级别会对该上下文下的所有页面生效。# 在Context级别设置路由,该上下文下所有页面共享 context = await browser.new_context() await context.route('**/api/**', api_intercept_handler) page1 = await context.new_page() page2 = await context.new_page() - 考虑使用
route.fallback():如果你只是想为未匹配其他路由的请求提供一个默认行为,可以使用fallback。但通常直接设置精确路由更清晰。
5.4 问题四:如何处理需要认证的请求?
对于需要携带Token、Cookie的请求,Route拦截同样能优雅处理。
方案一:从页面上下文中获取并注入如前文示例,在拦截API请求时,通过page.evaluate()执行JavaScript,从window对象或localStorage中获取令牌。
方案二:从外部存储中读取如果令牌是通过其他方式(如登录API)获取并保存在文件或数据库中的,可以在拦截函数中读取。
import json AUTH_TOKEN = None def load_token(): global AUTH_TOKEN with open('token.json', 'r') as f: data = json.load(f) AUTH_TOKEN = data.get('token') async def auth_intercept(route: Route): headers = dict(route.request.headers) if AUTH_TOKEN: headers['Authorization'] = f'Bearer {AUTH_TOKEN}' await route.continue_(headers=headers) # 在爬虫主逻辑开始前加载令牌 load_token()方案三:与page.set_extra_http_headers结合对于全局都需要的基础认证头,可以在Page或Context创建时通过set_extra_http_headers设置。对于动态的、需要从页面获取的令牌,再用Route拦截特定API进行添加。两者并不冲突,Route修改的优先级更高。
6. 超越请求头:Route的其他反爬应用场景
Route的能力不止于修改请求头,它在反爬对抗中还有其他妙用。
6.1 拦截并修改响应内容
有些网站会将数据加密后放在JavaScript变量中,或者返回一个非标准的JSONP格式。你可以拦截响应,在数据到达页面执行环境前,对其进行解密或重写。
async def modify_response(route: Route): # 先让请求继续,获取原始响应 response = await route.fetch() # 读取响应体 body = await response.text() # 对响应体进行处理,例如解密 # decrypted_body = your_decrypt_function(body) decrypted_body = body.replace('some_obfuscated_code', 'clear_data') # 使用修改后的内容完成请求,模拟服务器返回 await route.fulfill( response=response, body=decrypted_body, headers={**response.headers, 'Content-Type': 'application/json'} # 可以同时修改响应头 ) # 只拦截特定的数据接口 await page.route('**/api/encrypted-data', modify_response)6.2 屏蔽不必要的请求以提升速度
爬虫往往只关心数据,不关心页面渲染效果。我们可以拦截并阻止图片、字体、媒体文件甚至部分CSS/JS的加载,大幅提升页面加载速度。
async def block_assets(route: Route): request = route.request resource_type = request.resource_type # 阻止图片、样式表、字体、媒体文件加载 if resource_type in ['image', 'stylesheet', 'font', 'media']: # 直接中止该请求,返回一个空响应 await route.abort() # 或者返回一个模拟的成功响应,避免页面JS报错 # await route.fulfill(status=200, body='') else: await route.continue_() await page.route('**/*', block_assets)注意:粗暴地屏蔽资源可能导致页面布局错乱或JavaScript功能异常,进而影响数据加载。最好先观察目标页面,确认哪些资源对数据抓取是无关紧要的,再进行屏蔽。一个更安全的方法是只屏蔽已知的第三方跟踪、广告脚本的域名。
6.3 模拟网络错误或延迟
为了测试爬虫的健壮性,或者模拟真实的网络环境,可以随机让一部分请求失败或延迟响应。
import random import asyncio async def chaos_monkey(route: Route): rand = random.random() if rand < 0.1: # 10%的概率失败 await route.abort('failed') elif rand < 0.3: # 20%的概率延迟2-5秒 await asyncio.sleep(random.uniform(2, 5)) await route.continue_() else: # 70%的概率正常通过 await route.continue_() # 可以对非关键请求应用这种混沌测试 await page.route('**/*.{png,jpg,css}', chaos_monkey)7. 架构建议:构建可维护的Playwright爬虫项目
当爬虫逻辑变得复杂,包含多个路由处理器、不同的伪装策略时,代码容易变得混乱。这里分享一些项目组织上的心得。
1. 使用类来组织路由处理器将相关的路由处理器和其依赖的数据封装成类,提高内聚性。
class HeaderManagement: def __init__(self, user_agent_pool): self.user_agent_pool = user_agent_pool self.current_headers = {} async def random_ua_intercept(self, route: Route): headers = dict(route.request.headers) ua = random.choice(self.user_agent_pool) headers['User-Agent'] = ua self.current_headers = headers # 记录当前使用的头 await route.continue_(headers=headers) async def api_auth_intercept(self, route: Route, auth_token): headers = dict(route.request.headers) headers.update(self.current_headers) # 继承基础头 headers['X-Auth-Token'] = auth_token await route.continue_(headers=headers) # 使用 header_mgr = HeaderManagement(USER_AGENT_POOL) await page.route('**/*.html', header_mgr.random_ua_intercept) await page.route('**/api/**', lambda route: header_mgr.api_auth_intercept(route, API_TOKEN))2. 配置文件管理伪装参数将User-Agent池、基础请求头、拦截规则等写入配置文件(如config.yaml或config.py),便于管理和切换不同网站的爬取策略。
3. 分离采集逻辑与反爬逻辑将Route设置、请求头伪装、响应处理等反爬相关代码,与具体的数据解析、存储业务逻辑分开。可以创建专门的anti_spider.py模块,导出设置路由的函数。
4. 做好日志记录在关键的路由处理器中记录拦截的URL、修改的头信息、发生的错误等。这不仅是调试的利器,也能帮助你分析爬虫的行为模式,优化策略。
import logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) async def logged_intercept(route: Route): try: # ... 处理逻辑 await route.continue_(headers=new_headers) logger.info(f"Successfully modified headers for: {route.request.url}") except Exception as e: logger.error(f"Failed to handle route for {route.request.url}: {e}") # 即使出错,也最好继续请求,避免页面卡死 await route.continue_()Playwright的Route拦截功能,为我们提供了一把锋利且精准的手术刀,能够深入到网络请求的毛细血管中进行操作。用它来修改请求头,是应对“请求头检测”型反爬最高效、最彻底的方法之一。它背后的思想——在请求发出前最后一刻进行伪装——也适用于其他许多需要精细化控制网络行为的场景。掌握它,你的爬虫技术就从“能用”进阶到了“好用且稳健”的层次。在实际项目中,结合BrowserContext隔离、智能等待、错误重试等机制,你就能构建出能够应对大多数现代网站反爬策略的数据采集系统。