设为首页 收藏本站
查看: 1108|回复: 0

[经验分享] 从零开始,做一个NodeJS博客(三):API实现

[复制链接]

尚未签到

发表于 2017-2-24 09:30:11 | 显示全部楼层 |阅读模式
  标签: 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 抓包网易云
  应用装好,先不急着开抓包工具。打开网易云,等升级提示,首页推广加载完成。之后到搜索页面搜自己的用户名,查看资料,看看是不是加载了听歌排行。
  
DSC0000.png
  好了,打开那个 最近常听。App并没有出现加载中的提示,网络流量也没有跑。那说明在加载用户资料的时候,已经把这些东西下载好了。
  
DSC0001.png
  明确了网络请求发出的时机,我们来开启 Packet Capture 进行抓包。这个东西的原理是设置一个VPN接管设备的所有网络连接,没root也就只能这么干了吧。
  然后退出用户详情界面,再次点进去,又在加载了。加载完成,我们再到 最近常听 里面看看。没问题,加载的很好。现在切回 Packet Capture,看看抓到了什么。
  
DSC0002.png
  里面有两个网易云的请求,挨个进去看看。
  
DSC0003.png
  点击右上角的 HTTP, 可以把收到的内容进行 HTTP Decode。
  
DSC0004.png
  GET 的地址是 /api/user/playlist?MUSIC_A=******,后面是一堆不明所以的东西。再往下看看,请求内容是用户的歌单信息,包括收藏的和创建的。这没啥意思,不是我想要的。去看下一个请求。
  
DSC0005.png
  这个是 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(<--),把请求存起来,开始模拟请求。
  
DSC0006.png

2 模拟请求
  在电脑上打开得到的请求文件,是个纯文本文件。就是Http协议的信息流嘛。
  
DSC0007.png
  前一部分是headers,后一部分是body。现在照样把它填到Postman里面。不过注意,要先在Postman主界面右上角打开 Interceptor 的开关,还要保证Chrome中有一个标签页,空白的新标签页也可以,有一个就行,否则是无法模拟HttpHeaders的。
  
DSC0008.png
  然后照样把请求填进去,headers和body。
  
DSC0009.png
DSC00010.png

  按下Send按钮,loading一会,成功返回了!!和之前抓到的数据一毛一样!!!
  然后可以按下一旁的 Generate Code,选择直接生成NodeJS代码!
  
DSC00011.png
  
DSC00012.png
  这样虽说生成了代码,但请求头和返回都很长。经过的反复尝试(这点东西调了一天啊),决定保留这些:
var options = {  &quot;protocol&quot;: 'http:',
  &quot;method&quot;: &quot;POST&quot;,
  &quot;hostname&quot;: &quot;music.163.com&quot;,
  &quot;path&quot;: &quot;/api/batch&quot;,
  &quot;headers&quot;: {
  &quot;user-agent&quot;: &quot;android&quot;,
  &quot;accept-encoding&quot;: &quot;gzip&quot;,
  &quot;content-type&quot;: &quot;application/x-www-form-urlencoded&quot;,
  &quot;host&quot;: &quot;music.163.com&quot;,
  &quot;connection&quot;: &quot;Keep-Alive&quot;,
  &quot;cookie&quot;: &quot;MUSIC_A=17d8dda86b092bd628e6efb951d4dc6134f4eee4a3dc5eab6d1d5a05b2290cea3b873d710a9f4ce80af3bb97fd207b7f989e5cca1a78fb6410a30504a6c1324ada80406b02449f800fe035ea4cdbd2c4c3061cd18d77b7a0; deviceId=0; appver=2.0.2; os=android;&quot;,
  }
  
};
  

  
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 里运行一下:
  
DSC00013.png
  什么鬼,怎么还乱码!难道API坏了?不可能,刚才还在Postman里跑的好好的啊!
  还记得刚刚抓包的时候,在打开 HTTP Decode 之前,返回值也是一通乱码。
  再看看请求头吧,发现了什么?
&quot;headers&quot;: {  &quot;user-agent&quot;: &quot;android&quot;,
  &quot;accept-encoding&quot;: &quot;gzip&quot;,
  &quot;content-type&quot;: &quot;application/x-www-form-urlencoded&quot;,
  &quot;host&quot;: &quot;music.163.com&quot;,
  &quot;connection&quot;: &quot;Keep-Alive&quot;,
  &quot;cookie&quot;: &quot;MUSIC_A=.....&quot;,
  
}
  里面的 accept-encoding 就是关键。返回值用Gzip压缩过了。

3 Gzip 解压
  NodeJS提供了原生的gzip库 zlib:
const zlib = require('zlib');  在这之前,还是看一下 Postman自动生成的代码吧,要对它动刀,先看看它是怎么做的:
var req = http.request(options, function (res) {  var chunks = [];
  

  res.on(&quot;data&quot;, function (chunk) {
  chunks.push(chunk);
  });
  

  res.on(&quot;end&quot;, 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文档,正好提供了“解压缩流”,就是>  好了,那皆大欢喜,直接把返回数据流pipe到解压流,然后继续pipe到文件流就好了!这样还可以顺便把返回的文件存起来,加速之后的API调用(有缓存而且数据比较新鲜,直接读文件返回,不用看网易云服务器的脸色;而且网易云的统计数据貌似都是每天早上6点才刷新一次,请求太频繁了也没用)。
  好,那么我直接贴代码了!
'use strict';  

  
const qs = require(&quot;querystring&quot;);
  
const fs = require('fs');
  
const http = require(&quot;http&quot;);
  
const zlib = require('zlib');
  

  
var outputFileName = 'netease_record.json';
  

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

  
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!`);
  二话不说,直接开跑:
  
DSC00014.png
  解压成功!接下来只需要把函数打包,加到首页的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>  然后就是请求了。这里仍然只贴函数:
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 里面调用它!起飞吧,少年(不要吐槽样式,以后会有的)!!
  
DSC00015.png

仓库地址

GitHub仓库:BlogNode

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


HerokuApp:rocka-blog-node

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


运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-346438-1-1.html 上篇帖子: nodejs--微信支付 下篇帖子: 在Windows中安装NodeJS的正确姿势
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表