手摸手教你使用 nodejs 编写爬虫
手摸手教你使用 nodejs 编写爬虫
一、概述:
- 爬虫思路:
- 发送网络请求获取 html 文档页面
- 解析 html 文档,拿到原图和预览图的 url
- 将 url 转换为 html 格式。
- 将转换后的格式写入邮件并发送
二、准备工作:
- 安装 nodejs 环境:(已经安装过的同学可以跳过这一步了)在nodejs官网下载安装包后傻瓜式下一步安装即可,这里推荐安装 lts 版本,即长期维护版。相比较 java,nodejs 的环境搭建就显得比较简单了。
安装完成以后,接着在 cmd 命令行窗口键入 node --version 查看版本。
若打印版本信息,说明安装成功可以进行下一步了。若提示没有该命令,则需要手动配置一下环境变量。具体如何配置本文不做讲解,自行百度吧,其实并不复杂,也就两分钟能解决的事。
选择壁纸站点: 这里我选择知名壁纸站点:https://wallhaven.cc,这个站点的壁纸质量如何,想必不需要我多说了吧。
分析站点节点结构:
我们拿到该网站打开后,能看到首页显示了很多图片,这些图片其实都是预览图,分辨率很低,不是我们想要的,并且我们希望每次抓到的图都是随机的,所以在首页抓并不合适。这里我们点击 Random 进入到随机页面。
我们发现,在这个页面下,每次刷新都能获取到不一样的图片,并且这里的图片的也都是预览图,所以我们需要找到原图的 url 在什么位置。 随便点开一张图片,跳转页面后,右键原图点击检查。
此步骤我们找到了关键元素: img
,该元素节点中的src
属性值就是原图的 url
将鼠标放在 url 上,我们发现,该 url 图片的实际大小为 6016x3384。 恭喜,我们已经找到了原图的 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 格式的原图,就不可以了。 那我们怎么才能在预览图页面就能知道该图片的原图到底是什么格式呢?其实该网站已经告诉我们了:
相信你已经发现了,每张预览图都存在额外的标签,会将图片格式标注出来,带 PNG 的表示该原图为 png 格式,不带则表示该图片为 jpg 格式。 我们鼠标右键检查该标签:
发现它在 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/loclink/wallpaper-crawler,如果你觉得本工具还不错,请留下一个star吧~
总结:
本文我花费了大量的篇幅来讲解如何分析网站结构,代码写的精不精妙需要我们不断的积累经验,但是养成一个好的思考习惯,总能让我们事半功倍。如果你觉得本文有帮助到你,希望你能点赞转发支持一下。
关于:
原文地址:https://www.tj520.top/views/articles/back-end/nodejs-crawler.html
我的微信:coder7915 欢迎来撩~
如需转载,请注明出处~