- // ==UserScript==
- // @name QQ群成员列表导出工具
- // @namespace http://tampermonkey.net/
- // @version 1.0.1
- // @description 将QQ群成员的列表存储为csv格式文件
- // @author 御琪幽然
- // @match https://qun.qq.com/*
- // @icon https://www.google.com/s2/favicons?sz=64&domain=qq.com
- // @grant none
- // @run-at document-end
- // ==/UserScript==
-
- /**
- * 参考文章:https://www.lanol.cn/post/253.html
- * 使用教程视频:https://www.bilibili.com/video/BV1QK4y1C7ZU
- * 发布于:https://gf.qytechs.cn/zh-CN
- * 开源于:https://github.com
- * 用于:https://qun.qq.com/#/member-manage/base-manage
- *
- */
-
- (function () {
- // 0.如果当前的网页链接不为targetURL,则不执行脚本
-
- // let targetURL = "https://qun.qq.com/qun-manage/#/member-manage/base-manage";
- // 在不知道什么时候换了新的网址
- let targetURL = "https://qun.qq.com/#/member-manage/base-manage";
- if (window.location.href != targetURL) {
- console.log("当前网页链接不为" + targetURL + ",脚本不执行");
- alert("当前网页链接不为" + targetURL + ",脚本不执行");
- return;
- }
- console.log("当前网页链接为" + targetURL + ",脚本开始执行");
-
- // 1.变量定义
- let isLogDebug = true;
-
- // 2.函数定义
- function consoleLog(message) {
- if (isLogDebug == false) {
- return;
- }
- console.log(message);
- }
-
- function getSkey() {
- let e = "skey";
- const t = document.cookie.match(new RegExp(`(^| )${e}=([^;]*)(;|$)`));
- // 如果t不为null,则返回t[2],否则返回空字符串
- return t ? decodeURIComponent(t[2]) : '';
- }
-
- /**
- * 生成发送请求需要的bkn参数
- */
- function generateBKN() {
- let e = getSkey(); // 类似于@xCmnZlnC6
- let t = 5381;
- for (let n = 0, r = e.length; n < r; ++n) {
- t += (t << 5) + e.charAt(n).charCodeAt(0);
- }
- return String(t & 2147483647)
- };
-
- /**
- * 将Date对象转换成yyyy-MM-dd HH-mm-ss格式的字符串
- * @param {Date} date
- * @returns
- */
- function convertDateToString(date) {
- return date.getFullYear()
- + "-" + (date.getMonth() + 1)
- + "-" + date.getDate() + " " + date.getHours()
- + "-" + date.getMinutes() + "-" + date.getSeconds();
- }
-
- /**
- * 获取群成员信息
- * @param {*} gc QQ群号
- * @param {*} st 开始的索引
- * @param {*} end 结束的索引(与开始的索引最大值不能相差40以上)
- * @param {*} sort 排序方式
- * @param {*} bkn bkn参数
- * @returns
- */
- function get_members(gc, st, end, sort, bkn) {
- let url = "https://qun.qq.com/cgi-bin/qun_mgr/search_group_members";
- let data = `gc=${gc}&st=${st}&end=${end}&sort=${sort}&bkn=${bkn}`;
-
- let result = fetch(url, {
- credentials: "include",
- headers: {
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/116.0",
- Accept: "application/json, text/javascript, */*; q=0.01",
- "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
- "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
- "X-Requested-With": "XMLHttpRequest",
- "Sec-Fetch-Dest": "empty",
- "Sec-Fetch-Mode": "cors",
- "Sec-Fetch-Site": "same-origin",
- },
- referrer: "https://qun.qq.com/member.html",
- body: data,
- method: "POST",
- mode: "cors",
- })
- .then((response) => response.json())
- .then((data) => {
- consoleLog("data内容为:")
- consoleLog(data)
- consoleLog("==========")
- if (isLogDebug) {
- // consoleLog(data);
- data.mems.forEach(function (item) {
- consoleLog(combineTextFromItem(item));
- });
- }
-
- return data;
- });
- consoleLog("result内容为:")
- consoleLog(result)
- consoleLog("==========")
- return result;
- }
-
- /**
- * 将item内的信息拼接成文本
- * @param {*} item
- * @returns
- */
- function combineTextFromItem(item) {
- // 0为群主,1为管理员,2为普通成员
- let role = item.role == 0 ? "群主" : item.role == 1 ? "管理员" : "普通成员";
- // 0为男,1为女,未知为-1
- let g = item.g == 0 ? "男" : item.g == 1 ? "女" : "未知";
- let jt = item.join_time == 0 ? "未知" : new Date(item.join_time * 1000).toLocaleString();
- let lst = item.last_speak_time == 0 ? "未曾发言" : new Date(item.last_speak_time * 1000).toLocaleString();
-
- // 将item.nick和item.card中的转义文本修改为正常文本
- item.nick = item.nick.replace(/&/g, "&");
- item.nick = item.nick.replace(/</g, "<");
- item.nick = item.nick.replace(/>/g, ">");
- item.nick = item.nick.replace(/"/g, "\"\"");
- item.nick = item.nick.replace(/'/g, "'");
- item.nick = item.nick.replace(/ /g, " ");
-
- item.card = item.card.replace(/&/g, "&");
- item.card = item.card.replace(/</g, "<");
- item.card = item.card.replace(/>/g, ">");
- item.card = item.card.replace(/"/g, "\"\"");
- item.card = item.card.replace(/'/g, "'");
- item.card = item.card.replace(/ /g, " ");
-
- //将每个变量用英文引号包裹起来,然后用逗号连接起来,最后加上换行符
- return `"${item.nick}","${item.card}","${role}","${item.uin}","${g}","${item.qage}","${jt}","${lst}"\n`;
- }
-
- function createButton() {
- // 点击按钮,调用get_members函数,把结果以csv格式保存到本地
- // 编码使用utf-8 with bom,以防止乱码
- // 创建type="button" class="t-button t-button--theme-primary t-button--variant-base"的button
- let button = document.createElement("button");
- button.type = "button";
- button.className = "t-button t-button--theme-primary t-button--variant-base";
- button.innerHTML = "将群成员列表存储为csv文件";
-
- button.onclick = async function () {
- // 此时禁用按钮并修改按钮文字
- button.innerHTML = "正在获取群成员列表中";
- button.disabled = true;
-
- let selectedGroup = document.getElementsByClassName("_selectQun_1mksq_1 t-select-option t-is-selected")[0];
- if (selectedGroup == null || selectedGroup == undefined) {
- alert("请先手动选择一个群后,再点击按钮!");
- button.innerHTML = "将群成员列表存储为csv文件";
- button.disabled = false;
- return;
- }
-
- // innerText是群名
- let groupName = selectedGroup.innerText
- // innerText是带括号的群号,需要去掉括号
- let gc = selectedGroup.children[0].children[0].children[1].children[0].innerText.match(/(\d+)/)[1];
-
- // 从class="t-pagination__total"的元素里获取群成员数量,它的格式为“共 xx 条”,需要提取数字
- let count = document.getElementsByClassName("t-pagination__total")[0].innerText.match(/(\d+)/)[1];
-
- let bkn = generateBKN();
-
- // csv的标题
- let csvTitle = [
- "QQ昵称",
- "群昵称",
- "群身份",
- "QQ号",
- "性别",
- "Q龄",
- "入群时间",
- "最后发言时间"
- ];
- let csvContent = ""
- //将csvTitle数组里的每个元素用制表符连接起来,然后加上换行符,存储到csvContent变量里
- csvContent += csvTitle.join(",") + "\n";
-
- /* 旧的方法
- let memberList = [];
- let memberCount = parseInt(count);
-
- for (let startIndex = 0; startIndex < memberCount; startIndex += 21) {
- let endIndex = Math.min(startIndex + 20, memberCount);
-
- let result = get_members(gc, startIndex, endIndex, 0, bkn);
- result.then((data) => {
- // 把mems数组里所有对象放到result数组里
- memberList = memberList.concat(data.mems);
- });
- saveButton.innerHTML = `正在获取群成员列表(${startIndex}/${memberCount}),请耐心等待`;
- await new Promise(resolve => setTimeout(resolve, 200));
- }
- */
-
- let memberList = [];
- let memberCount = parseInt(count);
-
- let promises = [];
- for (let startIndex = 0; startIndex < memberCount; startIndex += 21) {
- let endIndex = Math.min(startIndex + 20, memberCount);
- let result = get_members(gc, startIndex, endIndex, 0, bkn);
- promises.push(result);
- button.innerHTML = `正在获取群成员列表(${startIndex}/${memberCount}),请耐心等待`;
- await new Promise(resolve => setTimeout(resolve, 200));
- }
-
- Promise.all(promises).then((results) => {
- results.forEach((data) => {
- memberList = memberList.concat(data.mems);
- });
-
- //改了这里↓
- //遍历memberList数组,把每个对象的属性输出到csv变量里
- memberList.forEach((item) => {
- csvContent += combineTextFromItem(item);
- });
-
- //存储为csv文件
- let blob = new Blob(["\ufeff" + csvContent], { type: "text/csv;charset=utf-8" });
- let a = document.createElement("a");
- a.download = "群成员列表-" + groupName + "-" + convertDateToString(new Date()) + ".csv";
- a.href = URL.createObjectURL(blob);
- a.click();
-
- //将标题切换回去
- button.innerHTML = "将群成员列表存储为csv文件";
- button.disabled = false;
- //改了这里↑
-
- }).catch((error) => {
- console.error(error);
- });
- };
-
-
- // 创建class="t-button__text" style="z-index: 1;"的span
- let span = document.createElement("span");
- span.className = "t-button__text";
- span.style.zIndex = "1";
- // 将span添加到button里
- button.appendChild(span);
-
- return button;
- }
-
- /**
- * 创建容器元素
- */
- function createElement(headerPanel) {
- // 创建一个class="t-col t-col-12 t-col-offset-0 t-col-pull-0 t-col-push-0 t-col-order-0" style="padding-left: 4px; padding-right: 4px;"的div
- let div1 = document.createElement("div");
- div1.className = "t-col t-col-12 t-col-offset-0 t-col-pull-0 t-col-push-0 t-col-order-0";
- div1.style.paddingLeft = "4px";
- div1.style.paddingRight = "4px";
-
- // 创建一个class="t-form__item t-form-item__groupId" style="width: 100%; max-width: 580px;"的div
- let div2 = document.createElement("div");
- div2.className = "t-form__item t-form-item__groupId";
- div2.style.width = "100%";
- div2.style.maxWidth = "580px";
-
- // 创建一个class="t-form__label t-form__label--right" style="width: 100px;"的div
- let labelDiv = document.createElement("div");
- labelDiv.className = "t-form__label t-form__label--right";
- labelDiv.style.width = "100px";
- // 创建一个label并添加到labelDiv里
- let label = document.createElement("label");
- label.innerHTML = "脚本功能";
- labelDiv.appendChild(label);
-
- // 创建button
- let button = createButton();
-
- // 将labelDiv添加到div2里
- div2.appendChild(labelDiv);
- // 将button添加到div2里
- div2.appendChild(button);
- // 将div2添加到div1里
- div1.appendChild(div2);
- // 将div1添加到headerPanel的最前面
- headerPanel.insertBefore(div1, headerPanel.firstChild);
- }
-
- // 3.执行功能
- consoleLog("脚本开始执行");
-
- // 顶部面板元素
- let headerPanel;
-
- // 等到headerPanel的元素加载完毕后,再添加元素
- let checkheaderPanelExist = setInterval(function () {
- if (headerPanel != null && headerPanel != undefined) {
- createElement(headerPanel);
- clearInterval(checkheaderPanelExist);
- }
- else {
- headerPanel = document.querySelector("#app > div > div > div.t-layout._sideContainer_199np_6 > main > main > div > div > div._defaultLayout_1866x_1._powerDesignBlock_zj60n_1._powerDesignBlock_zj60n_1 > section > form > div")
- }
- }, 200);
- })();