使用 Puppeteer 和 Clash 代理服务器创建一个截图 API
最近,我一直在研究如何使用 Node.js 搭建一个简单的 API,可以将任意网页 URL 转换成截图。之前我尝试过多种方法,但始终无法解决跨域访问的问题。后来我发现可以通过 Puppeteer 控制无头浏览器来实现截图,再配合 Clash 代理服务器就可以方便地解决频繁访问限制。在这里分享一下我的实现过程。
你只需要准备一个 VPS,安装 Docker, 准备 clash 的配置文件, 就可以开始了。
项目需求
我想要实现一个 RESTful API,接受任意网页 URL 作为参数,返回一张对应网页的截图。主要需求如下:
- 接口路径为
/api
,方法为 GET - 参数为
url
,传入要截图的网址 - 返回 PNG 格式的截图
- 考虑错误处理、访问频率限制等
实现步骤
1. 搭建 Express 服务器
我选择使用 Express 来开发服务端程序。首先初始化一个项目,安装 Express 和 Puppeteer 等必要模块:
npm init -y
npm install express puppeteer
然后编写一个简单的 Express 服务器,监听 7860 端口:
/**
* 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
路由中,我编写了主要业务逻辑:
- 检查
url
参数是否传入 - 校验请求频率避免过载
- 启动无头 Chrome 并设置代理服务器
- 使用 Puppeteer 打开目标网页截图
- 返回 PNG 图片
关键代码如下:
/**
* 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 连续访问网站被封禁的问题。
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 应用,并指定要监听的端口号。
/**
* 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
方法同步读取文件,并将文件内容分割成一个数组。
/**
* 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
,也可以是其他任何你喜欢的名字。
const userAgents = readUserAgentsFromFile("user-agents.txt");
在下面的代码中,我定义了三个不同的路由端点:根路由'/'
用于显示 API 的使用说明,'/status'
用于显示服务器状态信息,'/health'
用于健康检查。
app.get("/", (req, res) => {
// API使用说明
});
app.get("/status", (req, res) => {
// 服务器状态信息
});
app.get("/health", (req, res) => {
// 健康检查
});
接下来是 API 的主要端点'/api'
。当客户端发送 GET 请求到'/api'
时,它需要提供一个'url'
参数,用于指定要截图的网页的 URL。
/**
* 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。
/**
* 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
。
/**
* 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 秒钟。
/**
* 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 图像的缓冲区。
/**
* 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,
});
最后,我将截图作为响应发送回客户端,并在发送响应后关闭浏览器实例。
/**
* 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 映像。
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 镜像:
docker build -t url-to-png-api .
然后,我们可以运行该镜像并映射端口到主机:
docker run -p 7860:7860 -d url-to-png-api
这样,我们就完成了使用 Docker 部署这个 URL 到 PNG 的 API,同时运行 Clash 代理服务器的过程。
Docker 的使用使得部署和管理这个 API 变得更加简单,并且容器化的环境可以提供可靠和可重复的运行。现在,您可以通过 API 调用来生成网页截图,并在需要时结合 Clash 代理服务器使用。
总结
通过上述步骤,我搭建了一个基于 Puppeteer 和 Clash 的网页截图 API,可以稳定地处理跨域网页截图需求。接下来我会继续优化服务性能,并添加身份验证等功能。
Puppeteer 是实现浏览器自动化的好工具,配合代理服务可以大大降低跨域难题。我认为这种组合为服务端渲染提供了新的可能性。
如果你也对此感兴趣,完整代码如下,欢迎参考和改进!
完整代码
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}`);
});
希望这篇文章对您有所帮助!如果您还有其他问题,请随时向我提问。祝您编写出优雅高效的代码!
参考资料
以下是本文章所使用的参考资料:
Puppeteer 官方文档:https://pptr.dev/
- 这是 Puppeteer 的官方文档,提供了详细的 API 参考和使用示例。
Express 官方文档:https://expressjs.com/
- 这是 Express 框架的官方文档,提供了关于 Express 的详细介绍、API 文档和示例代码。
Docker 官方文档:https://docs.docker.com/
- 这是 Docker 的官方文档,包含了有关 Docker 的安装、使用和部署的详细指南。
Clash 代理服务器 GitHub 页面:https://github.com/Dreamacro/clash
- 这是 Clash 代理服务器的 GitHub 页面,其中包含了关于 Clash 的介绍、安装方法以及配置文件的详细说明。
Clash meta 的 GitHub 页面:https://github.com/MetaCubeX/Clash.Meta
- 这是 Clash meta 的 GitHub 页面,其中包含了关于 Clash meta 的介绍、安装方法以及配置文件的详细说明。
Note: 本文由 GPT-4 && Claude 生成,如有雷同,纯属巧合。