Skip to content
On this page

使用 Puppeteer 和 Clash 代理服务器创建一个截图 API

最近,我一直在研究如何使用 Node.js 搭建一个简单的 API,可以将任意网页 URL 转换成截图。之前我尝试过多种方法,但始终无法解决跨域访问的问题。后来我发现可以通过 Puppeteer 控制无头浏览器来实现截图,再配合 Clash 代理服务器就可以方便地解决频繁访问限制。在这里分享一下我的实现过程。

你只需要准备一个 VPS,安装 Docker, 准备 clash 的配置文件, 就可以开始了。

项目需求

我想要实现一个 RESTful API,接受任意网页 URL 作为参数,返回一张对应网页的截图。主要需求如下:

  • 接口路径为 /api,方法为 GET
  • 参数为 url,传入要截图的网址
  • 返回 PNG 格式的截图
  • 考虑错误处理、访问频率限制等

实现步骤

1. 搭建 Express 服务器

我选择使用 Express 来开发服务端程序。首先初始化一个项目,安装 Express 和 Puppeteer 等必要模块:

bash
npm init -y
npm install express puppeteer

然后编写一个简单的 Express 服务器,监听 7860 端口:

js
/**
 * Required dependencies for the application.
 */
const express = require("express");

/**
 * Create an instance of the Express application.
 */
const app = express();

/**
 * The port number that the application will listen on.
 */
const port = 7860;

/**
 * Define a route for the root URL that sends a "Hello World!" message.
 */
app.get("/", (req, res) => {
  res.send("Hello World!");
});

/**
 * Start the application and listen for incoming requests on the specified port.
 */
app.listen(port, () => {
  console.log(`Example app listening on port ${port}`);
});

2. 实现主要业务逻辑

/api 路由中,我编写了主要业务逻辑:

  1. 检查 url 参数是否传入
  2. 校验请求频率避免过载
  3. 启动无头 Chrome 并设置代理服务器
  4. 使用 Puppeteer 打开目标网页截图
  5. 返回 PNG 图片

关键代码如下:

js
/**
 * This route handles GET requests to the '/api' endpoint, which accepts a 'url' query parameter
 * and returns a screenshot of the corresponding webpage in PNG format. The function performs
 * the following steps:
 * 1. Validates the 'url' parameter and checks the request frequency to avoid overloading the server.
 * 2. Launches a headless Chrome browser instance with a proxy server set to 'http://127.0.0.1:7891'.
 * 3. Navigates to the specified URL and takes a screenshot of the page.
 * 4. Sends the PNG image file as a response to the client.
 * 5. Closes the browser instance.
 * @param {Object} req - The HTTP request object.
 * @param {Object} res - The HTTP response object.
 * @returns {Promise<void>} - A Promise that resolves when the response has been sent.
 */
const puppeteer = require("puppeteer");

app.get("/api", async (req, res) => {
  // ...省略校验逻辑

  const browser = await puppeteer.launch({
    headless: true,
    args: [
      "--proxy-server=http://127.0.0.1:7891", // 设置代理
    ],
  });

  const page = await browser.newPage();

  await page.goto(url);
  await page.screenshot({ path: "screenshot.png" });

  res.type("image/png");
  res.sendFile("/screenshot.png");

  await browser.close();
});

3. 连接 Clash 代理

我使用 Docker 来运行 Clash,直接把代理地址设为 http://127.0.0.1:7891 即可。

这样 Puppeteer 就会通过 Clash 发出请求,可以避免使用相同 IP 连续访问网站被封禁的问题。

bash
docker run -d --name clash --net=host -v /clash/config:/root/.config/clash Dreamacro/clash

开发过程

当我开始开发这个 URL 到 PNG 的 API 时,我使用了 Puppeteer 库和 Clash 代理服务器来实现。下面是完整的代码和逐行解释。

首先,我引入了必要的依赖项,包括 Express 框架、Puppeteer、fs 模块和 child_process 模块。然后,我创建了一个 Express 应用,并指定要监听的端口号。

javascript
/**
 * Required dependencies for the application.
 */
const express = require("express");
const puppeteer = require("puppeteer");
const fs = require("fs");
const { exec } = require("child_process");

/**
 * Create an instance of the Express application.
 */
const app = express();

/**
 * The port number that the application will listen on.
 * Defaults to 7860 if not specified in the environment variables.
 */
const port = process.env.PORT || 7860;

/**
 * The hostname of the space where the application is deployed.
 * Defaults to "http://localhost:port" if not specified in the environment variables.
 */
const space_host = process.env.SPACE_HOST || `http://localhost:${port}`;

接下来,我编写了一个辅助函数readUserAgentsFromFile,用于从文件中读取用户代理列表。这个函数使用fs模块的readFileSync方法同步读取文件,并将文件内容分割成一个数组。

javascript
/**
 * Reads user agents from a file and returns them as an array.
 * @param {string} filename - The name of the file to read from.
 * @returns {string[]} An array of user agents.
 */
function readUserAgentsFromFile(filename) {
  try {
    const data = fs.readFileSync(filename, "utf8");
    return data.split("\n");
  } catch (err) {
    console.error(`Error reading file from disk: ${err}`);
  }
  return [];
}

然后,我使用上面的函数从文件中读取用户代理,并将其存储在userAgents数组中。你要准备一个包含用户代理的文本文件,每行一个用户代理。文件名可以是user-agents.txt,也可以是其他任何你喜欢的名字。

javascript
const userAgents = readUserAgentsFromFile("user-agents.txt");

在下面的代码中,我定义了三个不同的路由端点:根路由'/'用于显示 API 的使用说明,'/status'用于显示服务器状态信息,'/health'用于健康检查。

javascript
app.get("/", (req, res) => {
  // API使用说明
});

app.get("/status", (req, res) => {
  // 服务器状态信息
});

app.get("/health", (req, res) => {
  // 健康检查
});

接下来是 API 的主要端点'/api'。当客户端发送 GET 请求到'/api'时,它需要提供一个'url'参数,用于指定要截图的网页的 URL。

javascript
/**
 * Endpoint for generating a screenshot of a specified URL.
 * @name GET /api
 * @function
 * @memberof module:routes
 * @inner
 * @param {string} url - The URL of the webpage to take a screenshot of.
 * @returns {Object} - The generated screenshot image.
 */
app.get("/api", async (req, res) => {
  const urlParam = req.query.url;

  if (!urlParam) {
    return res.status(400).json({ error: "Missing URL parameter" });
  }
  // ...
});

在这个端点的代码中,我首先检查是否提供了正确的 URL 参数。如果没有提供参数,我将返回一个 400 错误。

下一步,我使用 Puppeteer 库来启动一个无头 Chrome 实例,并与 Clash 代理服务器建立连接。我使用了puppeteer.launch方法,并传递了headless: true选项,以在无头模式下运行 Chrome。

javascript
/**
 * Launches a new instance of Puppeteer with the specified options.
 * @param {boolean} headless - Whether to run Puppeteer in headless mode.
 * @param {string[]} args - Additional arguments to pass to the Chromium process.
 * @returns {Promise<Object>} - A Promise that resolves to a Puppeteer browser instance.
 */
const browser = await puppeteer.launch({
  headless: true,
  args: [
    "--no-sandbox",
    "--disable-setuid-sandbox",
    "--proxy-server=socks5://127.0.0.1:7891",
  ],
});

同时,我将 Clash 代理服务器的地址和端口指定为参数--proxy-server=socks5://127.0.0.1:7891

然后,我创建一个新的页面,并设置视窗的大小。我从用户代理列表中选择一个随机的用户代理,并将其设置为页面的User-Agent

javascript
/**
 * Creates a new Puppeteer page, sets the viewport size to 1366x762, selects a random user agent from the `userAgents` array, and sets it as the page's user agent.
 * @returns {Promise<Object>} - A Promise that resolves to a Puppeteer page instance.
 */
const page = await browser.newPage();
await page.setViewport({
  width: 1366,
  height: 762,
});

const userAgent = userAgents[Math.floor(Math.random() * userAgents.length)];
await page.setUserAgent(userAgent);

现在,Puppeteer 会导航到提供的 URL,并等待页面完全加载。在这里,我们可以自定义等待的时间,这里我等待了 4 秒钟。

javascript
/**
 * Navigate to the URL provided in the `urlParam` query parameter and wait for 4 seconds for the page to fully load before taking a screenshot.
 */
await page.goto(urlParam);
await new Promise((resolve) => setTimeout(resolve, 4000));

等待页面加载完成后,我使用page.screenshot方法对页面进行截图,将截图保存为 PNG 图像的缓冲区。

javascript
/**
 * Takes a screenshot of the entire page and returns it as a PNG image buffer.
 * @returns {Promise<Buffer>} - A Promise that resolves to a PNG image buffer.
 */
const screenshotBuffer = await page.screenshot({
  fullpage: true,
});

最后,我将截图作为响应发送回客户端,并在发送响应后关闭浏览器实例。

javascript
/**
 * Set the response content type to PNG, send the screenshot buffer as the response body, and close the Puppeteer browser instance.
 */
res.set("Content-Type", "image/png");
res.send(screenshotBuffer);
await browser.close();

这就是整个 API 的主要代码流程。使用 Puppeteer 和 Clash 代理服务器,您可以创建一个功能强大的 URL 到 PNG 的 API,能够捕捉网页截图并提供给客户端。

4. 优化访问体验

为了提高服务稳定性,我加入了以下几点优化:

  • 设置视窗大小避免截图失真
  • 随机选择 user-agent
  • 增加加载等待时间
  • 实现频率限制避免攻击
  • 增加健康检查接口
  • 添加更详细的错误处理

Docker 部署 Puppeteer 服务和 Clash 代理

在部署这个 URL 到 PNG 的 API 时,我们可以使用 Docker 来简化整个过程。下面是一个示例的 Dockerfile,可以用于构建具有 Puppeteer 和 Clash 代理服务器的 Docker 映像。

dockerfile
FROM alpine

# 安装最新的Chromium软件包
RUN apk add --no-cache \
      chromium \
      nss \
      freetype \
      harfbuzz \
      ca-certificates \
      ttf-freefont \
      font-adobe-100dpi \
      ttf-dejavu \
      fontconfig \
      nodejs \
      yarn

# 设置Puppeteer跳过安装Chrome
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser \
    PORT=7860

# 安装Puppeteer和Express
RUN yarn add puppeteer express

# 添加非特权用户
RUN adduser -D -u 1000 pptruser \
    && mkdir -p /home/pptruser/Downloads /app \
    && chown -R pptruser:pptruser /home/pptruser \
    && chown -R pptruser:pptruser /app

WORKDIR /puppeteer

# 下载并安装Clash代理服务器
RUN wget -t 2 -T 10 https://github.com/MetaCubeX/Clash.Meta/releases/download/v1.15.0/clash.meta-linux-amd64-v1.15.0.gz && \
    gzip -d clash.meta-linux-amd64-v1.15.0.gz && \
    mv clash.meta-linux-amd64-v1.15.0 clash && \
    chmod +x clash && \
    rm -rf clash.meta-linux-amd64-v1.15.0.gz

# 将项目文件复制到容器中
COPY . .
RUN chown -R pptruser:pptruser / 2>/dev/null || true

# 以非特权用户身份运行容器。将端口7860暴露给主机。

USER pptruser

EXPOSE 7860

# 启动Clash代理服务器和API应用
CMD ["sh", "-c", "./clash -f config.yaml -d /puppeteer & node index.js"]

现在,我们可以通过以下命令构建 Docker 镜像:

shell
docker build -t url-to-png-api .

然后,我们可以运行该镜像并映射端口到主机:

shell
docker run -p 7860:7860 -d url-to-png-api

这样,我们就完成了使用 Docker 部署这个 URL 到 PNG 的 API,同时运行 Clash 代理服务器的过程。

Docker 的使用使得部署和管理这个 API 变得更加简单,并且容器化的环境可以提供可靠和可重复的运行。现在,您可以通过 API 调用来生成网页截图,并在需要时结合 Clash 代理服务器使用。

总结

通过上述步骤,我搭建了一个基于 Puppeteer 和 Clash 的网页截图 API,可以稳定地处理跨域网页截图需求。接下来我会继续优化服务性能,并添加身份验证等功能。

Puppeteer 是实现浏览器自动化的好工具,配合代理服务可以大大降低跨域难题。我认为这种组合为服务端渲染提供了新的可能性。

如果你也对此感兴趣,完整代码如下,欢迎参考和改进!

完整代码

js
const express = require("express");
const puppeteer = require("puppeteer");
const fs = require("fs"); // 引入fs模块
const { exec } = require("child_process");
const app = express();
const port = process.env.PORT || 7860;
const space_host = process.env.SPACE_HOST || `http://localhost:${port}`;

// 创建一个 user-agent 数组
function readUserAgentsFromFile(filename) {
  try {
    // 同步读取文件,将文件内容分割为数组(每行一个元素)
    const data = fs.readFileSync(filename, "utf8");
    return data.split("\n");
  } catch (err) {
    console.error(`Error reading file from disk: ${err}`);
  }
  // 如果读取失败,返回一个空数组
  return [];
}

// 使用上面的函数从文件中读取 user-agents
const userAgents = readUserAgentsFromFile("user-agents.txt");

// 创建一个记录访问 URL 的对象
let visitRecord = {};

app.get("/", (req, res) => {
  const usage = `
    <h1>API usage</h1>
    <p><b>GET ${space_host}/api?url={URL}</b></p>
    <ul>
      <li>{URL} - The URL of the webpage to screenshot.</li>
    </ul>`;
  res.set("Content-Type", "text/html");
  res.send(usage);
});

app.get("/status", (req, res) => {
  exec("ps -ef && ls -a -l", (err, stdout, stderr) => {
    if (err) {
      console.log(err);
      res.status(500).send(`<pre>${stderr}<pre>`);
    }
    res.send(`<pre>${stdout}<pre>`);
  });
});

app.get("/health", (req, res) => {
  console.log("health check");
  res.status(200).json({ message: "ok" });
});

app.get("/api", async (req, res) => {
  const urlParam = req.query.url;

  if (!urlParam) {
    return res.status(400).json({ error: "Missing URL parameter" });
  }

  // 检查是否超过了频率限制 (5分钟内只能访问一次)
  if (
    visitRecord[urlParam] &&
    Date.now() - visitRecord[urlParam] <= 5 * 60 * 1000
  ) {
    console.log("visitRecord: " + visitRecord[urlParam]);
    return res.status(429).json({ error: "Too Many Requests" });
  }

  try {
    const browser = await puppeteer.launch({
      headless: true,
      args: [
        "--no-sandbox",
        "--disable-setuid-sandbox",
        "--proxy-server=socks5://127.0.0.1:7891",
      ],
    });
    const page = await browser.newPage();

    // 设置视窗大小(可选)
    await page.setViewport({
      width: 1366,
      height: 762,
    });

    // 选择一个随机的 user-agent
    const userAgent = userAgents[Math.floor(Math.random() * userAgents.length)];

    // 设置 user-agent
    await page.setUserAgent(userAgent);

    // 模拟浏览器访问目标Url
    await page.goto(urlParam);

    visitRecord[urlParam] = Date.now(); // 更新访问记录

    // 等待加载完成(可根据情况调整等待时间)
    await new Promise((resolve) => setTimeout(resolve, 4000));

    const screenshotBuffer = await page.screenshot({
      fullpage: true,
    });

    res.set("Content-Type", "image/png");
    res.send(screenshotBuffer);

    await browser.close(); // 在这里添加浏览器关闭的代码段
  } catch (err) {
    console.log(`Screenshot failed : ${err}`);
    return res.sendStatus(500);
  }
});

app.listen(port, () => {
  console.log(`Server started on port http://localhost:${port}`);
});

希望这篇文章对您有所帮助!如果您还有其他问题,请随时向我提问。祝您编写出优雅高效的代码!

参考资料

以下是本文章所使用的参考资料:

  1. Puppeteer 官方文档:https://pptr.dev/

    • 这是 Puppeteer 的官方文档,提供了详细的 API 参考和使用示例。
  2. Express 官方文档:https://expressjs.com/

    • 这是 Express 框架的官方文档,提供了关于 Express 的详细介绍、API 文档和示例代码。
  3. Docker 官方文档:https://docs.docker.com/

    • 这是 Docker 的官方文档,包含了有关 Docker 的安装、使用和部署的详细指南。
  4. Clash 代理服务器 GitHub 页面:https://github.com/Dreamacro/clash

    • 这是 Clash 代理服务器的 GitHub 页面,其中包含了关于 Clash 的介绍、安装方法以及配置文件的详细说明。
  5. Clash meta 的 GitHub 页面:https://github.com/MetaCubeX/Clash.Meta

    • 这是 Clash meta 的 GitHub 页面,其中包含了关于 Clash meta 的介绍、安装方法以及配置文件的详细说明。

Note: 本文由 GPT-4 && Claude 生成,如有雷同,纯属巧合。

上次更新于: