豆瓣FM批量抓取上万首音乐

豆瓣FM批量抓取上万首音乐

月光魔力鸭

2018-09-17 14:49 阅读 552 喜欢 0 豆瓣FM 批量抓取音乐

学习爬虫的时候突然有想到想做一个音乐播放小站,可以给自己或朋友听,但是音乐哪里来呢??想到自己常听的豆瓣FM,就越发的想把这些音乐都拿下来,因此有了下文通过豆瓣FM批量抓取上万首音乐,目前已经3W+。

从豆瓣FM抓音乐主要是看网站的音乐获取方式,有的可能是异步有的可能是同步,当然大部分还是异步的,有些可能数据要求比较严格,会有一些校验等等会很麻烦,不过还好,豆瓣的比较简单,找到了一个比较简单的请求接口,可以获取下一首歌曲。

构思

其实,抓音乐也可以对音乐进行分析,所以准备把从豆瓣上获得的所有信息都存储起来,这里使用的是Mysql数据库,结构也比较简单,有四个表:音乐、歌手、频道、专辑。

由于接口是下一首,而且是随机的,所以有很大的几率是重复的,需要在处理过程中进行去重,而且有时候一个200首歌的频道,随机半天也不一定会把所有的歌曲全部拿下来,不过考虑到会一直抓取,而且本身歌曲数量就比较多了,也没必要太过完美。

大体的思路就是:

实现

这里先放个豆瓣FM的链接:https://douban.fm/j/v2/playlist?channel=1&kbps=128&client=s%3Amainsite%7Cy%3A3.0&app_name=radio_website&version=100&type=s&sid=382400&pt=&pb=128&apikey=

根据以上链接可以随机获得一首歌曲,然后循环频道即可。

module

代码

/*持续抓取豆瓣的音乐并保存到数据库*/

//https://douban.fm/j/v2/songlist/470992/?kbps=192
//根据歌单进行查找

const superagent = require('superagent');
const query = require('simple-mysql-query');
const tool = require('../util');
class DouBan {
    constructor ( opts ){
        opts = opts || {}
        this.ids = [];
        this.requestOpt = {
            'Accept':'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
            'Accept-Encoding':'gzip, deflate, sdch',
            'Accept-Language':'zh-CN,zh;q=0.8',
            'Cache-Control':'no-cache',
            'Connection':'keep-alive',
            'Cookie':'bid=RveZRi0m5ds; _ga=GA1.2.1305540715.1533201218; flag="ok"; ac="1533776043"; _gid=GA1.2.228834671.1533776081; _gat=1',
            'Host':'fm.douban.com',
            'Pragma':'no-cache',
            'Upgrade-Insecure-Requests':'1',
            'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2729.4 Safari/537.3'
        };
        /*channel*/
        this.channelUrl = 'https://fm.douban.com/j/v2/channel_info?id=';
        this.channelStart = 1;//开始channel
        this.channelEnd = 300;//结束channel
        this.channelPer = 3000;//从3000中随机,单位ms
        this.currentChannel = null;//可用的频道,每次启动的时候都自动检查一遍channel,然后更新并缓存。

        //频道相关的歌曲查询
        
        //请求频率
        this.existNum = 0;//如果连续50次都是已存在的歌,就切换频道
        this.channelError = 0;//超过5次获取失败,则切换频道
        this.maxErrorNum = 50;//最大失败数量

        //启动数据抓取
        this.log('启动数据抓取工具:------------------------',1);
        this.findChannel();
        this.findByChannel();

    }
    log(msg,iserror){
        tool.log('豆瓣:',msg,iserror);
    }
    //获得不同程度的频率
    getPer (flag){
        if(flag == 0){
            return 30 * 1000;//失败20s再继续
        }else if(flag == 1){
            return  5000 + Math.random() * 20000;
        }else if(flag == 2){
            return 10 * 1000;
        }
    }
    //获得当前的channel所在的频道查询music的url
    getMusicChannelUrl () {
        var that = this;
        return 'https://douban.fm/j/v2/playlist?channel='+that.currentChannel+'&kbps=128&client=s%3Amainsite%7Cy%3A3.0&app_name=radio_website&version=100&type=s&sid=382400&pt=&pb=128&apikey=';
    }

    //根据频道查找歌曲
    findByChannel () {
        this.log('开始根据频道查询歌曲')
        var that = this;
        that.existNum ++ ;
        if(that.currentChannel){
            //1.查询歌曲信息
            superagent.get(that.getMusicChannelUrl())
            .set(that.requestOpt)
            .end( (err,res) => {
                if(err){
                    that.channelError ++ ;
                    that.log('根据当前可用频道获取歌曲失败,20s后重新请求',0)
                    //如果报错,应该是被禁止或者网络超时
                    that.log(err,0);
                    //20s后重新发起
                    setTimeout(function(){
                        that.findByChannel();
                        if(that.channelError > 5){
                            that.channelError = 0;
                            that.findChannel();	
                        }
                    },that.getPer(0))
                }else{
                    that.channelError = 0;
                    that.log('获得可用歌曲,检查歌曲信息',1)
                    //获得歌曲信息
                    var resobj = JSON.parse(res.text);
                    if(resobj.song && resobj.song.length > 0){
                        var song = resobj.song[0];
                        //1.根据歌曲ID检查歌曲是否存在,同时查询当前频道内的歌曲数量和频道歌曲总数
                        var songId = song.sid;
                        query({sql : 'select count(1) as num from music_music where sid=? ',params : [songId]})
                        .then( rs => {
                            var rst = rs[0];
                            if(rst[0].num > 0){//
                                return 0;
                            }else{
                                //检查专辑
                                if(song.release && song.release.id){
                                    return query({sql : 'select count(1) as num from music_album where id=?',params : [song.release.id]});
                                }else{
                                    return 1;
                                }
                            }
                        }).then( rs => {
                            //处理专辑
                            if(rs=== 0){//忽略该歌曲
                                return 0;
                            }else if(rs == 1){
                                //该歌曲没有专辑
                                return 1;
                            }else{
                                var rst = rs[0];
                                if(rst[0].num > 0){
                                    return 1;
                                }else{
                                    //插入专辑记录
                                    return query({sql : 'insert into music_album (id,link,ssid) values (?,?,?) ',params : [song.release.id,song.release.link,song.release.ssid]});
                                }
                            }
                        }).then( rs=> {
                            //处理歌手
                            if(rs === 0){
                                return 0;//忽略该歌曲
                            }else{
                                //查询歌手是否存在
                                var singers = song.singers;
                                if(singers.length > 0){
                                    var sql = 'select * from music_singer where id in (';
                                    var params = [];
                                    for(var i=0;i<singers.length;i++){
                                        var sing = singers[i];
                                        sql += '?' + (i == singers.length -1 ? '' : ',');
                                        params.push(sing.id);
                                    }
                                    sql +=')'
                                    return query({
                                        sql : sql,
                                        params : params
                                    });
                                }else{
                                    return 1;
                                }
                            }
                        }).then(rs => {
                            if(rs === 0){
                                return 0;
                            }else if(rs ===1 ){
                                return 1;
                            }else{
                                //处理歌手信息
                                var singers = rs[0] || [];
                                var hasId = {};
                                singers.forEach(function(item){
                                    hasId[item.id] = true;
                                });
                                var saveSingers = [];
                                song.singers.forEach(function(item){
                                    if(hasId[item.id] !== true){
                                        //不存在
                                        saveSingers.push({
                                            id : item.id,
                                            name : item.name,
                                            "name_usual":item.name_usual,
                                            region : item.region.join(','),
                                            avatar : item.avatar,
                                            genre : item.genre.join('__')
                                        });
                                    }
                                })
                                if(saveSingers.length > 0){
                                    var mmm = {};
                                    var sql = 'insert into music_singer (id,name,avatar,name_usual,region,genre) values ';
                                    var params = [];
                                    for(var i=0;i<saveSingers.length;i++){
                                        var singer = saveSingers[i];
                                        if(!mmm[singer.id]){
                                            mmm[singer.id] = true;//过滤重复数据
                                            sql += (params.length > 0 ? ',' : '') + '(?,?,?,?,?,?)';
                                            params = params.concat([singer.id,singer.name,singer.avatar,singer.name_usual,singer.region,singer.genre]);
                                        }
                                    }
                                    return query({sql : sql,params : params})
                                }else{
                                    return 1;//没有要保存的歌手信息,或者已经都保存过了
                                }
                            }
                        }).then(rs=>{
                            //开始保存歌曲
                            if(rs === 0){
                                that.log('歌曲重复,当前频道:'+that.channelNow,0);
                                return 0;
                            }else{
                                that.existNum = 0;
                                that.log('保存歌曲:'+song.title,0);
                                var sql = 'insert into music_music (sid,ssid,aid,album,albumtitle,artist,file_ext,kbps,length,picture,public_time,sha256,title,url,albumid,singerid,channelid) values (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)';

                                var singerId= '';
                                var singers = song.singers;
                                if(singers && singers.length >0){
                                    singerId = singers.map(item => {
                                        return item.id;
                                    }).join(',');
                                }
                                var params = [
                                    song.sid,
                                    song.ssid,
                                    song.aid,
                                    song.album,
                                    song.albumtitle,
                                    song.artist,
                                    song.file_ext,
                                    song.kbps,
                                    song.length,
                                    song.picture,
                                    song.public_time,
                                    song.sha256,
                                    song.title,
                                    song.url,
                                    song.release && song.release.id ? song.release.id : '',
                                    singerId,
                                    that.currentChannel
                                ];
                                return query({sql : sql,params : params});
                            }
                        }).then(rs=>{
                            //处理歌曲数量信息
                            return query([
                            {
                                sql : 'select count(1) as num from music_music where channelid=? ',
                                params : [that.currentChannel]
                            },{
                                sql : 'select song_num as num from music_channel where channelId=?',
                                params : [that.currentChannel]
                            }
                            ]);
                        }).then(rs=>{
                            //判断歌曲数量和已存数量
                            var rst1 = rs[0],rst2 = rs[1];
                            var num1 = rst1[0].num || 0,num2 = rst2[0].num || 0;
                            num1 = parseInt(num1,10);
                            num2 = parseInt(num2,10);
                            if((num1 > num2 - 50 && num2 > 100) || that.existNum > that.maxErrorNum){
                                that.existNum = 0;
                                //更换channel
                                that.log('当前频道歌曲数量过少,更换频道',1)
                                that.currentChannel = null;
                                that.findChannel();
                                setTimeout(function(){
                                    that.findByChannel();
                                },that.getPer(1))
                            }else{
                                that.log('本次查询完毕,5s后继续重新请求当前频道',1)
                                setTimeout(function(){
                                    that.findByChannel();
                                },that.getPer(1))
                            }
                        }).catch(err=>{
                            that.log(err);
                            that.log('保存歌曲信息过程中出错',0);
                            setTimeout(function(){
                                that.findByChannel();
                            },that.getPer(0))
                        })
                    }else{
                        that.log('获得歌曲,但无信息,重新请求',0);
                        setTimeout(function(){
                            if(that.existNum > 10){
                                that.existNum = 0;
                                that.findChannel();
                            }
                            that.findByChannel();
                        },that.getPer(1));
                    }
                }
            })
        }else{
            that.log('未获得可用频道,5s后重新请求',0)
            //当前没有可用频道,则5秒后,重新查询
            setTimeout( () => {
                that.findByChannel();
            },that.getPer(1));
        }
    }

    //从不同类型里面持续检索歌曲,并请求,请求频率要低

    updateChannel ( channelObj ){
        var that = this;
        const sqlA = {
            sql : 'select * from music_channel where channelId=?',
            params : [channelObj.channelId]
        };
        return query(sqlA).then(function(rs){
            var rst = rs[0];
            if(rst.length == 0){//不存在,插入
                var sqlB = {
                    sql : 'insert into music_channel (channelId,name,intro,cover,song_num) values (?,?,?,?,?)',
                    params : [channelObj.channelId,channelObj.name,channelObj.intro,channelObj.cover,channelObj.song_num]
                };
                that.currentChannel = channelObj.channelId;//更新当前可用channelID
                that.log('该频道是新频道,设为可用频道')
                return query(sqlB);
            }else{
                //判断下num
                var obj = rst[0];
                if(channelObj.song_num > obj.song_num){
                    var sqlC = {
                        sql : 'update music_channel set song_num=? where channelId=?',
                        params : [channelObj.song_num,channelObj.channelId]
                    };
                    that.currentChannel = channelObj.channelId;//更新可用ID。
                    that.log('当前频道为旧频道,但有歌曲添加,设为可用频道')
                    return query(sqlC);
                }else{
                    //
                    that.currentChannel = channelObj.channelId;//更新可用ID。
                    that.log('当前频道为旧频道,无变化,设为可用频道')
                    return true;//
                }
            }
        });
    }

    /*从0 - 300 检查可用channel*/
    findChannel () {
        this.log('开始重新查找可用频道')
        const that = this;
        if(!that.channelNow){
            that.channelNow = that.channelStart;	
        }
        if(that.channelNow >= that.channelEnd){
            //结束查询channel
            that.channelNow = that.channelStart;
        }
        superagent.get(that.channelUrl+that.channelNow).set(that.requestOpt).end(function(err,res){
            that.channelNow++;
            if(err){
                that.log('查找可用频道报错')
                that.findChannel();
            }else{
                const resObj = JSON.parse(res.text);
                const data = resObj.data;
                if(data && data.channels && data.channels.length > 0){
                    const item = data.channels[0];
                    const channelObj = {
                        name : item.name,
                        cover : item.cover,
                        intro : item.intro,
                        song_num : item.song_num,
                        channelId : item.id
                    };
                    that.log('获得可用频道准备更新数据库')
                    //更新到数据库中,并判断当前频道是否可采集,如果可采集则停止,否则进入下一个循环。
                    that.updateChannel(channelObj);
                }else{
                    that.findChannel();
                }
            }
        })
    }
}
module.exports = DouBan;

以上。


具体的数据库表结构我就不发了,有需要的可以联系我。

以上所有全部用于学习、研究使用,若有侵权请联系本站管理员删除。

具体也可以参考github : https://github.com/chrunlee/spider/tree/master/douban

转载请注明出处: https://chrunlee.cn/article/nodejs-douban-fm-music.html


感谢支持!

赞赏支持
提交评论
评论信息 (请文明评论)
暂无评论,快来快来写想法...
推荐
碰到个小需求,本来实现挺简单的,用的electron,开发模式下各种顺畅...半个小时就搞定了,结果倒在了electron打包上..这个坑我应该跳进来了好多次了..
也不知道咋回事 ... 哈哈,忽然想研究下磁力网站,其实并不是很想懂里面的原理,只是搞不明白他们的资源是从哪里来的..很是纳闷?
对于开发来说,看到别人家的小程序都这么靓,这么顺畅,这么好用,用户又多... 自然是眼馋的..用户馋不来,可以先馋他的身子..啊不,代码啊。
因为自己的记录笔记的应用是有道云,又想着把有道云跟自己的小网站联通起来,所以查找了有道云的,然后实现了nodejs版本的sdk.
thinkjs框架使用ueditor记录。
介绍几个日常开发中常用的几个小工具: anywhere / anywhere-auth / watchlessc / changeext
在开发项目过程中,经常需要将开发的项目部署到服务器上,但是每个环境都有每个环境的配置等等,如果每次打包的时候都要去调整(可能删除、替换等),那就很烦人了,这里分享下自己实现的几个简单的小工具(当然这个工具可能只对我自己有用),希望能够帮到你。
经常会遇到需要系统重启后自动执行的一些任务,在windows 上可以将对应的程序打包成service 然后自启动即可