node抓图片资源数据

介绍

近期了解了下关于如何使用node抓取数据资源,可能这个对于前端实际开发中用处不大,但个人觉得对于前端开发者提升自己能力,扩展技能是很有帮助的,而且这本身也比较有趣。

那么下面就介绍下我用到的两款抓包插件:cheeriopuppeteer

说明:

  1. 代码运行的环境是基于node的,但是并没有要求读者必须对node很熟悉,因此对于部分node不太了解的读者,也可以先行尝试,再去了解其中部分node的api即可。
  2. 数据爬取采用的是获取页面的dom元素,比如获取到img元素,拿到src上的图片地址数据即可;

补充:本人爬取了两个网站的部分图片资源,爬取过程发现网页渲染模式不一样,因此获取资源方式也就不同,故分两种情况讲解,读者实际尝试时也可以就情况使用。

方式1:使用cheerio

爬取对象:豆瓣电影top250。

node抓图片资源数据_第1张图片
分析:

  • 在测试网页的时候,会发现这里设置了分页,每页25条,通过控制query参数start等于多少去获取以该条为起点的后续25条数据;
  • 右键查看源码会发现,网页渲染top250数据的dom元素是直接写出来的,每一页都是一个单独的html文件,数据不是根据动态js脚本获取的,这种情况我们就可以直接获取页面的dom元素了(动态渲染的会有时间差,不能保证能拿到dom);
    node抓图片资源数据_第2张图片
    下面附上代码,代码逻辑还是比较简单清晰的:
const cheerio = require("cheerio"); // 获取页面dom的插件
const https = require("https"); // 请求数据插件
const fs = require("fs");
let dataArr = []; // 全局存储数据

// 封装获取分页数据:startNum为数据起始位置,同地址栏的start参数
function getPageData(startNum) {
  let htmlStr = "";
  return new Promise((resolve, reject) => {
    https.get(`https://movie.douban.com/top250?start=${startNum}`, (res) => {
      // 监听请求到的数据
      res.on("data", (chunk) => {
        // 获取html结构:拼接获取完整的html内容字符串
        htmlStr += chunk;
      });
      // 监听结束
      res.on("end", () => {
        // 获取html中的数据
        const $ = cheerio.load(htmlStr);
        let allFiles = [];
        // 获取每一个item的数据
        $("li .item").each(function (index) {
          const title = $(".title", this).text();
          const star = $(".info .bd .star .rating_num", this).text();
          const pic = $(".pic img", this).attr("src");
          allFiles.push({
            title,
            star,
            pic,
            id: dataArr.length + index + 1 + "",
          });
        });
        resolve(allFiles);
      });
    });
  });
}
let step = 25; // 豆瓣每页获取条数固定的25条,因此此处步数按25来
// 递归获取所有页数据
async function recuseGetData() {
  let startNum = 0;
  let resArr = [];
  do {
    resArr = [];
    resArr = await getPageData(startNum);
    dataArr = dataArr.concat(resArr);
    startNum += step;
  } while (resArr.length > 0);
}
(async function () {
  await recuseGetData();
  // 写入文件
  fs.writeFile("./files.json", JSON.stringify(dataArr), (err, data) => {
    if (err) {
      throw err;
    }
    console.log("文件保存成功");
  });
})();

node抓图片资源数据_第3张图片
上述是爬取豆瓣top250的代码逻辑,整体还是比较简单。

方式2:使用puppeteer

大部分利用node抓包的用户都是使用puppeteer这款插件的,下面介绍下使用方式。

爬取对象:淘宝首页的推荐图片

node抓图片资源数据_第4张图片

分析:

  • 第一次尝试,我使用方式1的cheerio去抓取数据的,但是抓失败了,就很奇怪,然后查资料,右键查看源码,发现html文件并没有我要抓取的图片dom,明白淘宝这个跟豆瓣不一样,淘宝这个是单页面应用,数据是动态js渲染的,因此换成puppeteer这个插件;

  • 第二次尝试,我用puppeteer去抓数据(后面会有代码),按照正常的思路逻辑去抓,最后抓到数据了,但是抓到的数据很少,而且图片的地址都是一样的,而且并不是原图地址。

  • 于是我又开始思考原因,最后得出结论:

    1. 页面有下拉加载更多的效果,因此初始化加载的页面并不能获取到所有dom;
    2. 页面图片是有做懒加载效果的,因此初始化拿到的数据其实是占位图的资源地址;
  • 最终,我们第一要实现滚动到底,保证所有页面都渲染,其次需要保证所有图片都懒加载结束。当然懒加载的时间取决于你的网速;

node抓图片资源数据_第5张图片
为了方便使用,我对抓包的方法进行了封装,处理dom的逻辑属于自定义代码,采用函数参数传参的方式让内部执行即可。
代码如下:

// lib.js
// 封装方法
const puppeteer = require("puppeteer");
const fs = require("fs");

// puppeteer启动浏览器参数的默认参数数据
let puppeteerOption = {
  // headless: true, // 无头
  headless: false, // 采用有头浏览器,会打开浏览器
  // slowMo: 3000, // 每个步骤耗时3秒
  devtools: true, // 打开浏览器控制塔
};

// page页面跳转和页面滚动、其他自定义参数默认值
let pageLoadOption = {
  url: "", // 目标地址
  waitDom: "", // 等待渲染的dom
  isScreenshot: false, //是否需要截取快照图片(暂时限定保存图片的名称和格式,有需求可扩展自定义)
  evaluateFn: () => "", // 自定义evaluate处理dom数据的回调函数
  isLazyLoad: false, //目标网页加载的图片是否是懒加载的
  // 懒加载时,自定义加载的参数
  lazyloadOption: {
    distance: 700, // 每次浏览器滚动距离
    delay: 1000, //滚动间隔时间,避免滚动过快,未触发图片懒加载
    waitGetDataTime: 120000, // 滚动到底后,过多久获取数据,该数据不固定,基于用户网速,用户可自行测试,只要保证触底后该时间之后数据能拿到即可
  },
  filename: "./file.json", //文件存储地址
};

// 抓包的方法:
async function captureWebData(launchOption = puppeteerOption, pageOption = pageLoadOption) {
  // 创建浏览器:有无头自定义,默认有头,且打开浏览器控制台
  const browser = await puppeteer.launch(launchOption);
  // 打开新页面:
  const page = await browser.newPage();
  // 覆盖浏览器的 user-agent
  await page.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36");
  // 监听console的输出:page.evaluate中运行在浏览器中的日志输出,可以通过监听在node终端输出
  page.on("console", async (message) => {
    console.log("message:", message.text());
  });
  const { url, waitDom, isScreenshot, isLazyLoad, evaluateFn, lazyloadOption, filename } = pageOption;
  // 导航到目标页:
  await page.goto(url);
  // 保存页面截图:注意这个快照截图并不准确,只是一瞬间的截图,截取不到滚动部分
  await (isScreenshot && page.screenshot({ path: "screenshot.png" }));
  // 阻塞:指定页面某元素加载完成,避免page.evaluate获取元素时还没加载完
  // (该bodyHandle元素可传进page.evaluate中,作为回调函数的参数)
  const bodyHandle = await page.waitForSelector(waitDom);
  // 浏览器处理dom及数据:
  // 1.注意此内部回调是在浏览器执行,无法使用node模块和上文的变量。如要使用只能通过额外参数传进去;
  // 2.传进去的参数必须是可序列化的,不可序列化的都会被处理成undefined,故等下的自定义处理函数需要用toString()和Function构造函数去序列化成字符串和反序列化成函数;
  const resData = await page.evaluate(
    async (bodyHandleDom, { isLazyLoad, evaluateFnStr }, { distance, delay, waitGetDataTime }) => {
      // resData:此处通过返回值方式把浏览器运行处理后的结果传到外部node环境中
      // 特殊说明:此处采用toString+Function的方式拿到自定义的操作函数
      let evalStr = evaluateFnStr.substring(evaluateFnStr.indexOf("{") + 1, evaluateFnStr.lastIndexOf("}"));
      let evalFn = new Function(evalStr);
      return new Promise((resolve, reject) => {
        if (!isLazyLoad) {
          // 不需要处理页面懒加载图片时
          resolve(evalFn());
        } else {
          // 网页图片懒加载,需要特殊处理获取数据:
          // 1.手动滚动到底部,注意滚动的时间间距和距离,保证所有图片都触发懒加载,并自定义获取指定时间后获取资源
          // 2.注意此处代码无法获取外部作用域的变量,因此如果需要自定义变量,可以通过evaluate的额外参数传递
          let totalHeight = 0;
          const timer = setInterval(() => {
            // 获取浏览器所有内容高度(每次都重新获取,因为下拉会加载更多元素)
            const scrollHeight = document.body.scrollHeight;
            // 手动滚动distance距离
            window.scrollBy(0, distance);
            // 滚动总高度
            totalHeight += distance;
            // 一直到滚动的总高度和浏览器高度一致时,表示滚动到底
            if (totalHeight >= scrollHeight) {
              clearInterval(timer);
              let timer2 = setTimeout(() => {
                clearTimeout(timer2);
                resolve(evalFn());
              }, waitGetDataTime);
            }
          }, delay);
        }
      });
    },
    // 传参到evaluate回调函数里面
    bodyHandle,
    {
      isLazyLoad,
      evaluateFnStr: evaluateFn.toString(), // 此处单独传函数会被处理成undefined,参数必须是可序列化的
    },
    lazyloadOption
  );
  console.log("resData:", resData);
  // 保存文件
  fs.writeFile(filename, JSON.stringify(resData), (err, data) => {
    if (err) {
      throw err;
    }
    console.log("文件保存成功");
  });
  // 关闭浏览器
  console.log("结束");
  await browser.close();
}

module.exports = captureWebData;

调用代码:

const captureWebData = require("../lib.js");
// 抓包方法调用:
captureWebData(
  // { headless: false, devtools: true },
  { headless: true },
  {
    url: "https://www.taobao.com/",
    waitDom: ".tb-recommend-content",
    isScreenshot: true,
    // 自定义处理逻辑方法每一个网页渲染dom的结构不一样,自行定义即可
    evaluateFn: function() {
      let arrData = [];
      const contents = document.querySelector(".tb-recommend-content");
      const items = contents.querySelectorAll(".tb-recommend-content-item");
      for (let i = 0; i < items.length; i++) {
        const a = items[i].querySelector("a");
        const imgWrap = a.querySelector(".img-wrapper");
        const infoWrap = a.querySelector(".info-wrapper");
        const pic = imgWrap.querySelector("img").src;
        const title = infoWrap.querySelector(".title").innerText;
        arrData.push({ pic, title });
      }
      console.log("arrData:", arrData);
      return arrData;
    },
    isLazyLoad: true,
    lazyloadOption: {
      distance: 500,
      delay: 900,
      waitGetDataTime: 10000,
    },
    filename: "./taobao.json",
  }
);

node抓图片资源数据_第6张图片
node抓图片资源数据_第7张图片

主要的逻辑都进行了封装,用户调用处的代码逻辑比较简单。上述代码中注释说明比较详细,此处就不再赘述说明了,用户可自行用其他网站测试。

总结

以上就是关于node抓包的过程,当然第二种抓淘宝首页图片数据,我们解决了懒加载的模式,由于该页面是采用滚动获取更多资源的,主要模拟滚动即可。那么如果该网页也是采用分页点击参数获取到的下一页数据呢?有兴趣的读者可找网站测试下,结合方式一和方式二处理获取dom数据的方式,去自行扩展功能吧。

另附代码地址:https://github.com/ali-go/some-test/tree/main/test-packet-capture

你可能感兴趣的:(前端,javascript,node.js)