学习爬虫的时候突然有想到想做一个音乐播放小站,可以给自己或朋友听,但是音乐哪里来呢??想到自己常听的豆瓣FM,就越发的想把这些音乐都拿下来,因此有了下文通过豆瓣FM批量抓取上万首音乐,目前已经3W+。
从豆瓣FM抓音乐主要是看网站的音乐获取方式,有的可能是异步有的可能是同步,当然大部分还是异步的,有些可能数据要求比较严格,会有一些校验等等会很麻烦,不过还好,豆瓣的比较简单,找到了一个比较简单的请求接口,可以获取下一首歌曲。
其实,抓音乐也可以对音乐进行分析,所以准备把从豆瓣上获得的所有信息都存储起来,这里使用的是Mysql数据库,结构也比较简单,有四个表:音乐、歌手、频道、专辑。
由于接口是下一首,而且是随机的,所以有很大的几率是重复的,需要在处理过程中进行去重,而且有时候一个200首歌的频道,随机半天也不一定会把所有的歌曲全部拿下来,不过考虑到会一直抓取,而且本身歌曲数量就比较多了,也没必要太过完美。
大体的思路就是:
根据以上链接可以随机获得一首歌曲,然后循环频道即可。
/*持续抓取豆瓣的音乐并保存到数据库*/
//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