一、先说清楚这是个啥事
写爬虫,很多人第一反应就是发个请求,拿回HTML,解析一下,就完事了。但现在的网站,尤其是那些数据值钱的网站,早就不吃这一套了。它们会看你浏览器长啥样,会观察你鼠标怎么动,会检查你有没有打开调试工具,甚至能感知到你用的是不是真人在操作。如果你用普普通通的爬虫去碰,分分钟被拦下来,轻则弹个验证码,重则封你IP。
那怎么办?有个工具叫Playwright,它不像普通HTTP请求那样光秃秃的,它是真真实实打开一个浏览器来访问网页,就像你亲手在浏览器里操作一样。但光打开还不够,因为网站还是能通过一些痕迹判断出“这不是真人”。所以我们需要再做一些手脚,把浏览器“伪装”成普通用户的样子,同时绕过那些专门检测自动化工具的机制。这篇文章就是带着大家,从零开始,用Playwright搭一套能对付反爬的爬虫架构,用最生活化的说法,把这些技术讲明白。
二、爬虫为啥会被认出来
想不被认出来,就得先知道别人是怎么认出你的。这里有两个关键点:一个是浏览器指纹,一个是自动化操作痕迹。
2.1 浏览器指纹
每个人都有指纹,浏览器也有。你的浏览器会显示自己的User-Agent,会告诉你它支持哪些字体,屏幕分辨率是多少,用什么显卡渲染画面,甚至连时区、语言、插件列表这些信息,都可以被网站收集起来,组合成一个独特的“指纹”。正常用户的指纹是自然多样的,而爬虫用Playwright打开浏览器时,如果没有做任何处理,它的指纹是千篇一律的,而且有很多奇怪的特征。比如,正常Chrome浏览器不会有所谓的“自动化控制”标记,但Playwright打开的浏览器却有。网站一看就知道,这不是正常人用的浏览器。
2.2 自动化痕迹
除了指纹,操作行为也很重要。真人打开网页,会先看顶部,再慢慢往下滑,鼠标移动也不是一条直线,而是有加速有停顿的。而自动化脚本,动作往往特别快,鼠标移动是瞬间就到位,点击也没有延迟,输入文字更像是粘贴进去的。网站前端会记录这些操作轨迹,用机器学习模型判断是不是机器人。另外,一些浏览器插件、cookies的状态、WebDriver的全局变量,都会暴露自动化的身份。所以说,光用Playwright还不够,我们必须把这些漏洞一一堵上。
三、用Playwright搭个基础架子
技术栈:Python(配合Playwright库)
先装一下Playwright,命令很简单:
# 安装Python库
pip install playwright
# 安装浏览器内核(推荐chromium)
playwright install chromium
装好之后,我们来写一个最基础的例子,就是用Playwright打开一个网页,并且打印出标题。这个代码谁都能看懂,就是先启动一个浏览器,然后开个新页面,访问URL,最后把标题取出来。请看:
# 导入playwright库
from playwright.sync_api import sync_playwright
# 用with语法,让浏览器自动关闭
with sync_playwright() as p:
# 启动chromium浏览器,headless=False表示显示界面,你可以看到操作过程
browser = p.chromium.launch(headless=False)
# 新建一个页面标签页
page = browser.new_page()
# 告诉页面去访问指定网址
page.goto("https://example.com")
# 获取页面的标题
title = page.title()
# 打印出来看看
print("页面标题:", title)
# 关闭浏览器
browser.close()
这段代码很简单,但如果你直接拿它去爬那些有反爬机制的网站,大概率会被识破。因为这里的浏览器是最“素”的状态,什么都没做,一眼就能被看出来是自动化工具。接下来,我们一步步给它“化妆”。
四、给浏览器做一套“衣服”——指纹伪装
指纹伪装的思路,就是把浏览器里那些能暴露身份的信息,改得和普通用户一模一样。我们分几步走。
4.1 修改User-Agent和视图数据
User-Agent就是浏览器的“自我介绍”。普通Chrome会写“Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...”,而Playwright默认的User-Agent里常常带着“HeadlessChrome”字样,太显眼了。我们需要把它换成真实浏览器的样子,并且把屏幕分辨率、颜色深度等也设置成常见的值。
4.2 清除WebDriver标记
网站会检查 navigator.webdriver 这个属性,如果它是 true,就说明是自动化控制。Playwright打开浏览器后,这个属性是 true,我们得想办法把它改成 undefined 或者 false。在Playwright里,可以用初始化脚本,在页面加载前就把这个属性篡改掉。
4.3 干扰Canvas指纹
网站会画一张看不见的图片,然后读取图片的像素数据,不同浏览器画出来的结果会有一点点差异,这个差异就是Canvas指纹。如果我们不修改,Playwright的Canvas指纹和真正Chrome不一样。解决办法是往像素数据里加一点随机噪声,让每次访问看起来都像不同的普通用户。注意,噪声不能太大,不然网站会觉得你的浏览器有问题。
4.4 设置时区、语言、字体
这些细节也很重要。比如你人在中国,但爬一个日本网站,浏览器时区却显示美国东部,那肯定不对劲。我们可以统一设置成目标用户常用的时区、语言,并且把系统里的字体列表也伪装一下。
下面这段代码,把上面这些东西都融合在一起,展示了如何用一个“穿了衣服”的浏览器去访问页面:
import random
from playwright.sync_api import sync_playwright
# 一个常见的真实User-Agent,来自某台Windows上的Chrome
fake_ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
with sync_playwright() as p:
# 启动浏览器,注意这里用 headless=False,方便观察效果
browser = p.chromium.launch(headless=False)
# 创建一个上下文(相当于一个独立的浏览器会话)
context = browser.new_context(
user_agent=fake_ua, # 替换成准备好的UA
viewport={"width": 1920, "height": 1080}, # 常见屏幕尺寸
screen={"width": 1920, "height": 1080}, # 屏幕的实际大小
timezone_id="Asia/Shanghai", # 时区设为北京时间
locale="zh-CN", # 语言设为简体中文
color_scheme="light", # 主题颜色设为浅色
extra_http_headers={
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"Sec-Ch-Ua-Platform": '"Windows"',
}
)
# 在页面加载前,先执行一段脚本,把webdriver标记去掉,再给Canvas加噪声
context.add_init_script("""
// 把 navigator.webdriver 属性删掉,让它变成 undefined
Object.defineProperty(navigator, 'webdriver', {
get: undefined
});
// 获取原来的toDataURL方法
const originalToDataURL = HTMLCanvasElement.prototype.toDataURL;
// 重写它,让每次生成的图片数据都不一样
HTMLCanvasElement.prototype.toDataURL = function(...args) {
// 先调用原来的方法拿到图片数据
let data = originalToDataURL.apply(this, args);
// 随机决定要不要加噪声(80%概率加)
if (Math.random() < 0.8) {
// 在base64数据的尾部追加一个随机字符,影响很小,但足以改变指纹
data = data.replace(/[A-Za-z0-9+/=]$/,
String.fromCharCode(65 + Math.floor(Math.random() * 26)));
}
return data;
};
""")
# 打开一个页面
page = context.new_page()
# 访问目标网站
page.goto("https://example.com")
# 故意停留几秒,让页面好好加载一下
page.wait_for_timeout(3000)
# 在页面里执行js,看看webdriver变成了什么
result = page.evaluate("navigator.webdriver")
print("当前webdriver值:", result) # 应该是 None 或 undefined
# 生成一个canvas指纹看看
fingerprint = page.evaluate("""
() => {
const canvas = document.createElement('canvas');
canvas.width = 200;
canvas.height = 200;
const ctx = canvas.getContext('2d');
ctx.fillText('hello world', 10, 50);
return canvas.toDataURL().slice(0, 50);
}
""")
print("Canvas指纹前缀:", fingerprint)
browser.close()
注意看,这里我们用 context.add_init_script 在页面任何脚本执行之前,就修改了 navigator.webdriver,并且给Canvas的 toDataURL 方法加了点随机噪声。这样每次生成的指纹都不同,网站就很难把你和某一个自动化脚本关联起来。
五、绕过反自动化检测的常用招数
指纹伪装解决了“你是谁”的问题,但还没解决“你怎么操作”的问题。如果一打开页面就疯狂点击,或者瞬间读完整个网页,那还是不像人。所以我们还得把鼠标键盘操作变得像真人一样。
5.1 模拟人类操作
Playwright本身提供了 page.mouse 和 page.keyboard,我们可以通过随机移动和随机停顿来模拟真人的动作。比如,要点击一个按钮,不是直接点,而是先把鼠标移动到按钮附近,再一点点挪过去,中间加一点随机的左右摆动,最后才按下。这里我们写一个简单的“人类化操作”示例:
import random
import time
from playwright.sync_api import sync_playwright
def human_move_mouse(page, x, y):
"""模拟真人的鼠标移动,从当前点移动到目标点"""
# 获取当前鼠标位置(如果不行就假设从屏幕中心开始)
try:
current = page.mouse.position()
start_x, start_y = current.get("x", 960), current.get("y", 540)
except:
start_x, start_y = 960, 540
# 分成很多小段移动,每段之间睡一小会儿
steps = random.randint(15, 25)
for i in range(steps):
# 用线性插值算出当前应该到哪
progress = (i + 1) / steps
# 加一点正弦波动的偏移,让轨迹不是直线
offset = random.uniform(-5, 5)
target_x = start_x + (x - start_x) * progress + offset
target_y = start_y + (y - start_y) * progress + offset
# 移动鼠标
page.mouse.move(target_x, target_y)
# 随机停顿 5~30 毫秒
time.sleep(random.uniform(0.005, 0.03))
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
context = browser.new_context(viewport={"width": 1280, "height": 800})
page = context.new_page()
page.goto("https://example.com")
# 假设我们要点击页面上的“开始”按钮(先找到它的坐标)
button = page.locator("a.button") # 选择器自行替换为真实页面
box = button.bounding_box() # 获取按钮的位置和大小
if box:
# 按钮中心的坐标
target_x = box["x"] + box["width"] / 2
target_y = box["y"] + box["height"] / 2
# 先移动鼠标过去
human_move_mouse(page, target_x, target_y)
# 再随机停一下
time.sleep(random.uniform(0.1, 0.3))
# 按下鼠标
page.mouse.click(target_x, target_y)
else:
print("按钮没找到")
browser.close()
这段代码里的移动轨迹不是一条直线,而是有抖动和停顿,更像真人手在动。但是要注意,你不能每次都按同一种模式走,否则网站同样会发现“这两个鼠标轨迹怎么一模一样”,所以随机性非常重要。
5.2 使用stealth插件
手动改来改去总有遗漏的地方,这里推荐一个现成的工具,叫 playwright-stealth,它相当于一个“隐身衣”补丁,会自动帮我们隐藏很多自动化特征。虽然不能完全代替手写,但能省很多事。下面演示一下怎么用:
# 先安装:pip install playwright-stealth
import asyncio
from playwright.async_api import async_playwright
from playwright_stealth import stealth_async
async def main():
async with async_playwright() as p:
# 启动带图形界面的浏览器
browser = await p.chromium.launch(headless=False)
# 创建一个上下文
context = await browser.new_context()
# 给这个上下文加上stealth补丁
await stealth_async(context) # 注意:stealth_async是异步函数
# 新建页面
page = await context.new_page()
# 访问一个检测缓存的网站,比如个人资料页
await page.goto("https://bot.sannysoft.com") # 这是一个专门检测自动化的页面
# 等几秒
await page.wait_for_timeout(5000)
# 截图看看哪些项被破解了
await page.screenshot(path="stealth_result.png")
await browser.close()
# 运行异步主函数
asyncio.run(main())
上面这个 bot.sannysoft.com 是开发者圈子里常用的自动化检测测试站。你自己跑一下截图,就能看到哪些指纹项是绿色的(通过),哪些是红色的(暴露)。playwright-stealth 能修复绝大多数问题,但仍然有少数站点使用了更高级的检测方式,需要我们配合自己改代码。
5.3 处理验证码
验证码是绕不开的坎。如果网站弹出了一个简单的滑块验证,用Playwright硬拼没什么用,因为滑块识别的往往不是单纯的位置,而是你的移动轨迹是不是人。前面我们写了模拟人类移动的代码,配合这个来拖滑块,成功率能提高。另外,对于文字验证码,可以考虑接入第三方识别服务,或者用深度学习模型,但那是另一个大话题,这里不展开。有一个原则:尽量让网站不弹出验证码,因为一旦弹出,你就已经暴露了一半了。所以指纹伪装和人类操作才是最重要的预防手段。
六、应用场景
这套架构适合哪些场景呢?首先是数据采集量不大的场景,比如你要采集某个电商平台上的商品信息,每天只采几百个页面,这时候用Playwright配指纹伪装,非常灵活,不需要维护一堆IP代理。其次是需要在浏览器里执行JavaScript才能拿到数据的网站,比如现在很多前端框架都是动态渲染的,用普通HTTP请求根本拿不到内容,必须用无头浏览器。还有就是需要模拟用户登录状态的爬虫,比如采集朋友圈数据、后台报表数据等,Playwright可以让你像真人一样输入账号密码,然后保持登录状态,后续的请求都带着这个会话。另外,一些对登录或验证码有严格限制的网站,用这套把“人味”做足的架构,也能有效降低风控级别。
七、技术优缺点
先说优点。Playwright本身支持多浏览器(Chromium、Firefox、WebKit),还支持多线程和异步,写起爬虫来非常顺手。配合指纹伪装,能绕过很大一部分反爬机制,比传统requests爬虫要强得多。它也能自动等待页面渲染,不用手动sleep,用 wait_for_selector 就能等到元素出现,非常方便。
再说缺点。最大的缺点是速度慢,因为要启动真正的浏览器,渲染每个页面都要花很长时间,比不过单纯用HTTP并发。第二个缺点是资源消耗大,开10个浏览器实例,内存和CPU占用会很高,所以不太适合大规模采集。第三个缺点是反爬技术也在进化,有些网站已经可以检测到Chromium的引擎特征,比如通过 navigator.plugins 列表之类的方式,即使你用了stealth也可能被发现。所以这不是一劳永逸的方案,需要不断更新补丁和策略。
八、注意事项
用这套架构的时候,有几个地方要特别留意。
第一,不要做违反网站规定的事情。绕过反爬本身可能违反服务条款,爬之前要看清robots.txt,并且尊重网站的数据版权,不要在短时间内疯狂请求。
第二,要设置随机延迟,不要每秒都去访问。比如每次请求之间睡上2~5秒,并且用随机数,让访问时间间隔看起来像人工作息。
第三,要处理好IP问题。单纯伪装指纹,但IP不变,一样容易暴露。如果你的爬虫要跑很久,建议配合代理IP池,每隔一段时间换一个出口IP。注意,代理的质量要高,公共代理很容易被识别。
第四,浏览器的缓存和cookie要管理好。你可以让同一个上下文一直保持登录状态,但别把不同用户的数据混在一起,不然网站会看到你的cookie和指纹不对称。
第五,及时更新浏览器版本和Playwright版本。因为网站前端检测脚本会跟着浏览器更新而调整,旧版本可能更容易被识别。
第六,代码里要加错误处理。网站可能偶尔弹出验证码,或者超时,遇到这种情况就重试几次,如果仍然失败就换一个IP再试,别硬着头皮一直撞。
第七,不要修改Cookies里的关键字段。有些网站会设置HttpOnly的cookie,你在JavaScript里取不到,但保持同一个浏览器上下文,cookie会自动带上,你不需要自己处理。
第八,建议用无头模式跑生产任务,但不建议一开始就用无头模式。调试的时候,打开界面看看哪里出了问题;真正上线后,再改成 headless=True,性能会好很多。注意,有些网站会把无头模式当做机器人,即使你用了headless=True,通过一些CSS渲染差异还是能检测出来。所以如果遇到个别网站死活过不去,试试用带界面的模式跑一下,说不定就过了。
九、文章总结
总的来说,基于Playwright的爬虫架构,是一个相当实用的方案。它让我们能以一个“仿真的真人用户”的身份在网络上活动,而不是一个干巴巴的请求。要想成功绕过反自动化检测,核心思路有两条:一是把浏览器指纹做得像普通用户,二是把操作行为做得像真实人类。指纹伪装包括修改User-Agent、去掉webdriver标记、干扰Canvas、设置时区语言等;行为模拟则包括鼠标随机移动、随机停歇、模拟点击等。这两者缺一不可,光改指纹没有行为模拟,网站看你操作太机械一样会拦你;光有行为模拟但指纹暴露,结果也是一样。
另外,推荐使用 playwright-stealth 这类补丁来简化工作,但也不能全信,自己写完代码后,最好找一个自动化检测网站测试一下,看看还有哪些项是红的,再针对性地修补。爬虫这项技术,本质上是一场猫鼠游戏,我们只能不断学习新的检测方法,不断调整自己的策略。但也要始终记住,技术本身没有好坏,关键是用的地方。希望这篇文章能帮你把Playwright爬虫的水平提高一个档次,少踩一些被反爬拦截的坑。
评论
围绕“基于Playwright的爬虫架构:如何结合浏览器指纹伪装与反自动化检测绕过”参与讨论