是时候该用自动化工具玩玩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 分钟前
🤔Token 存储方案有哪些
前端·javascript·后端
烛阴10 分钟前
从零开始:使用Node.js和Cheerio进行轻量级网页数据提取
前端·javascript·后端
liuyang___27 分钟前
日期的数据格式转换
前端·后端·学习·node.js·node
西哥写代码29 分钟前
基于cornerstone3D的dicom影像浏览器 第三十一章 从PACS服务加载图像
javascript·pacs·dicom
贩卖纯净水.1 小时前
webpack其余配置
前端·webpack·node.js
码上奶茶2 小时前
HTML 列表、表格、表单
前端·html·表格·标签·列表·文本·表单
抹茶san2 小时前
和 Trae 一起开发可视化拖拽编辑项目(1) :迈出第一步
前端·trae
风吹头皮凉2 小时前
vue实现气泡词云图
前端·javascript·vue.js
南玖i2 小时前
vue3 + ant 实现 tree默认展开,筛选对应数据打开,简单~直接cv
开发语言·前端·javascript