获取知乎用户文章、回答等数据

获取知乎某用户下的文章数据

目前為 2021-09-23 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         获取知乎用户文章、回答等数据
// @namespace    http://tampermonkey.net/
// @version      1.4.0
// @description  获取知乎某用户下的文章数据
// @author       Jazzu Lu
//-----------------------------------------------------------
// @require      https://code.jquery.com/jquery-1.9.1.min.js
// @require      https://cdn.bootcdn.net/ajax/libs/xlsx/0.17.0/xlsx.full.min.js
//-----------------------------------------------------------
// @require
// @resource css
//-----------------------------------------------------------
// @include      *.zhihu.com/people/*
//-----------------------------------------------------------
// @run-at       document-idle
// @original-author Jazzu Lu
// @original-license GPL License
// @charset		 UTF-8
// ==/UserScript==

/**
 * 有关导出 xslx 详见  https://github.com/sheetjs/sheetjs
 * **/

/** 工具函数 **/
Date.prototype.format = function (fmt="YYYY-mm-dd HH:MM") {
  let date = this;
  let ret;
  const opt = {
    "Y+": date.getFullYear().toString(),        // 年
    "m+": (date.getMonth() + 1).toString(),     // 月
    "d+": date.getDate().toString(),            // 日
    "H+": date.getHours().toString(),           // 时
    "M+": date.getMinutes().toString(),         // 分
    "S+": date.getSeconds().toString()          // 秒
    // 有其他格式化字符需求可以继续添加,必须转化成字符串
  };
  for (let k in opt) {
    ret = new RegExp("(" + k + ")").exec(fmt);
    if (ret) {
      fmt = fmt.replace(ret[1], (ret[1].length == 1) ? (opt[k]) : (opt[k].padStart(ret[1].length, "0")))
    }
  }
  return fmt;
}
window.$ = $;
window.sleep = (time)=>new Promise(resolve=>{ setTimeout(()=>{ resolve(); },time) })
function toCSV(header,jsonData,fileName){
  //列标题,逗号隔开,每一个逗号就是隔开一个单元格, 类似 `姓名,电话,邮箱`
  let str = header+='\n';
  //增加\t为了不让表格显示科学计数法或者其他格式
  for(let i = 0 ; i < jsonData.length ; i++ ){
    for(let item in jsonData[i]){
      str+=`${jsonData[i][item]},`;
    }
    str+='\n';
  }
  //encodeURIComponent解决中文乱码
  let uri = 'data:text/csv;charset=utf-8,\ufeff' + encodeURIComponent(str);
  //通过创建a标签实现
  let link = document.createElement("a");
  link.href = uri;
  //对下载的文件命名
  link.download = `${fileName}.csv`;
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
}
function toXSL(JSONData, FileName, worksheet) {
  worksheet = worksheet || FileName;
  let th = `<thead>${JSONData.headers.map(h=>`<th>${h.text}</th>`).join('')}</thead>`;
  let tbody = JSONData.data.map(d=> `<tr>${JSONData.headers.map(h=>`<td>${h.type=='link' ? `<a href="${d[h.value]}">${d[h.value]}</a>` : d[h.value]}</td>`).join('')}</tr>`).join('');
  tbody = `<tbody>${tbody}</tbody>`
  let excel = `<table>${th}${tbody}</table>`;
  let excelFile = `<html xmlns:o='urn:schemas-microsoft-com:office:office' xmlns:x='urn:schemas-microsoft-com:office:excel' xmlns='http://www.w3.org/TR/REC-html40'><meta http-equiv="content-type" content="application/vnd.ms-excel; charset=UTF-8"><meta http-equiv="content-type" content="application/vnd.ms-excel; charset=UTF-8"><head><!--[if gte mso 9]><xml><x:ExcelWorkbook><x:ExcelWorksheets><x:ExcelWorksheet><x:Name>${worksheet}</x:Name><x:WorksheetOptions><x:DisplayGridlines/></x:WorksheetOptions></x:ExcelWorksheet></x:ExcelWorksheets></x:ExcelWorkbook></xml><![endif]--></head><body>${excel}</body></html>`;
  let uri = 'data:application/vnd.ms-excel;charset=utf-8,' + encodeURIComponent(excelFile);
  let link = document.createElement("a");
  link.href = uri;
  link.style = "visibility:hidden";
  link.download = FileName + ".xls";
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
}
/** 显示右上方的提示信息, 类型有 normal success warning error **/
async function showToast({type = "normal", title = "", content = "", timing = 8*1000}){
  if(!$('.jl_toast_list').length){ $("body").append("<div class='jl_toast_list'></div>") }
  let toastIdx = `jl-${new Date().getTime()}`;
  let curToast = `<div class="jl_toast ${type} ${toastIdx}"><div class="jl_title">${title}</div><div class="jl_content">${content}</div></div>`;
  $('.jl_toast_list').append(curToast);
  $(`.${toastIdx}`).addClass('show');
  setTimeout(async ()=>{
    $(`.${toastIdx}`).removeClass("show");
    await window.sleep(800);
    $(`.${toastIdx}`).remove();
  },timing)
}
function jlLoading(show=true){
  let html = `<div class="jl_loading">
                <div class="wrapper"> 
                  <div class="circle"></div> <div class="circle"></div> <div class="circle"></div>
                  <div class="shadow"></div> <div class="shadow"></div> <div class="shadow"></div>
                </div>
                <style>
                 .jl_loading{width: 100vw;height: 100vh;position: fixed;top: 0;background: #070f1be3;z-index:10000;display: flex;align-items: center;justify-items: center;}
                 .jl_loading .wrapper{ width:200px; height:60px; position: absolute; left:50%; top:50%; transform: translate(-50%, -50%); }
                 .jl_loading .circle{ width:20px; height:20px; position: absolute; border-radius: 50%; background-color: #fff; left:15%; transform-origin: 50%; animation: jl_circle .5s alternate infinite ease; }
                 .jl_loading .circle:nth-child(2){ left:45%; animation-delay: .2s; }
                 .jl_loading .circle:nth-child(3){ left:auto; right:15%; animation-delay: .3s; }
                 @keyframes jl_circle{
                  0% { top:60px; height:5px; border-radius: 50px 50px 25px 25px; transform: scaleX(1.7); }
                  40%{ height:20px; border-radius: 50%; transform: scaleX(1); }
                  100%{ top:0%; }
                 }
                .jl_loading .shadow{ width:20px; height:4px; border-radius: 50%; background-color: rgba(0,0,0,.5); position: absolute; top:62px; transform-origin: 50%; z-index: -1; left:15%; filter: blur(1px); animation: jl_shadow .5s alternate infinite ease; }
                .jl_loading .shadow:nth-child(4){ left: 45%;animation-delay: .2s }
                .jl_loading .shadow:nth-child(5){ left:auto; right:15%; animation-delay: .3s; }
                @keyframes jl_shadow{
                  0%{ transform: scaleX(1.5); }
                  40%{ transform: scaleX(1); opacity: .7; }
                  100%{ transform: scaleX(.2); opacity: .4; }
                }
               </style>
              </div>`;
  if(show){
    !$('.jl_loading').length ? $('body').append(html) : '';
  }else {
    $('.jl_loading').remove();
  }
}

/** ----------------- 入口 ------------------- **/
(function() {
  createCss();    /** 创建 css **/
  let floatWidget = (`<div id='jl_float_container'><div class="jl_export_single">导出本页</div><div class="jl_export_all">导出全部</div></div>`);   /** 左侧小组件 **/
  $("body").append(floatWidget);
  renderBtn();
})();
/** 全局变量 **/
const HEADER_STRING = ["title","link","des","voteUp","commentCount","dateModified","dateCreate"]
const HEADER_OBJ = {title:'标题',link:'链接',des:'描述',voteUp:'赞同',commentCount:'评论',dateCreate:'创建时间',dateModified:'更新时间'}

/** ----------------- 事件 ------------------- **/
async function fetchSingle({scrollTiming = 2000, awaitTiming = 2000}){
  let rows = [];
  $('html, body').animate({ scrollTop: $(document).height() }, scrollTiming);
  await window.sleep(awaitTiming);
  $('.ListShortcut .List-item').each((idx,item)=>{
    /** 处理时间 **/
    let dateCreate = $(item).find('meta[itemprop=dateCreated]').attr('content') || $(item).find('meta[itemprop=datePublished]').attr('content');
    dateCreate = dateCreate ? new Date(dateCreate).format() : '';
    let dateModified = $(item).find('meta[itemprop=dateModified]').attr('content');
    dateModified = dateModified ? new Date(dateModified).format() : '';
    rows.push({
      title: $(item).find('.ContentItem-title a').text(),
      link: 'https:' + $(item).find('.ContentItem-title a').attr('href'),
      des: $(item).find('.RichContent-inner .RichText').text(),
      voteUp: $(item).find('.ContentItem-actions .VoteButton--up').text()?.split(' ')[1] || 0,
      commentCount: $(item).find('meta[itemprop=commentCount]').attr('content'),
      dateCreate, dateModified,
    })
  })
  return rows;
}
async function writeData(timing){
  let JL_DATA = await fetchSingle(timing);
  let oldData = localStorage.getItem('JL_DATA') || '[]';
  oldData = JSON.parse(oldData);
  oldData.push(...JL_DATA);
  localStorage.setItem('JL_DATA',JSON.stringify(oldData));
}
function exportFile(data){
  let sheetName = $('.ProfileMain-header .Tabs-link.is-active').eq(0).text();
  let fileName = $('.ProfileHeader-name').text() + sheetName;
  data.unshift(HEADER_OBJ)
  let ws = XLSX.utils.json_to_sheet(data, {header:HEADER_STRING,skipHeader:true});
  for (const wsKey in ws) {
    if(ws[wsKey]?.v && ws[wsKey]?.v?.indexOf('http')!=-1){
      ws[wsKey].l = {Target:ws[wsKey].v,Tooltip:ws[wsKey].v}
    }
  }
  ws['!cols'] = [ {wpx: 200}, {wpx: 280}, {wpx: 200}, {wpx: 60}, {wpx: 60}, {wpx: 120}, {wpx: 120}, ];
  let wb = { Sheets: {[sheetName]:ws}, SheetNames:[sheetName] }
  console.log('wb=========',data)
  XLSX.writeFile(wb, `${fileName}.xlsx`);
}
async function exportSingle(){
  jlLoading(true);
  let PL_ROWS = await fetchSingle({});
  exportFile(PL_ROWS);

  jlLoading(false);
  showToast({type:"success",content:'导出成功 0.0'})
}
async function exportAll(){
  localStorage.setItem('JL_DATA',[]);   /** 清空数据 **/
  jlLoading(true);
  let allPage = $('.ListShortcut .Pagination .PaginationButton-next').prev().text() || 1;
  allPage = Number(allPage);
  for (let i = 0; i < allPage; i++) {
    await window.sleep(1000);
    $('html, body').animate({ scrollTop: 0 }, 1000);    /** 滑动到上面 **/
    await writeData({awaitTiming:3500});                                        /** 滑动到底部记录数据 **/
    let $nextPage = $('.ListShortcut .Pagination .PaginationButton-next');
    if($nextPage.length){
      $nextPage.trigger('click')
    }else break;
  }
  jlLoading(false);

  /** 导出数据 **/
  let JL_DATA = localStorage.getItem('JL_DATA') || '[]';
  JL_DATA = JSON.parse(JL_DATA);
  exportFile(JL_DATA);
  showToast({type:"success",content:'导出成功 0.0'})
}

/** ----------------- 渲染 DOM ------------------- **/
/** 渲染 Button **/
function renderBtn(){
  $("#jl_float_container .jl_export_single").on("click",exportSingle);
  $("#jl_float_container .jl_export_all").on("click",exportAll);
}

/** ----------------- 渲染 Style ------------------- **/
function createCss(){
  let css = (`
    <style>
      /* toast 样式 */
      .jl_toast{position: fixed;right: -245px;top: 2vh;border-radius:5px;box-shadow: 0 1px 3px rgba(18,18,18,.1);padding: 15px 20px;width: 200px;z-index: 10000;transition: .8s ease all;color:#fff;}
      .jl_toast.show{right: 5px;}
      .jl_toast.normal{background: #009fdc;}  .jl_toast.success{background: #25d028;}
      .jl_toast.warning{background: #fd5b1f;} .jl_toast.error{background: #fe3737;}
      .jl_toast .jl_title{font-size: 16px;} .jl_toast .jl_content{font-size: 14px;}
      /* toast 样式 */
      #jl_snapshot{position: fixed;right: 0;top: 5vh;border: 2px #333 solid;padding: 0;display: none;}
      #jl_float_container{position: fixed;left: -0px;top: 20vh;z-index: 10000;padding: 10px;border: 1px #ddd solid;border-radius:5px;transition: .8s all ease;background: #fff;}
      #jl_float_container:hover{left: 0;}
      #jl_float_container .ma{margin: 10px;}
      #jl_float_container .ml{margin-left: 10px;} #jl_float_container .mr{margin-right: 10px;}
      #jl_float_container .mt{margin-top: 10px;}  #jl_float_container .mb{margin-bottom: 10px;}
      /* 按钮样式 */
      #jl_float_container div{ margin: 5px;border: 1px solid #8b8b8d;padding: 5px 10px;color:#0f1d34;border-radius: 4px;cursor: pointer; }
    </style>
  `)
  $("body").append(css);
}