是时候该用自动化工具玩玩12306了

在我们前端开发中,我们不仅要构建用户界面,还要应对各种自动化需求:端到端测试、网页内容抓取、自动化报表生成、性能监控甚至是一些定制化的工作流程。手动操作耗时耗力,效率低下。这时,浏览器自动化工具就成了我们的左膀右臂。

提到浏览器自动化,很多同学会想到 Python中的Selenium,实际上JS中也有属于前端的自动化工具Puppeteer,它是由 Google 团队维护的 Node.js 库,提供了一组高级 API 来通过 DevTools 协议控制 ChromeChromium 浏览器。但今天我们要聚焦的是它的一个"瘦身版"------ puppeteer-core

作为一名在前端领域摸爬滚打多年的老兵,我深知在生产环境中,资源的节约和灵活的部署至关重要。puppeteer-core 就是为此而生。它移除了 Puppeteer 包中自带的 Chromium 浏览器,让你能够自带一个 Chrome/Chromium 可执行文件来使用。这在 Docker 容器、云函数、或者需要使用特定版本浏览器等场景下,显得尤为灵活和高效。它就像一个精干的遥控器,负责控制外部的浏览器进程。

puppeteer-core 是什么?

puppeteer-corePuppeteer 的一个轻量级版本,它提供了完全相同的 API ,但不包含 Chromium 浏览器可执行文件。这意味着你需要在安装 puppeteer-core 后,手动指定或确保系统路径中存在一个可用的 Chrome 或 Chromium 浏览器。

相当于: puppeteer-core = Puppeteer API + 你的 Chrome/Chromium。

为什么选择 puppeteer-core 而不是 puppeteer

  1. 包体积小: puppeteer 包含了完整的 Chromium,通常体积巨大(大几百MB)。puppeteer-core 则非常小巧,只有几MB。这对于部署到云环境(如 AWS Lambda, Serverless Function)或 Docker 镜像时,能显著减少镜像大小和部署时间。
  2. 版本控制灵活: 你可以自由选择并使用任何你需要的 Chrome/Chromium 版本,而不是被 Puppeteer 绑定的版本限制。这对于兼容性测试或某些特定功能需求非常有帮助。
  3. 共享浏览器实例: 在一些场景下,你可能希望多个自动化脚本共享同一个浏览器实例,或者连接到一个已经运行的浏览器。puppeteer-core 使得连接到外部浏览器变得更加自然和直接。

实际用途

  1. 高度模拟用户行为: 相比于传统的 HTTP 请求抓取,puppeteer-core 运行在真实浏览器环境中,能够执行 JavaScript、处理 CSS 动画、模拟用户点击和输入,这使得它能够处理几乎所有复杂的现代网页(SPA 应用、各种弹窗、登录验证等)。
  2. 调试友好: 设置 headless: false,你就能看到浏览器窗口,配合 devtools: true,可以直接在 DevTools 中调试页面,这极大地简化了调试过程。
  3. 强大的生态和社区: 作为 Puppeteer 的核心,它共享着庞大的社区支持和丰富的插件,遇到问题很容易找到解决方案。
  4. 云原生友好: 小巧的包体积和灵活的浏览器路径配置,使其成为云函数(如 AWS Lambda, Google Cloud Functions)或 Docker 容器中进行 Web 自动化任务的理想选择

下面就利用实战真真切切的去感受他的用处吧~

示例:网页截图与动态内容抓取

示例1:给任意网页截图

这是一个最基础的用法,演示了如何启动浏览器、打开页面并截图。

准备工作:请找到你的Chrome的安装地址:

  1. Windows: C:\Program Files (x86)\Google\Chrome\Application\chrome.exe
  2. macOS: /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
  3. 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 模拟用户进行查询车票并尝试点击预订的过程。登录和验证码部分将留空,因为它们是自动化抢票中最难且最敏感的环节。

逻辑梳理:

  1. 启动浏览器,打开12306官网。
  2. (跳过登录,假设已登录或通过其他方式处理登录)
  3. 填写出发地、目的地、出发日期。
  4. 点击查询按钮。
  5. 等待查询结果加载。
  6. 找到特定车次或席别的"预订"按钮并点击。
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是会过段时间换取部分元素类名与元素结构,如果抛异常需要自己去官网=》开发者模式,去查找对应元素并修改获取元素逻辑。

相关推荐
牛奶10 分钟前
开发者的"奇技淫巧":那些让你效率翻倍的实战技巧
前端·后端·程序员
咸鱼翻身更入味10 分钟前
Vue创建一个简单的Agent聊天——工具调用
前端
Timo来了10 分钟前
indexDB的用法示例
前端
泉城老铁11 分钟前
springboot实现word转换pdf
vue.js·后端
walking95714 分钟前
重新学习前端之设计模式与架构
前端·javascript·面试
walking95717 分钟前
重新学习前端之TypeScript
前端·javascript·面试
walking95717 分钟前
重新学习前端之Linux
前端·vue.js·面试
walking95718 分钟前
重新学习前端之CSS
前端·vue.js·面试
walking95718 分钟前
重新学习前端之Git
前端·vue.js·面试
walking95718 分钟前
重新学习前端之小程序
前端