使用Puppeteer輕鬆爬取網易雲音樂、QQ音樂的精品歌單

背景

最近在學習Puppeteer進行自動化操做,另外一方面爲了防止上班時間被打擾,是時候爬點歌單在上班的時候,用來抵抗外界的干擾了。html

地址

項目完整代碼地址:github.com/BingKui/WeC…node

工具

  • NodeJS:基本環境 版本 >= v10.*.*
  • Puppeteer:Google官方出品的無頭Chrome node庫
  • MongoDB:用來存儲爬取到的數據
  • mongoose:node 中連接、操做 MongoDB 的驅動庫
  • dayjs:時間處理庫
  • node-schedule:node 定時任務庫,線上部署用到,本地本地沒有用到
  • pm2:node 進程管理工具,線上部署使用

目標

  • 爬取網易雲音樂播放量超過 1000W 的歌單
  • 爬取 QQ 音樂播放量超過 1000W 的歌單
  • 把爬取的歌單保存到數據庫
  • 建立定時任務,線上部署,天天定時爬取、保存

準備工做

  • 保證本地安裝了 MongoDB 數據庫,並能正常鏈接,具體請自行百度。
  • 安裝所需的庫文件
  • 編寫數據庫鏈接文件

安裝庫文件注意

因爲 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/playlistnpm

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);
}
複製代碼

保存數據到 MongoDB 數據庫

定義數據模型。

// 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 音樂精品歌單

因爲爬取方式基本同樣,下面只介紹不一樣的地方。

分析頁面結構

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
複製代碼

更多相關內容請查閱這裏

預告

接下來就是使用爬到的數據生成圖片了,先來兩張,看看效果。敬請期待!!!

QQ歌單
網易雲音樂歌單

2018-08-03補充:

圖片生成項目已經完成,能夠查看:拿着爬蟲數據,搞事情啊!!

相關文章
相關標籤/搜索