// ==UserScript==
// @name 知乎全能助手
// @namespace http://qtqz.zhihu/
// @version 3.6
// @description 修复“想法”在导出文件中显示“Invalid Date”的问题。1. 想法标题自动使用“内容摘要”;2. 修复时间获取;3. 完美支持回答/文章/想法/转发。
// @author AI & qtqz logic
// @match *://www.zhihu.com/*
// @match *://zhuanlan.zhihu.com/*
// @icon https://static.zhihu.com/heifetz/favicon.ico
// @require https://cdn.jsdelivr.net/npm/file-saver@2.0.5/dist/FileSaver.min.js
// @grant GM_xmlhttpRequest
// @grant GM_registerMenuCommand
// @grant GM_setClipboard
// @license MIT
// @downloadURL none
// ==/UserScript==
(function () {
'use strict';
// ==========================================
// 配置区域
// ==========================================
const CONFIG = {
commentLimit: 15,
requestDelay: 1000
};
const STATE = {
isRunning: false,
items: [],
currentType: '',
id: '',
pageTitle: '',
cancel: false
};
const UI = {
panel: null,
logArea: null,
progressBar: null,
statusDiv: null
};
// ==========================================
// 模块一:文件名生成器
// ==========================================
function getFormattedDate() {
const now = new Date();
const y = now.getFullYear();
const m = (now.getMonth() + 1).toString().padStart(2, '0');
const d = now.getDate().toString().padStart(2, '0');
const h = now.getHours().toString().padStart(2, '0');
const min = now.getMinutes().toString().padStart(2, '0');
const s = now.getSeconds().toString().padStart(2, '0');
return `${y}${m}${d}`;
}
function generateFilename(extension) {
const typeMap = { 'collection': '收藏夹', 'people_answers': '用户回答', 'people_articles': '用户文章', 'people_activities': '用户动态', 'people_pins': '用户想法' };
const typeStr = `[${typeMap[STATE.currentType] || '批量导出'}]`;
let rawTitle = STATE.pageTitle || document.title || '';
rawTitle = rawTitle.replace(/^[\((][\d\s\u4e00-\u9fa5\/_\-]+[\))]\s*/, '').replace(' - 知乎', '').trim();
let nameStr = rawTitle || STATE.id || '未知目标';
nameStr = sanitizeFileName(nameStr);
const timeStr = getFormattedDate();
const countStr = `(共${STATE.items.length}条)`;
return `${typeStr} ${nameStr}_${timeStr} ${countStr}.${extension}`;
}
// ==========================================
// 模块二:高级 DOM 解析器
// ==========================================
const cleanLink = (link) => {
if (!link) return '';
try {
const url = new URL(link);
if (url.hostname === "link.zhihu.com") {
const target = url.searchParams.get("target");
return target ? decodeURIComponent(target) : link;
}
} catch (e) {}
return link;
};
function parseContentToMarkdown(htmlString) {
if (!htmlString) return '';
const doc = new DOMParser().parseFromString(htmlString, 'text/html');
return walkNodes(doc.body).trim();
}
function walkNodes(node) {
if (node.nodeType === Node.TEXT_NODE) return node.textContent.replace(/\u200B/g, '');
if (node.nodeType !== Node.ELEMENT_NODE) return '';
const tagName = node.tagName.toLowerCase();
let childrenText = '';
node.childNodes.forEach(child => childrenText += walkNodes(child));
switch (tagName) {
case 'h1': return `\n# ${childrenText}\n\n`;
case 'h2': return `\n## ${childrenText}\n\n`;
case 'h3': return `\n### ${childrenText}\n\n`;
case 'h4': return `\n#### ${childrenText}\n\n`;
case 'p': return node.classList.contains('ztext-empty-paragraph') ? '' : `${childrenText}\n\n`;
case 'br': return ' \n';
case 'b': case 'strong': return ` **${childrenText}** `;
case 'i': case 'em': return ` *${childrenText}* `;
case 'blockquote': return `\n> ${childrenText.replace(/\n/g, '\n> ')}\n\n`;
case 'a': return `[${childrenText}](${cleanLink(node.getAttribute('href'))})`;
case 'img':
const src = node.getAttribute('data-actualsrc') || node.getAttribute('data-original') || node.getAttribute('src');
if (!src) return '';
if (node.classList.contains('ztext-math') || src.includes('equation?tex=')) {
const tex = node.getAttribute('data-tex') || node.getAttribute('alt');
return tex ? ` $${tex}$ ` : '';
}
return `\n\n`;
case 'span': return node.classList.contains('ztext-math') ? ` $${node.getAttribute('data-tex')}$ ` : childrenText;
case 'div':
if (node.classList.contains('highlight')) {
const code = node.textContent;
const lang = node.querySelector('pre > code')?.className.match(/language-(\w+)/)?.[1] || '';
return `\n\`\`\`${lang}\n${code}\n\`\`\`\n\n`;
}
return childrenText;
case 'li': return `- ${childrenText}\n`;
case 'ul': case 'ol': return `\n${childrenText}\n`;
case 'hr': return '\n---\n';
default: return childrenText;
}
}
// ==========================================
// 模块三:单篇复制按钮
// ==========================================
function injectCopyButtons() {
const items = document.querySelectorAll('.ContentItem, .Post-content, .SearchResult-Card, .PinItem');
items.forEach(item => {
if (item.getAttribute('data-md-btn-added')) return;
item.setAttribute('data-md-btn-added', 'true');
let targetArea = item.querySelector('.ContentItem-meta') || (item.classList.contains('Post-content') ? document.querySelector('.Post-Header') : null) || item.querySelector('.ContentItem-actions') || item.querySelector('.QuestionHeader-title') || (item.classList.contains('PinItem') ? item.querySelector('.PinItem-content') : null);
if (targetArea) {
const btn = document.createElement('span');
btn.className = 'zbc-copy-btn';
btn.style.cssText = `margin-left:2px; margin-top:8px; display:inline-flex; align-items:center; cursor:pointer; background-color:#f0f2f5; color:#374151; font-size:13px; line-height:1.4; padding:2px 8px; border-radius:4px; transition:all 0.2s ease;`;
if (targetArea.className && targetArea.className.includes('ContentItem-actions')) { btn.style.marginTop = '0px'; btn.style.order = '99'; } else if (item.classList.contains('PinItem')) { btn.style.marginBottom = '10px'; btn.style.display = 'inline-block'; }
btn.innerHTML = ` 复制为Markdown`;
btn.onclick = (e) => {
e.stopPropagation();
const originalHTML = btn.innerHTML;
btn.innerHTML = `解析中...`;
try {
let contentHtml = '', url = '', title = '';
const richText = item.querySelector('.RichContent-inner') || item.querySelector('.Post-RichText') || item.querySelector('.RichText') || item.querySelector('.PinItem-content');
if (richText) contentHtml = richText.innerHTML;
const titleEl = item.querySelector('.ContentItem-title') || document.querySelector('.QuestionHeader-title') || document.querySelector('.Post-Title') || item.querySelector('h1') || item.querySelector('h2');
title = item.classList.contains('PinItem') ? "想法" : (titleEl ? titleEl.innerText : '无标题');
const metaUrl = item.querySelector('meta[itemprop="url"]');
url = metaUrl ? metaUrl.content : window.location.href;
if (!contentHtml) { const expandBtn = item.querySelector('.ContentItem-expandButton'); if(expandBtn) { expandBtn.click(); throw new Error('需展开'); } throw new Error('无内容'); }
let markdown = `# ${title}\n\n` + parseContentToMarkdown(contentHtml) + `\n\n> 来源: [${url}](${url})`;
GM_setClipboard(markdown);
btn.innerHTML = `✔ 成功`; setTimeout(() => { btn.innerHTML = originalHTML; }, 2000);
} catch (err) { console.error(err); btn.innerHTML = `失败`; setTimeout(() => { btn.innerHTML = originalHTML; }, 2000); }
};
if (item.classList.contains('PinItem')) targetArea.parentNode.insertBefore(btn, targetArea); else targetArea.appendChild(btn);
}
});
}
// ==========================================
// 模块四:批量导出核心
// ==========================================
function initPanelUI() {
const style = document.createElement('style');
style.textContent = `#zbc-panel { position: fixed; top: 100px; right: 20px; width: 340px; background: #fff; box-shadow: 0 4px 12px rgba(0,0,0,0.15); border-radius: 8px; z-index: 9999; font-family: sans-serif; border: 1px solid #ebebeb; display: none; font-size: 14px; } #zbc-header { padding: 12px 16px; border-bottom: 1px solid #f0f0f0; background: #f6f6f6; border-radius: 8px 8px 0 0; font-weight: bold; color: #1772f6; display: flex; justify-content: space-between; align-items: center; } #zbc-body { padding: 16px; } .zbc-btn { display: block; width: 100%; padding: 8px; margin-bottom: 8px; border: 1px solid #1772f6; color: #1772f6; background: #fff; border-radius: 4px; cursor: pointer; text-align: center; transition: 0.2s; font-size: 13px; } .zbc-btn:hover { background: #eef6ff; } .zbc-btn:disabled { border-color: #ccc; color: #ccc; cursor: not-allowed; background: #f9f9f9;} .zbc-btn.primary { background: #1772f6; color: #fff; } .zbc-btn.primary:hover { background: #1062d6; } #zbc-log { height: 160px; overflow-y: auto; background: #f9f9f9; border: 1px solid #eee; padding: 8px; font-size: 12px; margin-bottom: 10px; color: #666; line-height: 1.4; } .zbc-progress { height: 4px; background: #eee; width: 100%; margin-bottom: 10px; } .zbc-progress-bar { height: 100%; background: #1772f6; width: 0%; transition: width 0.3s; } .zbc-close { cursor: pointer; color: #999; font-size: 18px; line-height: 1; }`;
document.head.appendChild(style);
const panel = document.createElement('div'); panel.id = 'zbc-panel';
panel.innerHTML = `
等待操作
策略:强制回源(防空) + 想法标题优化
`;
document.body.appendChild(panel);
UI.panel = panel; UI.logArea = document.getElementById('zbc-log'); UI.progressBar = document.getElementById('zbc-bar'); UI.statusDiv = document.getElementById('zbc-status');
document.getElementById('zbc-start').onclick = startScraping; document.getElementById('zbc-stop').onclick = () => { STATE.cancel = true; log('正在停止...'); }; document.getElementById('zbc-export-html').onclick = () => exportSingleHTML(); document.getElementById('zbc-export-json').onclick = () => exportJSON();
const toggleBtn = document.createElement('div'); toggleBtn.innerText = '📂'; toggleBtn.title = '打开批量导出面板'; toggleBtn.style.cssText = 'position:fixed; bottom:80px; right:20px; width:40px; height:40px; background:#1772f6; color:#fff; border-radius:50%; text-align:center; line-height:40px; cursor:pointer; z-index:9998; box-shadow:0 2px 10px rgba(0,0,0,0.2); font-size:20px;'; toggleBtn.onclick = () => { panel.style.display = panel.style.display === 'none' ? 'block' : 'none'; detectPage(); }; document.body.appendChild(toggleBtn);
}
function log(msg) { const p = document.createElement('div'); p.innerText = `[${new Date().toLocaleTimeString()}] ${msg}`; UI.logArea.prepend(p); }
function detectPage() {
const url = window.location.href;
STATE.pageTitle = document.title;
if (url.includes('/collection/')) { const match = url.match(/collection\/(\d+)/); if(match) return updateStatus('collection', match[1], '收藏夹'); }
else if (url.includes('/people/')) { const match = url.match(/people\/([^/]+)/); if(match) { if(url.includes('/answers')) return updateStatus('people_answers', match[1], '用户回答'); if(url.includes('/posts')||url.includes('/articles')) return updateStatus('people_articles', match[1], '用户文章'); if(url.includes('/pins')) return updateStatus('people_pins', match[1], '用户想法'); return updateStatus('people_activities', match[1], '用户动态(全能)'); } }
UI.statusDiv.innerText = '请进入用户主页或收藏夹'; return null;
}
function updateStatus(type, id, label) { STATE.currentType = type; STATE.id = id; UI.statusDiv.innerText = `当前:${label}`; return { type, id }; }
async function startScraping() {
const pageInfo = detectPage();
if (!pageInfo) { alert('无法识别当前页面'); return; }
STATE.isRunning = true; STATE.cancel = false; STATE.items = [];
document.getElementById('zbc-start').disabled = true; document.getElementById('zbc-stop').disabled = false; toggleExportBtns(false); UI.logArea.innerHTML = '';
let nextUrl = '';
if (pageInfo.type === 'collection') nextUrl = `https://www.zhihu.com/api/v4/collections/${pageInfo.id}/items?offset=0&limit=20`;
else if (pageInfo.type === 'people_answers') nextUrl = `https://www.zhihu.com/api/v4/members/${pageInfo.id}/answers?offset=0&limit=20&sort_by=created`;
else if (pageInfo.type === 'people_articles') nextUrl = `https://www.zhihu.com/api/v4/members/${pageInfo.id}/articles?offset=0&limit=20&sort_by=created`;
else if (pageInfo.type === 'people_pins') nextUrl = `https://www.zhihu.com/api/v4/members/${pageInfo.id}/pins?offset=0&limit=20`;
else if (pageInfo.type === 'people_activities') nextUrl = `https://www.zhihu.com/api/v4/members/${pageInfo.id}/activities?limit=20&desktop=true`;
try {
let count = 0;
while (nextUrl && !STATE.cancel) {
log(`请求列表中...`);
const data = await fetchAPI(nextUrl);
if (data.data && data.data.length > 0) {
for (const item of data.data) {
if (STATE.cancel) break;
let targetItem = item;
// --- 【核心修复】提取逻辑 ---
if (pageInfo.type === 'people_activities') {
if (['ANSWER_CREATE', 'ARTICLE_CREATE', 'PIN_CREATE', 'MEMBER_CREATE_PIN'].includes(item.verb)) {
targetItem = item.target;
} else continue;
}
else if (pageInfo.type === 'collection') {
if (item.content) targetItem = item.content; // 收藏夹有包裹
}
if (!targetItem || !targetItem.id) continue;
count++;
const idxStr = `[第${count}条]`;
let processed = null;
// 1. 提取显示标题 (此处仅用于日志,最终标题在 processItem 中生成)
let displayTitle = targetItem.title;
if (!displayTitle && targetItem.question) displayTitle = targetItem.question.title;
if (!displayTitle && (targetItem.type === 'pin' || targetItem.type === 'moment')) {
// 想法没有标题,尝试截取内容
let pinContent = targetItem.excerpt || (Array.isArray(targetItem.content) ? targetItem.content[0]?.content : '');
displayTitle = pinContent ? `想法: ${pinContent.slice(0,10)}...` : '想法动态';
}
if (!displayTitle) displayTitle = '无标题';
log(`${idxStr} 正在抓取: ${displayTitle}...`);
// 2. 强制回源抓取
try { const fullData = await fetchDetail(targetItem.id, targetItem.type); processed = processItem(fullData || targetItem); }
catch(e) { processed = processItem(targetItem); }
// 3. 评论抓取
if (processed.comment_count > 0) { try { processed.comments = await fetchComments(processed.id, processed.type); } catch(e) { processed.comments = []; } } else { processed.comments = []; }
STATE.items.push(processed);
await sleep(CONFIG.requestDelay);
}
if (!data.paging || data.paging.is_end || !data.paging.next) { nextUrl = null; } else { nextUrl = data.paging.next; UI.progressBar.style.width = '50%'; }
} else { nextUrl = null; }
}
} catch (e) { log('错误: ' + e.message); console.error(e); }
STATE.isRunning = false; document.getElementById('zbc-start').disabled = false; document.getElementById('zbc-stop').disabled = true; UI.progressBar.style.width = '100%';
if (STATE.items.length > 0) { log(`抓取完成,共 ${STATE.items.length} 条`); toggleExportBtns(true); } else { log('未找到有效内容'); }
}
function fetchDetail(id, type) {
return new Promise((resolve) => {
let url = '';
if (type === 'answer') url = `https://www.zhihu.com/api/v4/answers/${id}?include=content,voteup_count,comment_count,created_time,author,updated_time,question.title`;
else if (type === 'article') url = `https://www.zhihu.com/api/v4/articles/${id}?include=content,voteup_count,comment_count,created_time,author`;
else if (type === 'pin' || type === 'moment') url = `https://www.zhihu.com/api/v4/pins/${id}?include=content,origin_pin,content_html,created_time,author`;
if(!url) return resolve(null);
GM_xmlhttpRequest({ method: "GET", url: url, onload: (res) => { if (res.status === 200) try { resolve(JSON.parse(res.responseText)); } catch (e) { resolve(null); } else resolve(null); }, onerror: () => resolve(null) });
});
}
function fetchComments(id, type) {
return new Promise((resolve) => {
let resourceType = 'answers'; if (type === 'article') resourceType = 'articles'; if (type === 'pin' || type === 'moment') resourceType = 'pins';
const url = `https://www.zhihu.com/api/v4/${resourceType}/${id}/root_comments?order=normal&limit=${CONFIG.commentLimit}&offset=0&status=open`;
GM_xmlhttpRequest({ method: "GET", url: url, onload: (res) => { if (res.status === 200) { try { const data = JSON.parse(res.responseText).data; resolve(data.map(c => ({ author: c.author.member.name, content: c.content, vote_count: c.vote_count, created_time: new Date(c.created_time * 1000).toLocaleString() }))); } catch(e) { resolve([]); } } else { resolve([]); } }, onerror: () => resolve([]) });
});
}
function fetchAPI(url) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: url, onload: (res) => { if (res.status === 200) try { resolve(JSON.parse(res.responseText)); } catch (e) { reject(e); } else reject(new Error(res.status)); }, onerror: (err) => reject(err) }); }); }
function processItem(item) {
let content = '', title = '', type = item.type;
// --- 想法的标题提取逻辑 (核心修改) ---
if (type === 'pin' || type === 'moment') {
// 1. 生成时间字符串 (增强容错)
let dateStr = '未知日期';
if (item.created_time) {
try { dateStr = new Date(item.created_time * 1000).toLocaleString(); } catch(e) {}
}
// 2. 尝试提取摘要作为标题
let summary = '';
// 尝试获取 excerpt_title
if (item.excerpt_title) summary = item.excerpt_title;
// 尝试从 content_html 中提取纯文本
else if (item.content_html) summary = item.content_html.replace(/<[^>]+>/g, "");
// 尝试从 content 数组中提取文本
else if (Array.isArray(item.content)) {
summary = item.content.filter(c => c.type === 'text').map(c => c.content).join('');
}
if (summary) {
// 如果有摘要,使用摘要前20个字 + ...
title = `想法: ${summary.slice(0, 20).replace(/[\r\n]/g, ' ')}...`;
} else {
// 如果实在没内容,使用日期兜底
title = `想法 ${dateStr}`;
}
// 3. 内容拼接
if (item.content_html) content = item.content_html;
else if (Array.isArray(item.content)) { item.content.forEach(c => { if (c.type === 'text') content += `${c.content}
`; if (c.type === 'image') content += `
`; }); }
if (item.origin_pin) {
let originContent = item.origin_pin.content_html || '';
if (!originContent && Array.isArray(item.origin_pin.content)) { item.origin_pin.content.forEach(c => { if (c.type === 'text') originContent += `${c.content}
`; if (c.type === 'image') originContent += `
`; }); }
content += `转发自 ${item.origin_pin.author.name}:
${originContent}
`;
}
} else {
content = item.content || item.excerpt || '';
content = content.replace(/
]*data-actualsrc="([^"]+)"[^>]*>/g, '
').replace(/
]*data-original="([^"]+)"[^>]*>/g, '
');
title = item.title; if (!title && item.question) title = item.question.title;
if (!title) title = "无标题";
}
return {
id: item.id, type: type, title: title,
author: item.author ? item.author.name : '匿名',
content: content,
voteup_count: item.voteup_count || item.likes_count || 0,
comment_count: item.comment_count || 0,
created_time: new Date((item.created_time||Date.now()/1000) * 1000).toLocaleString(),
url: item.url ? item.url.replace("api.zhihu.com", "www.zhihu.com") : `https://www.zhihu.com/pin/${item.id}`,
comments: []
};
}
function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }
function sanitizeFileName(name) { return name.replace(/[\\/:*?"<>|]/g, "_").replace(/\s+/g, " ").trim(); }
function toggleExportBtns(enable) { document.getElementById('zbc-export-html').disabled = !enable; document.getElementById('zbc-export-json').disabled = !enable; }
function exportSingleHTML() {
log('生成HTML...');
const filename = generateFilename('html');
let tocHtml = STATE.items.map((item, index) => `${index + 1}. [${item.type}] ${item.title}`).join('');
let contentHtml = STATE.items.map((item, index) => {
let commentsHtml = '';
if (item.comments && item.comments.length > 0) {
const cList = item.comments.map(c => ``).join('');
commentsHtml = ``;
} else { commentsHtml = ``; }
return `${item.type}${item.author}${item.created_time}👍 ${item.voteup_count}
${item.content}
${commentsHtml}
`;
}).join('');
const finalHtml = `${filename}${filename}
目录 (共 ${STATE.items.length} 条)
${contentHtml}
`;
saveAs(new Blob([finalHtml], { type: "text/html;charset=utf-8" }), filename);
log(`HTML 已导出: ${filename}`);
}
function exportJSON() {
const filename = generateFilename('json');
saveAs(new Blob([JSON.stringify(STATE.items, null, 2)], { type: "application/json;charset=utf-8" }), filename);
log(`JSON 已导出: ${filename}`);
}
const observer = new MutationObserver(() => { setTimeout(injectCopyButtons, 500); });
observer.observe(document.body, { childList: true, subtree: true });
setInterval(injectCopyButtons, 2000);
injectCopyButtons();
initPanelUI();
})();