分享puppeteer封装定时清理page和browser及禁止在新标签打开页面

2023-10-13 后端开发 0

最近用Puppeteer来做网页截图和自动化任务,开发中遇到Puppeteer运行一段时间后内存占用很高,重启后会降下来,及经常在新标签中打页面不方便自动化任务。特封装了puppeteer定时清理page和browser及禁止在新标签打开页面等功能。在代码层实现Browser实例的创建和定时销毁,具体逻辑参考代码:

import puppeteer from 'puppeteer';//Puppeteer库
import puppeteerExtra from 'puppeteer-extra';//Puppeteer扩展库
import UserAgents from 'user-agents';
import puppeteerStealth from 'puppeteer-extra-plugin-stealth';//Puppeteer扩展库伪装插件

puppeteerExtra.use(puppeteerStealth());// 使用Puppeteer扩展库伪装插件

const puppeteerBrowser = {
  options: {
    headless: true, // 无头模式
    pageConsole: false, // 页面控制台输出
    pageOpenSelf: true, // 是否当前页面打开
    userAgentFilter: { deviceCategory: 'desktop' },
    browserExpire: 600, // browser最大空闲存活时间(秒)
    pageExpire: 60, // page最大空闲存活时间(秒)
  },
  data: {
    userAgent: '',
  },
  browser: '',
  checkTaskInterval: '',
  getUserAgent() {
    if (!this.data.userAgent) {
      const userAgent = new UserAgents(this.options.userAgentFilter).toString();
      this.data.userAgent = userAgent;
      return userAgent;
    }
    return this.data.userAgent;
  },
  reset() {
    for (const key in this.data) {
      this.data[key] = '';
    }
  },
  //创建Browser及初始化
  getBrowser: async function () {
    if (!this.browser || !this.browser.isConnected()) {
      const browser = await puppeteerExtra.launch({
        headless: this.options.headless,
        executablePath: puppeteer.executablePath(),
        ignoreHTTPSErrors: true,
        ignoreDefaultArgs: ['--enable-automation'],
        args: [
          '--no-sandbox', // 不使用沙箱
          '--disable-infobars', // 禁止信息提示栏
          '--disable-gpu', // 不用gpu
          '--disable-setuid-sandbox', // 与--no-sandbox配合
          '--no-default-browser-check', // 不检查默认浏览器
          '--disable-extensions', // 禁止扩展
          '--disable-default-apps', // 禁止默认应用
          '--disable-dev-shm-usage', // 禁止使用/dev/shm,防止内存不够用
          '--disable-hang-monitor', // 禁止页面无响应提示
          '--disable-popup-blocking', // 禁止popup
          '--disable-prompt-on-repost', // 禁止重新发送post请求的提示
          '--disable-sync', // 禁止同步
          '--disable-translate', // 禁止翻译
          '--disable-bundled-ppapi-flash', // 禁止内置的flash
          '--disable-component-update', // 禁止组件升级
          '--disable-client-side-phishing-detection', // 禁止危险页面检测
          '--disable-logging',
          '--mute-audio', // 静音
          '--single-process', // 单进程
          '--no-zygote', // 禁止zygote进程fork子进程
          '--safebrowsing-disable-auto-update',
          '--no-first-run', // 禁止首次运行界面
          '--use-mock-keychain', // 使用mock-keychain防止提示权限提示
          '--ignore-certificate-errors', // 忽略证书错误
        ],
      });
      await this.waitForTime(100);
      browser.runTime = Math.round(new Date().getTime() / 1000);
      console.log('browser:', 'launch', await browser.version());
      browser.on('disconnected', () => {
        console.log('browser:', 'disconnected');
      });
      browser.on('targetcreated', async (target) => {
        try {
          console.log('browser:', 'targetcreated', target.url());
          target.browser().runTime = Math.round(new Date().getTime() / 1000);
          if (target.type() == 'page') {
            const page = await target.page();
            page.runTime = Math.round(new Date().getTime() / 1000);
            await page.setUserAgent(this.getUserAgent());
            if (this.options.pageConsole) {
              page.on('console', (msg) => {
                const args = ['pageConsole'];
                for (let i = 0; i < msg.args().length; i++) {
                  args.push(msg.args()[i].toString().replace('JSHandle:', ''));
                }
                console.log.apply(null, args);
              });
            }
            //实现页面内链接不在新标签打开
            if (this.options.pageOpenSelf) {
              await page.evaluate(() => {
                if (!window.pageOpen) {
                  window.pageOpen = window.open;
                  window.open = function (url) {
                    window.pageOpen(url, '_self');
                  };
                }
                if (!window.pageTarget) {
                  window.pageTarget = setInterval(function () {
                    const elms = document.querySelectorAll('a,form');
                    // console.log('querySelectorAll', 'a,form', elms.length);
                    for (const key in elms) {
                      elms[key].setAttribute && elms[key].setAttribute('target', '_self');
                    }
                  }, 1000);
                }
              });
            }
          }
        } catch (error) {
          console.log('error:', error.message);
        }
      });
      browser.on('targetchanged', async (target) => {
        try {
          console.log('browser:', 'targetchanged', target.url());
          target.browser().runTime = Math.round(new Date().getTime() / 1000);
          target.browser().runTime = Math.round(new Date().getTime() / 1000);
          if (target.type() == 'page') {
            const page = await target.page();
            page.runTime = Math.round(new Date().getTime() / 1000);
            await page.setUserAgent(this.getUserAgent());
            if (this.options.pageConsole) {
              page.on('console', (msg) => {
                const args = ['pageConsole'];
                for (let i = 0; i < msg.args().length; i++) {
                  args.push(msg.args()[i].toString().replace('JSHandle:', ''));
                }
                console.log.apply(null, args);
              });
            }
            if (this.options.pageOpenSelf) {
              await page.evaluate(() => {
                if (!window.pageOpen) {
                  window.pageOpen = window.open;
                  window.open = function (url) {
                    window.pageOpen(url, '_self');
                  };
                }
                if (!window.pageTarget) {
                  window.pageTarget = setInterval(function () {
                    const elms = document.querySelectorAll('a,form');
                    // console.log('querySelectorAll', 'a,form', elms.length);
                    for (const key in elms) {
                      elms[key].setAttribute && elms[key].setAttribute('target', '_self');
                    }
                  }, 1000);
                }
              });
            }
          }
        } catch (error) {
          console.log('error:', error.message);
        }
      });

      browser.on('targetdestroyed', (target) => {
        console.log('browser:', 'targetdestroyed', target.url());
      });

      this.browser = browser;
      this.runCheckTask();
      return browser;
    }
    return this.browser;
  },
  //获取新标签页
  getPage: async function () {
    const browser = await this.getBrowser();
    const page = await browser.newPage();
    page.runTime = Math.round(new Date().getTime() / 1000);
    page.waitForTime = this.waitForTime;
    page.close;
    return page;
  },
  waitForTime: function (ms) {
    return new Promise((r) => setTimeout(r, ms));
  },
  //检测page和browser关闭
  checkBrowserExpire: async function () {
    if (this.browser && this.browser.isConnected()) {
      const nowTime = Math.round(new Date().getTime() / 1000);
      if (this.browser.runTime && this.browser.runTime + this.options.browserExpire < nowTime) {
        console.log('checkBrowserExpire:', 'browser.close');
        await this.browser.close();
        this.browser = '';
      } else {
        const pages = await this.browser.pages();
        for (const key in pages) {
          const page = pages[key];
          if (page.runTime && page.runTime + this.options.pageExpire < nowTime) {
            if (!page.isClosed()) {
              console.log('checkBrowserExpire:', 'page.close', page.url());
              await page.close();
            }
          }
        }
      }
    }
  },
  //定时清理page和browser
  runCheckTask: function () {
    if (!this.checkTaskInterval) {
      this.checkTaskInterval = setInterval(() => {
        puppeteerBrowser.checkBrowserExpire();
      }, this.options.pageExpire * 500);
    }
  },
};

export default puppeteerBrowser;