Skip to content

Instantly share code, notes, and snippets.

@huiliu
Created May 16, 2026 16:05
Show Gist options
  • Select an option

  • Save huiliu/48ef5a9a2e490e7d48cf3b5da7a54f09 to your computer and use it in GitHub Desktop.

Select an option

Save huiliu/48ef5a9a2e490e7d48cf3b5da7a54f09 to your computer and use it in GitHub Desktop.
知乎收藏夹导出
// ==UserScript==
// @name 知乎收藏夹导出
// @namespace http://tampermonkey.net/
// @version 4.1
// @description 静默请求原生 API,并按照“名字+ID+数量+时间”的格式化文件名导出
// @author Gemini
// @match https://www.zhihu.com/collection/*
// @grant none
// @run-at document-end
// ==/UserScript==
// Gemini 对话:https://gemini.google.com/app/e641a19324d20907?hl=zh-CN
(function() {
'use strict';
let collectionId = window.location.pathname.split('/').pop();
let isWorking = false;
// --- 1. 模拟网络请求函数 ---
async function fetchAllCollectionData() {
let allItems = [];
let offset = 0;
let limit = 20;
let isEnd = false;
updateBtnStatus(`准备开始...`);
while (!isEnd && isWorking) {
const apiUrl = `https://www.zhihu.com/api/v4/collections/${collectionId}/items?offset=${offset}&limit=${limit}`;
console.log(`[API模拟] 正在请求: ${apiUrl}`);
try {
const response = await window.fetch(apiUrl, {
headers: {
'accept': 'application/json, text/plain, */*',
'x-requested-with': 'XMLHttpRequest'
}
});
if (!response.ok) {
throw new Error(`HTTP 错误! 状态码: ${response.status}`);
}
const resBody = await response.json();
if (resBody && resBody.data) {
allItems.push(...resBody.data);
updateBtnStatus(`已下载: ${allItems.length} 条`);
if (resBody.paging && resBody.paging.is_end === false) {
offset += limit;
// 随机延迟防风控
await new Promise(resolve => setTimeout(resolve, 1000 + Math.random() * 500));
} else {
isEnd = true;
}
} else {
isEnd = true;
}
} catch (error) {
console.error('[API模拟] 请求出错:', error);
alert(`请求失败。当前已抓取 ${allItems.length} 条。`);
break;
}
}
if (allItems.length > 0) {
triggerDownload(allItems);
}
isWorking = false;
updateBtnStatus('🚀 导出全量 JSON');
}
// --- 2. 格式化当前时间 (YYYYMMDD_HHmmss) ---
function getFormattedTime() {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
return `${year}${month}${day}_${hours}${minutes}${seconds}`;
}
// --- 3. 动态组装文件名并下载 ---
function triggerDownload(finalData) {
// 动态获取收藏夹名称(兼容两种知乎常见的类名)
const titleEl = document.querySelector('.CollectionDetailPageHeader-title') ||
document.querySelector('.CollectionDetailHeader-title');
const folderName = titleEl ? titleEl.innerText.trim() : '知乎收藏夹';
const recordCount = finalData.length;
const timeStr = getFormattedTime();
// 核心:按照“收藏夹名_收藏夹Id_记录数_导出时间.json”拼接
const filename = `${folderName}_${collectionId}_${recordCount}条_${timeStr}.json`;
const blob = new Blob([JSON.stringify(finalData, null, 2)], { type: 'application/json' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = filename;
document.body.appendChild(a);
a.click();
setTimeout(() => {
document.body.removeChild(a);
URL.revokeObjectURL(a.href);
alert(`导出成功!\n文件已保存为:${filename}`);
}, 500);
}
// --- 4. UI 注入与状态控制 ---
function injectButton() {
if (document.getElementById('tm-export-btn')) return;
const target = document.querySelector('.CollectionDetailPageHeader-actions') ||
document.querySelector('.CollectionDetailHeader-actions');
if (!target) return;
const btn = document.createElement('button');
btn.id = 'tm-export-btn';
btn.innerText = '🚀 导出全量 JSON';
btn.style = `margin-left:10px; padding:6px 16px; background:#0066ff; color:#fff; border:none; border-radius:999px; cursor:pointer; font-weight:600; vertical-align: middle;`;
btn.onclick = () => {
if (isWorking) {
if (confirm("正在抓取中,是否强行停止并保存当前已下载的数据?")) {
isWorking = false;
}
return;
}
if (confirm("开始在后台静默请求知乎 API 获取最完整的无损数据,确定吗?")) {
isWorking = true;
fetchAllCollectionData();
}
};
target.appendChild(btn);
}
function updateBtnStatus(text) {
const btn = document.getElementById('tm-export-btn');
if (btn) btn.innerText = text;
}
setInterval(injectButton, 1000);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment