deno

Deno TypeScript 抖音小姐姐下载

2021-04-21  本文已影响0人  坚果jimbowhy

Deno 是新一代基于 TypeScript 语言的编程平台,是 Node 平台之外的又一选择,它们都是由 Ryan Dahl 发起的项目,鉴于 Node 的一些不足,他决定放弃 Node.js,从头写一个替代品:

Deno - A secure runtime for JavaScript and TypeScript.


抖音小姐姐.jpg

Deno 是一个简单又现代化而且安全的 JavaScript/TypeScript 运行时,基于 V8 引擎和 Rust(Tokio 异步编程框架),Deno 本身也是 Rust 的一个模块。

由于 TS 无法为 Deno runtime 生成高性能的代码,目前部分内部实现从 ts 变更为 js。

但 Deno 并没有放弃 TypeScript,Deno 依然是一个安全的 TS/JS runtime,目前 Deno 彻底用 Rust 替代 C++/C,各语言比例大概是:

Deno VS Node

Node Deno
API 引用方式 模块导入 全局对象
模块系统 CommonJS & 实验性 ES Module 全面 ES Module
安全 无安全限制 默认安全
TypeScript 通过第三方模块支持 ts-node 原生支持
包管理 npm + node_modules 原生支持
异步操作 回调 Promise
包分发 中心化 npmjs.com 去中心化 import url
入口 package.json 配置 import url 直接引入
打包、测试、格式化 第三方如 eslint、gulp、webpack、babel 等 原生支持
抖音小姐姐下载

下载速度简直不要太快:

wifi flow

需要安装 Deno,再运行示范程序:

deno run -A douyin.ts

代码仓库为 Deno 演示,包含 demo/douyin.ts:https://github.com/jimboyeah/deno-demo

此工具需要在抖音主界面上获取视频博主的分享链接,通过链接获取到视频列表后进行批量下载:

http://v.douyin.com/ehSh5Cy
https://www.iesdouyin.com/web/api/v2/user/info/?sec_uid=MS4wLjABAAAA06xUG37YAhRl8nWJ3vEG_CMMJZ47rnxLY96CAvUqoRg
https://www.iesdouyin.com/web/api/v2/aweme/licke/?sec_uid=MS4wLjABAAAA06xUG37YAhRl8nWJ3vEG_CMMJZ47rnxLY96CAvUqoRg
https://www.iesdouyin.com/web/api/v2/aweme/post/?sec_uid=MS4wLjABAAAA06xUG37YAhRl8nWJ3vEG_CMMJZ47rnxLY96CAvUqoRg&count=21&max_cursor=0&aid=1128&_signature=VHoupQAANAiyH7H6JvRmvVR6Lr&dytk=

URL 签名 signature 随时间变化动态生成,可以在页面使用调式工具设置 fetch breakpoints,再根据调用栈定位到 init 方法:

function init(config) {
  dytk = config.dytk;
  params.user_id = config.uid;
  params.sec_uid = _utils2.default.getUrlParam(window.location.href, "sec_uid");

  if (params.sec_uid != "") {
    delete params.user_id;
  }

  config.sec_uid = params.sec_uid;
  nonce = config.uid;
  signature = (0, _bytedAcrawler.sign)(nonce);
  // ...
}

根据打包机嵌入的信息找到 bytedAcrawler 的实现,其导出模块位置 vendor.a59687bc.js:1096。

所谓模块,就是一个独立命名空间的闭包,在需要使用时就请求加载它。将模块提取出来,用它对 uid 进行处理就可以得到签名。

模块提供的是混淆过的代码,参考 JavaScript Obfuscator Tool https://obfuscator.io

由于 bytedAcrawler 提供的签名算法借用了浏览器对象 navigator 来保证算法运行环境为浏览器,否则尝试读取 userAgent 会导致运行出错,实现了运行环境安全。

通过逆向,即像脚本引擎一样破解算法内部,只需给代码打一个补订即可解决,不过时间也是花了大半天:

.replace(/return r$/,"this.navigator = {userAgent:''};return r")

主程序:

class Douyin {
  SavePath: string = "videos";
  userAgent: string = "userAgent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.128 Safari/537.36"

  downloadLikes(homeurl:string){
    this.downloadHome(homeurl, "like");
  }
  downloadPosts(homeurl:string){
    this.downloadHome(homeurl, "post");
  }
  async downloadHome(homeurl:string, type:"post"|"like"){
    let shareurl = await this.fetch_url(homeurl);
    let sec_uid = this.sec_uid(shareurl);
    let uid = this.uid(shareurl);
    // let uid = "59583160290";
    let sign = this.signature_module();
    let signature = sign(uid);
    let url = `https://www.iesdouyin.com/web/api/v2/aweme/${type}/?sec_uid=${sec_uid}&count=21&max_cursor=0&aid=1128&_signature=${signature}&dytk=`;
    
    let req = new Request(url);
    req.headers.append("userAgent", this.userAgent);

    let posts = await fetch(req).then(res => res.json() as Promise<Posts>);
    this.log(posts, url, sec_uid, uid, signature);
    for(let it of posts.aweme_list){
      this.downloadItem(it, homeurl, url);
    }
  }
  downloadList(list:string[]){
    Deno.mkdirSync(this.SavePath, { recursive: true });
    for (let url of list) {
      try{
        this.fetchItem(url);
      }catch(e){ console.error("Error when process item", url, e.message); }
    }
  }
  parsePosts(file:string){
    const buffer = Deno.readFileSync(file);
    const decoder = new TextDecoder("utf-8");
    let lines = decoder.decode(buffer).split('\n');
    for(let it of lines){
      if(it.indexOf("[last parse]")>=0){
        return this.log("tag found:", it);
      }
      let infourl = "";
      let match = /\[(\d+)\]/.exec(it);
      let id = match?match[1]:"";
      id && this.iteminfo(id).then(res => {
        infourl = res.url;
        return res.json();
      }).then((it: ItemInfo) => {
        this.downloadItem(it.item_list[0], "", infourl);
      }).catch(e => console.error(e));
    }
  }

  public fetchItem(shorturl: string) {
    fetch(shorturl).then(res => {
      let shareurl = res.url, infourl = "";
      this.log(shorturl, shareurl);
      let id = this.aweme_id(shareurl);
      id && this.iteminfo(id).then(res => {
        infourl = res.url;
        return res.json();
      }).then((it: ItemInfo) => {
        this.downloadItem(it.item_list[0], shorturl, infourl);
      }).catch(e => console.error(e));
      return res.text();
    }).then(function (this: any, text) {
    }).catch(e => console.error(e.message, shorturl));
  }

  private downloadItem(item: VideoInfo, shorturl: string, infourl: string) {
    let videourl = item.video.play_addr.url_list[0];
    videourl = videourl.replace("playwm", "play");
    let uid = item.author.unique_id || item.author.short_id;
    let aweme_id = item.aweme_id;
    let duration = item.video.duration;
    let coverurl = item.video.origin_cover.url_list[0];
    console.log(
      shorturl,
      item.author.nickname,
      uid,
      item.desc,
      infourl,
      videourl
    );
    this.get_cover(coverurl, `${this.SavePath}/${uid}-${aweme_id}-${duration}.jpg`);
    this.get_video(videourl, `${this.SavePath}/${uid}-${aweme_id}-${duration}.mp4`, false);
  }

  async fetch_url(shorturl:string) {
    return fetch(shorturl).then(res => res.url);
  }

  aweme_id(url: string): string {
    let reg = /share\/video\/(.+?)\//;
    let match = reg.exec(url);
    if(match) {
      return match[1];
    }
    return "";
  }
  sec_uid(url: string): string {
    let reg = /sec_uid=(.+?)&/;
    let match = reg.exec(url);
    if(match) {
      return match[1];
    }
    return "";
  }
  uid(url: string): string {
    let reg = /share\/user\/(\d+?)/;
    let match = reg.exec(url);
    if(match) {
      return match[1];
    }
    return "";
  }
  async iteminfo(id:string): Promise<Response> {
    let url = `https://www.iesdouyin.com/web/api/v2/aweme/iteminfo/?item_ids=${id}`;
    return await fetch(url);
  }
  get_cover(url: string, name:string) {
    fetch(url).then( res => {
      return res.arrayBuffer();
    }).then(ab => {
      let ua8 = new Uint8Array(ab);
      Deno.writeFile(name, ua8).then(val =>{});
    }).catch(e => console.error("Error when to download ", url));
  }
  get_video(url: string, name:string, autoplay = false) {
    fetch(url).then(res => {
      return res.arrayBuffer()
    }).then(ab =>{
      let ua8 = new Uint8Array(ab);
      Deno.writeFile(name, ua8).then(val => {
        if (!autoplay) return;
        let cmd =  ["cmd", "/c", "start", name];
        const p = Deno.run({
          cmd: cmd,
        }).status();
      });
    }).catch(e => console.error("Error when to download ", url))
  }
  log(...args:any) {
    console.log(...args);
  }
  
  signature_module() {
    let exports = {sign:(s:string):string => ""};
    let navigator = {userAgent:"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.128 Safari/537.36"};
    // let module = {exports};
    Function(function(t){return'�e(e,a,r){�(b[e]||(b[e]=t("x,y","�x "+e+" y"�)(r,a)}�a(e,a,r){�(k[r]||(k[r]=t("x,y","�new x[y]("+Array(r+1).join(",x[�y]")�(1)+")"�)(e,a)}�r(e,a,r){�n,t,s={},b=s.d=r?r.d+1:0;for(s["$"+b]=s,t=0;t<b;t�)s[n="$"+t]=r[n];for(t=0,b=s�=a�;t<b;t�)s[t]=a[t];�c(e,0,s)}�c(t,b,k){�u(e){v[x�]=e}�f�{�g=�,t�ing(b�g)}�l�{try{y=c(t,b,k)}catch(e){h=e,y=l}}for(�h,y,d,g,v=[],x=0;;)switch(g=�){case 1:u(!�)�4:�f��5:u(�(e){�a=0,r=e�;���{�c=a<r;�c&&u(e[a�]),c}}(���6:y=�,u(�(y��8:if(g=�,l��g,g=�,y===c)b+=g;else if(y!==l)�y�9:�c�10:u(s(���11:y=�,u(�+y)�12:for(y=f�,d=[],g=0;g<y�;g�)d[g]=y.charCodeAt(g)^g+y�;u(String.fromCharCode.apply(null,d��13:y=�,h=delete �[y]�14:���59:u((g=�)?(y=x,v.slice(x-=g,y�:[])�61:u(�[�])�62:g=�,k[0]=65599*k[0]+k[1].charCodeAt(g)>>>0�65:h=�,y=�,�[y]=h�66:u(e(t[b�],�,���67:y=�,d=�,u((g=�).x===c?r(g.y,y,k):g.apply(d,y��68:u(e((g=t[b�])<"<"?(b--,f�):g+g,�,���70:u(!1)�71:�n�72:�+f��73:u(parseInt(f�,36��75:if(�){b��case 74:g=�<<16>>16�g�76:u(k[�])�77:y=�,u(�[y])�78:g=�,u(a(v,x-=g+1,g��79:g=�,u(k["$"+g])�81:h=�,�[f�]=h�82:u(�[f�])�83:h=�,k[�]=h�84:�!0�85:�void 0�86:u(v[x-1])�88:h=�,y=�,�h,�y�89:u(��{�e�{�r(e.y,arguments,k)}�e.y=f�,e.x=c,e}�)�90:�null�91:�h�93:h=��0:��;default:u((g<<16>>16)-16)}}�n=this,t=n.Function,s=Object.keys||�(e){�a={},r=0;for(�c in e)a[r�]=c;�a�=r,a},b={},k={};�r'.replace(/[�-�]/g,function(m){return t[m.charCodeAt(0)&15]})}("v[x++]=�v[--x]�t.charCodeAt(b++)-32�function �return �))�++�.substr�var �.length�()�,b+=�;break;case �;break}".split("�")).replace(/return r$/,"this.navigator = {userAgent:''};return r"))()('gr$Daten Иb/s!l y͒yĹg,(lfi~ah`{mv,-n|jqewVxp{rvmmx,&eff�kx[!cs"l".Pq%widthl"@q&heightl"vr*getContextx$"2d[!cs#l#,*;?|u.|uc{uq$fontl#vr(fillTextx$$龘ฑภ경2<[#c}l#2q*shadowBlurl#1q-shadowOffsetXl#$$limeq+shadowColorl#vr#arcx88802[%c}l#vr&strokex[ c}l"v,)}eOmyoZB]mx[ cs!0s$l$Pb<k7l l!r&lengthb%^l$1+s$j�l  s#i$1ek1s$gr#tack4)zgr#tac$! +0o![#cj?o ]!l$b%s"o ]!l"l$b*b^0d#>>>s!0s%yA0s"l"l!r&lengthb<k+l"^l"1+s"j�l  s&l&z0l!$ +["cs\'(0l#i\'1ps9wxb&s() &{s)/s(gr&Stringr,fromCharCodes)0s*yWl ._b&s o!])l l Jb<k$.aj;l .Tb<k$.gj/l .^b<k&i"-4j!�+& s+yPo!]+s!l!l Hd>&l!l Bd>&+l!l <d>&+l!l 6d>&+l!l &+ s,y=o!o!]/q"13o!l q"10o!],l 2d>& s.{s-yMo!o!]0q"13o!]*Ld<l 4d#>>>b|s!o!l q"10o!],l!& s/yIo!o!].q"13o!],o!]*Jd<l 6d#>>>b|&o!]+l &+ s0l-l!&l-l!i\'1z141z4b/@d<l"b|&+l-l(l!b^&+l-l&zl\'g,)gk}ejo{�cm,)|yn~Lij~em["cl$b%@d<l&zl\'l $ +["cl$b%b|&+l-l%8d<@b|l!b^&+ q$sign ',[Object.defineProperty(exports,'__esModule',{value:!0})]);
    return exports.sign;
  }
}

if(Deno.args.length){
  let act = Deno.args[0];
  let dy = new Douyin();
  if("posts"===act){
    dy.downloadPosts(Deno.args[1])
  }else if("likes" === act){
    dy.downloadLikes(Deno.args[1])
  }else if("parse" === act){
    dy.parsePosts(Deno.args[1])
  }else{
    dy.downloadList(Deno.args);
  }
}else{
  console.log(`
  download video from v.douying.com
  deno run -A douyin.ts parse posts.txt
  deno run -A douyin.ts https://v.douyin.com/eh1PpKh/ https://v.douyin.com/eh1Hs4L/
  deno run -A douyin.ts posts http://v.douyin.com/eh55UYC
  deno run -A douyin.ts likes http://v.douyin.com/ehSh5Cy
  `  );
}

Douyin 桌面看,使用 Deno Webview

自由缩放 自动列表加载 无水印自动缓存 浏览阿婆主页
上一篇下一篇

猜你喜欢

热点阅读