Skip to content

核心概念

  • 浏览器管理
  • 页面交互
  • JavaScript 执行器
  • 网络侦听

浏览器管理:

  1. 启动浏览器

    javascript
    import puppeteer from 'puppeteer'
    
    (async () => {
      	// 启动浏览器
        const browser = await puppeteer.launch()
        const page = await browser.newPage()
    })()
  2. 关闭浏览器

    javascript
    import puppeteer from 'puppeteer'
    
    (async () => {
        const browser = await puppeteer.launch()
        const page = await browser.newPage()
        // 关闭浏览器
        await browser.close()
    })()
  3. 上下文:用来隔离自动化任务中 Cookie 和 本地存储共享的问题,当上下文关闭时所关联的页面会一同关闭;

    javascript
    import puppeteer from 'puppeteer'
    
    (async () => {
        const browser = await puppeteer.launch()
        // Cookie 和 本地存储只会在此上下文中共享
        const context = await browser.createBrowserContext()
        
        const page1 = await context.newPage()
        const page2 = await context.newPage()
    
        await context.close()
    })()
  4. 上下文权限:例如在第一次访问高德地图时浏览器会提示你手动开启定位权限,如遇到类似情况,就可以执行 overridePermissions() 函数提前配置权限;

    javascript
    import puppeteer from 'puppeteer'
    
    (async () => {
        const browser = await puppeteer.launch({
            headless: false,
        })
        const context = await browser.defaultBrowserContext()
        
        const url = 'https://ditu.amap.com/'
        await context.overridePermissions(url, ['geolocation'])
        
        const page = await context.newPage()
        await page.goto(url)
    })()
  5. 连接到正在运行的浏览器

    javascript
    import puppeteer from 'puppeteer'
    
    (async () => {
      	// 携带远程调试端口参数打开第一个浏览器窗口,并导航到如下地址获取 webSocketDebuggerUrl
        const browser = await puppeteer.launch({
            headless: false,
            args: ['--remote-debugging-port=9222']
        })
        const page = await browser.newPage()
        await page.goto('http://localhost:9222/json/version')
    })()
    javascript
    import puppeteer from 'puppeteer'
    
    (async () => {
        const browser = await puppeteer.connect({
            headless: false,
            browserWSEndpoint: '' // 将 webSocketDebuggerUrl 填写到此,并连接第一个浏览器窗口
        })
    
        await browser.newPage()
      	// 任务处理完毕后可断开连接而不会关闭浏览器窗口
      	browser.disconnect()
    })()

页面交互:

Puppeteer 允许通过鼠标、触摸事件和键盘输入与页面上的元素进行交互。通过,首选 CSS 选择器查询 DOM 元素,然后对元素调用操作。除 CSS 选择器被 Puppeteer 默认 API 支持以外,还提供了自动以选择器语法,允许使用 XPath、Text、Accessibility 属性查找元素并访问 Shadow DOM。

定位器:

定位器(Locators)是选择元素并与之交互的推荐方式,定位器 API 支持 Puppeteer 自动等待元素在 DOM 中处于可操作的正确状态。当 定位器 API 无法满足要求时仍可以使用低级别的 API,例如:page.waitForSelector()ElementHandle

  1. 使用定位器单机元素:

    javascript
    await page.locator('button').click();

    PS:定位器会在单击前自动检查以下内容:

    • 确保元素位于视口中
    • 等待元素变得可见或隐藏
    • 等待元素启用
    • 等待元素在两个连续的动画帧上具有稳定的边界框
  2. 使用定位器录入内容:

    javascript
    await page.locator('input').fill('hello world');

    PS:定位器会在输入前自动检查以下内容:

    • 确保元素位于视口中
    • 等待元素变得可见或隐藏
    • 等待元素启用
    • 等待元素在两个连续的动画帧上具有稳定的边界框
  3. 使用定位器实现鼠标悬停(hover):

    javascript
    await page.locator('div').hover();

    PS:定位器会在悬停前自动检查以下内容:

    • 确保元素位于视口中
    • 等待元素变得可见或隐藏
    • 等待元素在两个连续的动画帧上具有稳定的边界框。
  4. 使用定位器滚动元素:

    javascript
    await page.locator('div').scroll({
        scrollTop: 10,
        scrollLeft: 20
    });

    PS:定位器会在滚动前自动检查以下内容:

    • 确保元素位于视口中
    • 等待元素变得可见或隐藏
    • 等待元素在两个连续的动画帧上具有稳定的边界框
  5. 使用定位器等待元素可见:

    javascript
    await page.locator('.loading').wait();

    PS:定位器在返回前会检查以下内容:

    • 等待元素变得可见或隐藏
  6. 使用定位器实现等待函数:

    示例中展示了使用定位器实现 JavaScript 函数 等待 MutationObserver 检测页面出现 MutationObserver 元素:

    javascript
    await page.locator(() => {
        let resolve;
        const promise = new Promise((res) => {
            return (resolve = res)
        });
        const observer = new MutationObserver((records) => {
            for (const record of records) {
                if (record.target instanceof HTMLCanvasElement) {
                    resolve(record.target);
                }
            }
        });
        observer.observe(document);
        return promise;
    }).wait()
  7. 定位器与过滤器配合:

    示例中展示了使用定位器匹配 button 且通过过滤器再次匹配 button 文本中包含 Click Me 的元素,最后才会点击此元素。

    javascript
    await page.locator('button')
    	.filter(el = el.innerText().includes('Click Me'))
      .click();
  8. 从定位器获取值:

    使用 map 函数将元素映射为 JavaScript 值,再调用 wait() 将返回序列化的 JavaScript 值:

    javascript
    const enabled = await page.locator('button')
    	.map(el => !el.disabled).wait();
  9. 从定位器获取 ElementHandles:

    使用 waitHandle() 函数将返回 ElementHandles 实例。如果定位器内置 API 无法满足时,此操作会很有用。

    javascript
    const buttonHandle = await page.locator('button').waitHandle();
    await buttonHandle.click();
  10. 配置定位器自检项:

    配置定位器以改变定位器默认的自动检查项,实例中将不做任何检查对按钮触发单击事件:

    javascript
    await page.locator('button')
      .setEnsureElementIsInTheViewport(false)
      .setVisibility(null)
      .setWaitForEnabled(false)
      .setWaitForStableBoundingBox(false)
      .click();
  11. 配置定位器超时:

    由于网页的响应速度存在差异,默认的超时时间不满足需要的情况下,可使用 setTimeout() 函数适当延长,超时后将抛出 TimeoutError 异常:

    javascript
    await page.locator('button').setTimeout(5 * 1000).click();
  12. 获取定位器事件:

    目前定位器支持一个单独的事件,事件会在定位器准备执行某项动作前触发,以此表示所有前置条件已经得到满足,此事件可在日志记录或调试等场景发挥一定的作用:

    javascript
    await page.locator('button')
      .on(LocatorEvent.Action, () => {
          console.log('clicked');
      }).click();

等待执行选择器 :

等待选择器(waitForSelector)与定位器相比是一个比较低级别的 API,等待选择器允许等待元素在 DOM 中可用但不会自动重试该操作,并且需要手动释放生成的 ElementHandle 以防止内存泄漏。

javascript
import pprt from "puppeteer"

(async () => {
    
    const browser = await pprt.launch()
    const page = await browser.newPage()
    await page.goto("URL_ADDRESS")

    // 使用 waitForSelector 查询句柄
    const element = await page.waitForSelector("div > .class-name")
    await element.click();

    // 注意释放资源
    await element.dispose();

    await browser.close();
    
})()

立即执行选择器:

立即查询选择器是由 Puppeteer 提供查询已知存在于页面元素的一组 API:

API描述
page.$()返回与选择器匹配的单个元素
page.$$()返回与选择器匹配的多个元素
page.$eval()返回与选择器匹配的第一个元素上运行 JavaScript 函数的结果
page.$$eval()返回与选择器匹配的每一个元素上运行 JavaScript 函数的结果

选择器扩展:

Puppeteer 在选择器 API 中接受 CSS 选择器,此外还扩展了额外的选择器语法来为 CSS 选择器提供更多的功能。

  1. 非 CSS 选择器:Puppeteer 自定义伪元素扩展 CSS语法实现非 CSS 选择器选择元素。

    1. XPath 选择器(-p-path):

      javascript
      import pptr from 'puppeteer'
      
      (async () => {
          const browser = await pptr.launch({ headless: false })
          const page = await browser.newPage()
          await page.setViewport({ width: 1080, height: 1024 })
          await page.goto('https://developer.mozilla.org/zh-CN/')
      
        	// XPath 选择器
          const textContent = await page.locator('::-p-xpath((//*[@class="tile-container"]/div/h3/a)[1])')
            .map(el => el.textContent)
            .wait()
          console.log(textContent)
      
          await page.screenshot({ path: 'screenshot.png' })
          await browser.close()
      })()
    2. 文本选择器(-p-text):

      javascript
      import pptr from 'puppeteer'
      
      (async () => {
          const browser = await pptr.launch({ headless: false })
          const page = await browser.newPage()
          await page.setViewport({ width: 1080, height: 1024 })
          await page.goto('https://developer.mozilla.org/zh-CN/')
      		
        	// 文本选择器
          const textContent = await page.locator('::-p-text(Developer essentials: JavaScript console methods)')
          	.map(el => el.textContent)
          	.wait()
      		console.log(textContent)
      
          await page.screenshot({ path: 'screenshot.png' })
          await browser.close()
      })()
    3. ARIA 选择器(-p-aria):

      javascript
      import pptr from 'puppeteer'
      
      (async () => {
          const browser = await pptr.launch({ headless: false })
          const page = await browser.newPage()
          await page.setViewport({ width: 1080, height: 1024 })
          await page.goto('https://developer.mozilla.org/zh-CN/')
      
        	// 无障碍属性选择器
          const innerHTML = await page.locator('::-p-aria(MDN homepage)')
            .map(el => el.innerHTML)
            .wait()
          console.log(innerHTML)
      
          await page.screenshot({ path: 'screenshot.png' })
          await browser.close()
      })()
    4. Pierce 选择器(pierce/):

      javascript
      import pptr from 'puppeteer'
      
      (async () => {
          const browser = await pptr.launch({ headless: false })
          const page = await browser.newPage()
          await page.setViewport({ width: 1080, height: 1024 })
          await page.goto('https://mdn.github.io/web-components-examples/composed-composed-path/')
        
        	// 穿透 shadow DOM 选择器,深度组合器
          const textContent = await page.locator('& >>> p')
          	.map(el => el.textContent)
          	.wait()
          console.log(textContent)
      
          await page.screenshot({ path: 'screenshot.png' })
          await browser.close()
      })()
    5. 自定义选择器:

      javascript
      import puppeteer, { Puppeteer } from "puppeteer"
      
      (async () => {
          // 注册 class 选择器 
          Puppeteer.registerCustomQueryHandler('class', {
              queryOne: (node, selector) => {
                  return node.querySelector(`.${selector}`)
              },
              queryAll: (node, selector) => {
                  return [...node.querySelectorAll(`.${selector}`)]
              }
          })
      
          const browser = await puppeteer.launch()
          const page = await browser.newPage()
          await page.goto("https://developer.mozilla.org/zh-CN/")
      
          // 使用 class 选择器
          const innerHTML = await page.locator('::-p-class(tile-title)').map(el => el.innerText).wait()
          console.log(innerHTML)
      
          await browser.close()
      })()
  2. Shadow DOM 选择器:CSS 选择器不允许穿透 Shadow DOM,因此,Puppeteer 在 CSS 选择器语法中添加了两个组合器,允许在 Shadow DOM 中进行搜索。

    1. >>> 深度组合器:类似于 CSS 后代组合器(用单空格表示,例如 div button);
    2. >>>> 深度子组合器:类似于 CSS 子组合器(用 > 表示,例如 div > button);
  3. 带前缀选择器语法:${nonCssSelectorName}/${nonCssSelector} 为旧语法,允许一次运行一个非 CSS 选择器,不允许组合多个选择器,切不再推荐使用。

    1. 通过文本选择器匹配元素,并返回该元素的文本内容:

      javascript
      // 文本选择器
      const textContent = await page.locator('text/Developer essentials: JavaScript console methods')
        .map(el => el.textContent)
        .wait()
      console.log(textContent)
    2. 通过 XPath 选择器匹配元素,并返回该元素的文本内容:

      javascript
      // XPath 选择器
      const textContent = await page.locator('xpath/(//*[@class="tile-container"]/div/h3/a)[1]')
         .map(el => el.textContent)
         .wait()
      console.log(textContent)
    3. 通过无障碍选择器匹配元素,并返回该元素的 HTML 结构:

      javascript
      // 无障碍属性选择器
      const innerHTML = await page.locator('aria/MDN homepage')
         .map(el => el.innerHTML)
         .wait()
      console.log(innerHTML)
    4. 通过 Pierce 选择器匹配 shadow DOM 元素,并返回第一个元素的文本内容:

      javascript
      // 穿透 shadow DOM 选择器
      const textContent = await page.locator('pierce/p')
      	.map(el => el.textContent)
      	.wait()
      console.log(textContent)

JavaScript 执行器:

Puppeteer 运行在 Puppeteer 驱动的页面上下文中执行 JavaScript 函数。

在示例中通过编写 JavaScript 函数来获取 MDN 网页中的指定元素:

javascript
import puppeteer from "puppeteer"

(async () => {
    const browser = await puppeteer.launch()
    const page = await browser.newPage()
    await page.goto("https://developer.mozilla.org/zh-CN/")

  	// 获取第一个 h1 元素
    const innerHTML = await page.evaluate(() => document.querySelector('h1').innerHTML)
    console.log(innerHTML)
  
    await browser.close()
})()

重构此函数以支持参数传递,灵活获取元素:

JavaScript
const innerHTML = await page.evaluate(
    (selector) => document.querySelector(selector).innerHTML,
    'h1'
)

如果此函数返回一个对象,Puppeteer 会将其序列化为 JSON 并在脚本端对齐重建。此过程可能会得到不正确的结果,为了处理返回的对象,Puppeteer 提供了一种通过引用返回对象的方法:

javascript
const handle = await page.evaluateHandle(
    (selector) => document.querySelector(selector),
    'h1'
)
console.log(handle instanceof ElementHandle)

网络请求日志:

Puppeteer 默认监听所有网络的请求和响应,并在页面上发出网络事件。

javascript
import puppeteer from "puppeteer"

(async () => {

    const browser = await puppeteer.launch()
    const page = await browser.newPage()
    await page.goto("https://developer.mozilla.org/zh-CN/")

    page.on("request", request => {
        console.log(request.url())
    })

    page.on("respone", respone => {
        console.log(respone.url())
    })
})()