ovioni 发表于 2023-9-26 13:40

[油猴脚本] 小红书一键导出收藏+一键下载单个笔记内容

本帖最后由 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);
};
})();
```

yourbestrun 发表于 2023-9-26 16:01

请问导出的内容,是按照笔记标题生成不同文件夹吗 ?

marsjojo 发表于 2023-9-26 13:54

感谢分享,正好想保存几篇笔记,下来试试

ithero0512 发表于 2023-9-26 14:30

谢谢楼主   不喜欢批量的 就喜欢这种脚本

keber 发表于 2023-9-26 14:32

感谢楼主的分享我来试用一下!

山丁 发表于 2023-9-26 14:33

感谢分享

醉酒听风 发表于 2023-9-26 14:45

不错的脚本,先收藏一下

庄家 发表于 2023-9-26 16:18

哥们 点赞收藏评论呢??这些数据

ufoboyxj 发表于 2023-9-26 16:19

感谢楼主分享

Gyy66 发表于 2023-9-26 17:00

感谢分享
页: [1] 2 3
查看完整版本: [油猴脚本] 小红书一键导出收藏+一键下载单个笔记内容