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 的一个模块。
- 初始即安全,除非明确准许,初始以沙盒状态运行(无文件、网络、环境变量访问权限);
- 自身支持 TypeScript;
- 运行时本体以单一二进制文件形式发布;
- 拥有大量的自带工具,例如依赖检查(deno info)和代码格式化工具(deno fmt);
- 拥有较为完备的官方标准库,确保能适配对应 Deno 版本运行;
- Deno 最初由 Node.js 原作者 Ryan Dahl 于 2018 年 5 月在 JSConf.EU 首次提出。
由于 TS 无法为 Deno runtime 生成高性能的代码,目前部分内部实现从 ts 变更为 js。
但 Deno 并没有放弃 TypeScript,Deno 依然是一个安全的 TS/JS runtime,目前 Deno 彻底用 Rust 替代 C++/C,各语言比例大概是:
- TypeScript:64.7%
- Rust:31.9%
- JavaScript:1.4%
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
自由缩放 自动列表加载 无水印自动缓存 浏览阿婆主页
网友评论