在我们前端开发中,我们不仅要构建用户界面,还要应对各种自动化需求:端到端测试、网页内容抓取、自动化报表生成、性能监控甚至是一些定制化的工作流程。手动操作耗时耗力,效率低下。这时,浏览器自动化工具就成了我们的左膀右臂。
提到浏览器自动化,很多同学会想到 Python
中的Selenium
,实际上JS中也有属于前端的自动化工具Puppeteer
,它是由 Google 团队维护的 Node.js 库,提供了一组高级 API 来通过 DevTools
协议控制 Chrome
或 Chromium
浏览器。但今天我们要聚焦的是它的一个"瘦身版"------ puppeteer-core
。
作为一名在前端领域摸爬滚打多年的老兵,我深知在生产环境中,资源的节约和灵活的部署至关重要。puppeteer-core
就是为此而生。它移除了 Puppeteer
包中自带的 Chromium
浏览器,让你能够自带一个 Chrome/Chromium 可执行文件来使用。这在 Docker 容器、云函数、或者需要使用特定版本浏览器等场景下,显得尤为灵活和高效。它就像一个精干的遥控器,负责控制外部的浏览器进程。
puppeteer-core
是什么?
puppeteer-core
是 Puppeteer
的一个轻量级版本,它提供了完全相同的 API ,但不包含 Chromium 浏览器可执行文件。这意味着你需要在安装 puppeteer-core
后,手动指定或确保系统路径中存在一个可用的 Chrome 或 Chromium 浏览器。
相当于: puppeteer-core
= Puppeteer
API + 你的 Chrome/Chromium。
为什么选择 puppeteer-core
而不是 puppeteer
?
- 包体积小:
puppeteer
包含了完整的 Chromium,通常体积巨大(大几百MB)。puppeteer-core
则非常小巧,只有几MB。这对于部署到云环境(如 AWS Lambda, Serverless Function)或 Docker 镜像时,能显著减少镜像大小和部署时间。 - 版本控制灵活: 你可以自由选择并使用任何你需要的 Chrome/Chromium 版本,而不是被
Puppeteer
绑定的版本限制。这对于兼容性测试或某些特定功能需求非常有帮助。 - 共享浏览器实例: 在一些场景下,你可能希望多个自动化脚本共享同一个浏览器实例,或者连接到一个已经运行的浏览器。
puppeteer-core
使得连接到外部浏览器变得更加自然和直接。
实际用途
- 高度模拟用户行为: 相比于传统的 HTTP 请求抓取,
puppeteer-core
运行在真实浏览器环境中,能够执行 JavaScript、处理 CSS 动画、模拟用户点击和输入,这使得它能够处理几乎所有复杂的现代网页(SPA 应用、各种弹窗、登录验证等)。 - 调试友好: 设置
headless: false
,你就能看到浏览器窗口,配合devtools: true
,可以直接在 DevTools 中调试页面,这极大地简化了调试过程。 - 强大的生态和社区: 作为
Puppeteer
的核心,它共享着庞大的社区支持和丰富的插件,遇到问题很容易找到解决方案。 - 云原生友好: 小巧的包体积和灵活的浏览器路径配置,使其成为云函数(如 AWS Lambda, Google Cloud Functions)或 Docker 容器中进行 Web 自动化任务的理想选择
下面就利用实战真真切切的去感受他的用处吧~
示例:网页截图与动态内容抓取
示例1:给任意网页截图
这是一个最基础的用法,演示了如何启动浏览器、打开页面并截图。
准备工作:请找到你的Chrome的安装地址:
- Windows: C:\Program Files (x86)\Google\Chrome\Application\chrome.exe
- macOS: /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
- Linux: /usr/bin/google-chrome 或 /usr/bin/chromium-browser
创建project,并cd进入npm -y 初始化依赖之后执行 npm i puppeteer-core, 注意:node需 > V18
javascript
const puppeteer = require('puppeteer-core');
async function takeScreenshot(url, outputPath, executablePath) {
let browser = null
try {
// 1. 启动一个浏览器实例
// 注意:executablePath 是 puppeteer-core 核心参数,指定 Chrome/Chromium 路径
browser = await puppeteer.launch({
executablePath, // 替换为你的 Chrome/Chromium 可执行文件路径(例:C:/Program Files/Google/Chrome/Application/chrome.exe)
headless: false, // 设为new则是无浏览模式,节省电脑内存资源,false为观察者模式
// args: ['--no-sandbox', '--disable-setuid-sandbox'] // Linux 环境下常用参数
});
// 2. 创建一个新页面
const page = await browser.newPage();
// 设置视口大小,模拟常见的桌面分辨率
await page.setViewport({ width: 1920, height: 1080 });
// 3. 导航到指定 URL
console.log(`正在前往:${url}`);
await page.goto(url, {
waitUntil: 'networkidle2', // 等待网络空闲,确保所有资源加载完成(包括JS渲染的动态内容)
timeout: 60000 // 60秒超时
});
// 4. 截图并保存
await page.screenshot({ path: outputPath, fullPage: true });
console.log(`截图已保存到:${outputPath}`);
} catch (error) {
console.error('截图失败:', error);
} finally {
// 5. 关闭浏览器实例
if (browser) {
await browser.close();
}
}
}
const CHROME_PATH = 'C:/Program Files/Google/Chrome/Application/chrome.exe'; // 替换为你的路径
takeScreenshot('https://www.baidu.com', 'baidu.png', CHROME_PATH);
// takeScreenshot('https://juejin.cn/post/7472573150003167243', 'juejin.png', CHROME_PATH); // 试试复杂的页面
效果如下:

模拟用户在浏览器中的真实操作流程,验证整个应用从UI到后端逻辑的正确性。这是最常见的用途之一,尤其在 CI/CD 流程中。
示例2:抓取知乎热榜标题
这个例子会展示如何等待页面元素加载,并执行浏览器内的 JavaScript 来提取数据。
javascript
// zhihu_hot.js
const puppeteer = require('puppeteer-core');
async function scrapeZhihuHotList(executablePath) {
let browser;
try {
browser = await puppeteer.launch({
executablePath: executablePath,
headless: false,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const page = await browser.newPage();
await page.setViewport({ width: 1200, height: 800 });
console.log('正在前往知乎热榜...');
await page.goto('https://www.zhihu.com/hot', {
waitUntil: 'domcontentloaded', // 等待 DOM 加载完成即可,不关心所有资源
timeout: 60000
});
await new Promise(resolve => setTimeout(resolve, 15000)) // 空留15秒登录
// 等待热榜列表加载完成
// 使用 CSS 选择器来定位元素,这里假设热榜项的标题在 .HotItem-title 类中
await page.waitForSelector('.HotItem-content .HotItem-title', { timeout: 10000 });
console.log('知乎热榜页面加载完成,开始抓取数据...');
// 在浏览器环境中执行 JavaScript 代码来提取数据
const hotList = await page.evaluate(() => {
const items = document.querySelectorAll('.HotItem-content');
const data = [];
items.forEach(item => {
const titleElement = item.querySelector('.HotItem-title');
const linkElement = item.querySelector('a');
if (titleElement && linkElement) {
data.push({
title: titleElement.textContent.trim(),
link: linkElement.href
});
}
});
return data;
});
console.log('抓取到的知乎热榜:');
hotList.forEach((item, index) => {
console.log(`${index + 1}. ${item.title} - ${item.link}`);
});
} catch (error) {
console.error('抓取知乎热榜失败:', error);
} finally {
if (browser) {
await browser.close();
}
}
}
const CHROME_PATH = 'C:/Program Files/Google/Chrome/Application/chrome.exe';
scrapeZhihuHotList(CHROME_PATH);
效果如下:

这个数据抓取完全可以用于模拟真实用户操作去抓取动态加载、JS 渲染的页面内容,这是传统 HTTP 请求抓取无法做到的。比如抓取电商商品信息、新闻内容、数据报告等。
好了重头戏来了!!!
实战例子:12306 刷票(技术演示)
免责声明: 本部分内容仅为 puppeteer-core
技术能力演示,旨在说明其处理复杂网页交互的能力。铁路客运服务具有公共服务性质,请遵守相关法律法规及12306官网的使用规定。任何利用自动化工具进行非法抢票、囤票、倒卖行为都可能触犯法律,并受到处罚。请勿将此代码用于任何非法目的。
12306 网站以其复杂的用户交互、动态内容和严苛的反爬机制而闻名。实现一个完整的12306自动化刷票系统会非常复杂,涉及验证码识别、多用户管理、订单提交支付等,这些都不是 puppeteer-core
单独能轻松解决的。
这里,我将展示一个简化版 的例子,聚焦于如何使用 puppeteer-core
模拟用户进行查询车票并尝试点击预订的过程。登录和验证码部分将留空,因为它们是自动化抢票中最难且最敏感的环节。
逻辑梳理:
- 启动浏览器,打开12306官网。
- (跳过登录,假设已登录或通过其他方式处理登录)
- 填写出发地、目的地、出发日期。
- 点击查询按钮。
- 等待查询结果加载。
- 找到特定车次或席别的"预订"按钮并点击。
javascript
// 12306_ticket_grabber.js
const puppeteer = require('puppeteer-core');
const CHROME_PATH = 'C:/Program Files/Google/Chrome/Application/chrome.exe'; // **请替换为你的Chrome可执行文件路径**
async function grab12306Ticket(
fromStation,
toStation,
trainDate,
trainNo = '', // 目标车次,为空则不限制
seatType = '' // 目标席别,为空则不限制,例如 '硬卧', '二等座'
) {
let browser;
try {
console.log('正在启动浏览器...');
browser = await puppeteer.launch({
executablePath: CHROME_PATH,
headless: false,
devtools: false, // 不打开开发者工具
slowMo: 50, // 减慢操作速度,方便观察和调试
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-web-security', // 可能需要绕过一些安全策略,谨慎使用
'--disable-features=IsolateOrigins,site-per-process', // 应对某些iframe/跨域问题
]
});
const page = await browser.newPage();
await page.setViewport({ width: 1920, height: 1080 });
// 绕过WebDriver检测(部分网站会识别自动化环境)
await page.evaluateOnNewDocument(() => {
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined
});
});
console.log('正在前往 12306 官网...');
await page.goto('https://kyfw.12306.cn/otn/resources/login.html', { waitUntil: 'networkidle2', timeout: 60000 });
// // --- 登录部分 (简化处理,实际中需处理验证码和复杂登录逻辑) ---
// console.log('请手动在浏览器中完成登录操作。');
// // 实际应用中,你需要处理:
// // 1. 验证码识别 (极难,通常需要第三方打码平台或AI识别)
// // 2. 输入用户名密码
// // 3. 点击登录按钮
// // 4. 等待登录成功跳转
// // 这里为了演示,我们假设你已经登录成功,或者直接跳转到查询页
const delay = s => new Promise((reslove, reject) => setTimeout(reslove, s))
await delay(10000)
console.log('尝试跳转到查票页面...');
await page.goto('https://kyfw.12306.cn/otn/leftTicket/init', { waitUntil: 'networkidle2', timeout: 60000 });
await page.waitForSelector('#fromStationText', { timeout: 10000 }); // 等待出发地输入框加载
// --- 填写查询信息 ---
console.log(`正在填写查询信息:${fromStation} -> ${toStation}, 日期: ${trainDate}`);
// 1. 填写出发地
await page.click('#fromStationText');
// await page.waitForSelector('#form_cities'); // 等待下拉列表出现
await delay(500)
await page.type('#fromStationText', fromStation); // 模拟输入
await delay(1000)
await page.keyboard.press('ArrowDown');
await delay(500)
await page.keyboard.press('Enter'); // 模拟回车选择第一个匹配项
await delay(500)
// 验证出发地是否正确填写
const fromStationValue = await page.$eval('#fromStationText', el => el.value);
console.log(`出发地已选择: ${fromStationValue}`);
console.log(toStation)
// 2. 填写目的地
await page.click('#toStationText');
// await page.waitForSelector('#form_cities');
await delay(500)
await page.type('#toStationText', toStation);
await delay(1000)
await page.keyboard.press('ArrowDown');
await delay(500)
await page.keyboard.press('Enter');
// 3. 填写出发日期 (12306 的日期选择器比较特殊,直接修改 value 或模拟点击)
await page.evaluate((date) => {
const dateInput = document.getElementById('train_date');
if (dateInput) {
dateInput.value = date;
// 触发change事件,让12306的JS感知到日期变化
const event = new Event('change', { bubbles: true });
dateInput.dispatchEvent(event);
}
}, trainDate);
console.log('日期已填写。');
// 4. 点击查询按钮
console.log('点击查询...');
await page.click('#query_ticket');
// --- 等待查询结果并尝试预订 ---
console.log('等待查询结果...');
// 等待票务列表的表格加载完成
await page.waitForSelector('#queryLeftTable tr', { timeout: 30000 }); // 等待表格行出现
let foundAndClicked = false;
let attemptCount = 0;
const maxAttempts = 10; // 最多尝试查询10次 (模拟刷新)
const refreshInterval = 3000; // 每3秒刷新一次
while (!foundAndClicked && attemptCount < maxAttempts) {
attemptCount++;
console.log(`尝试刷新并查找预订按钮 (第 ${attemptCount} 次)...`);
// 重新点击查询,模拟刷新
if (attemptCount > 1) {
await page.click('#query_ticket');
await delay(refreshInterval); // 等待页面加载
await page.waitForSelector('#queryLeftTable tr');
}
await delay(500)
// 在表格中查找目标车次和席别对应的"预订"按钮
const bookButton = await page.evaluate((targetTrainNo, targetSeatType) => {
const rows = Array.from(document.querySelectorAll('#queryLeftTable tr'));
for (const row of rows) {
const trainNoElement = row.querySelector('.train a'); // 车次
if (!trainNoElement) {
continue;
}
const currentTrainNo = trainNoElement.textContent.trim();
// 如果指定了车次,且当前车次不匹配则跳过
if (targetTrainNo && currentTrainNo !== targetTrainNo) {
continue;
}
// 查找预订按钮
const bookBtn = row.querySelector('.btn72'); // 预订按钮通常有这个类
if (bookBtn && bookBtn.textContent.includes('预订')) {
// 进一步检查席别(如果需要)
if (targetSeatType) {
// 遍历这一行的所有席别列
const seatCells = row.querySelectorAll('td');
let seatTypeFound = false;
let arr = []
for (let i = 0; i < seatCells.length; i++) {
const header = document.querySelector(`#float th:nth-child(${i + 1})`).textContent.trim();
arr.push(header)
if (header.includes(targetSeatType)) {
seatTypeFound = true;
break;
}
}
if (!seatTypeFound) {
continue; // 席别不匹配则跳过
}
}
// 返回预订按钮的 outerHTML,以便在 Node.js 中重新定位点击
return bookBtn.outerHTML;
}
}
return null;
}, trainNo, seatType);
if (bookButton) {
console.log('找到预订按钮!');
// 重新在 Node.js 环境中定位并点击该按钮
// 因为 page.evaluate 返回的是 HTML 字符串,我们需要用选择器重新定位
const buttonSelector = `#queryLeftTable tr .btn72[outerHTML="${bookButton.replace(/"/g, '\\"')}"]`; // 这是一个简陋的定位方式
// 更好的方式是 page.evaluate 返回一个唯一ID,然后用page.$('selector').click()
// 或者直接在 page.evaluate 中点击,然后返回成功状态
try {
await page.evaluate((btnHtml) => {
const tempDiv = document.createElement('div');
tempDiv.innerHTML = btnHtml;
const btn = tempDiv.firstChild;
btn.click();
}, bookButton);
console.log('已点击预订按钮。');
foundAndClicked = true;
} catch (clickError) {
console.warn('点击预订按钮失败,可能页面已变化:', clickError.message);
}
} else {
console.log('未找到符合条件的预订按钮,继续尝试...');
}
}
if (!foundAndClicked) {
console.log('在多次尝试后,未能找到并点击预订按钮。');
} else {
console.log('已进入订单提交页面,请手动完成后续操作(选择乘车人、提交订单、支付等)。');
// 实际中这里需要更复杂的逻辑来选择乘车人、提交订单等
// await page.waitForSelector('#qrCodeDiv', { timeout: 60000 }); // 等待二维码支付页面
// ... 处理支付 ...
}
} catch (error) {
console.error('自动化抢票过程中出现错误:', error);
} finally {
console.log('自动化流程结束,浏览器将在20秒后关闭,以便你手动操作或查看结果。');
await(() => new Promise((reslove, reject) => { // 空留15S登录
setTimeout(() => {
reslove()
}, 20000)
}))()
if (browser) {
await browser.close();
}
}
}
// 示例调用
// 请确保 CHROME_PATH 指向你的 Chrome 可执行文件
grab12306Ticket('guangzhou', 'shanghai', '2025-06-20', 'G818', '二等座'); // 示例:查询2025-6-20北京到上海 G1次二等座
// grab12306Ticket('广州', '长沙', '2025-06-20', '', '硬卧'); // 示例:查询2025-6-20广州到长沙,任意车次硬卧
由于12306是会过段时间换取部分元素类名与元素结构,如果抛异常需要自己去官网=》开发者模式,去查找对应元素并修改获取元素逻辑。