Puppeteer 入门

引言

团队最近经常需要分析一些网站数据,需要从多个数据网站去手动复制数据到 Excel 里面,这种重复劳动且没有意义的体力活应该交给机器去干,释放出人的劳动力去干更有意思的事,所以有了学习采集方法的这篇文章。开源的采集库有 python 的 scraper,java 的 selenium,ruby 的 watir,nodejs 的 puppeteer,golang 的 chromedp。基于快速上手入门就选择了 puppeteer,备选是 chromedp,因为日常是使用 golang 开发项目。


目录

  1. 环境搭建
  2. 网页截屏 demo
  3. terminal 运行 script 采集目标数据
  4. web 服务化运行 script 采集目标数据
  5. 总结
  6. 了解更多


1、环境搭建

# mac terminal 运行
# 安装homebrew,配置国内镜像的参考 https://mirrors.ustc.edu.cn/help/brew.git.html
$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

# 等待 node 安装完成
$ brew install node

# 查看版本
$ node -v
$ npm -v

# 安装puppeteer 环境
$ npm i puppeteer

2、网页截屏 demo

// https://github.com/puppeteer/puppeteer/blob/main/examples/screenshot.js

"use strict";

const puppeteer = require("puppeteer");

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto("http://example.com");
  await page.screenshot({ path: "example.png" });
  await browser.close();
})();

mac 下面直接运行可能会提示浏览器版本问题,需要指定下载对应的版本才能运行起来。所以修改之后的代码

// 版本号查找链接 http://omahaproxy.appspot.com/

const puppeteer = require("puppeteer");
const browserFetcher = puppeteer.createBrowserFetcher();

// 下载指定版本的chrome浏览器,下载完成之后返回chrome浏览器对象
browserFetcher.download("809590").then(async (res) => {
  // options 参数见
  // https://zhaoqize.github.io/puppeteer-api-zh_CN/#?product=Puppeteer&version=v8.0.0&show=api-class-puppeteer
  const options = {
    executablePath: res.executablePath, // chrome执行路径
    headless: true, // 浏览器无头模式,后台运行,false 会打卡浏览器
    defaultViewport: {
      width: 1800,
      height: 768,
    },
    args: ["--start-maximized"],
  };

  puppeteer.launch(options).then(async (browser) => {
    const page = await browser.newPage();
    await page.goto("http://www.baidu.com");
    await page.screenshot({ path: "baidu.png" });
    await browser.close();
  });
});
# 运行上面的脚本文件
$ node test.js
$ ls baidu.png

至此已经 puppeteer 入门了。

3、terminal 运行 script 采集目标数据

接下来就是开始针对团队需要分析的数据采集了,先熟悉 puppeteer api 文档,主要熟悉 Page、JSHandle、以及 ElementHandle 对象,下面的代码会经常用到这 3 个 api。

除了上面的 3 个常用对象之外,还要熟悉 chrome 开发者工具的 api,知道怎么去查找 dom 节点的路径。下图是直接打开 chrome 开发者工具,在 Elements 面板里面,选择要查找的 dom 节点上右击弹出菜单。

具体几个功能见 Console Utilities API reference。

下面就开始实战,要想拿到目标网站的数据,需要 2 个步骤,账号登陆和打开指定网页。登陆的代码是通过脚本登陆之后把 cookie 保存到本地文件里面,然后采集的脚本就可以直接载入 cookie 文件直接使用,这样避免了没有身份的问题。这个登陆脚本的弊端是无非解决无头模式下面的扫码登陆。
直接上代码,登陆代码脚本如下,

// login.js
const puppeteer = require("puppeteer");
const fs = require("fs").promises;
const fs2 = require("fs");
const puppeteerNode = puppeteer;
const browserFetcher = puppeteerNode.createBrowserFetcher();

browserFetcher.download("809590").then((res) => {
  let options = {
    executablePath: res.executablePath, //chrome执行路径
    headless: false, //浏览器无头模式
    defaultViewport: {
      width: 1800,
      height: 768,
    },
    args: ["--start-maximized"],
  };
  puppeteer.launch(options).then(async (browser) => {
    let cookies = {};
    const page = await browser.newPage();
    if (fs2.existsSync("./cookies.json")) {
      const cookiesString = await fs.readFile("./cookies.json");
      let cookies = JSON.parse(cookiesString);
      await page.setCookie(...cookies);
      await page.goto("https://dy.mock.com/login?routerstr=workbench");
    } else {
      await page.goto("https://dy.mock.com/login?routerstr=workbench");
      await page.waitForSelector("#app > div > div.bg_login > div > div > div.of_hidden > div");
      const tabs = await page.$$("#app > div > div.bg_login > div > div > div.of_hidden > div");
      await tabs[1].click().then(async (res) => {
        await page.waitForSelector("#input-msg > div > input");
        await page.waitForSelector("#app > div > div > div > div > div > div > form > div.form_item.pointer");
        const inputs = await page.$$("#input-msg > div > input");
        await inputs[0].type("账号");
        await inputs[1].type("密码");
        await page.click(
          "#app > div > div > div > div > div > div > form > div.form_item.pointer"
        );
        ////有头模式下面,需要等待时间,以便扫码登陆操作完成
        // await page.waitForTimeout(5000);
        cookies = await page.cookies();
        await fs.writeFile("./cookies.json", JSON.stringify(cookies, null, 2));
      });
    }
    browser.close();
  });
});
$ node login.js
$ ls cookies.json

下面是抓取页面内容的脚本代码。

// grab.js
const puppeteer = require("puppeteer");
const fs = require("fs").promises;
const browserFetcher = puppeteer.createBrowserFetcher();

browserFetcher.download("809590").then((res) => {
  puppeteer
    .launch({
      executablePath: res.executablePath, //chrome执行路径
      headless: false, //浏览器无头模式
    })
    .then(async (browser) => {
      const page = await browser.newPage();
      await page.setViewport({ width: 1800, height: 768 });
      // 缺少登陆验证,默认已经执行过上面的登陆脚本
      const cookiesString = await fs.readFile("./cookies.json");
      let cookies = JSON.parse(cookiesString);
      await page.setCookie(...cookies);
      await page.goto("https://dy.fake.com/kol_list/kol_list");
      await page.waitForSelector("table");
      await page.waitForSelector("#app > div > div.main_view > div > div:nth-child(1) > div:nth-child(1) > div > div > div > div > div");
      const tabs = await page.$$("#app > div > div.main_view > div > div:nth-child(1) > div:nth-child(1) > div > div > div > div > div");

      for (let [i, tab] of tabs.entries()) {
        const tabName = await (await tab.getProperty("innerText")).jsonValue();
        tab.click().then(async (res) => {
          await page.waitForSelector("table");
          const result = await page.$$eval("table", (tables) => {
            let trs = tables[2].children[1].children;
            let t = [];
            let csv ="名称,粉丝总数,粉丝质量,中位点赞数,中位评论数,中位分享数,指数\n";
            for (const tr of trs) {
              let name = tr.children[2].innerText;
              let fans = tr.children[3].innerText.replace(",", "");
              let fansQ = tr.children[4].innerText.replace(",", "");
              let likeAvg = tr.children[5].innerText.replace(",", "");
              let commentAvg = tr.children[6].innerText.replace(",", "");
              let shareAvg = tr.children[7].innerText.replace(",", "");
              let index = tr.children[8].innerText.replace(",", "");
              let tmp = [name,fans,fansQ,likeAvg,commentAvg,shareAvg,index];
              t.push({name: name,fans: fans,q: fansQ,likeAvg: likeAvg,commentAvg: commentAvg,shareAvg: shareAvg,index: index});
              csv += tmp.join(",") + "\n";
            }
            return [t, csv];
          });
          await fs.writeFile("./" + i + "-" + tabName + "-index.csv",result[1]);
        });
        await page.waitForTimeout(3000);
      }
    });
});
# 运行脚本
$ node grab.js
$ ls -la *.csv

可以看一下抓取到的csv文件内容

至此 terminal 爬取数据的部分就结束了。

4、web 服务运行脚本采集数据

web 服务运行脚本这部分代码很简单,就是把上面的 terminal 的代码包装一下,通过 web 服务 对外访问提供服务,这样可以通过浏览器直接打开网页进行操作,无需开 terminal 去运行一些命令。

web 服务这块,我直接选择了 eggjs 框架搭建业务逻辑代码,不用再去写一些 http 相关的代码。
代码如下

'use strict';
const Controller = require('egg').Controller;
const path = require('path');
const puppeteer = require('puppeteer');
const fs = require('fs').promises;
const fs2 = require('fs');
const archiver = require('archiver');


class HomeController extends Controller {

  async index() {
    const { ctx } = this;
    await ctx.render('home/index.tpl');
  }

  async login() {
    const { ctx } = this;
    const v = await this.puppeteerLogin(ctx);
    await ctx.render('home/login.tpl', { v });
  }

  async grab() {
    const { ctx } = this;
    const account = ctx.cookies.get('account');
    if (account === '' || account === undefined) {
      return ctx.redirect('/');
    }
    const puppeteerNode = puppeteer;
    const browserFetcher = puppeteerNode.createBrowserFetcher();
    let v = await browserFetcher.download('809590').then(res => {
        const options = {
          executablePath: res.executablePath, // chrome执行路径
          headless: true, // 浏览器无头模式
          defaultViewport: {
            width: 1800,
            height: 768,
          },
          args: [ '--start-maximized' ],
        };
        const v = puppeteer.launch(options)
          .then(async browser => {
            const page = await browser.newPage();
            await page.setViewport({ width: 1800, height: 768 });
            const sessionCookieDir = path.join(ctx.app.config.sessionDir, account);
            const fileName = account + '-target.zip';
            const publicZipFile = path.join(ctx.app.config.publicDir, fileName);

            if (!fs2.existsSync(sessionCookieDir)) {
              return 'pls wait seconds for login ';
            }
            const lockFile = sessionCookieDir + '/start.lock';
            if (fs2.existsSync(lockFile)) {
              console.log('pls wait seconds for done');
              return 'pls wait seconds for done';
            }
            const zipFilePath = path.join(sessionCookieDir, fileName);
            if (fs2.existsSync(publicZipFile)) {
              console.log(publicZipFile);
              return publicZipFile;
            }
            await fs.writeFile(sessionCookieDir + '/start.lock', 'lock');
            const sessionDataDir = path.join(sessionCookieDir, 'data');
            if (!fs2.existsSync(sessionDataDir)) {
              fs2.mkdirSync(sessionDataDir, '0777', true);
            }
            const sessionCookiePath = path.join(sessionCookieDir, 'cookies.json');
            const cookiesString = await fs.readFile(sessionCookiePath);
            const cookies = JSON.parse(cookiesString);
            await page.setCookie(...cookies);

            await page.goto('https://dy.fake.com/kol_list/kol_list');
            await page.waitForSelector('table');
            await page.waitForSelector('#app > div > div.main_view > div > div:nth-child(1) > div:nth-child(1) > div > div > div > div > div');
            const tabs = await page.$$('#app > div > div.main_view > div > div:nth-child(1) > div:nth-child(1) > div > div > div > div > div');

            for (const [ i, tab ] of tabs.entries()) {
              const tabName = await (await tab.getProperty('innerText')).jsonValue();
              tab.click().then(async res => {
                  await page.waitForSelector('table');
                  const result = await page.$$eval('table', tables => {
                    const trs = tables[2].children[1].children;
                    const t = [];
                    let csv = '名称,粉丝总数,粉丝质量,中位点赞数,中位评论数,中位分享数,指数' + "\n";
                    for (const tr of trs) {
                      const name = tr.children[2].innerText;
                      const fans = tr.children[3].innerText.replace(',', '');
                      const fansQ = tr.children[4].innerText.replace(',', '');
                      const likeAvg = tr.children[5].innerText.replace(',', '');
                      const commentAvg = tr.children[6].innerText.replace(',', '');
                      const shareAvg = tr.children[7].innerText.replace(',', '');
                      const index = tr.children[8].innerText.replace(',', '');
                      const tmp = [ name, fans, fansQ, likeAvg, commentAvg, shareAvg, index ];
                      t.push({name,fans,fansQ,likeAvg,commentAvg,shareAvg,index});
                      csv += tmp.join(',') + "\n";
                    }
                    return [ t, csv ];
                  });
                  await fs.writeFile(sessionDataDir + '/' + i + '-' + tabName + '-cassindex.csv', result[1]);
                });
              await page.waitForTimeout(1000);
            }
            const output = fs2.createWriteStream(zipFilePath);
            const archive = archiver('zip', {zlib: {level: 9}});
            await output.on('close', function() {
              console.log(archive.pointer() + ' total bytes');
              console.log('archiver has been finalized and the output file descriptor has closed.');
              fs.copyFile(zipFilePath, publicZipFile);
              fs.rm(zipFilePath);
            });
            await output.on('end', function() {
              console.log('Data has been drained');
            });
            await archive.on('error', function(err) {
              throw err;
            });
            await archive.on('warning', function(err) {
              if (err.code === 'ENOENT') {
                console.log('warning---->' + err);
              } else {
                throw err;
              }
            });
            await archive.pipe(output);
            await archive.directory(sessionDataDir, false);
            await archive.finalize();
            await fs.rm(lockFile);
            return publicZipFile;
          });
        return v;
      });

    if (v.indexOf('/public/') >= 0) {
      v = v.replaceAll(ctx.app.config.webRootDir, '');
      await ctx.render('home/download.tpl', { v });
    } else {
      ctx.body = v;
    }
  }

   puppeteerLogin(ctx) {
    const username = ctx.request.body.username;
    const password = ctx.request.body.password;
    const puppeteerNode = puppeteer;
    const browserFetcher = puppeteerNode.createBrowserFetcher();
    const v = browserFetcher.download('809590')
      .then(res => {
        const options = {
          executablePath: res.executablePath, // chrome执行路径
          headless: true, // 浏览器无头模式
          defaultViewport: {
            width: 1800,
            height: 768,
          },
          args: [ '--start-maximized' ],
        };

        const v = puppeteer.launch(options)
          .then(
            async browser => {
              let cookies = {};
              const page = await browser.newPage();
              const sessionCookieDir = path.join(ctx.app.config.sessionDir, username);
              if (!fs2.existsSync(sessionCookieDir)) {
                fs2.mkdirSync(sessionCookieDir, '0777', true);
              }
              const sessionCookiePath = path.join(sessionCookieDir, 'cookies.json');

              if (fs2.existsSync(sessionCookiePath)) {
                const cookiesString = await fs.readFile(sessionCookiePath);
                const cookies = JSON.parse(cookiesString);
                await page.setCookie(...cookies);
                await page.goto('https://dy.fake.com/login?routerstr=workbench');
              } else {
                await page.goto('https://dy.fake.com/login?routerstr=workbench');
                await page.waitForSelector('#app > div > div.bg_login > div > div > div.of_hidden > div');
                const tabs = await page.$$('#app > div > div.bg_login > div > div > div.of_hidden > div');
                await tabs[1].click()
                  .then(async res => {
                    await page.waitForSelector('#input-msg > div > input');
                    await page.waitForSelector('#app > div > div > div > div > div > div > form > div.form_item.pointer');
                    const inputs = await page.$$('#input-msg > div > input');
                    await inputs[0].type(username);
                    await inputs[1].type(password);
                    await page.click('#app > div > div > div > div > div > div > form > div.form_item.pointer');
                    await page.waitForTimeout(1500);
                    cookies = await page.cookies();
                    await fs.writeFile(sessionCookiePath, JSON.stringify(cookies, null, 2));
                  });
              }
              await browser.close();
              ctx.cookies.set('account', username);
              return 'success';
            }
          );
        return v;
      });
    return v;
  }
}
module.exports = HomeController;

web 服务部署参见 eggjs 官网文档,根据步骤部署完之后,启动服务可能会遇到一些问题,在 linux 服务器下面安装 puppeteer 涉及一些库依赖,这些错误根据具体提示,直接 Google 一下应该能解决。环境安装完之后运行代码还是依然会遇到代码问题,这是因为在 linux 下面上面的代码需要做出调整,把浏览器启动的参数里面改成

 args: [ '--start-maximized', '--no-sandbox', '--disable-setuid-sandbox'],

关闭沙箱模式之后,再启动服务就可以正常运行了。

5、总结

在 puppeteer 入门过程中,遇到各种问题,节点查找,节点文本提取,循环遍历节点等等,这些通过不断的输出调试以及查找 api 和 google 搜索,至此把遇到的问题都给解决了,虽然都解决了问题,但是代码还是不够完善的,只能是跑起来的一个 demo,还需要持续优化代码的。

对于 nodejs 使用的不多,在遇到异步回调的会忘记等待返回或者没有执行 promoise 的 callback,导致写这块的代码比较慢,要查找 api 再来写代码。接下来要再系统的学一下nodejs的知识。

代码里涉及到的抓取网页链接地址被打码了,无法正常访问的。需要的同学可以通过了解更多联系获取。



项目里相关链接

  • puppeteer
  • puppeteer api: 中文API
  • chrome devtools: Console Utilities API reference
  • eggjs
  • chromedp
  • risk of spider

6、了解更多


原文链接:Puppeteer 入门

你可能感兴趣的:(Puppeteer 入门)