node.js 實現爬蟲批量下載喜馬拉雅音訊
前提:最近一直在看node,平時碎覺喜歡聽盜墓筆記啥的有聲小說,然後突然就就想試著寫個爬蟲自己下載下來,雖然有點多此一舉,但是就當學習練練手了,在這裡記錄一下!
沒有express基礎的,請先行了解
確定需求
- 1,拿到xmly的資料,分析api
- 2,用node開發介面
- 3,構建前端頁面
一,前端頁面構建
技術:vue-cli,iview,axios
ps:因為準備以後開發其他的demo都放到一起,用一個server去轉發介面,所以準備搭建一個vue專案,方便以後加入其他的demo,如果你只是為了實現本文的功能,前端頁面大可不必搭我這套,可以自選方法實現,具體實現效果如下圖
二,拿到xmly的資料,分析api,用node發起請求拿到資料
2.1 找到xmly音訊的真實連結
開啟喜馬拉雅的PC官網,隨意選一個免費的小說點進去,開啟瀏覽器控制後臺,觀察http請求,如下圖
通過分析http請求,我們可以拿到api,在經過你不咋嚴密的觀察後,你可以很容易分析出一些關鍵的欄位
首先,我們先從請求引數分析,看下圖:
可以看出albumId是識別每一本小說的唯一標識,這個欄位我們將來就用於查詢資料,後面的pageNum,pageSize很容觀察出是用來做分頁的。
接下來看響應的欄位,在又經過你不咋嚴密的觀察後,你會很容易的發現,下圖的欄位,肯定就是每條音訊的詳細api了
展開之後,在最後一次經過你不咋嚴密的觀察後,你會發現很巧,你要的資料都在這裡
想我這種英語四級都沒過的人都看出來了,src:音訊真實連結,tranckName:音訊每集的名字,albumName:小說名
2.2 用node寫一個簡單的爬蟲,抓取資料
需要用到的模組如下圖:
superagent是nodejs裡一個非常方便的客戶端請求代理模組,他很大的特色就是鏈式呼叫,這裡我用到superagent向https://www.ximalaya.com/revision/play/album傳送get請求,拼上之前我們提到的請求引數
app.get('/', function (req, res, next) { let _res = res; let params = { albumId:'123131', pageNum:'1', sort:'-1', pageSize:'30' } let param = qs.stringify(params) superagent .get('https://www.ximalaya.com/revision/play/album?' + param) .then(res => { _res.send(res.body.data.tracksAudioPlay) }) .catch(err => { _res.send(err) }) }) app.listen(5050,function(){ console.log('正在監聽5050埠') })
啟動node服務,在瀏覽器上輸入http://localhost:5050/,應該就可以看到你請求到資料了,如下圖:
OK,到現在我們的資料已經拿到了,喝杯茶水泡點枸杞休息一下,然後繼續~
三,用node開發介面
3.1 確定我們的介面需求:
- /getXmlyList:實現通過albumId查詢,獲取音訊的list,傳給前端
- /downloadSingle:實現批量下載,將下載結果返回給前端
3.2 建立app.js 編寫node服務:
//app.js
const express = require('express')
const querystring=require('querystring');
const path = require('path');
const api = require('./route/api')
let app = express();
//使用router中介軟體
app.use('/xmly',api)
//路由
app.get('/', function (req, res, next) {
res.sendFile(path.join(__dirname + '/html/index.html'));
});
//監聽埠
app.listen(5050,function(){
console.log('正在監聽5050埠')
})
3.3 建立route/api.js 編寫api:
3.3.1 /getXmlyList 介面:
//獲取資料列表
router.get('/getXmlyList',(req,res) => {
let _res = res;
let params = {
albumId:req.query.albumId,//get請求的引數在req.query裡
pageNum:'1',
sort:'-1',
pageSize:'30'
}
let param = qs.stringify(params)
//這裡使用superagent代理髮送get請求喜馬拉雅的介面獲取資料
superagent
.get('https://www.ximalaya.com/revision/play/album?' + param)
.then(res => {
//這裡請求成功將資料響應給前端
_res.json({
status: 1,
msg: 'success',
list: res.body.data.tracksAudioPlay
})
})
.catch(err => {
//響應獲取資料失敗
_res.json({
status: -1,
msg: 'error'
})
})
});
3.3.2 /downloadSingle 介面:
1.使用fs.createWriteStream()寫入檔案:
var downLoadMp3 = function(dir,name,filePath){
request(dir).pipe(fs.createWriteStream( filePath + name +'.mp3')).on('close',function(){
console.log('saved' + name)
})
}
2.使用async非同步批量下載:
將檔案下載在根目錄的./mp3路徑下
關於async的map操作,詳見:async_demo/map.js,對集合中的每一個元素,執行某個非同步操作,得到結果。所有的結果將彙總到最終的callback裡。與forEach的區別是,forEach只關心操作不管最後的值,而map關心的最後產生的值。
- 並行執行。async.map同時對集合中所有元素進行操作,結果彙總到最終callback裡。如果出錯,則立刻返回錯誤以及已經執行完的任務的結果,未執行完的佔個空位
- 順序執行。async.mapSeries對集合中的元素一個一個執行操作,結果彙總到最終callback裡。如果出錯,則立刻返回錯誤以及已經執行完的結果,未執行的被忽略。
router.get('/downloadSingle',function(req, res, next){
let mp3List = JSON.parse(req.query.paramJson)
//檢查是否存在./mp3路徑如果不存在建立./mp3目錄後再下載,如存在直接下載
fs.exists('./mp3', function (exists) {
// console.log(exists ? "it's there" : "no file!");
if(!exists) {
//建立./mp3目錄
fs.mkdir('./mp3',0777, function (err) {
if (err) {
throw err;
}else {
//使用async非同步批量下載
async.mapSeries(mp3List,function(item, callback){
setTimeout(function(){
downLoadMp3(item.dir,item.name,'./mp3/')
callback(null, item);
},400);
}, function(err, results){
res.json({
status: 1,
msg: 'saved!'
})
});
}
});
}else {
async.mapSeries(mp3List,function(item, callback){
setTimeout(function(){
downLoadMp3(item.dir,item.name,'./mp3/')
callback(null, item);
},400);
}, function(err, results){
res.json({
status: 1,
msg: 'saved!'
})
});
}
});
});
var downLoadMp3 = function(dir,name,filePath){
request(dir).pipe(fs.createWriteStream( filePath + name +'.mp3')).on('close',function(){
console.log('saved' + name)
})
}
完整api.js程式碼如下:
const express = require('express');
const superagent = require('superagent');
const qs = require('querystring');
const router = express.Router();
const path = require('path')
const fs = require('fs')
const request = require('request')
const async = require('async')
//獲取資料列表
router.get('/getXmlyList',(req,res) => {
let _res = res;
console.log(req.query)
let params = {
albumId:req.query.albumId,
pageNum:'1',
sort:'-1',
pageSize:'300'
}
let param = qs.stringify(params)
superagent
.get('https://www.ximalaya.com/revision/play/album?' + param)
.then(res => {
// _res.send(res.body.data.tracksAudioPlay);
// console.log(typeof res.body)
_res.json({
status: 1,
msg: 'success',
list: res.body.data.tracksAudioPlay
})
})
.catch(err => {
res.json({
status: -1,
msg: 'error'
})
})
});
//下載音訊
router.get('/downloadSingle',function(req, res, next){
let mp3List = JSON.parse(req.query.paramJson)
fs.exists('./mp3', function (exists) {
// console.log(exists ? "it's there" : "no file!");
if(!exists) {
fs.mkdir('./mp3',0777, function (err) {
if (err) {
throw err;
}else {
async.mapSeries(mp3List,function(item, callback){
setTimeout(function(){
downLoadMp3(item.dir,item.name,'./mp3/')
callback(null, item);
},400);
}, function(err, results){
res.json({
status: 1,
msg: 'saved!'
})
});
}
});
}else {
async.mapSeries(mp3List,function(item, callback){
setTimeout(function(){
downLoadMp3(item.dir,item.name,'./mp3/')
callback(null, item);
},400);
}, function(err, results){
res.json({
status: 1,
msg: 'saved!'
})
});
}
});
});
var downLoadMp3 = function(dir,name,filePath){
request(dir).pipe(fs.createWriteStream( filePath + name +'.mp3')).on('close',function(){
console.log('saved' + name)
})
}
module.exports = router;
到此為止我們的node介面也解決了~,程式碼不多仔細分析分析相信所有人都能看懂的,接下來就啟動node服務,然後在前端請求node寫的介面就可以啦
原始碼連結戳此處
如有不足,歡迎指正~