标签: NodeJS


0

研究了一天,翻遍了GitHub上各种网易云API库,也没有找到我想要的听歌排行API,可能这功能比较小众吧。但收获也不是没有,在 这里 明白了云音乐API加密的凶险,我等蒟蒻还是敬而远之的好。

等会,不过之前的旧API好像没有加密?

赶紧跑到 隔壁乐园,下载云音乐Android版2.0.2。然后 酷安 扒来 Packet Capture,可以在Andorid上实现免Root抓包。

模拟请求,NodeJS肯定可以,不过这一块我还不熟(废话,这个模块的目的不就是熟悉一下网络请求么)。那么我们需要一个神奇的 Chrome 应用:Postman。如果要设定请求头的话,还要加上 Postman Interceptor 这个插件进行辅助,否则无法设置除 Content-Type 以外的 Http Headers。

准备工作完成了,正片开始。

1 抓包网易云

应用装好,先不急着开抓包工具。打开网易云,等升级提示,首页推广加载完成。之后到搜索页面搜自己的用户名,查看资料,看看是不是加载了听歌排行。

好了,打开那个 最近常听。App并没有出现加载中的提示,网络流量也没有跑。那说明在加载用户资料的时候,已经把这些东西下载好了。

明确了网络请求发出的时机,我们来开启 Packet Capture 进行抓包。这个东西的原理是设置一个VPN接管设备的所有网络连接,没root也就只能这么干了吧。

然后退出用户详情界面,再次点进去,又在加载了。加载完成,我们再到 最近常听 里面看看。没问题,加载的很好。现在切回 Packet Capture,看看抓到了什么。

里面有两个网易云的请求,挨个进去看看。

点击右上角的 HTTP, 可以把收到的内容进行 HTTP Decode。

GET 的地址是 /api/user/playlist?MUSIC_A=******,后面是一堆不明所以的东西。再往下看看,请求内容是用户的歌单信息,包括收藏的和创建的。这没啥意思,不是我想要的。去看下一个请求。

这个是 POST 请求,地址是 /api/batch。batch?不是批处理么?这有意思,接着看。

请求头的Cookie有一大串,里面有各种客户端信息,还有刚才的 MUSIC_A。

再看body。第一个是 MUSIC_A。这啥玩意,怎么到处都有!!?不管,接着看。

key value
/api/user/detail/76980626 {'all':true}
/api/user/bindings/76980626
/api/dj/program/76980626 {'limit':5,'offset':0}

三个参数,长的跟url一样,还有值,像个JSON字符串。后面的 76980626 应该是我的UID之类的东西了。

再看响应。这么长!这个JSON有两千多行,稍微划分一下结构的话,分了五部分:

  • Object //这是根节点

    • code
    • MUSIC_A
    • /api/user/detail/76980626
    • /api/user/bindings/76980626
    • /api/dj/program/76980626

code 的值是200,而且后面的每个数组里都有 "code": 200 这一元素,猜测是个状态码。然后继续分析。

  • /api/user/detail/76980626

    • listenedSongs

这不就是听过的歌么!!里面还有 id album artisist name 等各种信息,得,不用看下面了,就是你了。

退出 HTML Encode 页面,点击右上角菜单的 Save Upstream(<--),把请求存起来,开始模拟请求。

2 模拟请求

在电脑上打开得到的请求文件,是个纯文本文件。就是Http协议的信息流嘛。

前一部分是headers,后一部分是body。现在照样把它填到Postman里面。不过注意,要先在Postman主界面右上角打开 Interceptor 的开关,还要保证Chrome中有一个标签页,空白的新标签页也可以,有一个就行,否则是无法模拟HttpHeaders的。

然后照样把请求填进去,headers和body。

按下Send按钮,loading一会,成功返回了!!和之前抓到的数据一毛一样!!!

然后可以按下一旁的 Generate Code,选择直接生成NodeJS代码!



这样虽说生成了代码,但请求头和返回都很长。经过的反复尝试(这点东西调了一天啊),决定保留这些:

var options = {
"protocol": 'http:',
"method": "POST",
"hostname": "music.163.com",
"path": "/api/batch",
"headers": {
"user-agent": "android",
"accept-encoding": "gzip",
"content-type": "application/x-www-form-urlencoded",
"host": "music.163.com",
"connection": "Keep-Alive",
"cookie": "MUSIC_A=17d8dda86b092bd628e6efb951d4dc6134f4eee4a3dc5eab6d1d5a05b2290cea3b873d710a9f4ce80af3bb97fd207b7f989e5cca1a78fb6410a30504a6c1324ada80406b02449f800fe035ea4cdbd2c4c3061cd18d77b7a0; deviceId=0; appver=2.0.2; os=android;",
}
}; var postData = { '/api/user/detail/76980626': '{\'all\':true}' }

这里的 Cookie 是必须的,而且 MUSIC_A 必须包含在Cookie里面,根据后来的抓包结果,是跟用户验证有关的。即使你不进行登录,也会有一个匿名的账号分配给你。

把 postData 减少到一条,请求还是可以正常返回的,不过就只有用户的个人基本资料和我们需要的听歌排行内容了。返回值对象的结构也有所改变:

  • Object //这是根节点

    • code
    • MUSIC_A
    • /api/user/detail/76980626

这样东西就少了一些了。

!!!!!!!注意!!!!!!!这里有一个大坑!!!!!!!

当你测试精简请求参数的时候,一定要先在Chrome里面把 music.163.com Cookie 清理掉!!!!

因为 NodeJS 不是浏览器,不会保存返回的 Cookie,下一次请求还是新的;Postman 可是用了 Chrome 核心,它会共享浏览器的 Cookie,而这个请求的返回则会给客户端 set cookie,而这个 cookie 则是与 POST 请求提交的数据和 Request Headers 有关的。这样,你的下一次 Postman 请求实际上继承了之前的所有 cookie ,再改参数,那些继承来的cookie也不会消失,会随着请求一起发出去,影响返回结果。

切记要 清除Cookie 啊!!!

把生成的代码放在 Node 里运行一下:

什么鬼,怎么还乱码!难道API坏了?不可能,刚才还在Postman里跑的好好的啊!

还记得刚刚抓包的时候,在打开 HTTP Decode 之前,返回值也是一通乱码。

再看看请求头吧,发现了什么?

"headers": {
"user-agent": "android",
"accept-encoding": "gzip",
"content-type": "application/x-www-form-urlencoded",
"host": "music.163.com",
"connection": "Keep-Alive",
"cookie": "MUSIC_A=.....",
}

里面的 accept-encoding 就是关键。返回值用Gzip压缩过了。

3 Gzip 解压

NodeJS提供了原生的gzip库 zlib:

const zlib = require('zlib');

在这之前,还是看一下 Postman自动生成的代码吧,要对它动刀,先看看它是怎么做的:

var req = http.request(options, function (res) {
var chunks = []; res.on("data", function (chunk) {
chunks.push(chunk);
}); res.on("end", function () {
var body = Buffer.concat(chunks);
console.log(body.toString());
});
}); req.write(qs.stringify({ '/api/user/detail/76980626': '{\'all\':true}' }));
req.end();

request 的 data 事件是在每次数据流入时触发,将数据推入 chunks。当请求结束触发 end 事件时,把数据通过命令行输出。。。似乎是这样的吧。但 Buffer 是个什么东西?

算了,还是查一下 http.ClientRequest.API的文档 ,我找到了这个 response 事件。它会给回调函数传入一个 http.IncomingMessage 型的参数,里面包含了响应的数据;而它本身又是一个 Readable Stream ,可以直接 pipe() 到其它流。

那么再看一下 Zlib的API文档,正好提供了“解压缩流”,就是 Class: zlib.Gunzip 。

好了,那皆大欢喜,直接把返回数据流pipe到解压流,然后继续pipe到文件流就好了!这样还可以顺便把返回的文件存起来,加速之后的API调用(有缓存而且数据比较新鲜,直接读文件返回,不用看网易云服务器的脸色;而且网易云的统计数据貌似都是每天早上6点才刷新一次,请求太频繁了也没用)。

好,那么我直接贴代码了!

'use strict';

const qs = require("querystring");
const fs = require('fs');
const http = require("http");
const zlib = require('zlib'); var outputFileName = 'netease_record.json'; var options = {
"protocol": 'http:',
"method": "POST",
"hostname": "music.163.com",
"path": "/api/batch",
"headers": {
"user-agent": "android",
"accept-encoding": "gzip",
"content-type": "application/x-www-form-urlencoded",
"host": "music.163.com",
"connection": "Keep-Alive",
"cookie": "MUSIC_A=17d8dda86b092bd628e6efb951d4dc6134f4eee4a3dc5eab6d1d5a05b2290cea3b873d710a9f4ce80af3bb97fd207b7f989e5cca1a78fb6410a30504a6c1324ada80406b02449f800fe035ea4cdbd2c4c3061cd18d77b7a0; deviceId=0; appver=2.0.2; os=android;",
}
}; var output = fs.createWriteStream(outputFileName); var req = http.request(options); req.write(qs.stringify({ '/api/user/detail/76980626': '{\'all\':true}' })); req.on('response', (response) => {
console.log('[Netease API] Record Data Received!');
response.pipe(zlib.createGunzip()).pipe(output);
fs.readFile(outputFileName, (err, data) => {
console.log(`[File] ${data.toString()}`);
});
}) req.on('error', (para) => {
console.log(`[Netease API] ${para.message}`);
}) req.end(); console.log(`[Netease API] Get Record Request Sent!`);

二话不说,直接开跑:

解压成功!接下来只需要把函数打包,加到首页的API路径里就好了!

4 实现一个 “API 模块”

那个 server.js 已经够长了,看起来头晕。。。再把上面的代码加进去,岂不是更乱了?还是把它写成一个单独的 js 文件吧,用 require 方法去引用它。

文件就是一个模块,模块的名字就是文件名(去掉.js后缀),所以hello.js文件就是名为hello的模块。

———— 模块 - 廖雪峰的官方网站

所以我们需要改写一下,把API调用打包成函数,最好可以自定义输出的文件名:

function fileName(name) {
if (name) {
return outputFileName = name;
} else return outputFileName;
} function getRecord(callback) {
var output = fs.createWriteStream(outputFileName);
var req = http.request(options);
req.write(qs.stringify({ '/api/user/detail/76980626': '{\'all\':true}' }));
req.on('response', (response) => {
console.log('[Netease API] Record Data Received!');
response.pipe(zlib.createGunzip()).pipe(output);
// invoke callback and pass parameter
callback && callback(outputFileName);
})
req.on('error', (para) => {
console.log(`[Netease API] ${para.message}`);
})
req.end();
console.log(`[Netease API] Get Record Request Sent!`);
} module.exports = {
fileName: fileName,
updateData: getRecord
}

这样,就可以通过require的方式引用这个模块里的函数;我的文件名是 NeteaseApiAndroid ,因为是在 Andorid 客户端抓的包嘛:

const NeteaseApi = require('./NeteaseApiAndroid');

NeteaseApi.fileName();                   // get
NeteaseApi.fileName('temp_list.json'); // set
NeteaseApi.updateData((fName) => { // invoke
// do something
});

而且给回调函数传入的参数是输出文件名,让调用者做出自己的判断,是直接读文件,还是改更新缓存了。我的想法是,如果上一次请求网易API的事件超过一个小时,那么就更新列表缓存。

这里就只贴一个case好了,贴多了显得我是来凑字数的。

case '/api/music-record':
fs.stat(NeteaseApi.fileName(), (err, stats) => {
// got file
if (!err) {
var now = Date.now();
// now - last_modified_time >= an hour
if (now - stats.mtime >= 3600 * 1000) {
// update cache
NeteaseApi.updateData((fName) => {
sendMusicRecord(fName, response);
});
} else {
// read file and send request
sendMusicRecord(NeteaseApi.fileName(), response);
}
// no file: update cache
} else {
NeteaseApi.updateData((fName) => {
sendMusicRecord(fName, response);
});
}
});
break;

然后是这个响应处理函数,实际上就是一个读文件发送的过程:

function sendMusicRecord(fileName, response) {
fs.readFile(fileName, (err, data) => {
if (err) {
response.writeHead(400, { 'Content-Type': 'application/json' });
response.end(JSON.stringify(err));
} else {
response.writeHead(200, { 'Content-Type': 'application/json' });
response.end(data);
}
});
}

5 在页面上加载列表

这没什么好说的。请求API,然后解析JSON就是了。不过先要在首页加上负责显示的列表:

<h3>Rcecntly Listened</h3>
<ul id="index-music-record"></ul>

然后就是请求了。这里仍然只贴函数:

function loadMusicRecord() {
var ul = document.getElementById('index-music-record'); function success(data) {
var rawList = data.listenedSongs;
rawList.forEach((value, index) => {
// display 10 item only
if(index > 9) return;
var li = document.createElement('li');
var a = document.createElement('a');
a.innerText = `${value.name} - ${value.artists[0].name}`;
a.setAttribute('href', `http://music.163.com/#/song?id=${value.id}`);
a.setAttribute('target', '_blank');
li.appendChild(a);
ul.appendChild(li);
});
} function fail(code) {
ul.innerText = 'Load Faild: Please Refresh Page And Try Again.';
ul.innerText += `Error Code: ${code}`;
} var request = new XMLHttpRequest(); request.onreadystatechange = () => {
if (request.readyState === 4) {
if (request.status === 200) {
return success(JSON.parse(request.response)['/api/user/detail/76980626']);
} else {
return fail(request.status);
}
}
} request.open('GET', `/api/music-record`);
request.send();
}

这次还是用了原生的 XMLHttpRequest 。这样让我发现了一个小细节问题。

之前一直用 jQ 的 ajax 方法,大概要这样写:

$.ajax({
url: '/api/music-record',
mehtod: 'GET',
contentType: 'application/json',
success: (data) => {
// invoke here
}
});

这样的话,success 函数里面得到的 data 就会是js对象了,可以直接.出来。

但如果用原生xhr方法的话,会有个小坑:没有地方(或者说我没找到)去设置这个 contentType,success回调得到的其实只是一个字符串,处理之前还是要parse一下。

最后,在 window.onload 里面调用它!起飞吧,少年(不要吐槽样式,以后会有的)!!

仓库地址

GitHub仓库:BlogNode

主仓库,以后的代码都在这里更新。

HerokuApp:rocka-blog-node

上面GitHub仓库的实时构建结果。

从零开始,做一个NodeJS博客(三):API实现-加载网易云音乐听歌排行的更多相关文章

  1. 从零开始,做一个NodeJS博客(四):服务器渲染页面与Pjax

    标签: NodeJS 0 一个星期没更新了 = = 一直在忙着重构代码,以及解决重构后出现的各种bug 现在CSS也有一点了,是时候把遇到的各种坑盘点一下了 1 听歌排行 API 修复与重构 1.1 ...

  2. 从零开始,做一个NodeJS博客(零):整体规(chui)划(niu)

    标签:NodeJS,Heroku 0 搭建一个个人独立博客,这是我好久之前就在计划的一件事了. 这个暑假,我学习了廖雪峰老师的NodeJS教程,又偶然在V2EX上发现了Heroku这个平台,可以免费在 ...

  3. 从零开始,做一个NodeJS博客(二):实现首页-加载文章列表和详情

    标签: NodeJS 0 这个伪系列的第二篇,不过和之前的几篇是同一天写的.三分钟热度貌似还没过. 1 静态资源代理 上一篇,我们是通过判断请求的路径来直接返回结果的.简单粗暴,缺点明显:如果url后 ...

  4. 从零开始,做一个NodeJS博客(一):Heroku上的最简NodeJS服务器

    标签:NodeJS,Heroku 0 这里是这个伪系列的第一篇,因为我也不知道自己能不能做完,然后到底能做成什么样子.总之,尽力而为吧,加油. 1 Heroku App 的构成 Heroku 所谓的 ...

  5. Web前端:博客美化:四、网易云音乐单曲播放器

    1.页面定制CSS代码 /*3.音乐播放器*/ .content-wrap { overflow-y: scroll; -webkit-overflow-scrolling: touch; } /* ...

  6. 这几天有django和python做了一个多用户博客系统(可选择模板)

    这几天有django和python做了一个多用户博客系统(可选择模板) 没完成,先分享下 断断续续2周时间吧,用django做了一个多用户博客系统,现在还没有做完,做分享下,以后等完善了再慢慢说 做的 ...

  7. 如何搭建一个独立博客——简明Github Pages与Hexo教程

    摘要:这是一篇很详尽的独立博客搭建教程,里面介绍了域名注册.DNS设置.github和Hexo设置等过程,这是我写得最长的一篇教程.我想将我搭建独立博客的过程在一篇文章中尽可能详细地写出来,希望能给后 ...

  8. Do-Now—团队冲刺博客三

    Do-Now-团队 冲刺博客三 作者:仇夏 前言 不知不觉我们的项目已经做了三个多礼拜了,团队冲刺博客也写到了这第三篇,看着一个基本成型的APP安装在自己的手机上,一种喜悦感油然而生.好了,现在来看看 ...

  9. 前端设计师也有必要学习seo,推荐一个seo博客

    做前端设计师有一段时间了,现在越来越觉得作为一个前端设计师,必须要懂一些seo的知识. 因为公司的seo们,总是在网站做好以后,提出各种各样的网站修改的需求. 如果前端设计师,能够了解一些基本的seo ...

随机推荐

  1. Cocos2d-x 3.2 学习笔记(一)环境搭建

    目前项目无事,时间比较充裕,因此来学习下cocos2dx,当然本人也是新手一个, 写此笔记做备忘和脚步. 最近3.2版本更新出來了!官方说这是自2.x分支以来修复了超过450个bug,3.2版本是目前 ...

  2. Unity3D重要知识点

    数据结构和算法很重要!图形学也很重要!大的游戏公司很看重个人基础,综合能力小公司看你实际工作能力,看你的Demo. 1.什么是渲染管道? 是指在显示器上为了显示出图像而经过的一系列必要操作. 渲染管道 ...

  3. php操作mongodb中的ISODate格式日期

    mongodb 中数据记录的日期格式为"dateCreated" : ISODate("2011-12-20T07:22:50.836Z")经过翻阅php官网中 ...

  4. RobotFramework - Tips

    1 --- API的使用 Robot Framework的版本发展是向下包容,建议尽量使用robot本身的API. 例如:通过导入logger.py(...\Lib\site-packages\rob ...

  5. 使用NuGet管理项目类库引用

    NuGet 是微软开发平台(包括.NET平台)的一个包管理器,这里只介绍和.NET相关的NuGet Visual Studio扩展客户端, 在VS2010 ,VS2012 ,VS2013中默认集成了N ...

  6. 基于caffe的艺术迁移学习 style-transfer-windows+caffe

    这个是在去年微博里面非常流行的,在git_hub上的代码是https://github.com/fzliu/style-transfer 比如这是梵高的画 这是你自己的照片 然后你想生成这样 怎么实现 ...

  7. Ionic2学习笔记(2):自定义Component

    作者:Grey 原文地址: http://www.cnblogs.com/greyzeng/p/5536298.html                     上一篇提到,Ionic2提供了很多Co ...

  8. 【转】 Newtonsoft.Json高级用法

    手机端应用讲究速度快,体验好.刚好手头上的一个项目服务端接口有性能问题,需要进行优化.在接口多次修改中,实体添加了很多字段用于中间计算或者存储,然后最终用Newtonsoft.Json进行序列化返回数 ...

  9. iis 不能访问json文件

    我从网上查的资料,解决方案都是设置MIME 映射和“处理脚本映射”. 我按照网上的解决方案执行之后还没有解决我的这个问题,所以我想会不会是其他的原因. 在那么一瞬间,灵光一闪,我把json文件放到新建 ...

  10. How Will Java Technology Change My Life?

    How Will Java Technology Change My Life? We can't promise you fame, fortune, or even a job if you le ...