油猴脚本的执行时机: 元素还未生成
https://bbs.tampermonkey.net.cn/thread-3843-1-1.html
而在控制台执行时, 通常元素已经生成
逻辑就是在网页每次发送请求时, 拦截它请求的响应数据作操作; 所以当用户作品很多时, 也需要一直滚动到全部作品请求加载完成, 触发下载
(当然这都可以改 什么时候触发下载)
// ==UserScript==
// @name 抖音个人主页作品抓取注入
// @namespace http://tampermonkey.net/
// @version 2024-01-18
// @description try to take over the world!
// @author You
// @match https://www.douyin.com/user/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=douyin.com
// @grant none
// ==/UserScript==
(function() {
'use strict';
// 通过 hook XMLHttpRequest 的 send 方法,捕获返回的数据
let aweme_post_list = [];
let page = 0;
// 1. 保存原始的 send 方法
const nativeXMLHttpRequestSend = XMLHttpRequest.prototype.send;
// 2. 重写 send 方法
XMLHttpRequest.prototype.send = function (body) {
// https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/readystatechange_event
// XMLHttpRequest.readyState 属性返回一个 XMLHttpRequest 代理当前所处的状态, 4表示下载操作已完成。
// https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/readyState
// console.log('body', body);
this.addEventListener('readystatechange', function () {
console.debug({
'readyState': this.readyState,
'status': this.status,
'responseURL': this.responseURL
});
// 响应正常完成
if (this.readyState === XMLHttpRequest.DONE && this.status === 200) {
if (this.responseURL.includes('/aweme/v1/web/aweme/post/')) {
// 3. 解析返回数据
let json_data = JSON.parse(this.responseText);
console.log(`第${++page}页, 本页${json_data.aweme_list.length}条作品`);
const data = json_data.aweme_list.map(item => {
const { aweme_id, desc: title, create_time, video: { play_addr: { url_list: play_addr_list } } } = item;
const create_date = new Date(create_time * 1000);
const addr = 'https://www.douyin.com/video/' + aweme_id;
return { aweme_id, title, create_date, addr, play_addr_list };
})
aweme_post_list = aweme_post_list.concat(data);
// 判断是否完成
if (json_data.has_more === 0) {
console.log(`全部完成, 共${aweme_post_list.length}条作品, 开始下载...`);
downloadAwemePostJson(aweme_post_list, 'aweme_post_list.json');
// 下載視頻
aweme_post_list.forEach((item, index) => {
const title = legalizationFilename(item.title);
const filename = `${index + 1}-${title}.mp4`;
downloadAwemePostVideo(item.play_addr_list[0], filename);
});
}
}
}
});
// 4. 调用原始的 send 方法
return nativeXMLHttpRequestSend.call(this, body);
}
/**
* 将JS对象保存为JSON文件
* @param {object} data 需要下载的JS对象
*/
function downloadAwemePostJson(data, filename) {
let blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
let a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = filename;
a.click();
}
/**
* 下載抖音作品視頻
* @param {*} url
* @param {*} filename
*/
function downloadAwemePostVideo(url, filename) {
url = url.replace('http://', 'https://');
fetch(url)
.then(res => res.blob())
.then(blob => {
console.debug(url, filename);
const a = document.createElement("a");
const objectUrl = URL.createObjectURL(blob);
a.download = filename;
a.href = objectUrl;
a.click();
URL.revokeObjectURL(objectUrl);
a.remove();
}).catch(err => {
console.error(err);
});
}
/**
* 合法化文件名
* @param {*} filename
* @returns
*/
function legalizationFilename(filename) {
return filename.replace(/[\\/:*?"<>|]/g, '');
}
})();
其实DY本生也重写了XHR的send方法, 也可能是框架重写的
视频演示:
2.89 09/20 RKW:/ w@S.yG 油猴脚本js注入批量下载DY视频 https://v.douyin.com/iLfWLdeo/ 复制此链接,打开Dou音搜索,直接观看视频!
参考:
【小红书数据采集教学(2)通过重写XMLHttpRequest的send方法来获取POST响应体数】https://www.bilibili.com/video/BV1VC4y1e7RR?vd_source=603d76094f4b03d34ae4f468d5e77227