QQ群成员列表导出工具

将QQ群成员的列表存储为csv格式文件

  1. // ==UserScript==
  2. // @name QQ群成员列表导出工具
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0.1
  5. // @description 将QQ群成员的列表存储为csv格式文件
  6. // @author 御琪幽然
  7. // @match https://qun.qq.com/*
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=qq.com
  9. // @grant none
  10. // @run-at document-end
  11. // ==/UserScript==
  12.  
  13. /**
  14. * 参考文章:https://www.lanol.cn/post/253.html
  15. * 使用教程视频:https://www.bilibili.com/video/BV1QK4y1C7ZU
  16. * 发布于:https://gf.qytechs.cn/zh-CN
  17. * 开源于:https://github.com
  18. * 用于:https://qun.qq.com/#/member-manage/base-manage
  19. *
  20. */
  21.  
  22. (function () {
  23. // 0.如果当前的网页链接不为targetURL,则不执行脚本
  24.  
  25. // let targetURL = "https://qun.qq.com/qun-manage/#/member-manage/base-manage";
  26. // 在不知道什么时候换了新的网址
  27. let targetURL = "https://qun.qq.com/#/member-manage/base-manage";
  28. if (window.location.href != targetURL) {
  29. console.log("当前网页链接不为" + targetURL + ",脚本不执行");
  30. alert("当前网页链接不为" + targetURL + ",脚本不执行");
  31. return;
  32. }
  33. console.log("当前网页链接为" + targetURL + ",脚本开始执行");
  34.  
  35. // 1.变量定义
  36. let isLogDebug = true;
  37.  
  38. // 2.函数定义
  39. function consoleLog(message) {
  40. if (isLogDebug == false) {
  41. return;
  42. }
  43. console.log(message);
  44. }
  45.  
  46. function getSkey() {
  47. let e = "skey";
  48. const t = document.cookie.match(new RegExp(`(^| )${e}=([^;]*)(;|$)`));
  49. // 如果t不为null,则返回t[2],否则返回空字符串
  50. return t ? decodeURIComponent(t[2]) : '';
  51. }
  52.  
  53. /**
  54. * 生成发送请求需要的bkn参数
  55. */
  56. function generateBKN() {
  57. let e = getSkey(); // 类似于@xCmnZlnC6
  58. let t = 5381;
  59. for (let n = 0, r = e.length; n < r; ++n) {
  60. t += (t << 5) + e.charAt(n).charCodeAt(0);
  61. }
  62. return String(t & 2147483647)
  63. };
  64.  
  65. /**
  66. * 将Date对象转换成yyyy-MM-dd HH-mm-ss格式的字符串
  67. * @param {Date} date
  68. * @returns
  69. */
  70. function convertDateToString(date) {
  71. return date.getFullYear()
  72. + "-" + (date.getMonth() + 1)
  73. + "-" + date.getDate() + " " + date.getHours()
  74. + "-" + date.getMinutes() + "-" + date.getSeconds();
  75. }
  76.  
  77. /**
  78. * 获取群成员信息
  79. * @param {*} gc QQ群号
  80. * @param {*} st 开始的索引
  81. * @param {*} end 结束的索引(与开始的索引最大值不能相差40以上)
  82. * @param {*} sort 排序方式
  83. * @param {*} bkn bkn参数
  84. * @returns
  85. */
  86. function get_members(gc, st, end, sort, bkn) {
  87. let url = "https://qun.qq.com/cgi-bin/qun_mgr/search_group_members";
  88. let data = `gc=${gc}&st=${st}&end=${end}&sort=${sort}&bkn=${bkn}`;
  89.  
  90. let result = fetch(url, {
  91. credentials: "include",
  92. headers: {
  93. "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/116.0",
  94. Accept: "application/json, text/javascript, */*; q=0.01",
  95. "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",
  96. "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
  97. "X-Requested-With": "XMLHttpRequest",
  98. "Sec-Fetch-Dest": "empty",
  99. "Sec-Fetch-Mode": "cors",
  100. "Sec-Fetch-Site": "same-origin",
  101. },
  102. referrer: "https://qun.qq.com/member.html",
  103. body: data,
  104. method: "POST",
  105. mode: "cors",
  106. })
  107. .then((response) => response.json())
  108. .then((data) => {
  109. consoleLog("data内容为:")
  110. consoleLog(data)
  111. consoleLog("==========")
  112. if (isLogDebug) {
  113. // consoleLog(data);
  114. data.mems.forEach(function (item) {
  115. consoleLog(combineTextFromItem(item));
  116. });
  117. }
  118.  
  119. return data;
  120. });
  121. consoleLog("result内容为:")
  122. consoleLog(result)
  123. consoleLog("==========")
  124. return result;
  125. }
  126.  
  127. /**
  128. * 将item内的信息拼接成文本
  129. * @param {*} item
  130. * @returns
  131. */
  132. function combineTextFromItem(item) {
  133. // 0为群主,1为管理员,2为普通成员
  134. let role = item.role == 0 ? "群主" : item.role == 1 ? "管理员" : "普通成员";
  135. // 0为男,1为女,未知为-1
  136. let g = item.g == 0 ? "男" : item.g == 1 ? "女" : "未知";
  137. let jt = item.join_time == 0 ? "未知" : new Date(item.join_time * 1000).toLocaleString();
  138. let lst = item.last_speak_time == 0 ? "未曾发言" : new Date(item.last_speak_time * 1000).toLocaleString();
  139.  
  140. // 将item.nick和item.card中的转义文本修改为正常文本
  141. item.nick = item.nick.replace(/&amp;/g, "&");
  142. item.nick = item.nick.replace(/&lt;/g, "<");
  143. item.nick = item.nick.replace(/&gt;/g, ">");
  144. item.nick = item.nick.replace(/&quot;/g, "\"\"");
  145. item.nick = item.nick.replace(/&#39;/g, "'");
  146. item.nick = item.nick.replace(/&nbsp;/g, " ");
  147.  
  148. item.card = item.card.replace(/&amp;/g, "&");
  149. item.card = item.card.replace(/&lt;/g, "<");
  150. item.card = item.card.replace(/&gt;/g, ">");
  151. item.card = item.card.replace(/&quot;/g, "\"\"");
  152. item.card = item.card.replace(/&#39;/g, "'");
  153. item.card = item.card.replace(/&nbsp;/g, " ");
  154.  
  155. //将每个变量用英文引号包裹起来,然后用逗号连接起来,最后加上换行符
  156. return `"${item.nick}","${item.card}","${role}","${item.uin}","${g}","${item.qage}","${jt}","${lst}"\n`;
  157. }
  158.  
  159. function createButton() {
  160. // 点击按钮,调用get_members函数,把结果以csv格式保存到本地
  161. // 编码使用utf-8 with bom,以防止乱码
  162. // 创建type="button" class="t-button t-button--theme-primary t-button--variant-base"的button
  163. let button = document.createElement("button");
  164. button.type = "button";
  165. button.className = "t-button t-button--theme-primary t-button--variant-base";
  166. button.innerHTML = "将群成员列表存储为csv文件";
  167.  
  168. button.onclick = async function () {
  169. // 此时禁用按钮并修改按钮文字
  170. button.innerHTML = "正在获取群成员列表中";
  171. button.disabled = true;
  172.  
  173. let selectedGroup = document.getElementsByClassName("_selectQun_1mksq_1 t-select-option t-is-selected")[0];
  174. if (selectedGroup == null || selectedGroup == undefined) {
  175. alert("请先手动选择一个群后,再点击按钮!");
  176. button.innerHTML = "将群成员列表存储为csv文件";
  177. button.disabled = false;
  178. return;
  179. }
  180.  
  181. // innerText是群名
  182. let groupName = selectedGroup.innerText
  183. // innerText是带括号的群号,需要去掉括号
  184. let gc = selectedGroup.children[0].children[0].children[1].children[0].innerText.match(/(\d+)/)[1];
  185.  
  186. // 从class="t-pagination__total"的元素里获取群成员数量,它的格式为“共 xx 条”,需要提取数字
  187. let count = document.getElementsByClassName("t-pagination__total")[0].innerText.match(/(\d+)/)[1];
  188.  
  189. let bkn = generateBKN();
  190.  
  191. // csv的标题
  192. let csvTitle = [
  193. "QQ昵称",
  194. "群昵称",
  195. "群身份",
  196. "QQ号",
  197. "性别",
  198. "Q龄",
  199. "入群时间",
  200. "最后发言时间"
  201. ];
  202. let csvContent = ""
  203. //将csvTitle数组里的每个元素用制表符连接起来,然后加上换行符,存储到csvContent变量里
  204. csvContent += csvTitle.join(",") + "\n";
  205.  
  206. /* 旧的方法
  207. let memberList = [];
  208. let memberCount = parseInt(count);
  209.  
  210. for (let startIndex = 0; startIndex < memberCount; startIndex += 21) {
  211. let endIndex = Math.min(startIndex + 20, memberCount);
  212.  
  213. let result = get_members(gc, startIndex, endIndex, 0, bkn);
  214. result.then((data) => {
  215. // 把mems数组里所有对象放到result数组里
  216. memberList = memberList.concat(data.mems);
  217. });
  218. saveButton.innerHTML = `正在获取群成员列表(${startIndex}/${memberCount}),请耐心等待`;
  219. await new Promise(resolve => setTimeout(resolve, 200));
  220. }
  221. */
  222.  
  223. let memberList = [];
  224. let memberCount = parseInt(count);
  225.  
  226. let promises = [];
  227. for (let startIndex = 0; startIndex < memberCount; startIndex += 21) {
  228. let endIndex = Math.min(startIndex + 20, memberCount);
  229. let result = get_members(gc, startIndex, endIndex, 0, bkn);
  230. promises.push(result);
  231. button.innerHTML = `正在获取群成员列表(${startIndex}/${memberCount}),请耐心等待`;
  232. await new Promise(resolve => setTimeout(resolve, 200));
  233. }
  234.  
  235. Promise.all(promises).then((results) => {
  236. results.forEach((data) => {
  237. memberList = memberList.concat(data.mems);
  238. });
  239.  
  240. //改了这里↓
  241. //遍历memberList数组,把每个对象的属性输出到csv变量里
  242. memberList.forEach((item) => {
  243. csvContent += combineTextFromItem(item);
  244. });
  245.  
  246. //存储为csv文件
  247. let blob = new Blob(["\ufeff" + csvContent], { type: "text/csv;charset=utf-8" });
  248. let a = document.createElement("a");
  249. a.download = "群成员列表-" + groupName + "-" + convertDateToString(new Date()) + ".csv";
  250. a.href = URL.createObjectURL(blob);
  251. a.click();
  252.  
  253. //将标题切换回去
  254. button.innerHTML = "将群成员列表存储为csv文件";
  255. button.disabled = false;
  256. //改了这里↑
  257.  
  258. }).catch((error) => {
  259. console.error(error);
  260. });
  261. };
  262.  
  263.  
  264. // 创建class="t-button__text" style="z-index: 1;"的span
  265. let span = document.createElement("span");
  266. span.className = "t-button__text";
  267. span.style.zIndex = "1";
  268. // 将span添加到button里
  269. button.appendChild(span);
  270.  
  271. return button;
  272. }
  273.  
  274. /**
  275. * 创建容器元素
  276. */
  277. function createElement(headerPanel) {
  278. // 创建一个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
  279. let div1 = document.createElement("div");
  280. div1.className = "t-col t-col-12 t-col-offset-0 t-col-pull-0 t-col-push-0 t-col-order-0";
  281. div1.style.paddingLeft = "4px";
  282. div1.style.paddingRight = "4px";
  283.  
  284. // 创建一个class="t-form__item t-form-item__groupId" style="width: 100%; max-width: 580px;"的div
  285. let div2 = document.createElement("div");
  286. div2.className = "t-form__item t-form-item__groupId";
  287. div2.style.width = "100%";
  288. div2.style.maxWidth = "580px";
  289.  
  290. // 创建一个class="t-form__label t-form__label--right" style="width: 100px;"的div
  291. let labelDiv = document.createElement("div");
  292. labelDiv.className = "t-form__label t-form__label--right";
  293. labelDiv.style.width = "100px";
  294. // 创建一个label并添加到labelDiv里
  295. let label = document.createElement("label");
  296. label.innerHTML = "脚本功能";
  297. labelDiv.appendChild(label);
  298.  
  299. // 创建button
  300. let button = createButton();
  301.  
  302. // 将labelDiv添加到div2里
  303. div2.appendChild(labelDiv);
  304. // 将button添加到div2里
  305. div2.appendChild(button);
  306. // 将div2添加到div1里
  307. div1.appendChild(div2);
  308. // 将div1添加到headerPanel的最前面
  309. headerPanel.insertBefore(div1, headerPanel.firstChild);
  310. }
  311.  
  312. // 3.执行功能
  313. consoleLog("脚本开始执行");
  314.  
  315. // 顶部面板元素
  316. let headerPanel;
  317.  
  318. // 等到headerPanel的元素加载完毕后,再添加元素
  319. let checkheaderPanelExist = setInterval(function () {
  320. if (headerPanel != null && headerPanel != undefined) {
  321. createElement(headerPanel);
  322. clearInterval(checkheaderPanelExist);
  323. }
  324. else {
  325. 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")
  326. }
  327. }, 200);
  328. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址