油猴脚本的执行时机: 元素还未生成
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