1. 安装 puppeteer

安装puppeteer时会自动下载一个Chromium,所以比较慢。

  1. npm install puppeteer

2. 测试 puppeteer

安装完成后测试一下puppeteer是否可以正常运行,我们尝试打开启动一个无头浏览器,对百度首页进行自动截图操作。

  1. const puppeteer = require("puppeteer");
  2. (async () => {
  3. const browser = await puppeteer.launch({
  4. headless: true,
  5. args: ["--no-sandbox", "--disable-setuid-sandbox"],
  6. });
  7. const page = await browser.newPage();
  8. await page.goto("https://www.baidu.com");
  9. await page.screenshot({ path: "example.png" });
  10. await browser.close();
  11. })();

运行以上代码,会发现多了一个example.png文件,打开会发现时百度首页的截图。若不是,则检查安装环境。

3. 渲染一个vue单页面

以下代码的含义就是启动一个无头浏览器,打开一个新标签页,请求网址http://www.baidu.com,获取页面渲染后html的内容。是不是很简单!

  1. const puppeteer = require("puppeteer");
  2. (async () => {
  3. const browser = await puppeteer.launch({
  4. headless: true,
  5. args: ["--no-sandbox", "--disable-setuid-sandbox"],
  6. });
  7. const page = await browser.newPage();
  8. await page.goto('http://www.baidu.com', {
  9. timeout: 0, //连接超时时间,单位ms
  10. waitUntil: "networkidle0", //网络空闲说明已加载完毕
  11. });
  12. var html = await page.evaluate(() => {
  13. return document.getElementsByTagName("html")[0].outerHTML;
  14. });
  15. console.log(html);
  16. await browser.close();
  17. })();

4. 在服务器上部署一个ssr服务

1. 启动多个浏览器

为什么要启动多个呢?防止同一时刻收到多个请求,具体启动多少根据自己的业务场景和服务器配置自行决定。这里启动了2个浏览器。

  1. const puppeteer = require("puppeteer");
  2. const MAX_WSE = 2; //启动几个浏览器
  3. let WSE_LIST = []; //存储browserWSEndpoint列表
  4. (async () => {
  5. for (var i = 0; i < MAX_WSE; i++) {
  6. const browser = await puppeteer.launch({
  7. //无头模式
  8. headless: true,
  9. //参数
  10. args: [
  11. "--no-sandbox",
  12. "--no-zygote",
  13. "--single-process",
  14. ],
  15. });
  16. browserWSEndpoint = await browser.wsEndpoint();
  17. WSE_LIST.push(browserWSEndpoint);
  18. }
  19. })();
2. 渲染抓取链接

上面我们启动了两个浏览器,这里随机获取一个。

  1. const spider = async (url, userAgent) => {
  2. let tmp = Math.floor(Math.random() * WSE_LIST.length);
  3. //随机获取浏览器
  4. let browserWSEndpoint = WSE_LIST[tmp];
  5. //连接
  6. const browser = await puppeteer.connect({
  7. browserWSEndpoint,
  8. });
  9. //打开一个标签页
  10. var page = await browser.newPage();
  11. page.setUserAgent(userAgent);
  12. //打开网页
  13. await page.goto(url, {
  14. timeout: 0, //连接超时时间,单位ms
  15. waitUntil: "networkidle0", //网络空闲说明已加载完毕
  16. });
  17. // 操作前等待1000毫秒后执行,第一个方法在新版本中已废弃,也可以使用setTimeout代替
  18. // await page.waitForTimeout(1000);
  19. // await new Promise(r => setTimeout(r, 1000));
  20. // 获取页面内容
  21. var html = await page.evaluate(() => {
  22. /**
  23. * 这里可以对页面的内容进行操作,比如替换图片路径,删除不需要的标签等
  24. */
  25. // 如果vue单页面应用中使用了图片懒加载,一般img图片的路径都是data-src,所以我们把data-src替换为src
  26. var imgEl = document.getElementsByTagName("img");
  27. for (var i = 0; i < imgEl.length; i++) {
  28. if (imgEl[i].getAttribute("data-src")) {
  29. imgEl[i].setAttribute(
  30. "src",
  31. imgEl[i].getAttribute("data-src")
  32. );
  33. imgEl[i].removeAttribute("data-src");
  34. }
  35. }
  36. // 对页面标签进行删除
  37. var elements = document.getElementsByClassName("dia_login");
  38. while (elements.length > 0) {
  39. elements[0].remove();
  40. }
  41. return document.getElementsByTagName("html")[0].outerHTML;
  42. });
  43. await page.close();
  44. return html;
  45. };
3. 数据缓存、日志记录
  • 改一个系统的log方法,输入日志到文件中。
    1. var trueLog = console.log;
    2. console.log = function (msg) {
    3. const date = new Date();
    4. const name = `${date.getFullYear()}-${(date.getMonth() + 1)}-${date.getDate()}`;
    5. fs.appendFile(`./log/${name}.log`, `\r\n${new Date().toLocaleString()} ${msg}`, function (err) {
    6. if (err) {
    7. return trueLog(err);
    8. }
    9. });
    10. };
  • 一个url请求一次就渲染一个有点浪费资源,所以我们把渲染完成的内容缓存到文件中,下次直接在读取文件中内容,文件缓存多久根据业务场景自行控制。
  • 下面我们使用express, 启动一个服务,监听5000端口。下面demo中会根据UA判断是否移动端,缓存不同的页面,并且使用minify对内容进行压缩。
  1. app.get("*", async (req, res, next) => {
  2. // 部署到服务器的完整URL
  3. var ip = req.headers["x-real-ip"] ? req.headers["x-real-ip"] : req.ip.replace(/::ffff:/, "");
  4. var url = req.protocol + "://" + "www.baidu.com" + req.originalUrl;
  5. var userAgent = req.headers["user-agent"];
  6. console.log("\r\n请求的完整URL:" + url);
  7. console.log(ip + ": " + userAgent);
  8. var cache_url = url.replace(/[./:]/g, "-");
  9. // 根据请求的UA判断是否是移动端,抓取不同站点获取其他操作
  10. if (/Android|webOS|iPhone|iPod|BlackBerry/i.test(userAgent)) {
  11. 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";
  12. cache_url = "wap---" + cache_url;
  13. } else {
  14. userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36";
  15. }
  16. console.log(cache_url);
  17. fs.access("./cache/" + cache_url + ".html", fs.constants.F_OK, (err) => {
  18. if (!err) {
  19. console.log("缓存文件存在");
  20. fs.readFile("./cache/" + cache_url + ".html", "utf8", function (err, dataStr) {
  21. if (dataStr) {
  22. res.send(dataStr);
  23. return;
  24. }
  25. });
  26. return;
  27. }
  28. console.log("开始抓取");
  29. spider(url, userAgent).then((resP) => {
  30. console.log("抓取成功");
  31. let content = minify(resP, {
  32. removeComments: true,
  33. collapseWhitespace: true,
  34. minifyJS: true,
  35. minifyCSS: true,
  36. });
  37. fs.writeFile("./cache/" + cache_url + ".html", content, function (err) {
  38. if (err) {
  39. return trueLog(err);
  40. }
  41. });
  42. res.send(content);
  43. }).catch((err) => {
  44. console.log(err);
  45. console.log('抓取失败');
  46. });
  47. });
  48. });
  49. //监听5000端口
  50. app.listen(5000, () => {
  51. console.log("预渲染服务已启动!");
  52. });
  • node index.js 运行就OK了。

4. 服务器配置

服务器上可以通过判断UA,通过反向代理进行请求转发,nginx上可以如下配置

  1. if ($http_user_agent ~* "spider|bot") {
  2. proxy_pass http://localhost:5000;
  3. break;
  4. }

完整代码如下,涉及的依赖包自行安装

  1. npm install puppeteer express compression html-minifier
  1. const puppeteer = require("puppeteer");
  2. var express = require("express");
  3. var compression = require("compression");
  4. var minify = require("html-minifier").minify;
  5. var fs = require("fs");
  6. var app = express();
  7. app.use(compression());
  8. const MAX_WSE = 2; //启动几个浏览器
  9. let WSE_LIST = []; //存储browserWSEndpoint列表
  10. (async () => {
  11. for (var i = 0; i < MAX_WSE; i++) {
  12. const browser = await puppeteer.launch({
  13. //无头模式
  14. headless: true,
  15. //参数
  16. args: ["--no-sandbox", "--no-zygote", "--single-process"],
  17. });
  18. browserWSEndpoint = await browser.wsEndpoint();
  19. WSE_LIST.push(browserWSEndpoint);
  20. }
  21. })();
  22. const spider = async (url, userAgent) => {
  23. let tmp = Math.floor(Math.random() * WSE_LIST.length);
  24. //随机获取浏览器
  25. let browserWSEndpoint = WSE_LIST[tmp];
  26. //连接
  27. const browser = await puppeteer.connect({
  28. browserWSEndpoint,
  29. });
  30. //打开一个标签页
  31. var page = await browser.newPage();
  32. page.setUserAgent(userAgent);
  33. //打开网页
  34. await page.goto(url, {
  35. timeout: 0, //连接超时时间,单位ms
  36. waitUntil: "networkidle0", //网络空闲说明已加载完毕
  37. });
  38. // 操作前等待1000毫秒后执行,第一个方法在新版本中已废弃,也可以使用setTimeout代替
  39. // await page.waitForTimeout(1000);
  40. // await new Promise(r => setTimeout(r, 1000));
  41. // 获取页面内容
  42. var html = await page.evaluate(() => {
  43. /**
  44. * 这里可以对页面的内容进行操作,比如替换图片路径,删除不需要的标签等
  45. */
  46. // 如果vue单页面应用中使用了图片懒加载,一般img图片的路径都是data-src,所以我们把data-src替换为src
  47. var imgEl = document.getElementsByTagName("img");
  48. for (var i = 0; i < imgEl.length; i++) {
  49. if (imgEl[i].getAttribute("data-src")) {
  50. imgEl[i].setAttribute("src", imgEl[i].getAttribute("data-src"));
  51. imgEl[i].removeAttribute("data-src");
  52. }
  53. }
  54. // 对页面标签进行删除
  55. var elements = document.getElementsByClassName("dia_login");
  56. while (elements.length > 0) {
  57. elements[0].remove();
  58. }
  59. return document.getElementsByTagName("html")[0].outerHTML;
  60. });
  61. await page.close();
  62. return html;
  63. };
  64. var trueLog = console.log;
  65. console.log = function (msg) {
  66. const date = new Date();
  67. const name = `${date.getFullYear()}-${(date.getMonth() + 1)}-${date.getDate()}`;
  68. fs.appendFile(`./log/${name}.log`, `\r\n${new Date().toLocaleString()} ${msg}`, function (err) {
  69. if (err) {
  70. return trueLog(err);
  71. }
  72. });
  73. };
  74. app.get("*", async (req, res, next) => {
  75. // 部署到服务器的完整URL
  76. var ip = req.headers["x-real-ip"] ? req.headers["x-real-ip"] : req.ip.replace(/::ffff:/, "");
  77. var url = req.protocol + "://" + "www.mcwzg.com" + req.originalUrl;
  78. var userAgent = req.headers["user-agent"];
  79. console.log("\r\n请求的完整URL:" + url);
  80. console.log(ip + ": " + userAgent);
  81. var cache_url = url.replace(/[./:]/g, "-");
  82. // 根据请求的UA判断是否是移动端,抓取不同站点获取其他操作
  83. if (/Android|webOS|iPhone|iPod|BlackBerry/i.test(userAgent)) {
  84. 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";
  85. cache_url = "wap---" + cache_url;
  86. } else {
  87. userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36";
  88. }
  89. console.log(cache_url);
  90. fs.access("./cache/" + cache_url + ".html", fs.constants.F_OK, (err) => {
  91. if (!err) {
  92. console.log("缓存文件存在");
  93. fs.readFile("./cache/" + cache_url + ".html", "utf8", function (err, dataStr) {
  94. if (dataStr) {
  95. res.send(dataStr);
  96. return;
  97. }
  98. });
  99. return;
  100. }
  101. console.log("开始抓取");
  102. spider(url, userAgent).then((resP) => {
  103. console.log("抓取成功");
  104. let content = minify(resP, {
  105. removeComments: true,
  106. collapseWhitespace: true,
  107. minifyJS: true,
  108. minifyCSS: true,
  109. });
  110. fs.writeFile("./cache/" + cache_url + ".html", content, function (err) {
  111. if (err) {
  112. return trueLog(err);
  113. }
  114. });
  115. res.send(content);
  116. }).catch((err) => {
  117. console.log(err);
  118. console.log('抓取失败');
  119. });
  120. });
  121. });
  122. //监听5000端口
  123. app.listen(5000, () => {
  124. console.log("预渲染服务已启动!");
  125. });

Puppeteer 中文文档