node之前端er写爬虫

778 阅读5分钟

一、概述

爬虫,正式名称为网络爬虫(web crawler)或网络蜘蛛(web spider),是一种程序或脚本,设计用来自动地浏览互联网,并从中收集信息。它们通常被用来收集特定网站的数据,以供分析、索引或其他目的使用。网络爬虫通过HTTP请求获取网页内容,然后解析该内容以提取所需的信息,例如文本、链接、图像等。

简单来说,爬虫的工作流程通常包括以下步骤:

  1. 确定入口 URL:确定几个初始的网页地址作为爬取的起点。
  2. 发送HTTP请求:爬虫向目标网站发送HTTP请求,请求页面的内容。
  3. 获取网页内容:目标网站收到请求后,返回相应的HTML页面。
  4. 解析网页内容:爬虫使用解析库(如BeautifulSoup、Scrapy、Cheerio等)对网页内容进行解析,提取出需要的数据。
  5. 数据 处理:爬虫对提取的数据进行处理、清洗、存储或其他操作。
  6. 循环执行:重复以上步骤,直到满足预定的停止条件,比如抓取到足够多的数据,或者达到了预设的深度、广度限制。

网络爬虫在搜索引擎、数据挖掘、监控、信息聚合等领域都有广泛的应用。然而,值得注意的是,使用网络爬虫时需要遵守网站的robots.txt协议和相关法律法规,以确保爬取的行为合法合规。

二、准备流程

环境:node

步骤1:新建文件夹

步骤2:在终端输入npm init -y,可以初始化为后端项目,得到package.json项目描述文件。

步骤3 创建入口文件index.js,安排编程的流程,就可以开始写代码啦

步骤4:通过node index.js启用项目

三、node.js爬虫体操之练手

目标:获取豆瓣top250电影的信息

爬虫思想:他有我拿,首先要发送一个HTTP请求https://movie.douban.com/top250,得到html字符串。然后解析html字符串,拿到电影列表。最后将所有的电影对象组成数组,以json数组的方式返回。

解析工具:cheerio:cheerio是一个类jQuery的网页解析工具,主要是为了用在服务器端需要对DOM进行操作的地方。

其他工具:axios :前端Ajax请求基本工具,不再介绍

// 模块引入
let axios = require('axios');
let cheerio = require('cheerio'); // 使用cheerio模块进行HTML解析,需要先安
let fs = require('fs'); // 使用fs模块进行文件操作

// 豆瓣电影Top250的基础链接
const basicUrl = 'https://movie.douban.com/top250';
// 解析电影信息的函数
function getMovieInfo(node) {
  let $ = cheerio.load(node); // 使用cheerio加载HTML节点
  let title = $('.info .hd span'); // 获取电影标题元素
  title = [].map.call(title, t => {
    // 使用map方法将标题元素转换为文本数组
    return $(t).text();
  });
  let bd = $('.info .bd'); // 获取电影信息元素
  let allInfo = bd.find('p').text(); // 获取所有的简介信息
  let info = allInfo.split('\n')[1] || ''; // 获取电影简介文本
  let type = allInfo.split('\n')[2] || '';
  let score = bd.find('.star .rating_num').text(); // 获取电影评分文本
  return { title, info, score, type, allInfo }; // 返回电影信息对象
}

// 获取某一页的电影信息
async function getPage(url, num) {
  // 使用request-promise发送HTTP请求获取页面HTML
  let html = await axios.get(url);
  console.log('连接成功!', `正在爬取第${num + 1}页数据`);
  let $ = cheerio.load(html.data); // 使用cheerio加载HTML页面
  let movieNodes = $('#content .article .grid_view').find('.item'); // 获取电影节点
  let movieList = [].map.call(movieNodes, node => {
    // 遍历电影节点列表并解析电影信息
    return getMovieInfo(node);
  });
  return movieList; // 返回解析后的电影信息列表
}
// 主函数,用于控制爬取流程
async function main() {
  let count = 10; // 每页电影数量
  let list = [];
  // 循环爬取每一页的电影信息
  for (let i = 0; i < count; i++) {
    let url = basicUrl + `?start=${25 * i}`; // 构造当前页的URL
    list.push(...(await getPage(url, i))); // 获取当前页的电影信息并加入到列表中
  }
  console.log(list.length); // 输出爬取的电影数量
  // 将爬取的电影信息写入JSON文件
  fs.writeFile('./movie.json', JSON.stringify(list), 'utf-8', () => {
    // 将电影信息列表写入JSON文件
    console.log('生成json文件成功!');
  });
}
main();

四、node.js爬虫体操之进阶

目标:获取斗图啦的所有表情包

分析:网站的结构分为列表页:www.doutupk.com/article/lis…

爬虫思想:首先访问首页获取html元素,解析元素,拿到表情包列表,获取详情页访问地址列表。访问详情页,获取表情包列表元素,获取图片链接,将图片下载到本地。获取到页码总数,循环执行上边步骤。

爬取单页的图片

let axios = require('axios');
let cheerio = require('cheerio');
let fs = require('fs');
let path = require('path');
// 访问的基础路径
let httpUrl = 'https://www.doutupk.com/article/list/?page=1';

// 获取列表页详情
axios.get(httpUrl).then(res => {
  // 解析html
  let $ = cheerio.load(res.data);
  // 获取表情包列表元素
  $('#home .col-sm-9>a ').each((i, element) => {
    // console.log($(element).attr('href'))
    // 获取当前页面的所有表情包链接
    let pageUrl = $(element).attr('href');
    let title = $(element).find('.random_title').text();
    // 开学是一个悲伤的故事~ #装逼表情# #蘑菇头表情# #蘑菇头表情包# #开学了# 2024-02-21
    console.log('title:', title);
    // 正则匹配获取标题
    let reg = /(.*?)\d/gis;
    title = reg.exec(title)[1];
    // 创建文件夹 注意要有或创建imgs文件夹
    fs.mkdir('./imgs/' + title, function (err) {
      if (err) {
        // console.log(err)
      } else {
        console.log('成功创建目录:' + './imgs/' + title);
      }
    });
    parsePage(pageUrl, title);
  });
});
// 获取详情页信息,然后下载图片
async function parsePage(url, title) {
  let res = await axios.get(url);
  let $ = cheerio.load(res.data);
  $('.pic-content img').each((i, element) => {
    let imgUrl = $(element).attr('src');
    console.log(imgUrl);
    let extName = path.extname(imgUrl);
    // 创建写入路径及名称
    let imgPath = `imgs/${title}/${title}-${i}${extName}`;
    // 创建写入图片流
    let ws = fs.createWriteStream(imgPath);
    axios.get(imgUrl, { responseType: 'stream' }).then(function (res) {
      // 通过管道下载图片
      res.data.pipe(ws);
      console.log('图片加载完成:' + imgPath);
      // 低版本node需要手动关闭管道
      res.data.on('close', function () {
        ws.close();
      });
    });
  });
}

循环爬取数据

let axios = require('axios');
let cheerio = require('cheerio');
let fs = require('fs');
let url = require('url');
let path = require('path');

let initUrl = 'https://www.doutupk.com/article/list/?page=1';
// 获取所有的页数
async function getNum(httpUrl) {
  axios.get(httpUrl).then(res => {
    // 解析html
    let $ = cheerio.load(res.data);
    // 获取页码元素的个数
    let bthLength = $('.pagination li').length;
    // 获取页码的具体总数
    let pageNum = $('.pagination li')
      .eq(bthLength - 2)
      .find('a')
      .text();
    console.log('pageNum', pageNum);
    return pageNum;
  });
}
async function spider() {
  // 获取总页数
  let allPageNum = await getNum(initUrl);
  console.log('allPageNum:', allPageNum);

  let ifNum = 1;
  // delayQueue(ifNum, allPageNum);
  // 少来点内容,练手演示即可
  delayQueue(ifNum, 4);
}
// 延时队列,防止被网站屏蔽访问,服务死机
function delayQueue(i, all) {
  if (all > i) {
    setTimeout(() => {
      getRequest(i);
      i++;
      delayQueue(i, all);
    }, 3000);
  }
}
// 单个页面的请求
async function getRequest(pageNum) {
  let httpUrl = 'https://www.doutupk.com/article/list/?page=' + pageNum;
  let res = await axios.get(httpUrl);
  // 解析html
  let $ = cheerio.load(res.data);
  // 获取当前页面的所有表情包链接
  $('#home .col-sm-9>a ').each((i, element) => {
    // console.log($(element).attr('href'))
    let pageUrl = $(element).attr('href');
    let title = $(element).find('.random_title').text();
    let reg = /(.*?)\d/gis;
    title = reg.exec(title)[1];
    // 注意要有或创建imgs文件夹
    fs.mkdir('./imgs/' + title, function (err) {
      if (err) {
        // console.log(err)
      } else {
        console.log('成功创建目录:' + './imgs/' + title);
      }
    });
    parsePage(pageUrl, title);
  });
}
// 单个
async function parsePage(url, title) {
  let res = await axios.get(url);
  let $ = cheerio.load(res.data);
  $('.pic-content img').each((i, element) => {
    let imgUrl = $(element).attr('src');
    console.log(imgUrl);
    let extName = path.extname(imgUrl);
    // 创建写入路径及名称
    let imgPath = `imgs/${title}/${title}-${i}${extName}`;
    // 创建写入图片流
    let ws = fs.createWriteStream(imgPath);
    axios.get(imgUrl, { responseType: 'stream' }).then(function (res) {
      res.data.pipe(ws);
      console.log('图片加载完成:' + imgPath);
      // 低版本node需要手动关闭写入流
      res.data.on('close', function () {
        ws.close();
      });
    });
  });
}
spider();

五、node.js爬虫体操之魔高一尺

在了解具体的反爬虫措施之前,我们先介绍下反爬虫的定义和意义,限制爬虫程序访问服务器资源和获取数据的行为称为反爬虫。常见的反爬虫手段,主要包含文本混淆、页面动态渲染、验证码校验、请求签名校验、大数据风控、js混淆和蜜罐等

  1. css偏移反爬虫

在搭建网页的时候,需要用CSS来控制各类字符的位置,也正是如此,可以利用CSS来将浏览器中显示的文字,在HTML中以乱序的方式存储,从而来限制爬虫。CSS偏移反爬虫,就是一种利用CSS样式将乱序的文字排版成人类正常阅读顺序的反爬虫手段。这个概念不是很好理解,我们可以通过对比两段文字来加深对这个概念的理解:

  • **HTML 文本中的文字:**我的学号是 1308205,我在北京大学读书。
  • 浏览器显示的文字:我的学号是 1380205,我在北京大学读书。

如果我们按之前提到的爬虫步骤,分析网页后正则提取信息,会发现学号是错的。

示例:demo

看图所示的例子,如果我们想爬取该网页上的机票信息,首先需要分析网页。红框所示的价格621对应的是中国民航的从石家庄到上海的机票,但是分析网页源代码发现代码中有 3 对 b 标签,第 1 对 b 标签中包含 3 对 i 标签,i 标签中的数字分别是 6、1、5,也就是说第 1 对 b 标签的显示结果应该是 615。而第 2 对 b 标签中的数字是 2,第 3 对 b 标签中的数字是 1,这样的话我们会无法直接通过正则匹配得到正确的机票价格。

  1. 字体反爬虫:

实现思路:

  • 得有一套自定义字体 woff 文件或其他文件
  • 完成 自定义字体的编码和unicode编码的 映射关系 ( {key:value} )形式
  • 通过font-family 声明使用的字体包(一般设置多个 避免不支持 首个是自定义的字体包 如果自定义字体包没有 则用其他的字体包)
  • 在写的时候对于关键字体就可以 通过插入特殊的value了

示例:demo(工资信息进行了字体反爬)

  1. 图片伪装反爬虫

原理:将某些关键信息通过生成图片的形式插入dom中完成展示,避免爬虫程序通过爬去关键信息。

实现思路:

  • 对于关键信息,后端可以直接返回图片,比如手机号码等(图片传输慢&爬虫程序可以自行下载获取 不推荐 差评)
  • 也可以前端自行解决,在遇到敏感信息等通过canvas 标签 生成插入 dom 墙裂推荐 爬虫程序不可下载 不可通过dom获取 好评)

  1. 页面动态渲染

前后端分离的项目。页面的主要内容由 JavaScript 渲染而成,真实的数据是通过 Ajax 接口等形式获取的,通过查看网页源代码,无有效数据信息。

  1. 验证码反爬虫

几乎所有的应用程序在涉及到用户信息安全的操作时,都会弹出验证码让用户进行识别,以确保该操作为人类行为,而不是大规模运行的机器。常见的验证码形式包括图形验证码、行为验证码、短信、扫码验证码等。

  1. 蜜罐

蜜罐反爬虫,是一种在网页中隐藏用于检测爬虫程序的链接的手段,被隐藏的链接不会显示在页面中,正常用户无法访问,但爬虫程序有可能将该链接放入待爬队列,并向该链接发起请求,开发者可以利用这个特点区分正常用户和爬虫程序。

示例:demo 相同class的同级div,增加css属性:display:none;

  1. And more

还有许多反爬虫策略,例如请求签名反爬虫,埋点反爬虫,真人模式校验。。。。。。。。。。。。(个人知识储备不足)

推荐一个反爬虫特别牛的网站:起点中文网付费章节页面

六、node.js爬虫体操之道高一丈

针对上一节提到的反爬虫相关技术,有以下几类反反爬技术手段:css偏移反反爬、自定义字体反反爬、页面动态渲染反反爬、验证码破解等,下面对这几类方法进行详细的介绍。

  1. CSS偏移反反爬

那么对于以上5.1css偏移反爬虫的例子,怎么才能得到正确的机票价格呢。仔细观察css样式,可以发现每个带有数字的标签都设定了样式,第 1 对 b 标签内的i 标签对的样式是相同的,都是width: 16px;另外,还注意到最外层的 span 标签对的样式为width:48px。。。。。。。。。。。

不详细写了,内容太多了,一小时分享讲不完了,这里大概介绍一下就算了,直接写思路啦

分析css偏移逻辑,逆推方案,然后进行破解

  1. 自定义字体反反爬

针对于以上5.1自定义字体反爬虫的情况,解决思路就是提取出网页中自定义字体文件(一般为WOFF文件),并将映射关系包含到爬虫代码中,就可以获取到有效数据。

  1. 页面动态渲染反反爬

客户端渲染的反爬虫,页面代码在浏览器源代码中看不到,需要执行渲染并进一步获取渲染后结果。针对这种反爬虫,有以下几种方式破解:

  • 在浏览器中,通过开发者工具直接查看ajax具体的请求方式、参数等内容;
  • 通过selenium模拟真人操作浏览器,获取渲染后的结果,之后的操作步骤和服务端渲染的流程一样;
  • 如果渲染的数据隐藏在html结果的JS变量中,可以直接正则提取;
  • 如果有通过JS生成的加密参数,可以找出加密部分的代码,然后使用pyexecJS来模拟执行JS,返回执行结果。
  1. 验证码破解

  • 人工智能大模型处理
  • 雇人“打螺丝”

七、node.js爬虫体操之神兵利器

以上三、四节介绍了静态网页的爬取,但是遇到一些动态网页(ajax)的话,直接用之前的方法发送请求就无法获得我们想要的数据。这时就需要通过爬取动态网页的方法,selenium和puppeteer都不错。

这里推荐使用puppeteer,不为别的,只因为他是谷歌亲生的,一直在维护更新。第三方文档

Puppeteer 是一个提供一系列高级接口通过 DevTools(开发者工具)协议去控制 Chrome 或者 Chromium(谷歌开源)的 Node 库。它默认运行无头模式(没有浏览器的UI界面),通过配置也可以运行正常模式。

功能:

  • 生成页面 PDF。
  • 抓取 SPA(单页应用)并生成预渲染内容(即“SSR”(服务器端渲染))。
  • 自动提交表单,进行 UI 测试,键盘输入等。
  • 创建一个时时更新的自动化测试环境。 使用最新的 JavaScript 和浏览器功能直接在最新版本的Chrome中执行测试。
  • 捕获网站的 timeline trace,用来帮助分析性能问题。
  • 测试浏览器扩展。

安装依赖

npm i puppeteer

Api

启动浏览器

const browser = await puppeteer.launch( {
    headless: false, // 默认是无头模式,这里为了示范所以使用正常模式
    defaultViewport: {
      width: 1024,
      height: 3080
    }
  });

打开/关闭tab页面

 const page = await browser.newPage(); // 新建页面
 await browser.close(); // 关闭页面

页面跳转URL

await page.goto('https://saas.hailiangedu.com/platform/menu-manage');

选择元素及触发事件

  const searchResultSelector = '.devsite-result-item-link';
  await page.waitForSelector(searchResultSelector);
  await page.click(searchResultSelector);

评估页面上下文中的函数并返回结果。使用evaluate方法在浏览器中执行传入函数(完全的浏览器环境,所以函数内可以直接使用window、document等所有对象和方法)

const data = await page.evaluate(() => {
    let list = document.querySelectorAll('.theme-doc-markdown');
    console.log('list', list);
    let res = [];
    for (let i = 0; i < list.length; i++) {
      res.push({
        name: list[i].querySelector('h1').innerText
      });
    }
    // document
    //   .querySelector('.theme-doc-markdown')
    //   .querySelector('h1').innerHTML = '哈哈哈哈哈哈哈哈哈哈哈';
    // window.print();
    console.log('res', res);
    return Promise.resolve(res);
 });
 console.log('data',data)

demo

const puppeteer = require('puppeteer');
let cheerio = require('cheerio');
(async () => {
  let option = {
    headless: false, // 默认是无头模式,这里为了示范所以使用正常模式
    defaultViewport: {
      width: 1024,
      height: 3080
    }
  };
  // 启动浏览器
  const browser = await puppeteer.launch(option);

  // 控制浏览器打开新标签页面
  const page = await browser.newPage();
  // 在新标签中打开要爬取的网页
  // 在puppeteer官网,截取部分模块,更改输出文案
  //   await page.goto('https://pptr.nodejs.cn/guides/what-is-puppeteer');
  //   const fileElement = await page.waitForSelector('.markdown');

  //   await fileElement.screenshot({ path: 'example.png' });
  //   // 使用evaluate方法在浏览器中执行传入函数(完全的浏览器环境,所以函数内可以直接使用window、document等所有对象和方法)
  //   let data = await page.evaluate(() => {
  //     let list = document.querySelectorAll('.theme-doc-markdown');
  //     console.log('list', list);
  //     let res = [];
  //     for (let i = 0; i < list.length; i++) {
  //       res.push({
  //         name: list[i].querySelector('h1').innerText
  //       });
  //     }
  //     console.log('res', res);
  //     return res;
  //   });
  //   console.log('data', data);
  // 访问新页面
  // 爬取node官网优质插件列表信息  以及截取页面快照
  await page.goto('https://nodejs.cn/#pptr');
  const fileElement = await page.waitForSelector('html');
  await fileElement.screenshot({ path: 'example4.png' });
  const allHtml = await page.content();
  let $ = cheerio.load(allHtml);
  let res = [];
  const $html = $('.cate_item');
  console.log('$', $html);
  $html.each((i, element) => {
    let pageHref = $(element).find('a').attr('href');
    let pageTitle = $(element).find('.partner_name').text();
    res.push({
      pageHref,
      pageTitle
    });
  });
  console.log('res', res);
})();

八、总结

本次简单对爬虫以及反爬虫的技术手段进行了介绍,介绍的技术和案例均只是用于安全研究和学习,并不会进行大量爬虫或者应用于商业。

对于爬虫,本着爬取网络上公开数据用于数据分析等的目的,我们应该遵守网站robots协议,本着不影响网站正常运行以及遵守法律的情况下进行数据爬取;对于反爬虫,因为只要人类能够正常访问的网页,爬虫在具备同等资源的情况下就一定可以抓取到。所以反爬虫的目的还是在于能够防止爬虫在大批量的采集网站信息的过程对服务器造成超负载,从而杜绝爬虫行为妨碍到用户的体验,来提高用户使用网站服务的满意度。

参考资料:

  1. 用前端Node.js如何实现爬虫(爬虫篇一)
  2. 爬虫与反爬虫技术简介