Skip to content

Puppeteer ​

重要通知

。

基本概况 ​

Puppeteer是一个Node.js库,它提供了一个高级API来控制 Chrome/Chromium over the DevTools Protocol。

安装配置 ​

代码示例 ​

const puppeteer = require('puppeteer');


module.exports = async function () {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://example.com');
  await page.screenshot({path: 'screenshot.png'});
  await browser.close();
}

表单交互 ​

// 模拟登陆
await page.click('...');
await page.waitForSelecto('...');
// 输入百度账号密码然后登录
await page.type('#username',"用户提供的用户名");
await page.type('#password','用户提供的密码');
await page.click('button');
await page.waitForNavigation();
console.log(':white_check_mark: 登录成功');

//页面登录成功后,是否需要reload 根据实际情况来确定
await page.reload();

键盘与鼠标 ​

# 事件
mouse.click(x, y[, options])
mouse.down([options])
mouse.move(x, y[, options])
mouse.up([options])

# mouse.click
> 功能:单击鼠标
mouse.click(x, y, {
  button: 'left' | 'right' | 'middle',
  clickCount: 1,  #默认持续单击1次
  delay: 0
})

# mouse.down
> 功能:按下鼠标

# mouse.move
> 功能:
await page.mouse.move(0, 0);
await page.mouse.down();
await page.mouse.move(0, 100);
await page.mouse.move(100, 100);
await page.mouse.move(100, 0);
await page.mouse.move(0, 0);
await page.mouse.up();

# mouse.up
> 功能:释放鼠标



# 方法
> page.click(selector[, options])         # 在被选择元素上模拟点击
> page.type(selector, text[, options])    # 在被选择的输入框中输入
> page.hover(selector)                    # 模拟鼠标移动到被选择元素上
> page.select(selector, ...values)        # 在被选择元素上模拟选择select选项
> page.tap(selector)                      # 在被选择元素上模拟触摸


# 案例
> await page.click('div.article_showall a')  







# 事件
keyboard.down(key[, options])
keyboard.press(key[, options])
keyboard.sendCharacter(char)
keyboard.type(text[, options])
keyboard.up(key)

# keyboard.down
> 功能:按下键盘
await page.keyboard.down('Shift');

# keyboard.press
> 功能:按着键盘
await page.keyboard.press('Backspace');

# keyboard.sendCharacter
> 功能:输入文本
page.keyboard.sendCharacter('嗨');

# keyboard.type
> 功能:
await page.keyboard.type('World', {delay: 100});

# keyboard.up
> 功能:释放键盘
await page.keyboard.up('Shift');

浏览器相关属性与方法 ​

# 事件
browser.on('disconnected')
browser.on('targetchanged')
browser.on('targetcreated')
browser.on('targetdestroyed')

# 属性与方法
browser.browserContexts()
browser.close()
browser.createIncognitoBrowserContext()
browser.defaultBrowserContext()
browser.disconnect()
browser.newPage()
browser.pages()
browser.process()
browser.target()
browser.targets()
browser.userAgent()
browser.version()
browser.wsEndpoint()

路由管理 ​

page.goBack([options])


page.goForward([options])


page.goto(url[, options])


# 方法
page.goBack([options])
page.goForward([options])
page.goto(url[, options])
page.reload([options])


# 五个方法参数一样
> options <Object> 导航配置,可选值:timeout <number> 跳转等待时间,单位是毫秒, 默认是30秒, 传 0 表示无限等待。可以通过page.setDefaultNavigationTimeout(timeout)方法修改默认值。waitUntil <string|Array<string>> 满足什么条件认为页面跳转完成,默认是 load 事件触发时。指定事件数组,那么所有事件触发后才认为是跳转完成。事件包括:load - 页面的load事件触发时domcontentloaded - 页面的 DOMContentLoaded 事件触发时networkidle0 - 不再有网络连接时触发(至少500毫秒后)networkidle2 - 只有2个网络连接时触发(至少500毫秒后)
> 返回: <Promise<?Response>> Promise对象resolve后是主要的请求的响应. 如果有多个跳转, resolve后是最后一次跳转的响应. 如果不能向前,resolve后是null


# 代码示例单击, 单击跳转指定页面
await page.waitFor(1000);
await page.click("a[title='Node.js 常用工具']");

# 返回上一页
> page.goBack([options])
await page.waitFor(1000);
await RoutineHandle('page.goBack', async ()=> {
  await page.goBack();
});
process.stdout.write(`${page.url()}\n`);


# 继续下一页
> page.goForward([options])
await page.waitFor(1000);
await RoutineHandle('page.goForward', async ()=> {
  await page.goForward();
});
process.stdout.write(`${page.url()}\n`);

# 页面跳转
> page.goto(url[, options])

# 页面重加载
> page.reload([options])











/**
 * 路由控制
 * 
 */
const {Goto} = require('../common.js.js');
const SaveDataFile = require(ROOT_PATH + 'api/savedatafile.js');


module.exports = async function() {
  //console.log("|-----" + "路由控制" + "-----FILE_URL: " + __filename);

  //业务模型
  await init();
  
}

async function init() {
  //模块系统
  await Goto('https://music.163.com/#/discover/artist/signed/');
  process.stdout.write(`${page.url()}\n\n\n`);

  //获取指定frame
  let initCount = 0;
  let event_name = "frame";
  console.log(`${event_name} | time is starting ...... ${initCount}`); initCount++;
  let timingCount = await setInterval(()=> {console.log(`${event_name} | time is ${initCount} secoond. please wait a moment!`); initCount++;}, 1000);

  const URL = page.url();
  const frame = page.frames().find(frame => frame.name() === 'contentFrame');
  await frame.addScriptTag({url: 'https://cdn.bootcss.com/jquery/3.2.0/jquery.min.js'});
  await page.waitFor(1000);
  let result = await frame.$eval('body', async (body)=> {
    await new Promise((resolve)=> {
      let scrollTop = 0;
      let distance = 500;
  
      let interval = setInterval(()=> {
        height = body.scrollHeight; 
  
        window.scrollBy(0, distance);
        scrollTop += distance;
        if (scrollTop >= height) {
          clearInterval(interval); 
          resolve();
        }
      }, 1000);
    });

    let resultJSON = {status: 200, msg: 'success', data: {list: []}};
    let dom = $('#m-artist-box').children();
    dom.each((dn, dx)=> {
      let it = $(dx); 
      let item = {};
      item['portrait'] = it.children('div').children('img').attr('src');
      item['link'] = it.children('div').children('a').attr('href');
      item['name'] = it.children('p').children('a').text();
      item['home'] = it.children('p').children('a').attr('href');

      resultJSON['data']['list'].push(item);
    });

    return resultJSON['data']['list'];
  });

  clearInterval(timingCount);
  console.log(`${event_name} | timing has finished, this handle use up <<${initCount}>> seconds`);

  let save_file_path = rootsrc + 'example.json';
  let save_data = JSON.stringify(result);
  try {
    await SaveDataFile(save_file_path, save_data);
  } catch(e) {
    console.log(e);
  }




}

权限管理 ​

事件 ​

page.tap(selector)




# 单击与触屏事件
touchscreen.tap(x, y)

网页内容(增删改查) ​

# 基础操作
<script>
  const URL = "http://www.360doc.com/content/18/0521/18/8527076_755771471.shtml";
  const title = "1950-2011年中国历年财政收入一览表";
  await page.goto(URL, {waitUntil: 'networkidle2'});

  // 单击页面按钮
  const allResultsSelector = '.devsite-suggest-all-results';
  await page.waitForSelector(allResultsSelector);
  await page.click(allResultsSelector);

  // 删除遮挡内容的顶部DOM片段
  const deleteNode = await page.$('div.atfixednav');
  await page.evaluate((node) => node.remove(), deleteNode);

  let result = await page.content();  // 方法一:获取页面HTML
  const text = await page.$eval('html', element => element.outerHTML);  // 方法二:获取页面HTML


  // 检测DOM元素是否加载
  await page.waitFor(selectorOrFunctionOrTimeout[, options[, ...args]])
  await page.waitFor('.sect > .sele');


  // 解析网页内容
  const resultsSelector = '.gsc-results .gs-title';
  await page.waitForSelector(resultsSelector);

  const result = await page.evaluate((resultsSelector) => {


    const anchors = Array.from(document.querySelectorAll(resultsSelector));
    return anchors.map((anchor) => {
      const title = anchor.textContent.split('|')[0].trim();
      return `${title} - ${anchor.href}`;
    });

    // 检索图片
    const imgs = document.querySelectorAll('img');
    imgs.forEach((value)=> { 
      arr.push(value.src); 
    });





  }, resultsSelector);




</script>


# HTML文档 | await page.content();

page.target()
page.title()
page.type(selector, text[, options])
page.url()
page.viewport()




page.emulate(options)

page.setUserAgent(userAgent)
page.setViewport(viewport)



page.url()
page.content()
page.frames()
page.mainFrame()
page.metrics()
page.target()
page.title()
page.viewport()



page.metrics()
返回: <Promise<Object>> 包含指标数据的键值对:
Timestamp <number> 时间点(when the metrics sample was taken)
Documents <number> 页面的documents数量。
Frames <number> 页面的iframe数量。
JSEventListeners <number> 页面的js事件数量。
Nodes <number> 页面的dom节点数量。
LayoutCount <number> 整页面或部分页面的布局数量。
RecalcStyleCount <number> 页面样式重新计算数量。
LayoutDuration <number> 页面布局总时间。
RecalcStyleDuration <number> 页面样式重新计算总时间。
ScriptDuration <number> 页面js代码执行总时间。
TaskDuration <number> 页面任务执行总时间。
JSHeapUsedSize <number> 页面占用堆内存大小。
JSHeapTotalSize <number> 总的页面堆内存大小。

网页请求Request ​

# page.on('request')必须放在page.goto之前,否则失效

# 代码示例
<script>
  await page.setRequestInterception(true);  // 开启请求拦截 | 如果请求拦截没有开启会立即抛出异常

  page.on('request', async (request) => {
    //
  });
  
  page.on('requestfinished', (request) => {
    console.log(request);
  }); 

  page.on('requestfailed', (request) => {
    console.log(request.url() + ' ' + request.failure().errorText);
  });


  const includeList = [''],       // 过滤白名单
    excludeList = ['data:image/', 'main.bin'];  // 过滤黑名单

  // 具体业务
  if (request.isInterceptResolutionHandled()) return;

  if (request.resourceType() === 'image') {
    request.abort();
  } else {
    request.continue();
  }

  const headers = Object.assign({}, request.headers(), {
    foo: 'bar', // set "foo" header
    origin: undefined, // remove "origin" header
  });
  request.continue({ headers });

  const { action, priority } = request.interceptResolutionState();
  if (action === InterceptResolutionAction.AlreadyHandled) return;
  if (action === InterceptResolutionAction.Continue) return;

  request.respond({
    status: 404,
    contentType: 'text/plain',
    body: 'Not Found!',
  });
</script>


# 方法
> request.abort([errorCode]):中断请求
> request.continue([overrides]):
> request.failure():返回描述请求失败的对象
> request.frame():
> request.headers():该请求的 http 头对象。所有头都采用小写的命名方式
> request.isNavigationRequest():
> request.method():请求方法
> request.postData():请求提交的数据。
> request.redirectChain():
> request.resourceType():包含渲染引擎识别出的请求资源类型,包含:document,stylesheet,image,media,font,script,texttrack,xhr,fetch,eventsource,websocket,manifest,other。
> request.respond(response):当使用request.abort中断请求后,使用该方法来返回一个响应。[request.respond 不支持模拟响应 dataURL 请求。 对 dataURL请求使用 request.respond 并不会起任何作用。]
> request.response():
> request.url():请求的 URL。

网页响应Response ​

# page.on('response')必须放在page.goto之前,否则失效

# 代码示例
<script>
  page.on('response', async (response) => {
    console.log(response);
  });

  
  // 具体业务
</script>


# 方法
> response.buffer():
> response.frame():
> response.fromCache():
> response.fromServiceWorker():
> response.headers():
> response.json():
> response.ok():
> response.remoteAddress():
> response.request():
> response.securityDetails():
> response.status():
> response.statusText():
> response.text():
> response.url():

下拉页面至底部 ​

# 滚动
<script>
  let result = await page.evaluate(async ()=> {
    let $ = window.$;

    //下拉页面到底部
    await new Promise((resolve)=> {
      let scrollTop = 0;
      let distance = 250;

      let interval = window.setInterval(()=> {
        height = document.body.scrollHeight; 

        window.scrollBy(0, distance);
        scrollTop += distance;
        if (scrollTop >= height) {
          clearInterval(interval); 
          resolve();
        }
      }, 1000);
    });

    let items = null; 
    let backJSON = {type: 'array', msg: '提示消息', data: {}, list: []};

    //解析数据
    items = $('');
    if (items.length >= 1) {
      items.each((index, item)=> {
        let it = $(item);

        let name = null;

        backJSON.dataList.push({name});
      });  
    }

    return backJSON;
  });
</script>


# frame框架内部
<script>
  //获取指定frame
  const frame = page.frames().find(frame => frame.name() === 'contentFrame');
  //获取HTML
  let html = await frame.$eval('body', async (body)=> {
    await new Promise((resolve)=> {
      let scrollTop = 0;
      let distance = 500;
  
      let interval = setInterval(()=> {
        height = body.scrollHeight; 
  
        window.scrollBy(0, distance);
        scrollTop += distance;
        if (scrollTop >= height) {
          clearInterval(interval); 
          resolve();
        }
      }, 1000);
    });

    return body.innerHTML;
  });

  let save_file_path = rootsrc + 'name.html';
  let save_data = html;
  SaveDataFile(save_file_path, save_data);
</script>

性能测试与自动化测试 ​

在平时调试中我们通常用两种方法来看页面性能数据:
window.performance.timing 和 chrome的DevTools中的Performance工具。
经查阅得知,window.performance.timing是执行于网页上下文的,而DevTools是执行于浏览器上下文的。这边给出获取两种数据的代码


const puppeteer = require('puppeteer')

const test = (async () => {
  const browser = await puppeteer.launch({headless: false})
  const page = await browser.newPage()
  await page.goto('https://www.jianshu.com')
  // 获取 DevTools 中的数据
  let devData = await page._client.send('Performance.getMetrics')
  // 获取 window 中的数据
  let windowData = JSON.parse(await page.evaluate(() => {
    JSON.stringify(window.performance.timing
  })
})

test()

追加外部资源 ​

await page.goto(pageLink, {waitUntil: 'networkidle2'}); 

# page.addScriptTag(options)
  try {
    //载入jQuery
    await page.mainFrame().addScriptTag({url: 'https://cdn.bootcss.com/jquery/3.2.0/jquery.min.js'});
  } catch(e) {
    console.log(JSON.stringify(e));
  }

  page.addScriptTag(options)
  page.addStyleTag(options)


# page.addStyleTag(options)

api ​

page.cookies([...urls])

page.deleteCookie(...cookies)

DevTools ​

Chrome DevTools Protocol:https://chromedevtools.github.io/devtools-protocol/

await page._client.send('Network.emulateNetworkConditions', {
  offline: false,
  latency: 200, // ms
  downloadThroughput: 780 * 1024 / 8, // 780 kb/s
  uploadThroughput: 330 * 1024 / 8, // 330 kb/s
});

iframe操作技巧 ​

# 获取指定frame
const frame = page.frames().find(frame => frame.name() === 'contentFrame');

# 获取内容
const text = await frame.$eval('html', element => element.textContent);
console.log(text);

# 获取HTML
const html = await frame.$eval('html', element => element.outerHTML);

# 页面框架
page.frames()  // 页面包含的frame数组, 总数为page.frames().length
let frame = page.mainFrame()

# 嵌入frame页面的url
frame.url()

# frame数量
frame.childFrames()


# 方法
frame.$(selector)
frame.$$(selector)
frame.$$eval(selector, pageFunction[, ...args])
frame.$eval(selector, pageFunction[, ...args])
frame.$x(expression)
frame.addScriptTag(options)
frame.addStyleTag(options)
frame.childFrames()
frame.click(selector[, options])
frame.content()
frame.evaluate(pageFunction[, ...args])
frame.evaluateHandle(pageFunction[, ...args])
frame.executionContext()
frame.focus(selector)
frame.goto(url[, options])
frame.hover(selector)
frame.isDetached()
frame.name()
frame.parentFrame()
frame.select(selector, ...values)
frame.setContent(html[, options])
frame.tap(selector)
frame.title()
frame.type(selector, text[, options])
frame.url() 
frame.waitFor(selectorOrFunctionOrTimeout[, options[, ...args]])
frame.waitForFunction(pageFunction[, options[, ...args]])
frame.waitForNavigation([options])
frame.waitForSelector(selector[, options])
frame.waitForXPath(xpath[, options])

基础API ​

浏览器 Browser ​

  • 启动具有给定参数和选项(如果指定)的浏览器实例
const browser = await puppeteer.launch({
  headless: true,  // 无头模式, boolean | 'new', true-启用无头模式 false-启用有头模式 'new'-Chrome 112 推出的一种新的无头模式
  args: [ // , string[], 
    // 传递给浏览器实例的参数
    '--no-proxy-server',
    '--no-sandbox',
    '--disable-setuid-sandbox',
    '--disable-dev-shm-usage',
  ], 
  debuggingPort: , // , number,
  devtools: false, // , boolean,
  userDataDir: , // , string, 
});

browser.close();

键盘 Keyboard ​

鼠标 Mouse ​

触摸屏 Touchscreen ​

框架 Frame ​

延时、等待时间与超时 ​

// 等待
page.waitFor(selectorOrFunctionOrTimeout[, options[, ...args]])  # 延迟下一步操作
page.waitForFileChooser([options])
page.waitForFunction(pageFunction[, options[, ...args]])
page.waitForNavigation([options])   # 延迟跳转时间
page.waitForRequest(urlOrPredicate[, options])
page.waitForResponse(urlOrPredicate[, options])
page.waitForSelector(selector[, options])
page.waitForXPath(xpath[, options])


// page.waitFor(selectorOrFunctionOrTimeout[, options[, ...args]])
selector: 选择器, 检测DOM元素加载
Function: 函数, 等待函数执行完成
Timeout: 延迟时间, 延迟等待时间

​

​

网页 Page ​

const page = await browser.newPage();

page.viewport({
  width: ,
  height: ,
  deviceScaleFactor: 1,
  isMobile: false,
  hasTouch: false,
  isLandScape: false
});

page.close();

URL ​

const url = "";
page.goto(url, {
  timeout: 10000, // 页面超时容错时间
  waitUntil: [ // 等待条件
    'load', // window.onload 事件被触发时继续。
    'domcontentloaded', // Domcontentloaded 事件触发时继续。
    'networkidle0', // 在 500ms 内没有网络连接时(全部的request结束)则继续。
    'networkidle2' // 500ms 内有不超过 2 个网络连接时就算成功(还有两个以下的request)则继续。
  ]
});

​

​

​

​

​

导出 PDF ​

await page.pdf({
  path: STATIC_PATH + `${Date.now()}.pdf`, 
  width: 1920,
  printBackground: true,
  displayHeaderFooter: true,
  format: 'A4'
});

导出图片 ​

  • 截图整个页面范围
const image = page.screenshot({
  captureBeyondViewport: true,
  clip: {
    width: ,
    height: ,
    scale: 1,
    x: ,
    y: 
  },
  encoding: 'binary', // 'base64' | 'binary'
  fromSurface: false,
  fullPage: false,
  omitBackground: false, // 隐藏网页背景, 即transparent
  optimizeForSpeed: ,
  path: , // 保存图片磁盘路径,如果不存在,则返回流数据, '/home/meta/server/oss/'
  quality: 100,
  type: 'png', // 图片格式, 默认为'png', 取值'png' | 'jpeg' | 'webp'
});

// 方案一:返回图片数据结构
const base64Image = await image.toString('base64');

// 方案二:返回图片数据结构
ctx.body = image;
  • 截图指定DOM结构范围
/**
 * 图片生成模型
 * @param {*} data 数据
 * @param {*} headers 请求头
 * @returns 
 */
async function imageModel(data = {}, headers = {}) {
  const { userInfo, recordInfo, activityID, activityDetail, homeLink, shareLink } = data || {};
  const { sessionid: session_id, token } = headers || {};

  if (
    !userInfo ||
    !recordInfo ||
    !activityID ||
    !activityDetail ||
    !homeLink ||
    !shareLink
  ) {
    return null;
  }

  if (
    !session_id ||
    !token
  ) {
    return null;
  }


  // 渲染网页
  const page = await browser.newPage();

  await page.goto(homeLink, { waitUntil: 'networkidle2' });

  // 添加 localStorage 缓存
  await page.evaluate((token, session_id, activityID, activityDetail = {}, userInfo = {}, recordInfo = {}) => {
    localStorage.clear();
    localStorage.setItem('token', token);
    localStorage.setItem('session_id', session_id);

    localStorage.setItem('user_info', JSON.stringify(userInfo));

    localStorage.setItem('activity_id', activityID);
    localStorage.setItem('activity_detail', JSON.stringify(activityDetail));
    localStorage.setItem('record_detail', JSON.stringify(recordInfo));
  }, token, session_id, activityID, activityDetail, userInfo, recordInfo);

  // 基于以上操作实现的localStorage缓存再次加载网页
  await page.goto(shareLink, { waitUntil: 'networkidle2', timeout: 600000 });

  // 移除 蒙层
  const masklayer = "div.van-overlay.vant-dialog-mask";
  await page.waitForSelector(masklayer);
  const deleteMask = await page.$(masklayer);
  await page.evaluate((node) => node.remove(), deleteMask);

  // 移除 "打卡成功"弹窗
  const modallayer = "div.van-dialog.component-failed-dialog";
  await page.waitForSelector(modallayer);
  const deleteModal = await page.$(modallayer);
  await page.evaluate((node) => node.remove(), deleteModal);

  const scrollViewClsName = "body > div#app > section > div.scroll-container > div.scroll-view";
  await page.waitForSelector(scrollViewClsName);
  const scrollViewDOM = await page.$(scrollViewClsName);

  // 有效视口范围 截屏
  const options = { encoding: 'binary' };
  const image = await scrollViewDOM.screenshot(options);
  return image;
}

​

DOM 操作 ​

选择器 ​

const element = await page.waitForSelector('div > .class-name');
await element.click();
await element.dispose();
  • page.$(selector):此方法是page.mainFrame().$(selector) 的简写,在页面内执行 document.querySelector。如果没有元素匹配指定选择器,返回值是 null。
  • page.$$(selector):此方法是page.mainFrame().$$(selector) 的简写。在页面内执行 document.querySelectorAll。如果没有元素匹配指定选择器,返回值是 []。
  • page.$$eval(selector, pageFunction[, ...args]):此方法在页面内执行 Array.from(document.querySelectorAll(selector)),然后把匹配到的元素数组作为第一个参数传给 pageFunction。
  • page.$eval(selector, pageFunction[, ...args]):

执行JS ​

Puppeteer 允许在页面上下文中执行 JavaScript 函数。

const three = await page.evaluate(() => {
  return 1 + 2;
});

console.log(three);
  • page.evaluate 传参
const result = await page.evaluate((name, age) => {
  return {name, age};
}, 'Eshen', 19);

console.log(result);

拦截请求 HTTPRequest ​

​

​

​

​

拦截响应 HTTPResponse ​

​

​

​

​

权限与依赖数据 ​

sessionStorage ​

localStorage ​

headers请求头 ​

​

​

经典案例 ​

使用Nodejs和Puppeteer从HTML中导出PDF ​

<button onClick={()=> {this.savePDF()}}>generate PDF in puppeteer</button>

<script>
  const puppeteer = require('puppeteer')
  (async () => {
      const brower = await puppeteer.launch()
      const page = await brower.newPage()
      await page.goto('https://github.com/linpenghui958', { waitUntil: 'networkidle0'})
      const pdf = await page.pdf({ format: 'A4'})
      await brower.close()
      return pdf
    })();

  const result = await printPdf()
    res.set({'Content-Type': 'application/pdf', 'Content-Length': result.length})
    res.send(result)

  getPDF() {
    return axios.get('//localhost:3001/puppeteer', {
      responseType: 'arraybuffer',
      headers: {
        'Accept': 'application/pdf'
      }
    })
  }

  savePDF() {
    this.getPDF()
      .then(res => {
        const blob = new Blob([res.data], { type: 'application/pdf'})
        const link = document.createElement('a')
        link.href = window.URL.createObjectURL(blob)
        link.download = 'test.pdf'
        link.click()
      })
      .catch(e => console.log(e))
  }
</script>

Released under the MIT License.