一、概述:
为什么不选择使用Python?
因为我个人更喜欢使用JavaScript,其次也想体验一下用js来爬虫是个什么感觉,顺便提高一下业务能力。本人也算是前端刚入门级别的初学者,若在程序设计方面各位大佬有什么更好的建议或可优化的地方,欢迎评论区讨论。
本教程仅针对正在学习js,刚刚接触前端,对JavaScript有一定基础的同学。
爬虫思路:1. 发送网络请求获取html文档页面=> 2. 解析html文档,拿到原图和预览图的url=> 3.将url转换为html格式,打包到邮件,并发送。
二、准备工作:
- 安装nodejs环境:(已经安装过的同学可以跳过这一步了)在nodejs官网下载安装包后傻瓜式下一步安装即可,这里推荐安装lts版本,即长期维护版。相比较java,nodejs的环境搭建就显得比较简单了。
选择壁纸站点:
这里我选择知名壁纸站点:https://wallhaven.cc,这个站点的壁纸质量如何,想必不需要我多说了吧。-
分析站点节点结构:
随便点开一张图片,跳转页面后,右键原图点击检查。
img
,该元素节点中的src
属性值就是原图的url
恭喜,我们已经找到了原图的url。
然后我们发现,这个url是存在一个单独的页面中的,而这个页面居然只有一张原图。那我们怎么才能批量的获取原图的url呢?这里我曾花费大量时间尝试遍历请求预览图的跳转url至每一个原图页面,然后抓取原图url。但因为该网站对频繁请求做了限制,短时间内多次请求该页面,都会返回429。我反复尝试,甚至将每次请求都加上了间隔时间,最后都以失败告终,就算这个方法成功了,也将会在请求上花费大量不必要的时间。
我开始把思考方向转向url格式本身,在对比了预览图url和原图的url后,最后我发现了规律。返回上级页面(Random页面),在预览图上鼠标右键,点击检查。
怎么样,发现什么规律了吗?
没错,预览图中的url与原图url的重叠度很高,他们都有共同的特性,那就是都具有相同的唯一标识:dpv81j
。
根据这个线索我们可以把原图的url拆分为几个部分:
https://w.wallhaven.cc/full/
dp/
wallhaven- dpv8lj
.jpg
再将预览图的url拆分:
https://th.wallhaven.cc/lg/
dp/
dpv8lj
.jpg
按照以下方式排列组合后,最后发现,我们成功组合了一个原图的url
其中https://w.wallhaven.cc/full/
与wallhaven-
为固定值,其他位置的值则可直接从预览图的url中拿到。这样我们就可以直接从预览图页面中将每一个url拼接为原图url,不需要频繁的进入原图页面获取url了。
但是这样真的就可以了吗?我发现这个图片的url虽然没有问题,并且可以打开原图:
但其他的预览图也同样适用于这个方法吗?
正当我一个一个去尝试时,我发现,所有的预览图都是以.jpg格式结尾的,但是有些原图url却是以.png格式结尾,也就导致当原图格式也同样为.jpg时,按照此方法拼接可行,一但遇到.png格式的原图,就不可以了。
那我们怎么才能在预览图页面就能知道该图片的原图到底是什么格式呢?其实该网站已经告诉我们了:
我们鼠标右键检查该标签:
发现它在class为thumb-info的div标签里,我们后面可以以此为判断依据,判断该图片是什么格式,然后再将这个格式拼接到原图的url上:
至此,我们已经知道如何批量拿到原图url了,分析完毕,开始编写代码!
三、编写代码:
- 项目目录结构划分:
终于可以开始写代码了,我们首先新建一个文件夹,这里我就叫crawler
了。
在文件夹下鼠标右键,通过vscode打开。(啰嗦了...)
在vscode中打开终端窗口,键入npm init -y
此步骤是初始化一个项目,会自动生成一个package.json
的文件,该文件记录着这个项目的信息。
新建文件夹src
,在src下继续新建文件夹:app
用于存放核心代码,以及文件夹:utils
用于存放工具函数(个人习惯),src
下新建index.js
作为程序入口。
- 编写请求方法,拿到Random页的html文档元素:
首先我们需要用到一个第三方库来发送网络请求,这里我推荐使用axios
,目前前端开发用于网络请求最好用的第三方库之一。
cd到项目根目录,键入安装命令:npm install axios
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则是表示请求响应成功了。
对象中的data属性所包含的,其实就是我们需要的东西了,仔细观察,我们发现这其实就是一个html文档,对应的其实就是这个页面:
而我们恰恰正需要从这个页面中把所有预览图片的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
- 解析html文档,抓取预览图元素:
接下来我们在src/utils
下新建文件parse-element.js
,并在该文件中编写解析元素的方法。
为了解析dom元素,我们需要安装第二个第三方库,cheerio
用于解析html文件并操作dom元素,使用它在nodejs中操作dom,简直不要太方便。
在项目根目录中打开终端窗口,执行安装命令:npm install cheerio
,但在开始写代码前,我们需要知道预览图所在位置以及他的父级元素和祖先元素。鼠标右键预览图=>检查,我们发现所有的预览图都存在于figure标签中:
而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
执行代码,并查看打印结果,我们发现该方法返回的是一个个节点对象。
那我们怎么才能拿到url呢?在img元素上,我们发现url在
src
和data-src
中,我们只需要拿到这两个属性中的其中一个即可。使用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属性居然没有值。
所以我们只需要拿到
data-src
即可这样我们就拿到了所有的预览图url了,接下来我们需要判断后缀:
我们发现所有png格式的图片上,都会在一个class
为thumb-info
的div
中多出一个span
元素,并且class
为png
,而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)
})
运行结果如下:
我们已经拿到了所有关键信息,接下来就可以开始拼串操作了,将预览图和后缀按照之前分析的方法,拼接为原图的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
来存储图片的相关信息,因为后面还会用到,打印结果:
originalUrl
即原图Url,我们随便点开一个查看一下,这里我故意找了个.png格式的url: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
}
- 发送邮件:
最后一步,将我们解析好的url封装成邮件格式并发送出去,这里我们需要事先准备两个邮箱账号。
1:发送邮箱
2:接收邮箱
发送的邮箱需要登录进入邮箱官网申请IMAP/SMTP 服务授权码,以163邮箱为例:
执行命令: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
来执行代码了。
执行测试结果:
果然收到邮件了,上面的图片就是预览图了,我们点击“下载原图”就可以跳转至浏览器下载原图。
以上就是本教程的全部内容了,本人基于以上功能继续做了几项扩展:
- 可通过.env文件配置各项信息
- 可自由选择像素比例,以及爬取的数量
- 可配置定时任务并后台定时执行脚本等...
如果你感兴趣,可查看https://github.com/mihu915/wallpaperCrawler,也希望你能给我一个star。
总结:
本文我花费了大量的篇幅来讲解如何分析网站结构,其实无论做任何业务都应该养成这种思考的习惯,代码写的精不精妙需要我们不断的积累经验,但是养成一个好的思考习惯,总能让我们事半功倍。如果你觉得本文有帮助到你,希望你能点赞转发支持一下,也欢迎大家添加我的微信:bishu0913,我们一起学习前端技术。
转载请注明出处~