继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

Nodejs爬喜马拉雅音频

布宝
关注TA
已关注
手记 22
粉丝 9581
获赞 319

最近忙着组NAS,一直没有时间更新。昨天忘了从哪理翻出了喜马拉雅的API,顺藤摸瓜就花了一个小时写了这个爬虫。大概你们觉得听喜马拉雅直接开web或者app就能满足了呀,合并爬硬盘到本地呢?如果我说,喜马拉雅会下架一些我喜欢的音频,然后再也听不到了呢?放在自己的盘里,虽然保不准哪天盘烧了,安全性不高,但是至少在盘没烧之前,控制权在自己手上呀。

第一步:找API

本来我是想找人人影视的api的,想搭好NAS就下载订阅的美剧,发现加密方式没弄懂,哪位小伙伴知道的麻烦教教我哈。所以想着要不先爬取喜马拉雅的音频吧,反正无聊的时候用蓝牙音箱听听也不错。搜了github找到一些爬虫,分析代码找到一个音频真实地址的API。但是这远远不够啊,我要下载的是整个专辑。

在检查面板里翻了一圈,在很多个API里面找到了两个适合的API。

// config/index.js

module.exports = {
    tracks: "http://www.ximalaya.com/tracks/<%=tracksID%>.json",
    getTracksList: "http://www.ximalaya.com/revision/album/getTracksList?albumId=<%=albumId%>&pageNum=<%=pageNum%>",
    album: "http://www.ximalaya.com/revision/album?albumId=<%=albumId%>"
}

API中我使用了Lodash模板格式替换了参数,方便传参。

于是开始有了一个酱紫的思路。

找到想要的专辑->获取整个专辑下的所有的音频的ID->获取每一个音频ID的真实链接->保存成aria2批量下载文件->使用aria2下载

根据思路选工具

有了思路,还需要合适的工具,node有一些封装好的模块方便我们使用,当实在找不到合适的模块的时候,我们还能自己写模块。

因为这个爬虫不需要解析HTML,服务器返回的数据是json,所以我们只需要两个基础模块:requestlodash

然而request是callback回调,然而我想用Promise方式,自己封装一个。

// request.js
const request = require('request');
const isFunction = require('lodash/isFunction');
const fs = require('fs');

async function get(options, callback) {
    const { pipe, hiden, time, size, readable, ...opts } = options;
    const start = time !== undefined ? time.start : new Date().valueOf() / 1000;
    let read = options.read || 0;
    let response = 0;
    let total = 0;
    const value = await new Promise((resolve) => {
        let buffer = Buffer.alloc(0);
        const res = request(opts, (error, resp, body) => {
            resolve({ error, resp, body, read, bufferBody: buffer.toString("utf8") });
        }).on('response', (resp) => {
            if (resp.headers['content-length'] || size) {
                response = parseInt(resp.headers['content-length'] || size || 0, 10);
            }
        }).on('data', (data) => {
            read += data.length;
            if (readable) {
                buffer = Buffer.concat([buffer, data]);
            }
            total = ((size !== undefined || response === undefined) && size >= read) ? size : response || read + 1;
            if (isFunction(callback)) {
                callback({
                    completed: read,
                    total,
                    hiden,
                    time: { start },
                    status: {
                        down: '正在下载...',
                        end: '完成\n'
                    }
                });
            }
        });
        // 如果 pipe参数存在,则下载到指定路径
        if (pipe) {
            res.pipe(fs.createWriteStream(pipe.out || './'));
        }
    });
    return value;
}

module.exports = get;

emmmm......我在promise里面加了个callback,用于返回下载进度,方便以后接入进度条什么的。

哦,差点忘了,我们还需要安装aria2,linux的用户包管理安装,win用户自行下载exe文件后配置环境变量。要是实在不会,百度一下你知道。

这样工具都准备好了,接下来就是写代码的时间了。

写代码

先把封装好的模块和可能需要用的模块统一写在一个文件中,方便管理。

// commonModules.js
const fs = require('fs');
const path = require('path');
const request = require('./request');

module.exports = {
    fs,
    path,
    request,
};

获取专辑下所有的音频

// index.js

const getTracksList = async (albumId, arr = [], pageNum = 1) => {
    const opts = {
        uri: template(cfg.getTracksList)({ albumId, pageNum })
    }

    const body = JSON.parse((await request(opts)).body);
    arr = concat(arr, body.data.tracks);
    if (!(body.data.tracks.length < 30) || pageNum * 30 === body.data.trackTotalCount) {
        return await getTracksList(albumId, arr, pageNum + 1);
    } else {
        return arr;
    }
}

其中cfg这个是之前的API配置文件因为API每次返回的数据默认最大30条,所以直接在代码里写30了,这里返回专辑所以的音频的url,url里面有我们想要的ID。

获取所以的音频的真实链接

const getTracks = async (list, out = "./", str = "") => {
    const item = list.splice(0, 1)[0];
    const data = JSON.parse((await request({
        uri: template(cfg.tracks)({ tracksID: path.basename(item.url) }),
    })).body);
    str += `${data.play_path_64}\n\tout=${trim(item.title)}.m4a\n\tdir=${out}\n`
    if (list.length) {
        return await getTracks(list, out, str);
    } else {
        return str;
    }
}

返回的是一个字符串,按aria2批量下载文件的格式拼接的,最后要保存到文件中供aria2使用。

最后导出模块

module.exports = async (albumId, path = "./list.txt", out) => {
    const list = await getTracksList(albumId);
    fs.writeFileSync(path, await getTracks(list, out));
}

完整的代码如下:

const template = require("lodash/template");
const concat = require("lodash/concat");
const trim = require("lodash/trim");
const cfg = require("./config/index");
const { path, request, fs } = require('./tools/commonModules');

const getTracksList = async (albumId, arr = [], pageNum = 1) => {
    const opts = {
        uri: template(cfg.getTracksList)({ albumId, pageNum })
    }

    const body = JSON.parse((await request(opts)).body);
    arr = concat(arr, body.data.tracks);
    if (!(body.data.tracks.length < 30) || pageNum * 30 === body.data.trackTotalCount) {
        return await getTracksList(albumId, arr, pageNum + 1);
    } else {
        return arr;
    }
}

const getTracks = async (list, out = "./", str = "") => {
    const item = list.splice(0, 1)[0];
    const data = JSON.parse((await request({
        uri: template(cfg.tracks)({ tracksID: path.basename(item.url) }),
    })).body);
    str += `${data.play_path_64}\n\tout=${trim(item.title)}.m4a\n\tdir=${out}\n`
    if (list.length) {
        return await getTracks(list, out, str);
    } else {
        return str;
    }
}

module.exports = async (albumId, path = "./list.txt", out) => {
    const list = await getTracksList(albumId);
    fs.writeFileSync(path, await getTracks(list, out));
}

整个目录结构

图片描述

测试例子就不写了,emmmm.......我就是这么懒,来打我呀hiahiahiahiahia

打开App,阅读手记
1人推荐
发表评论
随时随地看视频慕课网APP