1. 安装 puppeteer
安装puppeteer
时会自动下载一个Chromium
,所以比较慢。
npm install puppeteer
2. 测试 puppeteer
安装完成后测试一下puppeteer
是否可以正常运行,我们尝试打开启动一个无头浏览器,对百度首页进行自动截图操作。
const puppeteer = require("puppeteer");
(async () => {
const browser = await puppeteer.launch({
headless: true,
args: ["--no-sandbox", "--disable-setuid-sandbox"],
});
const page = await browser.newPage();
await page.goto("https://www.baidu.com");
await page.screenshot({ path: "example.png" });
await browser.close();
})();
运行以上代码,会发现多了一个example.png
文件,打开会发现时百度首页的截图。若不是,则检查安装环境。
3. 渲染一个vue单页面
以下代码的含义就是启动一个无头浏览器,打开一个新标签页,请求网址http://www.baidu.com
,获取页面渲染后html
的内容。是不是很简单!
const puppeteer = require("puppeteer");
(async () => {
const browser = await puppeteer.launch({
headless: true,
args: ["--no-sandbox", "--disable-setuid-sandbox"],
});
const page = await browser.newPage();
await page.goto('http://www.baidu.com', {
timeout: 0, //连接超时时间,单位ms
waitUntil: "networkidle0", //网络空闲说明已加载完毕
});
var html = await page.evaluate(() => {
return document.getElementsByTagName("html")[0].outerHTML;
});
console.log(html);
await browser.close();
})();
4. 在服务器上部署一个ssr服务
1. 启动多个浏览器
为什么要启动多个呢?防止同一时刻收到多个请求,具体启动多少根据自己的业务场景和服务器配置自行决定。这里启动了2个浏览器。
const puppeteer = require("puppeteer");
const MAX_WSE = 2; //启动几个浏览器
let WSE_LIST = []; //存储browserWSEndpoint列表
(async () => {
for (var i = 0; i < MAX_WSE; i++) {
const browser = await puppeteer.launch({
//无头模式
headless: true,
//参数
args: [
"--no-sandbox",
"--no-zygote",
"--single-process",
],
});
browserWSEndpoint = await browser.wsEndpoint();
WSE_LIST.push(browserWSEndpoint);
}
})();
2. 渲染抓取链接
上面我们启动了两个浏览器,这里随机获取一个。
const spider = async (url, userAgent) => {
let tmp = Math.floor(Math.random() * WSE_LIST.length);
//随机获取浏览器
let browserWSEndpoint = WSE_LIST[tmp];
//连接
const browser = await puppeteer.connect({
browserWSEndpoint,
});
//打开一个标签页
var page = await browser.newPage();
page.setUserAgent(userAgent);
//打开网页
await page.goto(url, {
timeout: 0, //连接超时时间,单位ms
waitUntil: "networkidle0", //网络空闲说明已加载完毕
});
// 操作前等待1000毫秒后执行,第一个方法在新版本中已废弃,也可以使用setTimeout代替
// await page.waitForTimeout(1000);
// await new Promise(r => setTimeout(r, 1000));
// 获取页面内容
var html = await page.evaluate(() => {
/**
* 这里可以对页面的内容进行操作,比如替换图片路径,删除不需要的标签等
*/
// 如果vue单页面应用中使用了图片懒加载,一般img图片的路径都是data-src,所以我们把data-src替换为src
var imgEl = document.getElementsByTagName("img");
for (var i = 0; i < imgEl.length; i++) {
if (imgEl[i].getAttribute("data-src")) {
imgEl[i].setAttribute(
"src",
imgEl[i].getAttribute("data-src")
);
imgEl[i].removeAttribute("data-src");
}
}
// 对页面标签进行删除
var elements = document.getElementsByClassName("dia_login");
while (elements.length > 0) {
elements[0].remove();
}
return document.getElementsByTagName("html")[0].outerHTML;
});
await page.close();
return html;
};
3. 数据缓存、日志记录
- 改一个系统的log方法,输入日志到文件中。
var trueLog = console.log;
console.log = function (msg) {
const date = new Date();
const name = `${date.getFullYear()}-${(date.getMonth() + 1)}-${date.getDate()}`;
fs.appendFile(`./log/${name}.log`, `\r\n${new Date().toLocaleString()} ${msg}`, function (err) {
if (err) {
return trueLog(err);
}
});
};
- 一个url请求一次就渲染一个有点浪费资源,所以我们把渲染完成的内容缓存到文件中,下次直接在读取文件中内容,文件缓存多久根据业务场景自行控制。
- 下面我们使用
express
, 启动一个服务,监听5000
端口。下面demo中会根据UA
判断是否移动端,缓存不同的页面,并且使用minify
对内容进行压缩。
app.get("*", async (req, res, next) => {
// 部署到服务器的完整URL
var ip = req.headers["x-real-ip"] ? req.headers["x-real-ip"] : req.ip.replace(/::ffff:/, "");
var url = req.protocol + "://" + "www.baidu.com" + req.originalUrl;
var userAgent = req.headers["user-agent"];
console.log("\r\n请求的完整URL:" + url);
console.log(ip + ": " + userAgent);
var cache_url = url.replace(/[./:]/g, "-");
// 根据请求的UA判断是否是移动端,抓取不同站点获取其他操作
if (/Android|webOS|iPhone|iPod|BlackBerry/i.test(userAgent)) {
userAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1";
cache_url = "wap---" + cache_url;
} else {
userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36";
}
console.log(cache_url);
fs.access("./cache/" + cache_url + ".html", fs.constants.F_OK, (err) => {
if (!err) {
console.log("缓存文件存在");
fs.readFile("./cache/" + cache_url + ".html", "utf8", function (err, dataStr) {
if (dataStr) {
res.send(dataStr);
return;
}
});
return;
}
console.log("开始抓取");
spider(url, userAgent).then((resP) => {
console.log("抓取成功");
let content = minify(resP, {
removeComments: true,
collapseWhitespace: true,
minifyJS: true,
minifyCSS: true,
});
fs.writeFile("./cache/" + cache_url + ".html", content, function (err) {
if (err) {
return trueLog(err);
}
});
res.send(content);
}).catch((err) => {
console.log(err);
console.log('抓取失败');
});
});
});
//监听5000端口
app.listen(5000, () => {
console.log("预渲染服务已启动!");
});
node index.js
运行就OK了。
4. 服务器配置
服务器上可以通过判断UA
,通过反向代理进行请求转发,nginx上可以如下配置
if ($http_user_agent ~* "spider|bot") {
proxy_pass http://localhost:5000;
break;
}
完整代码如下,涉及的依赖包自行安装
npm install puppeteer express compression html-minifier
const puppeteer = require("puppeteer");
var express = require("express");
var compression = require("compression");
var minify = require("html-minifier").minify;
var fs = require("fs");
var app = express();
app.use(compression());
const MAX_WSE = 2; //启动几个浏览器
let WSE_LIST = []; //存储browserWSEndpoint列表
(async () => {
for (var i = 0; i < MAX_WSE; i++) {
const browser = await puppeteer.launch({
//无头模式
headless: true,
//参数
args: ["--no-sandbox", "--no-zygote", "--single-process"],
});
browserWSEndpoint = await browser.wsEndpoint();
WSE_LIST.push(browserWSEndpoint);
}
})();
const spider = async (url, userAgent) => {
let tmp = Math.floor(Math.random() * WSE_LIST.length);
//随机获取浏览器
let browserWSEndpoint = WSE_LIST[tmp];
//连接
const browser = await puppeteer.connect({
browserWSEndpoint,
});
//打开一个标签页
var page = await browser.newPage();
page.setUserAgent(userAgent);
//打开网页
await page.goto(url, {
timeout: 0, //连接超时时间,单位ms
waitUntil: "networkidle0", //网络空闲说明已加载完毕
});
// 操作前等待1000毫秒后执行,第一个方法在新版本中已废弃,也可以使用setTimeout代替
// await page.waitForTimeout(1000);
// await new Promise(r => setTimeout(r, 1000));
// 获取页面内容
var html = await page.evaluate(() => {
/**
* 这里可以对页面的内容进行操作,比如替换图片路径,删除不需要的标签等
*/
// 如果vue单页面应用中使用了图片懒加载,一般img图片的路径都是data-src,所以我们把data-src替换为src
var imgEl = document.getElementsByTagName("img");
for (var i = 0; i < imgEl.length; i++) {
if (imgEl[i].getAttribute("data-src")) {
imgEl[i].setAttribute("src", imgEl[i].getAttribute("data-src"));
imgEl[i].removeAttribute("data-src");
}
}
// 对页面标签进行删除
var elements = document.getElementsByClassName("dia_login");
while (elements.length > 0) {
elements[0].remove();
}
return document.getElementsByTagName("html")[0].outerHTML;
});
await page.close();
return html;
};
var trueLog = console.log;
console.log = function (msg) {
const date = new Date();
const name = `${date.getFullYear()}-${(date.getMonth() + 1)}-${date.getDate()}`;
fs.appendFile(`./log/${name}.log`, `\r\n${new Date().toLocaleString()} ${msg}`, function (err) {
if (err) {
return trueLog(err);
}
});
};
app.get("*", async (req, res, next) => {
// 部署到服务器的完整URL
var ip = req.headers["x-real-ip"] ? req.headers["x-real-ip"] : req.ip.replace(/::ffff:/, "");
var url = req.protocol + "://" + "www.mcwzg.com" + req.originalUrl;
var userAgent = req.headers["user-agent"];
console.log("\r\n请求的完整URL:" + url);
console.log(ip + ": " + userAgent);
var cache_url = url.replace(/[./:]/g, "-");
// 根据请求的UA判断是否是移动端,抓取不同站点获取其他操作
if (/Android|webOS|iPhone|iPod|BlackBerry/i.test(userAgent)) {
userAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1";
cache_url = "wap---" + cache_url;
} else {
userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36";
}
console.log(cache_url);
fs.access("./cache/" + cache_url + ".html", fs.constants.F_OK, (err) => {
if (!err) {
console.log("缓存文件存在");
fs.readFile("./cache/" + cache_url + ".html", "utf8", function (err, dataStr) {
if (dataStr) {
res.send(dataStr);
return;
}
});
return;
}
console.log("开始抓取");
spider(url, userAgent).then((resP) => {
console.log("抓取成功");
let content = minify(resP, {
removeComments: true,
collapseWhitespace: true,
minifyJS: true,
minifyCSS: true,
});
fs.writeFile("./cache/" + cache_url + ".html", content, function (err) {
if (err) {
return trueLog(err);
}
});
res.send(content);
}).catch((err) => {
console.log(err);
console.log('抓取失败');
});
});
});
//监听5000端口
app.listen(5000, () => {
console.log("预渲染服务已启动!");
});