// ==UserScript==
// @name 小红书下载链接获取
// @namespace http://tampermonkey.net/
// @version 0.1
// @description 记录已经打开的笔记对应的视频数据和链接,可以随时复制或下载csv
// @author xxmdmst
// @match https://www.xiaohongshu.com/*
// @icon https://www.xiaohongshu.com/favicon.ico
// @grant none
// @license MIT
// ==/UserScript==
(function () {
'use strict';
let table;
function initGbkTable() {
// https://en.wikipedia.org/wiki/GBK_(character_encoding)#Encoding
const ranges = [
[0xA1, 0xA9, 0xA1, 0xFE],
[0xB0, 0xF7, 0xA1, 0xFE],
[0x81, 0xA0, 0x40, 0xFE],
[0xAA, 0xFE, 0x40, 0xA0],
[0xA8, 0xA9, 0x40, 0xA0],
[0xAA, 0xAF, 0xA1, 0xFE],
[0xF8, 0xFE, 0xA1, 0xFE],
[0xA1, 0xA7, 0x40, 0xA0],
];
const codes = new Uint16Array(23940);
let i = 0;
for (const [b1Begin, b1End, b2Begin, b2End] of ranges) {
for (let b2 = b2Begin; b2 <= b2End; b2++) {
if (b2 !== 0x7F) {
for (let b1 = b1Begin; b1 <= b1End; b1++) {
codes[i++] = b2 << 8 | b1
}
}
}
}
table = new Uint16Array(65536);
table.fill(0xFFFF);
const str = new TextDecoder('gbk').decode(codes);
for (let i = 0; i < str.length; i++) {
table[str.charCodeAt(i)] = codes[i]
}
}
function str2gbk(str, opt = {}) {
if (!table) {
initGbkTable()
}
const NodeJsBufAlloc = typeof Buffer === 'function' && Buffer.allocUnsafe;
const defaultOnAlloc = NodeJsBufAlloc
? (len) => NodeJsBufAlloc(len)
: (len) => new Uint8Array(len);
const defaultOnError = () => 63;
const onAlloc = opt.onAlloc || defaultOnAlloc;
const onError = opt.onError || defaultOnError;
const buf = onAlloc(str.length * 2);
let n = 0;
for (let i = 0; i < str.length; i++) {
const code = str.charCodeAt(i);
if (code < 0x80) {
buf[n++] = code;
continue
}
const gbk = table[code];
if (gbk !== 0xFFFF) {
buf[n++] = gbk;
buf[n++] = gbk >> 8
} else if (code === 8364) {
buf[n++] = 0x80
} else {
const ret = onError(i, str);
if (ret === -1) {
break
}
if (ret > 0xFF) {
buf[n++] = ret;
buf[n++] = ret >> 8
} else {
buf[n++] = ret
}
}
}
return buf.subarray(0, n)
}
function copyToClipboard(text) {
try {
const textarea = document.createElement("textarea");
textarea.setAttribute('readonly', 'readonly');
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
let flag = document.execCommand("copy");
document.body.removeChild(textarea);
return flag;
} catch (e) {
console.log(e);
return false;
}
}
let title2urls = new Map();
function interceptResponse() {
const originalSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function () {
originalSend.apply(this, arguments);
const self = this;
let func = this.onreadystatechange;
this.onreadystatechange = (e) => {
if (func) {
func.apply(self, e);
}
if (self.readyState !== 4 || self.responseURL.indexOf("/api/sns/web/v1/feed") === -1) return;
let json = JSON.parse(self.response);
if (!json.success) return;
for (let item of json.data.items) {
let noteCard = item.note_card;
if (noteCard.type !== "video") {
continue;
}
let interactInfo = noteCard.interact_info;
let backup_urls = noteCard.video.media.stream.h264[0].backup_urls;
for (let video_url of backup_urls) {
if (video_url.indexOf("?") > -1) {
window.video_url = video_url;
title2urls.set(item.id, [
noteCard.title, interactInfo.liked_count, interactInfo.collected_count,
interactInfo.comment_count, interactInfo.share_count,
new Date(noteCard.time).toLocaleString(), video_url
]);
// console.log(Array.from(title2urls.keys()));
// console.log(Array.from(title2urls.values()));
break;
}
}
}
}
};
}
interceptResponse();
function createButton(title, top) {
top = top === undefined ? "60px" : top;
const button = document.createElement('button');
button.textContent = title;
button.style.position = 'fixed';
button.style.right = '5px';
button.style.top = top;
button.style.zIndex = '90000';
document.body.appendChild(button);
return button
}
function txt2file(txt, filename) {
const blob = new Blob([txt], {type: 'text/plain'});
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename.replace(/[\/:*?"<>|]/g, "");
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
function downloadData(encoding) {
let text = "标题,点赞数,收藏数,评论数,分享数,发布时间,下载链接\n";
Array.from(title2urls.values()).forEach(item => {
text += '"' + item[0] + '",' + item.slice(1).join(",") + "\n"
});
if (encoding === "gbk") text = str2gbk(text);
txt2file(text, document.title + ".csv");
}
let b1 = createButton("复制已加载链接", "60px");
let b2 = createButton("下载已加载数据", "81px");
let b3 = createButton("复制链接", "102px");
function copyUrl() {
let urls = Array.from(title2urls.values()).map(item => item[item.length - 1]).join("\n");
if (copyToClipboard(urls)) b1.textContent = "复制成功";
else b1.textContent = "复制失败";
setTimeout(() => {
b1.textContent = '复制已加载链接';
}, 2000);
}
function copyUrl2() {
if (copyToClipboard(window.video_url)) b3.textContent = "复制成功";
else b3.textContent = "复制失败";
setTimeout(() => {
b3.textContent = '复制链接';
}, 2000);
}
window.onload = () => {
b1.addEventListener('click', copyUrl);
b2.addEventListener('click', (e) => downloadData("gbk"));
b3.addEventListener('click', copyUrl2);
};
})();