您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
半自动获取抖音某用户全部视频与照片,配套网站随机获取资源
// ==UserScript== // @name 抖音用户视频照片打包下载(随机小姐姐) // @namespace http://tampermonkey.net/ // @version 0.0.5 // @description 半自动获取抖音某用户全部视频与照片,配套网站随机获取资源 // @author N0ts // @match https://www.douyin.com/user/* // ==/UserScript== (function () { // 数据操作密码 const apiPassword = ""; // API 地址 const apiServer = ""; // 视频列表 const videos = []; // 图片(图文封面)列表 const imgs = []; // 昵称 const nickname = document.querySelector("div[data-e2e=user-info]").children[0].children[0].innerText; // 主页地址 const userUrl = window.location.origin + window.location.pathname; // 已经爬过的数据 ID let successId = []; // 全部视频 li 标签 let li = null; // 一条龙(扒数据,下载,上传) let ok = false; /** * 修改颜色为完成 * @param {HTMLElement} dom dom */ function changeColor(dom) { dom.style.backgroundColor = "#00a32b"; scrollToTop(dom.offsetTop + dom.clientHeight / 2); } /** * 图文获取事件 * @param {*} event li */ function liHandle(event) { // 如果是图文的话 if ( event.target.querySelector(".user-video-stats-tag") && (event.target.querySelectorAll(".user-video-tag").length == 2 || event.target.querySelector(".user-video-stats-tag").innerText.includes("图文")) ) { // 获取图片链接 const url = event.target.querySelector("img").getAttribute("src"); // 存到图片列表 arrPush( { url, img: url, desc: event.target.querySelector("p").innerText, suffix: "jpg" }, imgs ); // 渲染到视图 addScreen(imgDom, imgs); // 改颜色 changeColor(event.target); console.log(`已获取 视频:${videos.length},图片:${imgs.length},共:${videos.length + imgs.length}`); start(); } } /** * li 标签数据处理 */ function liInit() { li = Array.from(document.querySelectorAll("div[data-e2e=user-post-list] li")); console.log(`插件启动,已检测到 ${li.length} 条数据`); li.forEach((li) => { // 遍历给上颜色 li.style.backgroundColor = "#ff4444"; // 鼠标进入事件指定 li.addEventListener("mouseenter", liHandle); }); } /** * 获取图片数据 */ function getImgs(eventDom) { function cb() { return setTimeout(() => { getImgs(eventDom); }, 100); } let infoImgs = document.querySelectorAll(".slider-video .focusPanel > div"); // 如果没读到则元素没加载完成 if (!infoImgs[0]) { return cb(); } // 获取当前屏幕中的图片元素 for (let dom of infoImgs) { if (dom.getBoundingClientRect().top == 0) { infoImgs = dom.children; break; } } // 图片链接 let cache = []; for (let dom of infoImgs) { const imgs = dom.querySelectorAll("img"); // 第二张为高清图片 const src = imgs[1].getAttribute("src"); // 是否为 dy 临时图片,临时图片则代表没加载完成 if (src.substring(0, 2) == "//") { return cb(); } if (!cache.includes(src)) { cache.push(src); } } // 可能出现因为太没拿到图片,再次检测数量是否符合 if (cache.length != infoImgs.length) { return cb(); } let end = false; // 存到图片列表 cache.forEach((img) => { // 获取图片唯一标识 const cache1 = img.split("/"); const key = cache1[cache1.length - 1].split("~")[0]; if (successId.includes(key)) { end = true; return; } arrPush( { url: img, img: img, desc: eventDom.querySelector("p").innerText, suffix: "jpg", key }, imgs ); }); // 关闭详情 document.querySelector(".isDark").click(); // 改颜色 changeColor(eventDom); if (end) { handleIndex = li.length; console.log("往期视频已经获取过,无需重复获取,程序结束"); return start(); } // 渲染到视图 addScreen(imgDom, imgs); console.log(`已获取 视频:${videos.length},图片:${imgs.length},共:${videos.length + imgs.length}`); setTimeout(() => { start(); }, 100); } /** * 获取视频数据 * @param {HTMLElement} dom li */ function getVideo(dom) { dom.querySelector("a > div").dispatchEvent(new Event("mouseenter")); if (dom.innerText.includes("购买后可看")) { return start(); } // 第三个 source 是真实地址 const videoSource = dom.querySelectorAll("video source")[2]; if (!videoSource) { return setTimeout(() => { getVideo(dom); }, 300); } // 获取视频 id,真实地址 const regex = /video_id=([^&]+)/; const match = videoSource.getAttribute("src").match(regex); if (successId.includes(match[1])) { handleIndex = li.length; console.log("往期视频已经获取过,无需重复获取,程序结束"); return start(); } // 存到视频列表 arrPush( { url: "//www.douyin.com/aweme/v1/play/?" + match[0], img: dom.querySelector("img").getAttribute("src"), desc: dom.querySelector("p").innerText, suffix: "mp4", key: match[1] }, videos ); // 改颜色 changeColor(dom); // 渲染到视图 addScreen(videoDom, videos); console.log(`已获取 视频:${videos.length},图片:${imgs.length},共:${videos.length + imgs.length}`); start(); } // 处理索引 let handleIndex = 0; /** * 程序开始 */ function start() { // 是否已经全部获取完成 if (li.length == handleIndex) { loadBarChange(false); // 删除全部事件 li.forEach((li) => { li.removeEventListener("mouseenter", liHandle); }); console.log( `获取完成!其中视频:${videos.length},图片:${imgs.length},共:${videos.length + imgs.length}` ); // 打印上传数据 console.log("上传数据", { pwd: apiPassword, userUrl, nickname, download: false, list: [...videos, ...imgs] }); if (ok) { uploadAll([...videos, ...imgs]); return downloadAll([...videos, ...imgs]); } // 延迟一下,不然最后个视频无法改背景色 return setTimeout(() => { alert(`获取完成!其中视频:${videos.length},图片:${imgs.length},共:${videos.length + imgs.length}`); }, 500); } // 取出单个元素 let oneLi = li[handleIndex]; handleIndex++; loadBarChange(true, ((handleIndex + 1) / li.length) * 100); // 是否为图文 if ( oneLi.querySelector(".user-video-stats-tag") && (oneLi.querySelectorAll(".user-video-tag").length == 2 || oneLi.querySelector(".user-video-stats-tag").innerText.includes("图文")) ) { oneLi.querySelector("div > a").click(); getImgs(oneLi); } else { getVideo(oneLi); } } /** * push 到数组并用 url 防止重复 * @param {*} obj 需 push 对象 * @param {*} arr 原数组 */ function arrPush(obj, arr) { for (const item of arr) { if (obj.url == item.url) { return; } } arr.push(obj); } /** * 渲染到对应视图 * @param {*} dom 视图 dom * @param {*} arr 视图数据 */ function addScreen(dom, arr) { let cache = ""; for (let i = 0; i < arr.length; i++) { cache = `<div><a href='${arr[i].url}' target='_blank'><img src='${arr[i].img}' /></a><p>${arr[i].desc}</p></div> ` + cache; } dom.innerHTML = cache; } /** * 下载全部 * @param {*} arr 需下载数据 * @param {*} list 结果合集 */ function downloadAll(arr, list = []) { console.log(`已下载 ${list.length},还剩 ${arr.length}`); if (arr.length == 0) { alert(`${list.length} 完成下载`); return; } let item = arr.splice(0, 1); item = item[0]; function fetchDownload() { fetch(item.url) .then((res) => res.blob().then((blob) => { const a = document.createElement("a"); const url = window.URL.createObjectURL(blob); const filename = `${item.key}.${item.suffix}`; a.href = url; a.download = filename; a.click(); window.URL.revokeObjectURL(url); a.remove(); list.push({ url: filename, desc: item.desc, suffix: item.suffix, key: item.key }); setTimeout(() => { downloadAll(arr, list); }, 100); }) ) .catch((err) => { console.log(`下载 ${list.length} 报错!尝试重新下载`, err.message); setTimeout(() => { fetchDownload(); }, 1000); }); } fetchDownload(); } /** * 滚动网页 * @param {*} top 滚动高度 */ function scrollToTop(top) { document.querySelector("html").scrollTo({ top, behavior: "smooth" }); } /** * 进度条操作 * @param {boolean} state 开启 Or 停止 * @param {number} num 百分比进度 */ function loadBarChange(state, num = 0) { if (state) { banDom.style.visibility = "visible"; loadBar.style.width = num + "%"; loadBar.innerText = `${num.toFixed(0)}%`; } else { banDom.style.visibility = "hidden"; loadBar.style.width = "0%"; loadBar.innerText = ""; } } /** * 滚动到最底部,没数据后开始 */ function scrollStart() { scrollToTop(document.body.scrollHeight); setTimeout(() => { if (document.querySelector("div[data-e2e=user-post-list]>div").innerText != "暂时没有更多了") { scrollStart(); } else { scrollToTop(0); setTimeout(() => { liInit(); start(); }, 1000); } }, 1000); } /** * 上传全部 * @param {*} arr 数据 */ function uploadAll(arr) { fetch(apiServer + "/save", { method: "post", body: JSON.stringify({ pwd: apiPassword, userUrl, nickname, download: !ok, list: arr }), headers: { "content-type": "application/json" } }) .then((res) => { return res.json(); }) .then((res) => { ok = false; alert(res.msg); }); } /** * 获取用户已经扒过的视频 id */ async function getUserKeys() { if (!apiServer) return; const res = await fetch(apiServer + "/user/key?url=" + userUrl); const { data } = await res.json(); successId = data; } // 主体操作区域 const mainDom = document.createElement("div"); mainDom.id = "n0ts"; // 预览区域 const viewDom = document.createElement("div"); viewDom.className = "viewDom viewDomHidden"; // 视频预览 const videoDom = document.createElement("div"); // 图片预览 const imgDom = document.createElement("div"); viewDom.appendChild(videoDom); viewDom.appendChild(imgDom); // 按钮们 const btnDom = document.createElement("div"); btnDom.classList.add("btns"); const btn1 = document.createElement("button"); btn1.innerText = "下载全部"; btn1.addEventListener("click", () => { downloadAll([...videos, ...imgs]); }); const btn2 = document.createElement("button"); btn2.innerText = "开始扒取"; btn2.addEventListener("click", async () => { banDom.style.visibility = "visible"; await getUserKeys(); scrollStart(); }); const btn3 = document.createElement("button"); btn3.innerText = "预览结果"; btn3.addEventListener("click", () => { if (viewDom.classList.contains("viewDomHidden")) { viewDom.classList.remove("viewDomHidden"); } else { viewDom.classList.add("viewDomHidden"); } }); const btn4 = document.createElement("button"); btn4.innerText = "上传全部"; btn4.addEventListener("click", () => { uploadAll([...videos, ...imgs]); }); const btn5 = document.createElement("button"); btn5.innerText = "扒&下载&上传"; btn5.addEventListener("click", async () => { ok = true; banDom.style.visibility = "visible"; await getUserKeys(); scrollStart(); }); btnDom.appendChild(btn2); btnDom.appendChild(btn1); btnDom.appendChild(btn4); btnDom.appendChild(btn3); btnDom.appendChild(btn5); // 遮挡遮罩,过程中不允许鼠标干涉 const banDom = document.createElement("div"); const loadBar = document.createElement("div"); banDom.appendChild(loadBar); banDom.id = "banDom"; // 元素插入 mainDom.appendChild(viewDom); mainDom.appendChild(btnDom); document.body.appendChild(mainDom); document.body.appendChild(banDom); // css const css = document.createElement("style"); css.innerText = ` #n0ts { position: fixed; z-index: 9999; bottom: 0; left: 0; } #n0ts img { width: 100%; } #n0ts .btns { width: 105px; height: 140px; position: absolute; bottom: 0; left: 5px; } #n0ts .btns button { width: 100%; margin-bottom: 5px; } .viewDom { position: fixed; left: 50%; top: 50%; width: 75%; height: 90%; background: #e1e1e1; transform: translate(-50%, -50%); display: flex; justify-content: space-between; } .viewDomHidden { visibility: hidden; } .viewDom div { width: 100%; display: flex; flex-wrap: wrap; justify-content: space-around; overflow-y: auto; } .viewDom div > div { width: 48%; } #banDom { position: fixed; top: 0; left: 0; z-index: 9998; width: 100%; height: 100%; background: rgb(0 0 0 / 20%); visibility: hidden; } #banDom>div { position: absolute; top: 0; left: 0; width: 0%; height: 30px; background: #00c0ff; text-align: center; color: white; line-height: 30px; transition: all 0.2s; } `; document.head.appendChild(css); })();