| 复制代码// ==UserScript==
// @name         mcbbs:将参与投票者的信息导出到csv
// @namespace    http://tampermonkey.net/
// @version      0.4
// @description  https://www.mcbbs.net/thread-1464590-1-1.html
// @author       破损的鞘翅
// [url=home.php?mod=space&uid=1330979]@connect[/url]     mcbbs.wiki
// @match        https://www.mcbbs.net/thread-*
// @match        https://www.mcbbs.net/forum.php?mod=viewthread*
// @icon         https://www.mcbbs.net/favicon.ico
// @grant       GM_xmlhttpRequest
// ==/UserScript==
(function () {
  "use strict";
  // Your code here...
  class CSV {
    headers = [];
    datas = [];
    width = 0;
    constructor(...headers) {
      this.headers = headers.join(",");
      this.width = headers.length;
    }
    add(...data) {
      while (data.length < this.width) {
        data.push("");
      }
      this.datas.push(data.join(","));
    }
    saveAs(filename) {
      const a = document.createElement("a");
      a.href = URL.createObjectURL(new Blob([this.export], { type: "text/plain" }));
      a.download = filename + ".csv";
      document.body.append(a);
      a.click();
      a.remove();
    }
    get export() {
      const exports = [this.headers, ...this.datas];
      return exports.join("\r\n");
    }
  }
  async function fetchXml(url, init = {}) {
    const domParser = new DOMParser();
    const response = await fetch(url, init);
    const resText = (await response.text()) || "";
    const resXml = domParser.parseFromString(resText, "text/xml");
    return {
      html: domParser.parseFromString(resXml.documentElement.childNodes[0].data, "text/html"),
      raw: resText,
    };
  }
  async function getUserInfoByApi(uid) {
    const url = `https://mcbbs.wiki/rest.php/mbwutils/v0/credit/${uid}`;
    const {
      notfound,
      nickname = "",
      credits = {},
      activities: { currentGroupText },
      err = null,
    } = await new Promise((resolve) => {
      GM_xmlhttpRequest({
        method: "GET",
        url: url,
        fetch: true,
        nocache: true,
        timeout: 7_000,
        onload: (r) => {
          try {
            resolve(JSON.parse(r.responseText));
          } catch (e) {
            resolve({ err: "解析json失败" });
          }
        },
        onabort: () => resolve({ err: "请求被阻止" }),
        onerror: (e) => resolve({ err: e }),
        ontimeout: () => resolve({ err: "请求超时" }),
      });
    });
    if (err) {
      console.warn(`获取数据失败。url: ${url} 原因:`, err);
      throw false;
    }
    if (notfound) {
      throw false;
    }
    return {
      uid,
      exp: credits.credit,
      nuggets: credits.nugget,
      emeralds: credits.gem,
      nickname,
      group: currentGroupText,
    };
  }
  async function getDingTie(uid) {
    const result = await fetch(`https://auto.xmdhs.com/getforuid?uid=${uid}`).then((res) => res.json());
    return result.data?.length || 0;
  }
  async function getVoteResult(tid, page = 1, optionId = 0) {
    const result = []; //单个选项的投票结果
    const classifiedResult = {}; //投票结果的集合
    const url = new URL(`https://www.mcbbs.net/forum.php?mod=misc&action=viewvote&tid=${tid}&handlekey=viewvote&inajax=1`);
    url.searchParams.set("page", page);
    if (optionId > 0) {
      //optionId 大于0,为url添加参数,获取对应选项的投票结果。不加该参数则是获取第一个选项的结果。
      url.searchParams.set("polloptionid", optionId);
    }
    let { html, raw } = await fetchXml(url.href);
    while (html.querySelector("ul") == null) {
      console.warn(`获取 ${url.href} 的数据失败:`, { raw });
      await new Promise((resolve) => setTimeout(resolve, 1000));
      ({ html, raw } = await fetchXml(url.href));
    }
    for (const li of html.querySelector("ul").children) {
      //将结果放入数组
      const uid = new URL(li.querySelector("a").href).searchParams.get("uid");
      result.push(uid);
    }
    if (html.querySelector(".nxt")) {
      //含有该元素,说明还有下一页,获取下一页的结果,放入数组
      const nextRes = await getVoteResult(tid, page + 1, optionId);
      result.push(...nextRes);
    }
    if (optionId > 0 || page > 1) {
      //optionId 大于 0 且 page 大于 1,说明该调用只获取单个选项的投票结果,返回之前获取的结果
      return result;
    } else {
      //反之,说明该调用是获取全部的投票结果。除了已经获取了的默认的结果,再获取其他选项的结果
      const options = html.querySelector("select").children;
      for (const option of options) {
        if (option.selected) {
          classifiedResult[option.innerText] = result;
          continue;
        }
        classifiedResult[option.innerText] = await getVoteResult(tid, 1, option.value);
      }
      return classifiedResult;
    }
  }
  function getButton() {
    const btn = document.createElement("a");
    btn.href = "javascript:;";
    btn.addEventListener("click", async () => {
      showPrompt(null, null, "<span>开始获取数据,在此期间请勿再次点击本按钮</span>", 5000);
      btn.style = "color:green";
      try {
        const csv = new CSV("Type", "TotalCredits", "Nuggets", "Emeralds", "DingTies", "Nickname", "UserGroupName", "UID");
        const result = await getVoteResult(unsafeWindow.tid);
        let taskQueueChunk = [];
        for (const option in result) {
          for (const uid of result[option]) {
            taskQueueChunk.push(
              (async () => {
                try {
                  const { exp, nuggets, emeralds, nickname, group } = await getUserInfoByApi(uid);
                  const dingTie = await getDingTie(uid);
                  csv.add(option, exp, nuggets, emeralds, dingTie, nickname, group, uid);
                } catch {
                  throw uid;
                }
              })()
            );
            if (taskQueueChunk.length >= 5) {
              //每5个promise一组
              await Promise.allSettled(taskQueueChunk);
              taskQueueChunk = [];
            }
          }
        }
        //等待没凑满5个的队列兑现
        await Promise.allSettled(taskQueueChunk);
        csv.saveAs(document.querySelector("#thread_subject").innerText);
        showPrompt(null, null, "<span>数据获取完毕,确认数据完整前请勿关闭网页,若数据有缺损,请打开控制台查看错误信息</span>", 5000);
      } catch (err) {
        showPrompt(null, null, "<span>数据获取出错,请打开控制台查看错误信息</span>", 5000);
        throw err;
      } finally {
        btn.style = "";
      }
    });
    btn.innerText = "将数据导出至CSV";
    return btn;
  }
  if (document.querySelector("#poll>.pinf") && document.querySelector("#poll>.pinf").innerText.search("查看投票参与人") > 1) {
    document.querySelector("#poll>.pinf").append(getButton());
  }
})();
 |