[油猴脚本] 小红书一键导出收藏+一键下载单个笔记内容
本帖最后由 bootislands 于 2023-9-26 13:42 编辑该脚本在电脑端使用。功能主要分两部分:
1. 「导出小红书账号里的收藏」。使用方法可以看作者录制的教程视频:https://www.bilibili.com/video/BV1Fo4y177pv/
主要是导出你自己的收藏。如果别人的收藏公开可见的话,也可以导出别人的。
2. 「一键下载单条笔记的内容(包括图片 视频 笔记文字,但不包括评论区)」。使用方法:点进单个笔记的页面,右下角就会出现下载按钮
楼主个人很喜欢这个脚本,因为理论上它够安全,不像一些爬虫大批量发送请求(这样很容易被封号),相反,功能1只抓取收藏页面的现成数据,不额外再发请求,功能2也只是下载单条笔记内容,因此很安全(只是理论上,万一你中奖了别找楼主负责)
注:这个脚本不能批量爬取某个博主发的笔记。如果你需要这种高风险功能,可以看看另一个项目:https://github.com/xisuo67/XHS-Spider 它功能更强,相应地被封号概率也更高
安装地址:https://greasyfork.org/zh-CN/scripts/464664
脚本代码:
```Javascript
// ==UserScript==
// @name 小红书转发
// @namespace https://mundane.ink/redbook
// @version 2.3
// @description在浏览小红书收藏时将数据转发到https://mundane.ink/redbook/index.html,方便收藏的管理和导出
// @match https://www.xiaohongshu.com/*
// @grant GM_xmlhttpRequest
// @license MIT
// ==/UserScript==
(function () {
"use strict";
console.log("小红书脚本生效了");
const baseUrl = "https://mundane.ink";
// const baseUrl = "http://localhost:8088";
document.body.addEventListener("click", (e) => {
if (
e.target.tagName === "A" &&
e.target.classList.value.includes("cover ld mask")
) {
setTimeout(() => {
const href = e.target.href;
const noteId = href.match(/\/([^/]+)$/);
if (noteId) {
createDownloadMdButton(noteId);
createMediaButton(noteId);
}
}, 1000);
}
});
// 创建下载md按钮
function createDownloadMdButton(noteId) {
const mask = document.querySelector("div.note-detail-mask");
const button = document.createElement("button");
button.textContent = "下载md文件";
button.style.position = "fixed";
button.style.bottom = "65px";
button.style.right = "20px";
button.style.padding = "10px 20px";
button.style.border = "none";
button.style.backgroundColor = "#056b00";
button.style.color = "#fff";
button.style.fontFamily = "Arial, sans-serif";
button.style.fontSize = "16px";
button.style.fontWeight = "bold";
button.style.cursor = "pointer";
button.addEventListener("click", function () {
exportMd(noteId);
});
mask.appendChild(button);
}
// 创建下载图片和视频按钮
function createMediaButton(noteId) {
const mask = document.querySelector("div.note-detail-mask");
const button = document.createElement("button");
button.textContent = "下载图片和视频";
button.style.position = "fixed";
button.style.bottom = "20px";
button.style.right = "20px";
button.style.padding = "10px 20px";
button.style.border = "none";
button.style.backgroundColor = "#056b00";
button.style.color = "#fff";
button.style.fontFamily = "Arial, sans-serif";
button.style.fontSize = "16px";
button.style.fontWeight = "bold";
button.style.cursor = "pointer";
button.addEventListener("click", function () {
getMedia(noteId);
});
mask.appendChild(button);
}
function getMedia(noteId) {
fetch(`${baseUrl}/mail/redbook/note/getMediaInfo`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ noteId }),
})
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
// 解析响应数据为 JSON 格式
return response.json();
})
.then((resp) => {
if (resp.code === 200) {
exportMedia(resp.data);
}
})
.catch((error) => console.error(error));
}
function exportMedia(data) {
const { title, videoUrl, imageUrls } = data;
exportImages(title, imageUrls);
if (videoUrl) {
exportVideo(title, videoUrl);
}
}
function exportVideo(title, videoUrl) {
fetch(videoUrl)
.then((response) => {
return response.blob();
})
.then((blob) => {
// 创建一个下载链接
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = title + ".mp4";
document.body.appendChild(a);
// 模拟点击下载链接
a.click();
// 清理对象 URL
URL.revokeObjectURL(url);
})
.catch((error) => console.error(error));
}
function exportImages(title, imageUrls) {
for (let i = 0; i < imageUrls.length; i++) {
const imageUrl = imageUrls;
fetch(imageUrl)
.then((response) => response.blob())
.then((blob) => {
const imageURL = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = imageURL;
a.download = title + "-" + (i + 1) + ".png";
a.click();
URL.revokeObjectURL(imageURL);
});
}
}
function exportMd(noteId) {
fetch(`${baseUrl}/mail/redbook/note/exportNoteMd`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ noteId }),
})
.then((response) => {
const contentDisposition = response.headers.get("Content-Disposition");
const filenameMatch = decodeURIComponent(
contentDisposition.match(/filename\=(.*)/)
);
const filename = filenameMatch || "filename.md";
response.blob().then((blob) => {
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
});
})
.catch((error) => console.error(error));
}
// 创建按钮元素
const btnScroll = document.createElement("button");
btnScroll.innerHTML = "自动滚动";
const btnJump = document.createElement("button");
btnJump.innerHTML = "去下载";
const btnTest = document.createElement("button");
btnTest.innerHTML = "测试";
// 设置按钮样式
btnScroll.style.position = "fixed";
btnScroll.style.top = "160px";
btnScroll.style.right = "20px";
btnScroll.style.backgroundColor = "#056b00";
btnScroll.style.color = "#fff";
btnScroll.style.padding = "8px";
btnScroll.style.borderRadius = "6px";
btnScroll.style.zIndex = "1000";
btnJump.style.position = "fixed";
btnJump.style.top = "210px";
btnJump.style.right = "20px";
btnJump.style.backgroundColor = "#056b00";
btnJump.style.color = "#fff";
btnJump.style.padding = "8px";
btnJump.style.borderRadius = "6px";
btnJump.style.zIndex = "1000";
btnTest.style.position = "fixed";
btnTest.style.top = "260px";
btnTest.style.right = "20px";
btnTest.style.backgroundColor = "#056b00";
btnTest.style.color = "#fff";
btnTest.style.padding = "8px";
btnTest.style.borderRadius = "6px";
btnTest.style.zIndex = "1000";
// 添加按钮到页面中
document.body.appendChild(btnScroll);
document.body.appendChild(btnJump);
// document.body.appendChild(btnTest);
let isScrolling = false;
let timerId;
function getUserId() {
const arr = window.location.href.match(/\/user\/profile\/(\w+)/);
if (!arr) {
return "";
}
if (arr.length < 2) {
return "";
}
return arr;
}
function simulateScroll() {
window.scrollBy(0, 200);
}
function startScroll() {
if (isScrolling) {
return;
}
isScrolling = true;
btnScroll.innerHTML = "停止滚动";
btnScroll.style.backgroundColor = "#ff2442";
timerId = setInterval(simulateScroll, 200);
}
function cancelScroll() {
if (!isScrolling) {
return;
}
isScrolling = false;
btnScroll.style.backgroundColor = "#056b00";
btnScroll.innerHTML = "自动滚动";
if (timerId) {
clearInterval(timerId);
}
}
// 给按钮添加点击事件
btnScroll.addEventListener("click", function () {
if (isScrolling) {
cancelScroll();
} else {
startScroll();
}
});
btnJump.addEventListener("click", function () {
const userId = getUserId();
window.open(
`https://mundane.ink/redbook/index.html?userId=${userId}`,
"_blank"
);
});
btnTest.addEventListener("click", function () {
let tab = document.querySelectorAll(".tab-content-item");
const elements = tab.querySelectorAll("a.cover.ld.mask");
elements.click();
let timeId = setInterval(function () {
let closeButton = document.querySelector("div.close-circle div.close");
if (closeButton) {
closeButton.click();
clearInterval(timeId);
}
}, 500);
});
const originOpen = XMLHttpRequest.prototype.open;
const collectUrl = "//edith.xiaohongshu.com/api/sns/web/v2/note/collect";
const feedUrl = "//edith.xiaohongshu.com/api/sns/web/v1/feed";
let patchIndex = 0;
XMLHttpRequest.prototype.open = function (_, url) {
const xhr = this;
if (url.startsWith(collectUrl) || url.startsWith(feedUrl)) {
const getter = Object.getOwnPropertyDescriptor(
XMLHttpRequest.prototype,
"response"
).get;
Object.defineProperty(xhr, "responseText", {
get: () => {
let result = getter.call(xhr);
// console.log("result =", result);
let myUrl = "";
let requestData = "";
if (url.startsWith(collectUrl)) {
const params = new URLSearchParams(url.split("?"));
const cursor = params.get("cursor");
if (!cursor) {
patchIndex = 0;
}
myUrl = `${baseUrl}/mail/redbook/collect/save`;
const userId = getUserId();
requestData = JSON.stringify({ result, userId, patchIndex });
} else if (url.startsWith(feedUrl)) {
myUrl = `${baseUrl}/mail/redbook/note/save`;
requestData = JSON.stringify({ result });
}
try {
// 将result发送到服务器
GM_xmlhttpRequest({
method: "POST",
url: myUrl,
headers: {
"Content-Type": "application/json",
},
data: requestData,
onload: function (response) {
console.log("Result sent to server successfully!");
},
});
if (url.startsWith(collectUrl)) {
patchIndex++;
const obj = JSON.parse(result);
if (!obj.data.has_more) {
cancelScroll();
patchIndex = 0;
console.log("没有更多了!!!");
alert("小红书收藏已发送完毕,没有更多了");
}
}
} catch (e) {
console.error(e);
}
return result;
},
});
}
originOpen.apply(this, arguments);
};
})();
``` 请问导出的内容,是按照笔记标题生成不同文件夹吗 ? 感谢分享,正好想保存几篇笔记,下来试试 谢谢楼主 不喜欢批量的 就喜欢这种脚本 感谢楼主的分享我来试用一下! 感谢分享
不错的脚本,先收藏一下 哥们 点赞收藏评论呢??这些数据 感谢楼主分享 感谢分享