最近在學習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
複製代碼
更多相關內容請查閱這裏。
接下來就是使用爬到的數據生成圖片了,先來兩張,看看效果。敬請期待!!!
圖片生成項目已經完成,能夠查看:拿着爬蟲數據,搞事情啊!!