Zhihu Answer Downloader

在用户资料页添加“下载10条回答”,自己主页/他人主页都适用。

// ==UserScript==
// @name         Zhihu Answer Downloader
// @namespace    https://tampermonkey.net/
// @version      0.2
// @description  在用户资料页添加“下载10条回答”,自己主页/他人主页都适用。
// @match        https://www.zhihu.com/people/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  /* ========== 通用工具 ========== */
  const sleep = ms => new Promise(r => setTimeout(r, ms));
  const downloadTxt = (name, content) => {
    const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = name;
    document.body.appendChild(a);
    a.click();
    a.remove();
    URL.revokeObjectURL(url);
  };

  /* ========== 抓取回答并下载 ========== */
  async function grabFirst10Answers() {
    const btn = document.getElementById('zhihu-dl-btn');
    btn.disabled = true;
    btn.innerText = '准备中…';

    /* 1. 向下滚动直到 ≥10 条回答或最多滚 20 次 */
    let tries = 0;
    while (document.querySelectorAll('div.List-item').length < 10 && tries < 20) {
      window.scrollBy(0, window.innerHeight);
      await sleep(800);
      tries++;
    }

    /* 2. 展开「阅读全文」 */
    const readMore = Array.from(document.querySelectorAll('button.Button--plain'))
      .filter(b => /阅读全文/.test(b.innerText))
      .slice(0, 10);

    for (const b of readMore) {
      b.click();
      await sleep(400);
    }

    /* 3. 组装 TXT */
    const items = Array.from(document.querySelectorAll('div.List-item')).slice(0, 10);
    const txt = items.map((it, i) => {
      const q = it.querySelector('a.QuestionItem-title, a[data-za-detail-view-element_name="Title"]')?.innerText.trim()
             || `Question ${i + 1}`;
      const a = it.querySelector('span.RichText')?.innerText.trim().replace(/\s+\n/g, '\n') || '';
      return `Q${i + 1}: ${q}\nA${i + 1}: ${a}\n\n`;
    }).join('');

    /* 4. 下载 */
    downloadTxt('zhihu_answers.txt', txt);
    btn.innerText = '已下载';
  }

  /* ========== 插入按钮 ========== */
  function insertButton() {
    if (document.getElementById('zhihu-dl-btn')) return;          // 已插入

    // ① 先找「关注」按钮
    let anchor = Array.from(document.querySelectorAll('button'))
      .find(b => /关注(他|她)?$/.test(b.textContent.trim()));

    // ② 若无,再找「编辑个人资料」/「编辑资料」
    if (!anchor) {
      anchor = Array.from(document.querySelectorAll('button'))
        .find(b => /编辑.*资料/.test(b.textContent.trim()));
    }

    // ③ 再不行,就放到 .ProfileHeader-operation 容器里
    if (!anchor) {
      anchor = document.querySelector('.ProfileHeader-operation');
      if (!anchor) return;      // 还没渲染出来,先退出
    }

    // 构造下载按钮
    const dlBtn = document.createElement('button');
    dlBtn.id = 'zhihu-dl-btn';
    dlBtn.className = 'Button Button--primary Button--blue';  // 跟知乎原生按钮保持一致
    dlBtn.style.marginLeft = '8px';
    dlBtn.textContent = '下载10条回答';
    dlBtn.onclick = grabFirst10Answers;

    // 插入
    if (anchor.parentNode.classList.contains('ProfileHeader-operation')) {
      // anchor 是容器
      anchor.appendChild(dlBtn);
    } else {
      anchor.parentNode.insertBefore(dlBtn, anchor.nextSibling);
    }
  }

  /* ========== 监听页面变化,路由切换也能保持 ========== */
  const obs = new MutationObserver(insertButton);
  obs.observe(document.body, { childList: true, subtree: true });
  insertButton();    // 首次执行
})();