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

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

月光魔力鸭

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

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

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

构思

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

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

大体的思路就是:

实现

这里先放个豆瓣FM的链接:undefined

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

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 : undefined

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


感谢支持!

赞赏支持
提交评论
评论信息 (请文明评论)
暂无评论,快来快来写想法...
推荐
写文章总会需要一些素材,但是好多素材都是收费或有限制的,还是我要求不高,在千库网看了下还不错,有各签到还送VIP,于是就有了想法....
在开发的时候,经常会有css js 文件的变更,然后部署后发现没有起到作用,最终发现是缓存的问题,如何来方便的解决
前段时间做了个微信小程序反编译的小东西,不过因为功能不全,没加分包处理,正好处理下加上,又考虑到后续可能的更新情况,准备上手增加下更新功能。
在使用puppeteer 跳转窗口的时候,发现waitForNavigator 并不起作用,最后找到通过browser 获得page 并继续操作。
产品版本更新的时候经常会有一些数据库的差异,如果版本管理好的话,一步一步升级即可.. 但是如果好久没更新的话,还是有很多不确定的,只能挨着比对表和字段。比对了一次就烦了,写了这么一个工具,查询差异表和字段并给出sql语句。
最近家里正在装修,实在是不知道怎么做,之前看好好住APP上有不少设计的图,部分还挺好看。。就去看了下有没有WEB端,结果还真有,就有了下文,我抓了几万张图片,然后根据关键字进行分类,从里面找心仪的设计。
最近有接触到针对二维码进行识别的功能,然后对图片进行位置纠正、二维码扫描,并将数据进行整理。以下是通过nodejs做的,相对简单一些,没有太过严格,识别率也不是很高大约80%左右
最近看到知乎上一话题:微信公众号文章里的视频怎么下载?。看还是有很多人推荐啥工具啊,很是捉急,当然本次的主题也是通过程序来获取内容,但是目前来说仅仅是娱乐吧。