最近在学习Puppeteer进行自动化操做,另外一方面为了防止上班时间被打扰,是时候爬点歌单在上班的时候,用来抵抗外界的干扰了。html
项目完整代码地址:github.com/BingKui/WeC…node
v10.*.*
因为 Puppeteer 会下载一个 Chrome 浏览器到本地,因此可能较慢,可使用 cnpm
或者切换为 淘宝镜像。git
const mongoose = require('mongoose');
// 数据库地址
const mongoDB = 'mongodb://127.0.0.1:27017/wechat';
// 连接数据库
mongoose.connect(mongoDB);
// 监听数据库事件
const db = mongoose.connection;
// 链接异常
db.on('error', function (err) {
console.log('Mongoose connection error: ' + err);
});
// 链接断开
db.on('disconnected', function () {
console.log('Mongoose connection disconnected');
});
// 链接成功
db.on('connected', function () {
console.log('Mongoose connection open to ' + mongoDB);
});
复制代码
首先咱们建立一个无头的浏览器对象。github
const browser = await puppeteer.launch({timeout: 300000, headless: true, args: ['--no-sandbox']});
复制代码
具体参数含义请点解这里查看。mongodb
说明:args 参数为可选项,以上参数是为了兼容 CentOS ,添加的参数数据库
打开地址:https://music.163.com/#/discover/playlist
。npm
let url = 'https://music.163.com/#/discover/playlist';
// 建立
const page = await browser.newPage();
// 跳转到歌单页面
await page.goto(url);
复制代码
网易云音乐的歌单页使用了 iframe
嵌套的方式,因此咱们要在页面中获取到 iframe
中的内容,并提取咱们须要的信息。api
// 获取歌单的iframe
let iframe = await page.frames().find(f => f.name() === 'contentFrame');
复制代码
Puppeteer 提供了能够在 iframe
中执行js的方法,咱们能够直接执行,经过原生js来获取想要的数据。数组
// 获取歌单
const result = await iframe.evaluate(() => {
// 获取全部元素
const elements = document.querySelectorAll('#m-pl-container > li');
// 建立数组,存放获取的数据
let res = [];
for (let ele of elements) {
let image = ele.querySelector('.j-flag').getAttribute('src');
let name = ele.querySelector('.tit').innerText;
let count = ele.querySelector('.nb').innerText;
let author = ele.querySelector('.nm').innerText;
let address = 'https://music.163.com/#' + ele.querySelector('.msk').getAttribute('href');
const flag = (count.indexOf('万') > -1) && (parseInt(count.split('万')[0]) > 1000);
if (flag) {
res.push({
image,
name,
count,
author,
address,
from: 'netease',
});
}
}
// 返回数据
return res;
});
复制代码
经过分析页面能够看到,歌单一共 35 页,而且每页有 35 条数据,而且分页是经过 url 参数区分的,因此咱们能够简单暴力一点,写个循环搞定(主要仍是懒)。浏览器
高级操做:能够经过 Puppeteer 的方法,获取页面,而后点击下一页,判断是否可以点击下一页来肯定是否存在下一页。须要了解的能够自行研究。
为了方便操做,咱们把获取每页数据封装成一个方法:getOnePageData
。
const getOnePageData = async (page, pageNumber) => {
const url = `https://y.qq.com/portal/playlist.html#t3=${pageNumber}&`;
// 跳转到页面
await page.goto(url);
await page.setViewport({
width: 1300,
height: 5227,
});
// 等待两秒,加载图片
await page.waitFor(2000);
// 获取歌单
const result = await page.evaluate(() => {
// 此处与上方方法同样,省略
...
});
return result;
}
复制代码
而后循环获取数据。
// 定于数组存储数据
let musicPlayList = [];
const page = await browser.newPage();
for (let i = 0; i < 1191; i += 35) {
const item = await getOnePageData(page, i);
console.log(`获取到数据${item.length}条。`);
musicPlayList = musicPlayList.concat(item);
}
复制代码
定义数据模型。
// models/music.js
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const MusicSchema = new Schema({
image: String,
name: String,
count: String,
author: String,
address: String,
from: String,
date: Date,
show: Boolean, // 是否展现
});
const MusicModel = mongoose.model('playlist', MusicSchema);
module.exports = MusicModel;
复制代码
封装基本的添加方法。
// server/music.js
const MusicModel = require('../models/music.js');
const save = (item) => {
findBuName(item.name, (obj) => {
if (obj) {
console.log('已经保存,数据');
obj.remove();
}
const saveObject = new MusicModel(item);
saveObject.save((err) => {
if (err) return handleError(err);
});
});
}
const findBuName = (name, callback) => {
MusicModel.findOne({name}, (err, item) => {
if (err) {
callback && callback(false);
}
callback && callback(item);
});
};
module.exports = {
save,
};
复制代码
因为爬取的数据存在重复的数据,为了减小没必要要的资源浪费,保存前先进行数据的去重。
// 保存以前去重
let hash = {};
musicPlayList = musicPlayList.reduce((item, next) => {
hash[next.address] ? '' : hash[next.address] = true && item.push(next);
return item
}, []);
复制代码
保存数据到 MongoDB 数据库。
const MusicServer = require('../server/music.js');
// 保存数据
for (let i = 0; i < musicPlayList.length; i++) {
const item = musicPlayList[i];
item.date = dayjs().format('YYYY-MM-DD HH:mm:ss');
item.show = true;
MusicServer.save(item);
}
复制代码
最后别忘了关闭开始的时候建立的浏览器。
browser.close();
复制代码
到这里,爬取网易云音乐的精品歌单已经完成了。接下来开始爬取 QQ 音乐。
因为爬取方式基本同样,下面只介绍不一样的地方。
分析页面,QQ 音乐,没有采用和网易云音乐同样的 iframe
方式,这样爬取更加简单。
能够经过在页面上执行方法就可以爬取到咱们须要的数据。
// 获取歌单
const result = await page.evaluate(() => {
const elements = document.querySelectorAll('#playlist_box > li');
let res = [];
for (let ele of elements) {
const _n = ele.querySelector('.js_playlist');
let image = 'https:' + ele.querySelector('.playlist__pic').getAttribute('src');
let name = _n.getAttribute('title');
let count = ele.querySelector('.playlist__other').innerText.split(':')[1].replace(/\s+/g, '');
let author = ele.querySelector('.playlist__author').innerText.replace(/\s+/g, '');
let address = `https://y.qq.com/n/yqq/playsquare/${_n.getAttribute('data-disstid')}.html#stat=${_n.getAttribute('data-stat')}`;
const flag = (count.indexOf('万') > -1) && (parseInt(count.split('万')[0]) > 1000);
if (flag) {
res.push({
image,
name,
count,
author,
address,
from: 'qq'
});
}
}
return res;
});
复制代码
因为 QQ 音乐采起的分页方式和网易云音乐同样,全部咱们还使用相同的方法,暴力爬取(可见我是有多懒~~)。
找到页面中一共有多少页歌单,而后写个像下面的循环。
// 定于数组存储数据
let musicPlayList = [];
const page = await browser.newPage();
// 爬取是总歌单也为 120 页
for (let i = 1; i < 120; i++) {
const item = await getOnePageData(page, i);
console.log(`获取到数据${item.length}条。`);
musicPlayList = musicPlayList.concat(item);
}
复制代码
而后像上边同样,保存进数据库就能够了。
因为天天歌单都会有大量的播放量,不断的更新,所以写个定时任务,天天定时爬取更新数据才是稳妥的方法,可以保证咱们的数据最新。
把爬取方法封装成模块方法,而后在固定的时候调用执行爬虫。
// qq.js
const QQMusic = async () => {
const browser = await puppeteer.launch({timeout: 300000, headless: true, args: ['--no-sandbox']});
// 定于数组存储数据
let musicPlayList = [];
const page = await browser.newPage();
for (let i = 1; i < 120; i++) {
const item = await getOnePageData(page, i);
console.log(`获取到数据${item.length}条。`);
musicPlayList = musicPlayList.concat(item);
}
// 保存以前去重
let hash = {};
musicPlayList = musicPlayList.reduce((item, next) => {
hash[next.address] ? '' : hash[next.address] = true && item.push(next);
return item
}, []);
MusicServer.updateAllHide(() => {
// 保存数据
for (let i = 0; i < musicPlayList.length; i++) {
const item = musicPlayList[i];
item.date = dayjs().format('YYYY-MM-DD HH:mm:ss');
item.show = true;
MusicServer.save(item);
}
}, { from: 'qq' });
await browser.close();
};
module.exports = QQMusic;
// netease.js
const NeteaseMusic = async () => {
const browser = await puppeteer.launch({timeout: 300000, headless: true, args: ['--no-sandbox']});
// 定于数组存储数据
let musicPlayList = [];
const page = await browser.newPage();
for (let i = 0; i < 1191; i += 35) {
const item = await getOnePageData(page, i);
console.log(`获取到数据${item.length}条。`);
musicPlayList = musicPlayList.concat(item);
}
// 保存以前去重
let hash = {};
musicPlayList = musicPlayList.reduce((item, next) => {
hash[next.address] ? '' : hash[next.address] = true && item.push(next);
return item
}, []);
MusicServer.updateAllHide(() => {
// 保存数据
for (let i = 0; i < musicPlayList.length; i++) {
const item = musicPlayList[i];
item.date = dayjs().format('YYYY-MM-DD HH:mm:ss');
item.show = true;
MusicServer.save(item);
}
}, { from: 'netease' });
await browser.close();
};
module.exports = NeteaseMusic;
复制代码
应用 node-schedule
模块,咱们可以简单的建立定时任务。
// 建立爬取歌单定时任务
const qqPlayList = () => {
TimeSchedule.scheduleJob('0 5 0 * * *', async () => {
await QQMusic();
});
}
const neteasePlayList = () => {
TimeSchedule.scheduleJob('0 50 0 * * *', async () => {
await NeteaseMusic();
});
}
const scheduleList = () => {
qqPlayList();
neteasePlayList();
};
scheduleList();
复制代码
注意两个爬虫之间的时间间隔,尽可能大一些,方式同时两个爬虫都运行,形成服务器的过大压力(土豪机,请随意~~~)。
使用 pm2
咱们能够方便的管理咱们的 NodeJS 服务。
安装 pm2
npm install -g pm2
复制代码
使用 pm2
启动咱们的服务。
pm2 start index.js
复制代码
更多相关内容请查阅这里。
接下来就是使用爬到的数据生成图片了,先来两张,看看效果。敬请期待!!!
图片生成项目已经完成,能够查看:拿着爬虫数据,搞事情啊!!