express + playwright 自动截取浏览器快照

124 阅读3分钟

可根据个人需求来自己改动编写逻辑

CSDN:与掉发不共戴天

示例用到的第三方库

node >= 18
express、playwright、fs、moment、crypto

安装浏览器快照工具
npx playwright codegen

目录结构图

目录结构

GET请求生成快照(仅支持单个地址截图)

需要传入快照截图的地址,例如:/snapshots?url=xxx

router.get('/snapshots', async (req, res) => {
    try {
        const browserType = 'chromium'; // 指定浏览器类型   'chromium'、'firefox'、'webkit'
        const browser = await playwright[browserType].launch();// 启动指定浏览器实例
        const context = await browser.newContext();// 创建窗口
        const page = await context.newPage();// 创建页面
        await page.goto(req.query.url);// 跳转url路径
        let screenshotBuffer = await page.screenshot({ fullPage: true });// 捕获截图
        const parentDir = path.dirname(__dirname);
        const screenshotDir = path.join(parentDir, 'screenshot');
        if (!fs.existsSync(screenshotDir)) {
            fs.mkdirSync(screenshotDir);
        }
        const timestamp = Date.now();
        const screenshotPath = path.join(screenshotDir, `screenshot-${timestamp}.png`);
        fs.writeFileSync(screenshotPath, screenshotBuffer);
        res.send(`Screenshot saved as ${path.basename(screenshotPath)}`);
        await browser.close();
    } catch (error) {
        console.error(error);
        res.status(500).send('Error taking screenshot');
    }
});

POST 请求生成快照(支持多个地址截图)

需要创建一个data.txt文本文件,编写需要快照的地址(以行切分)

router.get('/batch_snapshots', async (req, res) => {
    const parentDir = path.dirname(__dirname);
    const screenshotDir = path.join(parentDir, 'screenshot');
    if (!fs.existsSync(screenshotDir)) {
        fs.mkdirSync(screenshotDir);
    }
    let pngInfo = [] // 存取现有screenshot目录下照片的地址,区分新增和历史图片
    await fs.promises.readdir(parentDir + '/screenshot')
        .then(files => {
            const pngFiles = files.filter(file => file.endsWith('.png'));
            pngInfo = pngFiles
        })
        .catch(err => {
            console.error('Error reading directory:', err);
        });

    function generateUniqueHashForUrls(urlArray) {
        const timestamp = Date.now().toString(); // 获取当前时间戳
        const randomString = Math.random().toString(36).substring(2, 15); // 生成随机字符串
        const hash = crypto.createHash('sha256');

        // 更新哈希值,包括时间戳和随机字符串,以及所有URL
        hash.update(timestamp + randomString, 'utf8');
        urlArray.forEach(url => {
            hash.update(url, 'utf8');
        });

        return hash.digest('hex');
    }

    try {
        const batchTime = moment().format('YYYY-MM-DD');
        const startTime = moment().format('YYYY-MM-DD HH:mm:ss');
        fs.readFile('data.txt', 'utf8', async (err, data) => {
            let urlInfo = []
            if (err) {
                console.error('An error occurred:', err);
                return;
            }
            urlInfo = data.toString().split("\n")
            urlInfo = urlInfo.filter(url => url.trim() !== '');
            if (!urlInfo) {
                return res.json({});
            }
            const batch = generateUniqueHashForUrls(urlInfo);
            const browserType = 'chromium';
            const browser = await playwright[browserType].launch();
            const context = await browser.newContext();
            const results = [];
            const errors = []
            for (const url of urlInfo) {
                let status = false
                const page = await context.newPage();
                // networkidle
                // domcontentloaded
                // load
                await page.goto(url, { waitUntil: 'networkidle', timeout: 60000 }).catch(err => {
                    errors.push(url)
                    status = true
                });
                if (status){
                    continue
                }
                const screenshotBuffer = await page.screenshot({ fullPage: true });
                let urlName = url.split("://")[1].replace(/\./g, "_").replace(/\/+/g, '_')
                let urlNameIndex = 0
                pngInfo.map((item) => {
                    if (item.includes(urlName)) {
                        urlNameIndex = parseInt(item.match(/\d+/g).pop()) + 1;
                    }
                })
                const screenshotPath = path.join(screenshotDir, `${urlName}-${batch}-${batchTime}-${urlNameIndex}.png`);
                pngInfo.push(`${urlName}-${batch}-${batchTime}-${urlNameIndex}.png`)
                // 将截图写入文件
                fs.writeFileSync(screenshotPath, screenshotBuffer);
                results.push(`${url}` + " ←saved as→ " + `${path.basename(screenshotPath)}`);

                await page.close();
            }

            await browser.close();

            const endTime = moment().format('YYYY-MM-DD HH:mm:ss');
            // 告诉客户端所有截图已保存
            const timeDiff = moment(endTime).diff(moment(startTime), "seconds")

            // 输出时间差

            let resBody = {
                time: [startTime, endTime],
                timeDiff,
                batch,
                errorNum:errors.length,
                successfulNum:results.length,
                errors,
                results
            }
            res.json(resBody);
        });

    } catch (error) {
        console.error(error);
        res.status(500).send('Error taking screenshots');
    }

});

全部代码奉上

var express = require('express');
var router = express.Router();
const playwright = require('playwright');
const fs = require('fs');
const path = require('path');
const moment = require("moment")
const crypto = require('crypto');
/**
 *  @name GET 支持单个快照生成
 *  @description 可通过?url=地址 来创建截图
 *  @example  /download/screenshot?url=https://www.baidu.com/index.html
 */
router.get('/snapshots', async (req, res) => {
    try {
        const browserType = 'chromium'; // 指定浏览器类型   'chromium'、'firefox'、'webkit'
        const browser = await playwright[browserType].launch();// 启动指定浏览器实例
        const context = await browser.newContext();// 创建窗口
        const page = await context.newPage();// 创建页面
        await page.goto(req.query.url);// 跳转url路径
        let screenshotBuffer = await page.screenshot({ fullPage: true });// 捕获截图
        const parentDir = path.dirname(__dirname);
        const screenshotDir = path.join(parentDir, 'screenshot');
        if (!fs.existsSync(screenshotDir)) {
            fs.mkdirSync(screenshotDir);
        }
        const timestamp = Date.now();
        const screenshotPath = path.join(screenshotDir, `screenshot-${timestamp}.png`);
        fs.writeFileSync(screenshotPath, screenshotBuffer);
        res.send(`Screenshot saved as ${path.basename(screenshotPath)}`);
        await browser.close();
    } catch (error) {
        console.error(error);
        res.status(500).send('Error taking screenshot');
    }
});
/**
 * @name get 批量查询地址生成快照
 * @description 该代码取值的地址是data.txt文档里保存的地址,一行表示一个地址,支持空行跳过
 */
router.get('/batch_snapshots', async (req, res) => {
    const parentDir = path.dirname(__dirname);
    const screenshotDir = path.join(parentDir, 'screenshot');
    if (!fs.existsSync(screenshotDir)) {
        fs.mkdirSync(screenshotDir);
    }
    let pngInfo = [] // 存取现有screenshot目录下照片的地址,区分新增和历史图片
    await fs.promises.readdir(parentDir + '/screenshot')
        .then(files => {
            const pngFiles = files.filter(file => file.endsWith('.png'));
            pngInfo = pngFiles
        })
        .catch(err => {
            console.error('Error reading directory:', err);
        });

    function generateUniqueHashForUrls(urlArray) {
        const timestamp = Date.now().toString(); // 获取当前时间戳
        const randomString = Math.random().toString(36).substring(2, 15); // 生成随机字符串
        const hash = crypto.createHash('sha256');

        // 更新哈希值,包括时间戳和随机字符串,以及所有URL
        hash.update(timestamp + randomString, 'utf8');
        urlArray.forEach(url => {
            hash.update(url, 'utf8');
        });

        return hash.digest('hex');
    }

    try {
        const batchTime = moment().format('YYYY-MM-DD');
        const startTime = moment().format('YYYY-MM-DD HH:mm:ss');
        fs.readFile('data.txt', 'utf8', async (err, data) => {
            let urlInfo = []
            if (err) {
                console.error('An error occurred:', err);
                return;
            }
            urlInfo = data.toString().split("\n")
            urlInfo = urlInfo.filter(url => url.trim() !== '');
            if (!urlInfo) {
                return res.json({});
            }
            const batch = generateUniqueHashForUrls(urlInfo);
            const browserType = 'chromium';
            const browser = await playwright[browserType].launch();
            const context = await browser.newContext();
            const results = [];
            const errors = []
            for (const url of urlInfo) {
                let status = false
                const page = await context.newPage();
                // networkidle
                // domcontentloaded
                // load
                await page.goto(url, { waitUntil: 'networkidle', timeout: 60000 }).catch(err => {
                    errors.push(url)
                    status = true
                });
                if (status){
                    continue
                }
                const screenshotBuffer = await page.screenshot({ fullPage: true });
                let urlName = url.split("://")[1].replace(/\./g, "_").replace(/\/+/g, '_')
                let urlNameIndex = 0
                pngInfo.map((item) => {
                    if (item.includes(urlName)) {
                        urlNameIndex = parseInt(item.match(/\d+/g).pop()) + 1;
                    }
                })
                const screenshotPath = path.join(screenshotDir, `${urlName}-${batch}-${batchTime}-${urlNameIndex}.png`);
                pngInfo.push(`${urlName}-${batch}-${batchTime}-${urlNameIndex}.png`)
                // 将截图写入文件
                fs.writeFileSync(screenshotPath, screenshotBuffer);
                results.push(`${url}` + " ←saved as→ " + `${path.basename(screenshotPath)}`);

                await page.close();
            }

            await browser.close();

            const endTime = moment().format('YYYY-MM-DD HH:mm:ss');
            // 告诉客户端所有截图已保存
            const timeDiff = moment(endTime).diff(moment(startTime), "seconds")

            // 输出时间差

            let resBody = {
                time: [startTime, endTime],
                timeDiff,
                batch,
                errorNum:errors.length,
                successfulNum:results.length,
                errors,
                results
            }
            res.json(resBody);
        });

    } catch (error) {
        console.error(error);
        res.status(500).send('Error taking screenshots');
    }

});
module.exports = router;