您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
[雪喵空友列] 一键导出下载 QQ 好友列表到 JSON、TSV、CSV Excel 进行管理,或作为 .url 链接放到桌面或使用 Everything、Listary 等以快速批量打开好友的聊天窗口。本项目仅为学习研究使用,请保管好自己的个人数据,注意隐私安全。使用方法:登录 https://user.qzone.qq.com/ ,在顶栏获取好友列表。
// ==UserScript== // @name [雪喵空友列] QQ 空间一键获取自己的好友列表 // @namespace https://userscript.snomiao.com/ // @version 1.2.0-(20200714) // @description [雪喵空友列] 一键导出下载 QQ 好友列表到 JSON、TSV、CSV Excel 进行管理,或作为 .url 链接放到桌面或使用 Everything、Listary 等以快速批量打开好友的聊天窗口。本项目仅为学习研究使用,请保管好自己的个人数据,注意隐私安全。使用方法:登录 https://user.qzone.qq.com/ ,在顶栏获取好友列表。 // @supportURL https://github.com/snomiao/SNOMIAO-QZFriends.user.js // @author [email protected] // @match *://user.qzone.qq.com/* // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.5.0/jszip.min.js // @grant none // @noframes // ==/UserScript== // v1.1 (20200714) 修复 uin 匹配问题 // v1.0 (20200713) 完成 .URL 下载功能 ; (() => { // 常规函数定义 const 下载URL到文件 = (url, filename = '') => { var a = document.createElement('a'); a.style.display = 'none'; a.href = url a.download = filename; document.body.appendChild(a); a.click(); a.remove() } // 进度显示 const 自动恢复标题函数 = (函数) => async (...参数) => { const 原标题 = document.title; const 返 = await 函数(...参数) document.title = 原标题; return 返; } const 进度显示 = (正在) => { var 串 = `[雪喵友列] ${正在}` document.title = 串 console.log(串) } // 计算 Token const getCookieByRegex = (regex) => (e => e && e[1] || "")(document.cookie.match(regex)) // 用户常量 const uin = getCookieByRegex(/\buin=o(.*?)(?=;|$)/) const g_tk = (() => { var p_skey = getCookieByRegex(/\bp_skey=(.*?)(?=;|$)/) var skey = getCookieByRegex(/\bskey=(.*?)(?=;|$)/) var rv2 = getCookieByRegex(/\brv2=(.*?)(?=;|$)/) var b = p_skey || skey || rv2 var a = 5381; for (var c = 0, d = b.length; c < d; ++c) a += (a << 5) + b.charAt(c).charCodeAt(); return a & 2147483647 })(); // 好友列表解析 const 好友列表解析 = (json) => { 进度显示("正在解析好友列表...") const 元 = json.data; const 分组名表 = Object.fromEntries(元.gpnames.map(({ gpid, gpname }) => [gpid, gpname])) const 好友列表 = 元.items.map( ({ name, remark, uin, groupid }) => { const [Q号, 分组, 昵称, 备注] = [uin, ...[分组名表[groupid], name, remark].map(unescape)]; return { Q号, 分组, 昵称, 备注 } }) return { 分组名表, 好友列表 } } const 好友列表向TSV转换 = (json) => { const { 好友列表 } = 好友列表解析(json) 进度显示("正在制作TSV表格...") const 输出TSV = ['Q号', '分组', '昵称', '备注'].join('\t') + '\n' + 好友列表.map(({ Q号, 分组, 昵称, 备注 }) => [Q号, 分组, 昵称, 备注] // TSV 没有转义的定义,不兼容的字符只能直接删掉 .map(e => e.toString().replace(/\t|\r|\n/g, '_')).join('\t') ).join('\n') return 输出TSV } const 好友列表向CSV转换 = (json) => { const { 好友列表 } = 好友列表解析(json) 进度显示("正在制作CSV表格...") const 输出CSV = ['Q号', '分组', '昵称', '备注'].join(',') + '\n' + 好友列表.map(({ Q号, 分组, 昵称, 备注 }) => [Q号, 分组, 昵称, 备注] .map(e => e.toString() // CSV转义见 [CSV格式特殊字符转义处理_icycode的专栏-CSDN博客_csv 转义]( https://blog.csdn.net/icycode/article/details/80043956 ) .replace(/(.*?)("|,|\r|\n)(.*)/, (s) => '"' + s.replace(/"/g, '""') + '"') ).join(',') ).join('\n') return 输出CSV } // URL 文件打包下载 const URL文件生成 = (url) => `[InternetShortcut]\nURL=${url}` const 好友列表向URL文件转换并作为ZIP打包并下载 = async (json) => { alert( `点击确定后,开始下载你的好友列表,一般来说在 Windows 系统的浏览器中下载的文件,解压后点击url文件出现会安全警告,请看解决方法:\n`+ `方法1:请在解压前,对压缩包点一下右键属性,在属性下方,把安全警告勾掉,点确定,再解压即可。\n` `方法2:对解压后的文件夹重新压缩再解压一遍。`) const { 好友列表 } = 好友列表解析(json) // 替换掉文件名里不允许出现的特殊字符 const 文件名安全化 = (s) => s.toString().replace(/[\<\>\:\?\$\%\^\&\*\\\/\'\"\;\|\~\t\r\n-]+/g, "-") const 打开QQ的URL获取 = Q号 => `tencent://message?uin=${Q号}` const 时间戳 = '(' + new Date().toISOString().slice(0, -4).replace(/[^\dT]/g, '').replace('T', '.') + ')' const 文件名 = 时间戳 + `-好友列表@${uin}.zip` const zip = new JSZip() let n = 0; await Promise.all(好友列表.map(async ({ Q号, 分组, 昵称, 备注 }) => { [Q号, 分组, 昵称, 备注] = [Q号, 分组, 昵称, 备注].map(文件名安全化) zip.file(`${分组}/[${备注 || ''}@${昵称}](${Q号}@qq.com).url`, URL文件生成(打开QQ的URL获取(Q号))) const 完成率 = n++ / 好友列表.length; await 进度显示(`正在打包 ${Math.ceil(完成率 * 100)}%`); })) 进度显示(`正在压缩...`) await zip.generateAsync({ type: "blob" }).then(function (content) { 进度显示(`压缩完成,准备下载...`) 进度显示(`正在下载...`) let url = window.URL.createObjectURL(content); 下载URL到文件(url, 文件名) window.URL.revokeObjectURL(url); }) return '' } const 复制文本 = (content) => { const input = document.createElement('textarea'); input.setAttribute('readonly', 'readonly'); input.setAttribute('value', content); input.innerHTML = (content); input.setAttribute('style', 'position: fixed; top:0; left:0;z-index: 9999'); document.body.appendChild(input); input.select(); input.setSelectionRange(0, input.value.length); if (document.execCommand('copy')) { document.execCommand('copy'); console.log(`长度为${content.length}的内容已复制`); } document.body.removeChild(input); }; const 下载并复制文本 = (文本, 格式后缀) => { const 数据URL = "data:text/plain;base64," + btoa(unescape(encodeURIComponent(文本))); const 时间戳 = '(' + new Date().toISOString().slice(0, -4).replace(/[^\dT]/g, '').replace('T', '.') + ')' const 文件名 = 时间戳 + `-好友列表@${uin}` + 格式后缀 复制文本(文本) if (confirm(文件名 + " 内容已复制,是否下载为文件?")) { 下载URL到文件(数据URL, 文件名) } } // 从 TX 服务器获取好友列表 const 好友列表JSON获取 = async (g_tk, uin) => { 进度显示("正在获取好友列表...") const API地址 = `https://user.qzone.qq.com/proxy/domain/r.qzone.qq.com/cgi-bin/tfriend/friend_show_qqfriends.cgi?follow_flag=1&groupface_flag=0&fupdate=1&g_tk=${g_tk}&uin=${uin}` return await fetch(API地址).then(async (res) => { const jsonp = await res.text() const _Callback = (json) => json const json = eval(jsonp) return json }) } // 下面这个函数启发自: [csv 文件打开乱码,有哪些方法可以解决? - 知乎]( https://www.zhihu.com/question/21869078/answer/350728339 ) const 加UTF8文件BOM头 = (str) => '\uFEFF' + str // 输出函数 const 友列取 = async () => await 好友列表JSON获取(g_tk, uin) const 好友列表JSON输出 = 自动恢复标题函数(async () => 下载并复制文本(JSON.stringify(await 友列取()), ".json")) const 好友列表TSV输出 = 自动恢复标题函数(async () => 下载并复制文本(好友列表向TSV转换(await 友列取()), ".tsv")) const 好友列表CSV输出 = 自动恢复标题函数(async () => 下载并复制文本(加UTF8文件BOM头(好友列表向CSV转换(await 友列取())), ".csv")) const 好友列表EXCELCSV输出 = 自动恢复标题函数(async () => 下载并复制文本(加UTF8文件BOM头(好友列表向CSV转换(await 友列取())), ".csv")) const 好友列表ZIP输出 = 自动恢复标题函数(async () => 好友列表向URL文件转换并作为ZIP打包并下载(await 友列取())) // UI 定义 const 新元素 = (innerHTML, attributes = {}) => { const e = document.createElement("div"); e.innerHTML = innerHTML; return Object.assign(e.children[0], attributes) } const 按钮向页面插入 = () => { // 在“个人中心”按钮前插入一个按钮 const 获取好友列表控件 = 新元素(` <li class="nav-list" id="tb_friendlist_li"> <div class="nav-list-inner"> ☑👫📜好友列表 :<a onclick="好友列表JSON输出()" href="javascript:" style="padding: 0rem" title="复制并下载JSON格式">.JSON</a> /<a onclick="好友列表TSV输出()" href="javascript:" style="padding: 0rem" title="复制并下载TSV格式">.TSV</a> /<a onclick="好友列表CSV输出()" href="javascript:" style="padding: 0rem" title="复制并下载CSV格式(utf-8)">.CSV</a> /<a onclick="好友列表EXCELCSV输出()" href="javascript:" style="padding: 0rem" title="复制并下载 Excel 可直接打开的CSV格式" >.CSV(Excel)</a> /<a onclick="好友列表ZIP输出()" href="javascript:" style="padding: 0rem" title="下载按目录划分的 .url 文件为 .zip包" >.URL.ZIP</a> </div> </li>`) const 好友菜单按钮 = document.querySelector('#tb_friend_li') 好友菜单按钮 && 好友菜单按钮.parentNode.insertBefore(获取好友列表控件, 好友菜单按钮) window.好友列表JSON输出 = () => 好友列表JSON输出() window.好友列表TSV输出 = () => 好友列表TSV输出() window.好友列表CSV输出 = () => 好友列表CSV输出() window.好友列表EXCELCSV输出 = () => 好友列表EXCELCSV输出() window.好友列表ZIP输出 = () => 好友列表ZIP输出() } // 生成界面 if (location.hostname == 'user.qzone.qq.com') { 按钮向页面插入() } })()