用 Playwright 突破 SPA 抓取壁垒
用 Playwright 突破 SPA 抓取壁垒
华为云开发者成长中心(developer.huaweicloud.com/grow)是典型的 Vue 单页应用,传统 HTTP 客户端只能拿到一句 “Please enable JavaScript”。本文记录用 Playwright headless 浏览器渲染页面、提取全部 14 项任务的真实过程,附可运行代码。
一、问题背景
华为云开发者成长中心是开发者「做任务领积分、兑换权益」的入口。想程序化获取任务列表(比如做个人积分打卡助手),第一反应是 curl 或 requests:
import requests
r = requests.get("https://developer.huaweicloud.com/grow?ggw_kfz")
print(r.text[:200])
结果只得到:
We're sorry but HDGrowthPortal doesn't work properly without JavaScript enabled.
Please enable it to continue.
原因很明显:这是一个 Vue SPA(HDGrowthPortal),任务卡片是 JS 运行后动态渲染的,服务端返回的 HTML 只有一个 <div id="app"> 占位。requests 不执行 JS,自然拿不到内容。
二、方案选型
| 方案 | 能否拿到 SPA 内容 | 备注 |
|---|---|---|
requests / curl |
❌ | 不执行 JS |
| 抓 XHR 接口 | ⚠️ | 需逆向接口、带鉴权,脆弱 |
| Selenium | ✅ | 成熟但偏重 |
| Playwright | ✅ | 现代、快、API 简洁 |
选 Playwright:原生支持 networkidle 等待、evaluate 执行页内 JS、自动等待元素,最适合 SPA。
三、环境准备
pip install playwright
playwright install chromium
ARM64 环境坑:
playwright install下载的chrome-headless-shell在 aarch64 上可能缺失。若报Executable doesn't exist,改用完整版 Chromium 的路径:browser = await p.chromium.launch( headless=True, executable_path="/root/.cache/ms-playwright/chromium-1243/chrome-linux/chrome", args=["--no-sandbox"], )
四、核心代码
下面这段脚本完整抓取成长中心的任务列表与积分兑换商品:
import asyncio
from playwright.async_api import async_playwright
URL = "https://developer.huaweicloud.com/grow?ggw_kfz"
async def main():
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True, args=["--no-sandbox"])
page = await browser.new_page()
await page.goto(URL, wait_until="networkidle", timeout=60000)
await page.wait_for_timeout(4000) # 留时间给 Vue 渲染
# 展开折叠的任务
try:
await page.click("text=展开全部任务", timeout=5000)
await page.wait_for_timeout(2000)
except Exception:
pass
text = await page.evaluate("() => document.body.innerText")
print(text)
await browser.close()
asyncio.run(main())
关键点:
wait_until="networkidle"等到网络空闲,确保异步数据加载完。wait_for_timeout(4000)再留 4 秒给 Vue 完成渲染(networkidle不保证渲染完成)。click("text=展开全部任务")触发折叠区展开,拿到隐藏任务。evaluate("() => document.body.innerText")一次性取出渲染后的全文。
五、运行结果
脚本成功输出全部 14 项任务,节选如下:
任务中心
每日浏览一个鸿蒙案例 …… 积分+10
每日Star一个鸿蒙案例代码仓 …… 积分+30
每周通过一项微认证 …… 积分+200
每周实践一个实验 …… 积分+50
每周给Agent开发实践中心代码仓提 Issue …… 积分+60
每周Star一个Agent开发实践中心代码仓 …… 积分+50
每日体验AI DevSpace …… 积分+30
在开发者作品展览馆中发布作品 …… 积分+200
每日发布文章 …… 积分+60
每周参加开发者活动 …… 积分+10
每周参与开发者赛事 …… 积分+100
开发者赛事晋级 …… 积分+200
建议被采纳 …… 积分+60
建议被实现 …… 积分+200
以及积分兑换商品(代金券档位):
5元代金券 → 50积分
10元代金券 → 100积分
20元代金券 → 200积分
50元代金券 → 500积分
100元代金券 → 1000积分
200元代金券 → 2000积分
500元代金券 → 5000积分
六、进阶:结构化提取
innerText 是一整段文本,不便程序处理。改用 DOM 查询拿到结构化数据:
tasks = await page.evaluate("""
() => {
const cards = document.querySelectorAll('.task-card, [class*="task"]');
return Array.from(cards).map(c => c.innerText.trim());
}
""")
""")
# 更稳妥的做法:按已知任务标题正则切分
import re
items = re.findall(r'(每日[^。\n]*|每周[^。\n]*|在[^。\n]*发布作品|开发者赛事[^。\n]*|建议被[^。\n]*)', text)
若要拿每个任务卡片的「去完成」跳转链接,则查所有 <a>:
links = await page.evaluate("""
() => Array.from(document.querySelectorAll('a')).map(a => ({
text: (a.innerText || '').trim().slice(0, 80),
href: a.href
})).filter(l => l.text && l.href)
""")
七、合规与礼貌爬取提醒
- 成长中心是登录态产品,未登录只能看任务列表、不能领积分。脚本只读取公开渲染内容,不触碰鉴权接口。
- 页面有反多账号刷分公告,请勿用此脚本做刷分行为,仅用于个人任务提醒/看板。
- 抓取频率请加间隔,避免给服务端造成压力。
八、总结
| 步骤 | 要点 |
|---|---|
| 1. 识别 SPA | curl 拿到 “enable JavaScript” 即可判定 |
| 2. 选 Playwright | networkidle + evaluate 是 SPA 抓取黄金组合 |
| 3. 处理折叠 | click("text=展开全部任务") 触发隐藏内容 |
| 4. 结构化 | 优先 DOM 查询,退而求其次用正则切 innerText |
| 5. ARM64 | 指定 executable_path 指向完整版 Chromium |
整套方案不到 30 行代码,就把一个「看似只能手动浏览」的 SPA 页面变成了可程序化读取的数据源。同样的套路适用于绝大多数 Vue/React 开发者门户(华为云学堂、实践中心等),把 URL 换掉即可复用。
本文代码均在华为云 EulerOS 3.0 (aarch64) + Python 3.12 + Playwright 实测通过。
- 点赞
- 收藏
- 关注作者
评论(0)