手把手教你使用nodejs编写壁纸爬虫,并发送至你的邮箱。

一、概述:

  1. 为什么不选择使用Python?
    因为我个人更喜欢使用JavaScript,其次也想体验一下用js来爬虫是个什么感觉,顺便提高一下业务能力。

  2. 本人也算是前端刚入门级别的初学者,若在程序设计方面各位大佬有什么更好的建议或可优化的地方,欢迎评论区讨论。

  3. 本教程仅针对正在学习js,刚刚接触前端,对JavaScript有一定基础的同学。

  4. 爬虫思路:1. 发送网络请求获取html文档页面=> 2. 解析html文档,拿到原图和预览图的url=> 3.将url转换为html格式,打包到邮件,并发送。

二、准备工作:

  1. 安装nodejs环境:(已经安装过的同学可以跳过这一步了)在nodejs官网下载安装包后傻瓜式下一步安装即可,这里推荐安装lts版本,即长期维护版。相比较java,nodejs的环境搭建就显得比较简单了。
    nodejs
    安装完成以后,接着在cmd命令行窗口键入node --version查看版本。
    验证安装
    若打印版本信息,说明安装成功可以进行下一步了。若提示没有该命令,则需要手动配置一下环境变量。具体如何配置本文不做讲解,自行百度吧,其实并不复杂,也就两分钟能解决的事。
  1. 选择壁纸站点:
    这里我选择知名壁纸站点:https://wallhaven.cc,这个站点的壁纸质量如何,想必不需要我多说了吧。

  2. 分析站点节点结构:

    image.png
    我们拿到该网站打开后,能看到首页显示了很多图片,这些图片其实都是预览图,分辨率很低,不是我们想要的,并且我们希望每次抓到的图都是随机的,所以在首页抓并不合适。这里我们点击Random进入到随机页面。
    image.png
    我们发现,在这个页面下,每次刷新都能获取到不一样的图片,并且这里的图片的也都是预览图,所以我们需要找到原图的url在什么位置。
    随便点开一张图片,跳转页面后,右键原图点击检查。
    image.png
    此步骤我们找到了关键元素:img ,该元素节点中的src属性值就是原图的url
    image.png
    将鼠标放在url上,我们发现,该url图片的实际大小为6016x3384。
    恭喜,我们已经找到了原图的url。
    image.png

    然后我们发现,这个url是存在一个单独的页面中的,而这个页面居然只有一张原图。那我们怎么才能批量的获取原图的url呢?这里我曾花费大量时间尝试遍历请求预览图的跳转url至每一个原图页面,然后抓取原图url。但因为该网站对频繁请求做了限制,短时间内多次请求该页面,都会返回429。我反复尝试,甚至将每次请求都加上了间隔时间,最后都以失败告终,就算这个方法成功了,也将会在请求上花费大量不必要的时间。
    我开始把思考方向转向url格式本身,在对比了预览图url和原图的url后,最后我发现了规律。返回上级页面(Random页面),在预览图上鼠标右键,点击检查。
    预览图的url

    原图的url

    怎么样,发现什么规律了吗?
    没错,预览图中的url与原图url的重叠度很高,他们都有共同的特性,那就是都具有相同的唯一标识:dpv81j
    根据这个线索我们可以把原图的url拆分为几个部分:
    https://w.wallhaven.cc/full/ dp/ wallhaven- dpv8lj .jpg
    再将预览图的url拆分:
    https://th.wallhaven.cc/lg/ dp/ dpv8lj .jpg
    按照以下方式排列组合后,最后发现,我们成功组合了一个原图的url
    image.png

    其中https://w.wallhaven.cc/full/wallhaven- 为固定值,其他位置的值则可直接从预览图的url中拿到。这样我们就可以直接从预览图页面中将每一个url拼接为原图url,不需要频繁的进入原图页面获取url了。
    但是这样真的就可以了吗?我发现这个图片的url虽然没有问题,并且可以打开原图:
    原图

    但其他的预览图也同样适用于这个方法吗?
    正当我一个一个去尝试时,我发现,所有的预览图都是以.jpg格式结尾的,但是有些原图url却是以.png格式结尾,也就导致当原图格式也同样为.jpg时,按照此方法拼接可行,一但遇到.png格式的原图,就不可以了。
    那我们怎么才能在预览图页面就能知道该图片的原图到底是什么格式呢?其实该网站已经告诉我们了:
    image.png

    image.png
    相信你已经发现了,每张预览图都存在额外的标签,会将图片格式标注出来,带PNG的表示该原图为png格式,不带则表示该图片为jpg格式。
    我们鼠标右键检查该标签:
    image.png

    发现它在class为thumb-info的div标签里,我们后面可以以此为判断依据,判断该图片是什么格式,然后再将这个格式拼接到原图的url上:
    image.png
    具体该怎么操作,请耐心往下看。
    至此,我们已经知道如何批量拿到原图url了,分析完毕,开始编写代码!

三、编写代码:

  1. 项目目录结构划分:
    终于可以开始写代码了,我们首先新建一个文件夹,这里我就叫 crawler 了。
    在文件夹下鼠标右键,通过vscode打开。(啰嗦了...)
    image.png

在vscode中打开终端窗口,键入npm init -y 此步骤是初始化一个项目,会自动生成一个package.json的文件,该文件记录着这个项目的信息。

image.png

新建文件夹src,在src下继续新建文件夹:app 用于存放核心代码,以及文件夹:utils 用于存放工具函数(个人习惯),src下新建index.js作为程序入口。

image.png

  1. 编写请求方法,拿到Random页的html文档元素:
    首先我们需要用到一个第三方库来发送网络请求,这里我推荐使用axios,目前前端开发用于网络请求最好用的第三方库之一。
    cd到项目根目录,键入安装命令: npm install axios
    image.png

    utlis下新建文件get-element.js,在此文件中编写网络请求方法:

我们可以直接调用axios方法,该方法接收一个对象作为参数,对象中可配置属性method,既请求方法,这里我们使用get请求,url则是网站中random页对应的url。因为该方法返回一个Promise,所以我们可以用then方法来接收请求响应成功的结果,用catch方法来接收请求响应失败的结果。

const axios = require('axios')

axios({
  method: 'get',
  url: 'https://wallhaven.cc/random'
})
  .then(res => {
    console.log(res)
  })
  .catch(err => {
    console.log(err)
  })

写完请求方法后,我们使用执行命令来测试一下。打开终端窗口,执行命令:node ./src/utils/get-element.js,执行完毕后我们发现果然打印了一大串乱七八糟的东西,我们发现该方法请求成功后返回的是一个对象,status:200则是表示请求响应成功了。

image.png

image.png

对象中的data属性所包含的,其实就是我们需要的东西了,仔细观察,我们发现这其实就是一个html文档,对应的其实就是这个页面:

image.png

而我们恰恰正需要从这个页面中把所有预览图片的url地址给筛出来。

接下来将上面的代码简单封装一下:
get-element.js文件:

const axios = require('axios')  // commonjs的导入语法

const getElement = () => {
  return new Promise((resolve, reject) => {
    axios({
      method: 'get', // 请求方法
      url: 'https://wallhaven.cc/random'  // 请求地址
    })
      .then(res => {
        resolve(res.data)
      })
      .catch(err => {
        console.log(err)
        reject(err)
      })
  })
}
module.exports = {
  // commonjs的导出语法
  getElement
}

这里我使用了Promise来封装,并且直接将res中的data属性作为返回值,方便我们后面执行异步操作,若你对Promise不是很了解的话,可以参考:Promise

  1. 解析html文档,抓取预览图元素:
    接下来我们在src/utils下新建文件parse-element.js ,并在该文件中编写解析元素的方法。
    image.png

为了解析dom元素,我们需要安装第二个第三方库,cheerio用于解析html文件并操作dom元素,使用它在nodejs中操作dom,简直不要太方便。
在项目根目录中打开终端窗口,执行安装命令:npm install cheerio,但在开始写代码前,我们需要知道预览图所在位置以及他的父级元素和祖先元素。鼠标右键预览图=>检查,我们发现所有的预览图都存在于figure标签中:

image.png

而figure则位于ul下的li元素中,那么我们在可以通过子元素选择器ul>li>figure,拿到相关元素,代码如下:

const cheerio = require('cheerio')
const { getElement } = require('./get-element')

getElement()
  .then(res => {
    const $ = cheerio.load(res) //加载html
    const previewImg = $('ul>li>figure').find('img') //选择figure下所有的img  
    console.log(previewImg)
  })
  .catch(err => {
    console.log(err)
  })

首先调用getElement方法拿到html文档,然后加载html,通过$('ul>li>figure').find('img')方法拿到所有的img元素节点对象,相信熟悉jQuery的同学对这个语法应该并不陌生吧,这个库的用法其实和jQuery几乎是一样的,详情可查阅:cheerio,使用node ./src/utils/parse-element.js执行代码,并查看打印结果,我们发现该方法返回的是一个个节点对象。

image.png

那我们怎么才能拿到url呢?在img元素上,我们发现url在srcdata-src中,我们只需要拿到这两个属性中的其中一个即可。
image.png

使用each方法遍历previewImg节点对象,该方法接收一个回调函数,参数1:索引,参数2:节点对象,并通过$(节点).attr()方法拿到该元素包含的属性值:

const cheerio = require('cheerio')
const { getElement } = require('./get-element')

getElement()
  .then(res => {
    const $ = cheerio.load(res) //加载html
    const previewImg = $('ul>li>figure').find('img') //选择figure下所有的img

    previewImg.each((i, element) => {
      const dataSrc = $(element).attr('data-src') // 遍历拿到data-src属性
      const src = $(element).attr('src') // 拿到src属性
      console.log('dataSrc: ' + dataSrc)
      console.log('src: ' + src)
    })
  })
  .catch(err => {
    console.log(err)
  })

这里我直接把data-src和src两个属性都拿到了,因为我发现通过axios请求下来的结果里,src属性居然没有值。

image.png

所以我们只需要拿到 data-src即可
image.png

这样我们就拿到了所有的预览图url了,接下来我们需要判断后缀:
png

我们发现所有png格式的图片上,都会在一个classthumb-infodiv中多出一个span元素,并且classpng,而jpg格式的图片则没有该元素

jpg

所以我们可以以此为判断条件,来判断该图片的后缀,代码如下:

const cheerio = require('cheerio')
const { getElement } = require('./get-element')

getElement()
  .then(res => {
    const $ = cheerio.load(res) //加载html
    let postfix = ''
    const previewImg = $('ul>li>figure').find('img') //选择figure下所有的img
    previewImg.each((i, element) => {
      const dataSrc = $(element).attr('data-src') // 遍历拿到data-src属性
      console.log('dataSrc: ' + dataSrc)
    })

    const thumbInfo = $('ul>li>figure').find('.thumb-info')
    thumbInfo.each((i, element) => {
      if ($(element).find('.png').text()) {
        //使用.text()方法可以拿到元素内的文本信息
        // 根据是否有元素文本,来判断图片格式
        postfix = 'png'
      } else {
        postfix = 'jpg'
      }
      console.log(postfix)
    })
  })
  .catch(err => {
    console.log(err)
  })

运行结果如下:


image.png

我们已经拿到了所有关键信息,接下来就可以开始拼串操作了,将预览图和后缀按照之前分析的方法,拼接为原图的url:

const cheerio = require('cheerio')
const { getElement } = require('./get-element')

getElement()
  .then(res => {
    const $ = cheerio.load(res) //加载html
    let postfix = ''
    let previewUrl = ''
    let imgData = []
    const previewImg = $('ul>li>figure').find('img') //选择figure下所有的img
    previewImg.each((i, element) => {
      previewUrl = $(element).attr('data-src') // 遍历拿到data-src属性
      imgData.push({ previewUrl })
    })

    const thumbInfo = $('ul>li>figure').find('.thumb-info')
    thumbInfo.each((i, element) => {
      if ($(element).find('.png').text()) {
        //使用.text()方法可以拿到元素内的文本信息
        // 根据是否有元素文本,来判断图片格式
        postfix = 'png'
      } else {
        postfix = 'jpg'
      }
      imgData[i].postfix = postfix
    })

    imgData.forEach(item => {
      const prefix = item.previewUrl.split('/')[4] // 获取图片标识的前两个字符
      const imgName = item.previewUrl.split('/')[5].split('.')[0] // 获取图片标识
      item.originalUrl = `https://w.wallhaven.cc/full/${prefix}/wallhaven-${imgName}.${item.postfix}`  // 拼接为原图url
    })
    console.log(imgData)
  })
  .catch(err => {
    console.log(err)
  })

这里我搞了一个对象imgData来存储图片的相关信息,因为后面还会用到,打印结果:

image.png

originalUrl即原图Url,我们随便点开一个查看一下,这里我故意找了个.png格式的url:
image.png
没有任何问题。接下来还是做一个简单的封装,将工具函数分离:
parse-element.js:

const cheerio = require('cheerio')

const parseElement = html => {
  const $ = cheerio.load(html) //加载html
  let postfix = ''
  let previewUrl = ''
  let imgData = []

  const previewImg = $('ul>li>figure').find('img') //选择figure下所有的img
  previewImg.each((i, element) => {
    previewUrl = $(element).attr('data-src') // 遍历拿到data-src属性
    imgData.push({ previewUrl })  //  将对象push到数组中
  })

  const thumbInfo = $('ul>li>figure').find('.thumb-info')
  thumbInfo.each((i, element) => {
    if ($(element).find('.png').text()) {
      //使用.text()方法可以拿到元素内的文本信息
      // 根据是否有元素文本,来判断图片格式
      postfix = 'png'
    } else {
      postfix = 'jpg'
    }
    imgData[i].postfix = postfix
  })

  imgData.forEach(item => {
    const prefix = item.previewUrl.split('/')[4] // 获取图片标识的前两个字符
    const imgName = item.previewUrl.split('/')[5].split('.')[0] // 获取图片标识
    item.originalUrl = `https://w.wallhaven.cc/full/${prefix}/wallhaven-${imgName}.${item.postfix}` // 拼接为原图
  })

  return imgData
}

module.exports = {
  parseElement
}

src/app目录下新建文件app.js
app.js:

const { getElement } = require('../utils/get-element')
const { parseElement } = require('../utils/parse-element')

const crawler = async () => {
  const html = await getElement()  // 同步执行,等待执行结束拿到html
  const imgData = parseElement(html)  // 将html解析

  console.log(imgData)
}

module.exports = {
  crawler
}
  1. 发送邮件:
    最后一步,将我们解析好的url封装成邮件格式并发送出去,这里我们需要事先准备两个邮箱账号。
    1:发送邮箱
    2:接收邮箱
    发送的邮箱需要登录进入邮箱官网申请IMAP/SMTP 服务授权码,以163邮箱为例:
    image.png
    ,开启该服务后,将会获得授权码,拿到授权码后,我们就可以开始编写发送邮件的代码了。
    执行命令:npm install nodemailer,安装第三方库:nodemailer
    这个库个人感觉挺好用的,使用起来也非常的简单:
  let transporter = nodemailer.createTransport({
    host: 'smtp.163.com', //  发送邮件的服务器,163邮箱为smtp.163.com,qq邮箱为smtp.qq.com
    secure: true,  //  定义连接是否应该使用SSL
    auth: {
      user: 'xxxxxx@163.com',  // 发送方的邮件地址
      pass: 'asdasdasdasd'  // 发送方的IMAP/SMTP 服务授权码
    }
  })
  transporter.sendMail(
    {
      from: '"壁纸爬虫" xxxxxx@163.com',   // 从哪里发送 ' "标题"  邮件 '
      to: 'xxxxxxxxx@qq.com',    // 发送到哪里  
      subject: '只是一个描述',  // 邮件描述  
      // html: 这是html格式的字符串,   //  发送html格式的内容
      text: '这是一条内容'   //  发送文本内容,注意:text和html只能存在一个
    },
    (err, info) => {
      //  发送后的回调函数
      if (err) {
        logger.error('邮件发送失败。', err)
      } else {
        console.log('邮件发送成功')
      }
    }
  )

下面是完整代码,我这里就懒得单独封装发送邮件的方法了,大家可以自行封装:
app.js:

const nodemailer = require('nodemailer')
const path = require('path')

const { getElement } = require('../utils/get-element')
const { parseElement } = require('../utils/parse-element')

const crawler = async () => {
  const html = await getElement()
  const imgData = parseElement(html)
  let content = ''
  imgData.forEach(item => {
    //   拼接一个html格式的字符串,方便待会儿发送邮件
    content =
      content +
      `
    <div>
      预览:
      <img alt="壁纸" src="${item.previewUrl}" />
      <a href=${item.originalUrl}>下载原图</a>
    </div>
    `
  })

  let transporter = nodemailer.createTransport({
    host: 'smtp.163.com', //  发送邮件的服务器,163邮箱为smtp.163.com,qq邮箱为smtp.qq.com
    secure: true, //  定义连接是否应该使用SSL
    auth: {
      user: 'xxxxxx@163.com', // 发送方的邮件地址
      pass: 'asdasdasdasd' // 发送方的IMAP/SMTP 服务授权码
    }
  })

  transporter.sendMail(
    {
      from: '"壁纸爬虫" xxxxxx@163.com', // 从哪里发送 ' "标题"  邮件 '
      to: 'xxxxxxxxx@qq.com', // 发送到哪里
      subject: '这是一条描述', // 邮件描述
      // text: '这是一条内容'   //  发送文本内容,注意:text和html只能存在一个
      html: `
      <div>
      <span>本次抓取结果</span>
        ${content}
      </div>`
    },
    (err, info) => {
      //  发送后的回调函数
      if (err) {
        logger.error('邮件发送失败。', err)
      } else {
        console.log('邮件发送成功')
      }
    }
  )
  console.log(imgData)
}

module.exports = {
  crawler
}

在index.js文件中导入方法并调用:

const { crawler } = require('./app/app')
crawler()

然后编辑package.json文件,在scripts中,加入配置属性"start":"node ./src/index.js"

{
  "name": "crawler",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start":"node ./src/index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "axios": "^0.24.0",
    "cheerio": "^1.0.0-rc.10",
    "nodemailer": "^6.7.2"
  }
}

接下来我们就可以在项目目录中直接键入 npm run start 来执行代码了。
执行测试结果:

image.png

邮件截图

果然收到邮件了,上面的图片就是预览图了,我们点击“下载原图”就可以跳转至浏览器下载原图。

以上就是本教程的全部内容了,本人基于以上功能继续做了几项扩展:

  1. 可通过.env文件配置各项信息
  2. 可自由选择像素比例,以及爬取的数量
  3. 可配置定时任务并后台定时执行脚本等...

如果你感兴趣,可查看https://github.com/mihu915/wallpaperCrawler,也希望你能给我一个star。

总结:
本文我花费了大量的篇幅来讲解如何分析网站结构,其实无论做任何业务都应该养成这种思考的习惯,代码写的精不精妙需要我们不断的积累经验,但是养成一个好的思考习惯,总能让我们事半功倍。如果你觉得本文有帮助到你,希望你能点赞转发支持一下,也欢迎大家添加我的微信:bishu0913,我们一起学习前端技术。

转载请注明出处~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容