Deno使用Puppeteer开发实践

在当今的数字时代,Web自动化和数据采集已经成为许多企业和个人项目的重要组成部分。为了实现这些任务,需要一个强大而灵活的工具,能够模拟用户行为,自动执行各种网页操作,并提取所需的数据。而在这个领域中,Puppeteer无疑是最受欢迎的工具之一。

Puppeteer是由Google开发的一种无头浏览器工具,它提供了一个API和一组方法,用于控制和操作Chrome或Chromium浏览器的实例。通过Puppeteer,开发人员可以编写脚本来自动化网页操作,如点击按钮、填写表单、截取屏幕截图等。它还允许你拦截和修改网络请求、处理对话框以及执行自定义的JavaScript函数。无论是进行Web自动化测试、爬虫开发还是数据采集,Puppeteer都能为你提供强大的工具支持。

Puppeteer API 是分层次的,反映了浏览器结构。

image.png

安装与使用

我们使用Deno的库deno.land/x/puppeteer…

deno-puppeteer 与 Node 版本相比如何?
deno-puppeteer有效地运行常规版本的 Puppeteer,除了一些小改动以使其与 Deno 兼容。
最显著的区别可能是一些方法获取/返回 Node Buffer,而不是获取/返回 Node Uint8Array。一种方法还返回 web nativeReadableStream而不是 Node Readable。
除此之外,pptr.dev上的文档通常适用。

从下面Chromium与Puppeteer的对应关系来看,16.2.0版本差不多对应的是105或106版本,一两年内是够用了。

image.png

使用前需要先安装:

PUPPETEER_PRODUCT=chrome deno run -A --unstable https://deno.land/x/puppeteer@16.2.0/install.ts
PUPPETEER_PRODUCT=firefox deno run -A --unstable https://deno.land/x/puppeteer@16.2.0/install.ts

样例:

import puppeteer from "https://deno.land/x/puppeteer@16.2.0/mod.ts";




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

await browser.close();

运行:deno run -A –unstable example.js

一些技巧

新页面

正常情况下,就是下面这一句:

const page = await browser.newPage();

使用匿名上下文:

// 创建一个匿名的浏览器上下文
const context = await browser.createIncognitoBrowserContext();
// 在一个原生的上下文中创建一个新页面
const page = await context.newPage();

页面跳转

到某个页面

await page.goto('https://example.com');

回退

page.goBack();

前进

page.goForward();

这几个方法,都有可以配置的参数,其中waitUntil在各个需要等待的环境都是一样的参数。
:::info

  • timeout <number> 跳转等待时间,单位是毫秒, 默认是30秒, 传 0 表示无限等待。可以通过page.setDefaultNavigationTimeout(timeout)方法修改默认值
  • waitUntil <string|Array<string>> 满足什么条件认为页面跳转完成,默认是 load 事件触发时。指定事件数组,那么所有事件触发后才认为是跳转完成。事件包括:
    • load – 页面的load事件触发时
    • domcontentloaded – 页面的 DOMContentLoaded 事件触发时
    • networkidle0 – 不再有网络连接时触发(至少500毫秒后)
    • networkidle2 – 只有2个网络连接时触发(至少500毫秒后)
      :::

页面操作

获取DOM元素

使用$$eval$eval,区别就是复数与单数,document.querySelectorAll和document.querySelector。

// 多个
const divsCounts = await page.$$eval('div', divs => divs.length);

// 一个元素
const searchValue = await page.$eval('#search', el => el.value);
const preloadHref = await page.$eval('link[rel=preload]', el => el.href);
const html = await page.$eval('.main-container', e => e.outerHTML);

页面视口设置

await page.setViewport({ width: 1280, height: 800 });

click触发跳转

要注意如果 click() 触发了一个跳转,会有一个独立的 page.waitForNavigation() Promise对象需要等待。 正确的等待点击后的跳转是这样的:

const [response] = await Promise.all([
  page.waitForNavigation(waitOptions),
  page.click(selector, clickOptions),
]);

选中

当提供的选择器完成选中后,触发change和input事件。如果没有元素匹配指定选择器,将报错。

page.select('select#colors', 'blue'); // 单选择器
page.select('select#colors', 'red', 'green', 'blue'); // 多选择器

输入

每个字符输入后都会触发 keydown、keypress、input 和 keyup 事件。
要点击特殊按键,比如 Control 或 ArrowDown,用 keyboard.press

page.type('#mytextarea', 'Hello'); // 立即输入
page.type('#mytextarea', 'World', {delay: 100}); // 输入变慢,像一个用户

等待元素

通常页面跳转,或者DOM有变化后,需要等待某个元素出现才能继续。

// wait for selector
await page.waitFor('.foo');
// wait for 1 second
await page.waitFor(1000);
// wait for predicate
await page.waitFor(() => !!document.querySelector('.foo'));

键盘操作

await page.focus('input[name="username"]');
await page.keyboard.type('myusername');
await page.keyboard.press('Enter');

处理对话框

page.on('dialog', async (dialog) => {
  console.log('Dialog message:', dialog.message());
  await dialog.dismiss(); // 关闭对话框
});
await page.evaluate(() => {
  alert('This is an alert!');
});


执行JS代码

const result = await page.evaluate(x => {
  return Promise.resolve(8 * x);
}, 7); // (7 可以是你自己代码里任意方式得到的值)
console.log(result); // 输出 "56"

// 模拟用户滚动
await page.evaluate(() => {
  window.scrollBy(0, window.innerHeight);
});

也可以传递字符串:

console.log(await page.evaluate('1 + 2')); // 输出 "3"
const x = 10;
console.log(await page.evaluate(`1 + ${x}`)); // 输出 "11"

ElementHandle 实例 可以作为参数传给 page.evaluate:

const bodyHandle = await page.$('body');
const html = await page.evaluate(body => body.innerHTML, bodyHandle);
await bodyHandle.dispose();

给window注入一个方法

// 定义自定义函数
await page.exposeFunction('md5', text =>
  crypto.createHash('md5').update(text).digest('hex')
);
// 在页面中执行自定义函数
await page.evaluate(async () => {
  // 使用 window.md5 计算哈希
  const myString = 'PUPPETEER';
  const myHash = await window.md5(myString);
  console.log(`md5 of ${myString} is ${myHash}`);
});

通过 page.exposeFunction 挂载到页面的方法在多次跳转后仍然有用

绕过CSP安全策略

await page.setBypassCSP(true);

给页面注入请求头或cookie

await page.setUserAgent("xxx");
await page.setCookie({
  name: "authorization",
  value: "",
  domain: "xxx",
  path: "/",
  expires: Date.now() + 3600 * 1000,
});
await page.setExtraHTTPHeaders({
    "token": "xxx",
    "server": "deno",
});


拦截资源

这个在测试中很有用,因为大部分情况下,我们并不需要加载某个页面的图片、CSS之类,通常就是看页面是否正常,代码是否工作。这时就可以考虑将图片、视频这些资源禁止:

await page.setRequestInterception(true); // Optimize (no stylesheets, images)...
page.on("request", (request) => {
  if (
    ["image", "stylesheet", "media"].includes(request.resourceType()) ||
    request.url().endsWith(".wasm")
  ) {
    request.abort();
  } else {
    // console.log("request intercepted", request.url(), request.resourceType());
    request.continue();
  }
});


注意:启用请求拦截器会禁用页面缓存。

截图

全屏截图:

await page.screenshot({ path: 'fullpage.png', fullPage: true });

特定元素截图

const elementHandle = await page.$('#myElement');
await elementHandle.screenshot({ path: 'element.png' });

生成PDF

 await page.pdf({ path: 'example.pdf', format: 'A4' });

关闭页面

await page.close();
await browser.close();

虽然直接用browser.close就可以,在本地开发时并没有错,但我在GitLab CICD流水线上遇到一个问题,报错:
image.png
我这个用例page goto了600多个页面,可能与它有关,在页面少的时候也没事。百思不得其解,尝试加了page.close就好了。

样例

文心一言

使用用户名密码登陆。但卡在需要用手机接收短信验证这一步,没有办法,只能中断。

import puppeteer from "https://deno.land/x/puppeteer@16.2.0/mod.ts";




const browser = await puppeteer.launch({
  headless: false,
  // devtools: true,
  // args: ["--no-sandbox", "--disable-setuid-sandbox"],
});


const page = await browser.newPage();

await page.goto("https://yige.baidu.com/", {
  waitUntil: "domcontentloaded",
});



await page.waitForSelector(".login");


await Promise.all([
  page.click(".login"),
  page.waitForNavigation(),
]);

console.info("跳转到登陆页面成功");
await page.click(".tang-pass-footerBarULogin");
console.log("切换到用户名密码窗口");

await page.type('.pass-text-input-userName', 'xxx', {delay: 100});
await page.type('.pass-text-input-password', 'xxx', {delay: 100});

await page.waitForTimeout(1000);

await page.click(".pass-button-submit");

await page.close();
await browser.close();

扒取Bing图片

import puppeteer from "https://deno.land/x/puppeteer@16.2.0/mod.ts";


import { basename } from "https://deno.land/std@0.191.0/path/mod.ts";
import { ensureDir } from "https://deno.land/std@0.191.0/fs/mod.ts";

const browser = await puppeteer.launch({
  headless: false,
});



async function downloadImages() {
  const page = await browser.newPage();
  await page.goto("https://cn.bing.com/images");

  // 等待页面加载完成
  await page.waitForSelector(".img_cont.hoff");


  // 获取所有图片的DOM元素
  const imageElements = await page.$$(".img_cont.hoff");

  // 创建用于保存图片的文件夹
  await ensureDir("images");

  // 遍历图片元素并下载图片
  for (let i = 0; i < imageElements.length; i++) {
    const imageElement = imageElements[i];
    const imageUrl = await imageElement.$eval("img", (img) => img.src);
    const altText = await imageElement.$eval(
      "img",
      (img) => img.getAttribute("alt"),
    );

    // 下载图片并保存到本地文件夹
    const imageBuffer = await fetch(imageUrl).then((response) =>
      response.arrayBuffer()
    ).catch((err) => {
      console.warn("Failed to load imageUrl: " + imageUrl, err);
    });
    if (imageBuffer) {
      await Deno.writeFile(
        `images/${basename(altText)}.jpg`,
        new Uint8Array(imageBuffer),
      );
    }
  }

  console.log("图片下载完成!");
  await browser.close();
}

downloadImages().catch(console.error);

总结

Puppeteer是一个能够模拟浏览器行为的工具,可以用于获取网页数据、生成截图、自动填写表单等操作。本文分享了一些使用Puppeteer的技巧和样例,不管是Deno还是Node.js都是通用的,希望能帮助到大家。

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYFLWB7z' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片