当前位置:首页>python>Node.js 浏览器引擎 + Python 大脑:Playwright 混合架构爬虫系统深度解析

Node.js 浏览器引擎 + Python 大脑:Playwright 混合架构爬虫系统深度解析

  • 2026-10-11 06:17:25
Node.js 浏览器引擎 + Python 大脑:Playwright 混合架构爬虫系统深度解析

关注 霍格沃兹软件测试开发 公众号,回复「资料」, 领取人工智能测试开发技术合集

当 requests 拿不到数据、Selenium 跑不动并发时,这套架构救了我

一个让我失眠的夜晚

去年秋天接了一个电商价格监控的项目,甲方要求实时抓取日本雅虎拍卖的商品数据——当前出价、剩余时间、竞拍人数,延迟不能超过 5 秒。

一开始我想得挺简单:requests + BeautifulSoup,半小时写完,跑起来一看——页面一片空白。

原因不复杂:雅虎拍卖的商品页是典型的 SPA(单页应用),价格和倒计时全部通过 JavaScript 异步加载。requests 拿到的只是一个空壳 HTML,真正的数据根本不在里面。

换 Selenium?我试了。开了 20 个浏览器实例,机器 CPU 直接飙到 96%,内存暴涨,跑不到 20 分钟容器就 OOM 崩了。甲方还在催,我差点想跑路。

后来折腾出来的这套方案,就是今天想跟大家聊的——Node.js 做浏览器引擎,Python 做大脑的 Playwright 混合架构。

为什么要搞混合架构?

直接说结论:纯 Python Playwright 不是不能跑,但在高并发场景下,资源开销是个大问题。

Playwright 本身是多语言支持的——Python、Java、.NET、Node.js 都能用。但有一个细节很多人没注意到:Node.js 版本的 Playwright 不会像 Python 版本那样为每个浏览器窗口生成一个新进程,因此在管理多个标签页时,CPU 和内存开销要小得多。

什么意思呢?简单说——

  • Python Playwright:每个浏览器上下文会启动独立的子进程,开 20 个实例基本就把机器榨干了
  • Node.js Playwright:事件驱动模型,同一个进程里可以高效管理多个浏览器实例

所以我的思路很直接:用 Node.js 专门管浏览器(体力活),用 Python 做调度和数据处理(脑力活) 。两者通过 REST API 通信。

架构长什么样?

整体拆成三层:

┌─────────────────────────────────────────────────┐│                  Python 调度层                    ││   任务队列 → 调用渲染接口 → 解析数据 → 落库      │└────────────────────┬────────────────────────────┘                     │ REST API / JSON┌────────────────────▼────────────────────────────┐│              Node.js 浏览器服务层                 ││    浏览器池管理 → 页面渲染 → 执行 JS 交互        │└────────────────────┬────────────────────────────┘                     │ CDP 协议┌────────────────────▼────────────────────────────┐│               无头 Chromium 实例                  │└─────────────────────────────────────────────────┘

这个分层的好处很明显:浏览器服务可以独立扩容,Python 调度器完全不用关心浏览器怎么跑。想加并发?多部署几个 Node.js 服务实例就行。

Node.js 浏览器服务:核心代码

浏览器服务我用 Express 搭了一个 HTTP 接口,核心是维护一个浏览器实例池:

const express = require('express');const { chromium } = require('playwright');const app = express();app.use(express.json());const PORT = 3000;const browserPool = {};const MAX_BROWSERS = 5;asyncfunctiongetBrowserInstance(id) {if (!browserPool[id]) {        browserPool[id] = await chromium.launch({headless: true,args: ['--no-sandbox']        });    }return browserPool[id];}app.post('/render', async (req, res) => {const { url, js_actions, session_id = 'default' } = req.body;try {const browser = await getBrowserInstance(session_id);const context = await browser.newContext({userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'        });const page = await context.newPage();await page.goto(url, { waitUntil: 'networkidle', timeout: 60000 });// 执行自定义 JS 操作(点击、滚动等)for (const action of js_actions || []) {if (action.type === 'click') {await page.click(action.selector);await page.waitForTimeout(2000);            } elseif (action.type === 'scroll') {await page.evaluate(() =>window.scrollTo(0, document.body.scrollHeight));await page.waitForTimeout(1000);            }        }const content = await page.content();const screenshot = await page.screenshot({ fullPage: true });await context.close();        res.json({success: true,html: content,screenshot: screenshot.toString('base64')        });    } catch (error) {        res.status(500).json({ success: false, error: error.message });    }});app.listen(PORT, () => console.log(`Browser service running on port ${PORT}`));

踩坑提醒:浏览器实例池的大小要根据机器配置来调。我实测 8 核 16G 的机器,每个浏览器进程大概吃 150~250MB 内存,MAX_BROWSERS 设到 5 是比较稳妥的。想跑更多?上容器拆分。

Python 调度器:大脑怎么指挥手脚

Python 这边就简单多了——发 HTTP 请求让 Node.js 去干活,拿到 HTML 后用 BeautifulSoup 解析:

import requestsfrom bs4 import BeautifulSoupimport timeBROWSER_API = "http://localhost:3000/render"deffetch_rendered_page(url, actions=None):    payload = {"url": url,"js_actions": actions or [],"session_id": "default"    }    resp = requests.post(BROWSER_API, json=payload, timeout=120)if resp.status_code == 200:        data = resp.json()if data.get('success'):return data.get('html')returnNonedefparse_product_data(html):    soup = BeautifulSoup(html, 'html.parser')# 这里写具体的解析逻辑    price = soup.select_one('.product-price')    title = soup.select_one('.product-title')return {'price': price.text.strip() if price elseNone,'title': title.text.strip() if title elseNone    }# 调度循环urls = ['https://example.com/item/1', 'https://example.com/item/2']for url in urls:    html = fetch_rendered_page(url, [        {'type': 'scroll'},  # 先滚动加载更多        {'type': 'click', 'selector': '.load-more'}  # 再点加载按钮    ])if html:        data = parse_product_data(html)# 存数据库...    time.sleep(2)  # 礼貌爬虫,控制频率

关键点:js_actions 这个设计让 Python 可以灵活控制浏览器的行为——滚动、点击、输入,全都能远程指挥。这样就算页面逻辑再复杂,Python 调度层也不需要改动,只需调整 actions 参数就行。

人工智能技术学习交流群

伙伴们,对AI测试、大模型评测、质量保障感兴趣吗?我们建了一个 「人工智能测试开发交流群」,专门用来探讨相关技术、分享资料、互通有无。无论你是正在实践还是好奇探索,都欢迎扫码加入,一起抱团成长!期待与你交流!👇

反爬怎么办?三层防御

这套架构跑通之后,第二个问题来了:雅虎的反爬不是吃素的。

我踩过的坑包括:IP 封禁、自动化特征检测、请求频率限制。解决方案分三层:

第一层:指纹伪装

普通的 Playwright 跑的是 Chromium 标准构建版本,反爬系统早就把特征研究透了——哪些 JavaScript API 行为和真实 Chrome 不一样、Canvas 渲染有什么细微差异、WebGL 特征码对不上,全都有档案。

最基本的伪装:

browser = await p.chromium.launch(    headless=True,    args=['--disable-blink-features=AutomationControlled']  # 关掉自动化标记)context = await browser.new_context(    user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36',    viewport={'width': 1920, 'height': 1080})

更狠的可以用 playwright-stealth 这类库来抹掉自动化痕迹。但注意,老版的 playwright-stealth 已经停更了,现在推荐用 pw-stealth-enhanced。

第二层:代理轮换

高并发请求下,固定出口 IP 很快就会被封。我的做法是给每个浏览器会话分配独立代理:

proxies = get_proxy_from_pool()  # 从代理池取一个context = await browser.new_context(    proxy={'server': f'http://{proxies["host"]}:{proxies["port"]}','username': proxies['user'],'password': proxies['pass']    })

第三层:行为模拟

很多反爬系统会分析鼠标轨迹、点击节奏这些行为特征。纯粹的无头浏览器很容易被识破。解决方案是引入 humanization-playwright 这类库,模拟贝塞尔曲线的鼠标移动、随机延迟等人类行为。

这三层叠起来,我的采集系统现在已经稳定跑了 3 个月,日均采集超 10 万条数据。

异步改造:从串行到并发

一开始我用的是同步模式,一个一个爬,效率惨不忍睹。后来改成了 asyncio + Playwright 异步模式:

import asynciofrom playwright.async_api import async_playwrightasyncdeffetch_page(url, proxy=None):asyncwith async_playwright() as p:        browser = await p.chromium.launch(headless=True)        context = await browser.new_context(proxy=proxy)        page = await context.new_page()await page.goto(url, wait_until='networkidle')        content = await page.content()await browser.close()return contentasyncdefmain():    urls = ['url1', 'url2', 'url3', ...]    semaphore = asyncio.Semaphore(10)  # 限制并发数asyncdeffetch_with_limit(url):asyncwith semaphore:returnawait fetch_page(url)    results = await asyncio.gather(*[fetch_with_limit(url) for url in urls])# 处理结果...asyncio.run(main())

注意:并发数不是越大越好。我实测 10 个并发是比较舒服的数字,再高就容易触发反爬或者把机器搞崩。用 asyncio.Semaphore 做限流是基本操作。

这套架构的适用场景

折腾完这套东西之后,我总结了一下它最适合的场景:

  1. SPA 单页应用:React/Vue 渲染的页面,requests 完全拿不到数据的那种
  2. 需要交互操作的采集:要点击、滚动、登录才能看到完整内容的页面
  3. 高并发的动态渲染需求:纯 Python Playwright 扛不住并发的时候

不适合的场景:静态页面、简单的 API 接口采集——杀鸡用牛刀了,requests 一把梭更高效。

最后说几句

这套架构我从立项到稳定运行大概花了三周时间。最大的感悟是:不要在一个技术栈里死磕,合适的事情交给合适的工具。

Node.js 的 Playwright 管浏览器就是比 Python 版本省资源,这是架构层面的优势,不是写代码能弥补的。Python 做调度和数据处理就是方便,生态丰富。两者结合起来,各取所长。

如果你也在被 SPA 爬虫折磨,或者正愁 Python Playwright 跑不动并发,不妨试试这个思路。

推荐学习

测试智能体与智能化测试平台公开课,从Web/App/接口测试智能体,再到业务测试用例生成,爱测智能化测试平台,手把手带你掌握AI智能体与智能化测试平台!

👉 扫码进群,报名学习!


本文代码基于生产环境简化而来,完整版涉及业务敏感信息不便公开。有问题欢迎留言交流。

关于我们

霍格沃兹测试开发学社,隶属于 测吧(北京)科技有限公司,是一个面向软件测试爱好者的技术交流社区。

学社围绕现代软件测试工程体系展开,内容涵盖软件测试入门、自动化测试、性能测试、接口测试、测试开发、全栈测试,以及人工智能测试与 AI 在测试工程中的应用实践。

我们关注测试工程能力的系统化建设,包括 Python 自动化测试、Java 自动化测试、Web 与 App 自动化、持续集成与质量体系建设,同时探索 AI 驱动的测试设计、用例生成、自动化执行与质量分析方法,沉淀可复用、可落地的测试开发工程经验。

在技术社区与工程实践之外,学社还参与测试工程人才培养体系建设,面向高校提供测试实训平台与实践支持,组织开展 “火焰杯” 软件测试相关技术赛事,并探索以能力为导向的人才培养模式,包括高校学员先学习、就业后付款的实践路径。

同时,学社结合真实行业需求,为在职测试工程师与高潜学员提供名企大厂 1v1 私教服务,用于个性化能力提升与工程实践指导。

最新文章

随机文章