/** * Puppeteer无头浏览器工具类 * * 功能描述: * 这是一个基于Puppeteer的无头浏览器操作工具类,提供了网页自动化操作的封装。 * 支持页面访问、内容获取、元素操作、Cookie管理、PDF生成等功能。 * * 主要功能: * 1. 页面访问 - 支持普通页面访问和API请求 * 2. 内容获取 - 获取页面HTML内容、标题、URL等 * 3. 元素操作 - 点击、输入、获取文本和属性 * 4. Cookie管理 - 获取页面Cookie信息 * 5. 等待机制 - 等待元素出现 * 6. PDF生成 - 将页面保存为PDF * 7. 自定义脚本 - 在页面上下文中执行JavaScript * 8. 代理支持 - 支持HTTP代理配置 * * 使用场景: * - 网页数据抓取 * - 自动化测试 * - 页面截图和PDF生成 * - 模拟用户操作 * - 绕过反爬虫机制 * * 环境变量: * - CHROME_PATH: Chrome浏览器可执行文件路径 * * @author drpy-node * @version 1.0.0 */ import * as puppeteer from 'puppeteer' /** * Puppeteer助手类 * 封装了Puppeteer的常用操作,提供简化的API接口 */ class PuppeteerHelper { /** * 构造函数 * 初始化浏览器和页面实例为null */ constructor() { this.browser = null; // 浏览器实例 this.page = null; // 页面实例 } /** * 访问网页或发送HTTP请求 * 支持普通页面访问和API请求两种模式 * * @param {Object} config - 配置对象 * @param {string} config.url - 目标URL * @param {boolean} [config.headless=true] - 是否无头模式 * @param {boolean} [config.proxy=false] - 是否使用代理 * @param {Object} [config.headers] - 请求头 * @param {boolean} [config.isJSON=false] - 是否为JSON请求 * @param {boolean} [config.isReq=false] - 是否为API请求 * @param {string} [config.method='GET'] - HTTP方法 * @param {Object} [config.data] - 请求数据 * @param {Object} [config.options] - 页面访问选项 * @returns {Promise} 页面内容或响应文本 * * @example * // 访问普通页面 * const html = await helper.gotoHtml({url: 'https://example.com'}); * * // 发送API请求 * const data = await helper.gotoHtml({ * url: 'https://api.example.com/data', * isJSON: true, * method: 'POST', * data: {key: 'value'} * }); */ async gotoHtml(config) { let proxy = '' // 如果配置了代理,使用本地代理服务器 if(config.proxy){ proxy = 'http://127.0.0.1:7897'; } // 启动浏览器实例 this.browser = await puppeteer.launch({ headless: config.headless || true, // 无头模式,默认true // executablePath: process.env.CHROME_PATH, // Chrome路径(可选) args: [`--proxy-server=${proxy}`] // 代理服务器配置 }); // 创建新页面 this.page = await this.browser.newPage(); // 设置请求头 await this.page.setExtraHTTPHeaders(config.headers || { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36', }) let content = '' // 判断是否为API请求模式 if (config.isJSON || config.isReq) { // 在页面上下文中执行fetch请求 return await this.page.evaluate(async (url, mth, headers, data) => { // 构建请求选项 const requestOptions = { method: mth || 'GET', headers: headers || { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36', 'Content-Type': 'application/json' }, }; // 如果不是GET请求且有数据,添加请求体 if (mth !== 'GET' && (data !== '' && data !== undefined)) { requestOptions.body = JSON.stringify(data); } // 发送请求 const response = await fetch(url, requestOptions); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return await response.text(); }, config.url, config.method, config.headers, config.data); } else { // 普通页面访问模式 await this.page.goto(config.url, config.options); content = await this.page.content() return content } } /** * 获取页面Cookie信息 * 访问指定URL并获取该页面的所有Cookie * * @param {Object} config - 配置对象 * @param {string} config.url - 目标URL * @param {boolean} [config.headless=true] - 是否无头模式 * @param {Object} [config.options] - 页面访问选项 * @returns {Promise} Cookie数组 * * @example * const cookies = await helper.gotoCookie({url: 'https://example.com'}); */ async gotoCookie(config) { // 启动浏览器(使用环境变量中的Chrome路径) this.browser = await puppeteer.launch({ headless: config.headless || true, executablePath: process.env.CHROME_PATH }); this.page = await this.browser.newPage(); await this.page.goto(config.url, config.options || {}); // 获取当前页面的cookies const cookies = await this.page.cookies(); // 打印cookies(调试用,已注释) // log('Cookies:', cookies); return cookies || [] } /** * 在指定元素中输入文本 * * @param {string} selector - CSS选择器 * @param {string} text - 要输入的文本 * @param {Object} [options={}] - 输入选项(如延迟等) * @returns {Promise} * * @example * await helper.type('#username', 'myuser', {delay: 100}); */ async type(selector, text, options = {}) { return await this.page.type(selector, text, ...options);//{ delay: 100 } } /** * 点击指定元素 * * @param {string} selector - CSS选择器 * @returns {Promise} * * @example * await helper.gotoclick('#submit-button'); */ async gotoclick(selector) { return await this.page.click(selector); } /** * 生成PDF文件 * 将当前页面保存为PDF格式 * * @param {string} path - PDF文件保存路径 * @param {Object} [options={}] - PDF生成选项 * @returns {Promise} * * @example * await helper.pdf('./output.pdf', {format: 'A4'}); */ async pdf(path, options = {}) { return await this.page.pdf({path, ...options}); } /** * 获取页面标题 * * @returns {Promise} 页面标题 * * @example * const title = await helper.getTitle(); */ async getTitle() { return await this.page.title(); } /** * 获取当前页面URL * * @returns {Promise} 当前页面URL * * @example * const url = await helper.getUrl(); */ async getUrl() { return await this.page.url(); } /** * 在页面上下文中执行自定义JavaScript函数 * 将函数序列化后在浏览器环境中执行 * * @param {Function} fn - 要执行的函数 * @param {...any} args - 函数参数 * @returns {Promise} 函数执行结果 * * @example * const result = await helper.evaluate(() => { * return document.title; * }); */ async evaluate(fn, ...args) { const funcString = fn.toString();// 将函数转换为字符串 // 构造一个在页面上下文中调用的函数,该函数调用传入的自定义函数 const pageFunction = new Function(`return (${funcString})`).apply(null, args.map(arg => JSON.stringify(arg))); return await this.page.evaluate(pageFunction); } /** * 获取页面元素的文本内容 * * @param {string} selector - CSS选择器 * @returns {Promise} 元素文本内容,如果元素不存在返回null * * @example * const text = await helper.gotoText('.title'); */ async gotoText(selector) { return await this.page.evaluate((selector) => { const element = document.querySelector(selector); return element ? element.textContent : null; }, selector); } /** * 获取页面元素的属性值 * * @param {string} selector - CSS选择器 * @param {string} attribute - 属性名 * @returns {Promise} 属性值,如果元素不存在或属性不存在返回null * * @example * const href = await helper.gotoAttribute('a.link', 'href'); */ async gotoAttribute(selector, attribute) { return await this.page.evaluate((selector, attribute) => { const element = document.querySelector(selector); return element ? element.getAttribute(attribute) : null; }, selector, attribute); } /** * 等待指定选择器的元素出现 * 提供超时机制和回调处理 * * @param {string} selector - CSS选择器 * @param {Object} [options={}] - 等待选项 * @param {number} [options.timeout=30000] - 超时时间(毫秒),默认30秒 * @param {Function} [options.onFound] - 元素找到后的回调函数 * @returns {Promise} 元素句柄 * @throws {Error} 如果超时或元素未找到 * * @example * const element = await helper.waitForSelector('.dynamic-content', { * timeout: 10000, * onFound: (el) => console.log('Element found!') * }); */ async waitForSelector(selector, options = {}) { const {timeout = 30000, onFound} = options; // 默认超时为30秒 try { // 使用 Page 的 waitForSelector 方法,并设置超时 const elementHandle = await this.page.waitForSelector(selector, {timeout}); // 如果提供了 onFound 回调,则调用它并传入元素句柄 if (onFound) { await onFound(elementHandle); } // 返回元素句柄(如果需要的话,调用者可以进一步处理它) return elementHandle; } catch (error) { // 如果超时或选择器未找到,则抛出错误 throw new Error(`Element with selector "${selector}" not found within ${timeout / 1000} seconds.`); } } /** * 关闭浏览器和页面 * 清理资源,释放内存 * * @returns {Promise} * * @example * await helper.close(); */ async close() { // 关闭页面 if (this.page) { await this.page.close(); } // 关闭浏览器 if (this.browser) { await this.browser.close(); } } } // 导出PuppeteerHelper类 export const puppeteerHelper = PuppeteerHelper