QQ空间说说批量删除:从DOM模拟点击到接口直删实战 简介面向需要批量清理QQ空间说说的用户压缩包内提供一套2023年新版自动删除方案可解决逐条手动删除效率低下、流程冗长等痛点适用于日常空间整理、隐私内容快速清除等场景。资源共3个文件核心为一个现成的JavaScript脚本结合Tampermonkey扩展即可注入自动删除能力另有两个RAR压缩工具分别承担浏览器端辅助与图形化批量清理角色整体包体仅2.74MB部署成本较低。目前已有1128人学习下载脚本执行期间全自动完成删除操作并支持按下ESC键随时中止方便用户中途检查或临时暂停实用性较强。通过脚本与工具的组合即使不具备编程基础也能依照界面提示完成批量选择与清除实现空间说说的集中维护大幅节省手动操作时间。1. 批量删除QQ空间说说老脚本失效的背后是页面渲染机制的升级某个晚上我在群里看到有人问“谁能帮我删掉几百条说说”底下还有人报价。手动一条条删到凌晨两点这个场景太真实了。QQ空间一直没做全选删除的入口早期有人写浏览器脚本模拟点击后来页面改版老脚本大批量失效于是“批量删除QQ空间说说”这个需求又回到了起点。本文不打算介绍什么神秘的第三方软件而是直接讲清楚两件事2023年之后说说页面到底变成了什么样以及怎么用浏览器控制台和脚本插件把它删干净。适合的人群很明确自己有几个号要清理或者帮家里人删掉早年“黑历史”说说的普通用户不需要会写后端但至少能打开浏览器开发者工具。整个方案跑下来只用浏览器不扫码、不装额外软件数据全程在自己手里。接下来从页面结构开始一步步落到可复现的代码。2. 先搞清说说列表怎么加载和删除两条技术路线选一条2.1 说说列表的加载方式翻页变成了滚动加载意味着什么2023年前后PC端QQ空间说说页从传统的翻页改成了滚动加载。以前点“下一页”会整个刷新页面脚本的容错空间很大因为每次页面刷新后DOM结构是全新的写死的选择器在哪个页面都能用。滚动加载则完全不同页面底部出现“加载更多”的触发点滚动到那里才会异步请求下一批数据再把新的说说节点插入到列表尾部。这个改动对批量删除脚本的第一个影响是你不能在页面刚打开时一次性把“所有删除按钮”都抓出来。列表是分批出现的DOM节点是动态插入的。第二个影响更隐蔽浏览器为了控制内存滚动过远的旧的说说节点会在DOM里被回收掉。如果你在脚本里缓存了一份按钮列表滚动之后再拿这份列表去点击会出现“节点不存在”的报错或者点击毫无反应。这两个现象后面在避坑章节会详细对应这里先建立认知面对滚动加载脚本必须是“滚动一下、抓一次、删一批”的循环结构而不是“抓全部、删全部”的静态结构。从数据层面看滚动加载其实是一个一个JSON请求。页面本身不维护“所有说说”的数据它只维护当前渲染出来的窗口。这意味着脚本想批量操作只能操控当前可见的部分。想一次删一千条就必须让页面把一千条都滚动加载出来或者绕开页面、直接去调数据接口。这两条路正好对应接下来要讲的两种方案。2.2 删除一条说说的完整链路点击按钮只是序曲在页面上手动删除一条说说看起来是“点一下删掉了”实际后台经历了三个环节点删除按钮、处理确认弹窗、发送删除请求。老脚本失效往往不是删除请求的问题而是前两步在新页面里变了。先说删除按钮。旧版说说的每条信息下方是几个文字链接“删除”就是其中一个。新版页面把它换成了一个带图标的按钮或者一个悬浮菜单里的选项类名带了随机hash后缀比如classdel_82k3d。写死document.querySelector(.del)的老脚本自然找不到东西。再说确认弹窗。老版浏览器在点击删除后弹的是原生confirm()弹窗脚本可以一行window.confirm () true直接接管。新版改成了页面内渲染的div弹窗弹窗里的“确定”按钮是DOM元素不是浏览器原生控件。脚本如果只接管了window.confirm弹窗照样弹出来但没人去点“确定”删除请求根本没发出去。最后一步是删除请求本身。它会携带说说对应的tid、以及当前登录用户的校验凭证。这一步通常不会变但风控逻辑会变。连续删除几十条后接口大概率会返回需要验证码或者直接拒绝具体表现后面细说。这里先记住能走通“点击→确认→请求”这条链路批量删除就成功了一半。2.3 两条技术路线模拟点击还是直接调接口既然知道了完整链路批量删除就有两条路可以选。第一条是DOM模拟点击。脚本找到当前页面所有删除按钮依次触发点击识别弹窗点击确定等页面把该条说说移除后再处理下一条。这条路的优点是实现简单、逻辑直观不需要理解复杂的接口参数缺点是慢而且受页面结构影响大。每删一条平均要1到2秒删500条大约十分钟起步还要陪跑全程。第二条是构造删除请求直接调接口。手动删一条的同时在开发者工具的Network面板里观察请求拿到真实请求的地址和参数格式然后用脚本循环发送。这条路速度快得多一批几十条也就几秒钟而且不依赖DOM里有没有删除按钮。缺点是需要处理参数里的动态凭证字段一旦拿不到或者参数构造得不对接口会返回错误码再就是风控敏感度更高毕竟直接跳过了页面交互。我的建议是如果只删几十条、并且不确定自己能不能搞定接口参数就走模拟点击如果要清几千条、并且愿意在Network面板里花十分钟研究一下请求直接调接口才是长期方案。后面第6章会专门讲接口方案的封装。接下来先讲最直接的控制台脚本方案它能在三分钟内让你看到实际效果。3. 控制台直接跑通批量删除从采集按钮到边滚边删3.1 准备工作登录态、页面和控制台打开浏览器登录你的QQ空间进入“我的说说”列表页。这一步的登录态非常关键删除请求完全依赖cookie里的身份凭证所以不要开隐身窗口也不要用小号随便试。确认页面已经加载出几条说说后按F12打开开发者工具切到Console标签。接下来的所有脚本都在Console里粘贴执行。有一点要先说清楚QQ空间页面本身经过压缩混淆类名不稳定任何人的选择器脚本都不能保证永久有效。我下面给的代码用了模糊匹配的方案能扛住一部分改版但如果你执行后发现length为0那就需要回到第2章的方法自己在Elements面板里重新定位删除按钮的类名特征。这不是代码缺陷是前端页面的常态。3.2 第一步写一个采集删除按钮的函数先写一个函数把当前页面里所有候选的删除按钮收集出来。核心思路是用模糊属性匹配而不是写死类名。// 采集当前可见说说中的删除按钮 function collectDelButtons() { // 新版删除按钮的class里通常会包含 del/delete 字样但也可能混入页面其它模块 const all Array.from(document.querySelectorAll([class*del], [class*Del], [class*delete])); // 过滤按钮必须位于说说条目容器内部且按钮可见 const moodBox document.querySelectorAll([class*mood], [class*feed], [class*item]); return all.filter((btn) { const parent btn.closest([class*mood], [class*feed], [class*item]); const rect btn.getBoundingClientRect(); // 去掉隐藏按钮和页面顶栏里的“删除”入口 return parent rect.width 0 rect.height 0; }); } // 测试一下当前能抓到多少个 console.log(当前页面候选删除按钮数:, collectDelButtons().length);这段代码的逻辑分两层。第一层用属性模糊匹配把所有带del或delete字样的元素捞出来这样做的好处是无论类名带什么hash后缀都能命中。第二层做了过滤因为页面上除了说说还可能存在“删除评论”“删除相册”等其他删除入口只有位于说说卡片容器内部的按钮才是目标。参数上有一个值得调整的点mood、feed、item这三个容器关键词是按旧版页面结构总结的。如果执行后length为0先在Elements面板里随便选中一条说说的外层div看看它的class里有什么稳定关键词然后替换到代码里。这一步是在任何改版情况下都能自己定位问题的关键能力。3.3 第二步模拟点击并处理确认弹窗拿到按钮之后点击和确认是关键。新版弹窗可能是原生confirm也可能是div弹窗所以代码里要把两条路都覆盖。// 伪装原生确认弹窗防止被原生confirm卡住 window.confirm () true; // 点击一个删除按钮然后处理可能的div弹窗 function clickDelete(btn) { btn.click(); // 等待弹窗渲染出来 return new Promise((resolve) { setTimeout(() { // 在当前DOM里寻找“确定”按钮优先按文本匹配 const dialog document.querySelector([class*dialog], [class*modal], [class*pop]); if (dialog) { const sureBtn Array.from(dialog.querySelectorAll(a, button, span)) .find((el) el.innerText el.innerText.indexOf(确定) -1); if (sureBtn) { sureBtn.click(); resolve(true); return; } } // 找不到div弹窗就默认confirm已被接管 resolve(true); }, 300); }); } // 单条删除测试用当前页面第1个按钮试一下 const btnList collectDelButtons(); if (btnList.length 0) { await clickDelete(btnList[0]); console.log(已触发第1条删除等待网络请求); }这里有几个关键参数要说明。setTimeout(..., 300)是给弹窗渲染留的等待时间改得太短弹窗还没出现改得太长影响批量删除速度一般300到500毫秒比较合理。find((el) el.innerText.indexOf(确定) -1)是文本匹配寻找确认按钮比类名匹配稳妥得多因为弹窗按钮的文案“确定”是用户可见的很少变化。第一次测试时执行完去Network面板看有没有发出删除请求。如果有请求且返回成功说明链路走通了。如果请求没发出大概率是div弹窗没有渲染出来或者确认按钮的文案不是“确定”而是“删除”把文本匹配改成删除再试一次。这一步是整个方案里最需要耐心调试的地方一旦通了后面就是体力活。3.4 第三步边滚动边删控制节奏避免风控单条能删之后把它放进一个带滚动和延迟的循环里。这个循环要同时承担两件事触发加载更多以及控制删除频率。// 批量删除每轮先抓按钮再删除然后滚动到底部加载新的说说 const MAX_DELETE_PER_BATCH 10; // 每轮最多删10条防止被风控 const DELAY_BETWEEN_DELETE 800; // 每次删除之间的等待时间(ms) const SCROLL_WAIT 1500; // 滚动后等待新内容加载的时间(ms) let deletedCount 0; async function batchDelete() { while (deletedCount 500) { // 最多删500条按需修改 const btnList collectDelButtons(); // 取出本轮要处理的按钮 const batch btnList.slice(0, MAX_DELETE_PER_BATCH); if (batch.length 0) { // 当前页面没有更多按钮滚动到底部触发加载 window.scrollTo(0, document.body.scrollHeight); await new Promise((r) setTimeout(r, SCROLL_WAIT)); continue; } for (const btn of batch) { await clickDelete(btn); deletedCount; console.log(已删除 ${deletedCount} 条); await new Promise((r) setTimeout(r, DELAY_BETWEEN_DELETE)); // 删除成功后DOM里的该条说说会被移除需要重新刷新按钮列表 } // 每轮结束滚动一次加载下一批 window.scrollTo(0, document.body.scrollHeight); await new Promise((r) setTimeout(r, SCROLL_WAIT)); } console.log(批量删除结束总共处理, deletedCount, 条); } // 手动启动注意不要重复执行 batchDelete();代码里三个参数直接决定会不会触发风控。DELAY_BETWEEN_DELETE设成800毫秒意味着每秒只删1条多一点点这个速度对人工来说很快但对风控来说算是温和的。MAX_DELETE_PER_BATCH设成10条一轮是给页面一个“缓口气”的机会每删完10条就滚动加载新内容顺带把操作频率摊开。SCROLL_WAIT取决于你的网速如果滚动后经常出现“按钮数为0”说明加载没完成把它加到2000毫秒。一条血泪经验是千万不要把延迟改成0毫秒去追求速度。曾经有人为了删两千条记录把间隔压到200毫秒结果删到第80条时接口直接返回验证码整锅端走。批量删除本质是在平台的规则边缘做事慢就是快。4. 封装成插件并加保险间隔、上限、截止时间和防误删导出4.1 从控制台脚本到油猴脚本需要改什么控制台脚本能用但每次清理都要打开开发者工具、复制粘贴代码长期用不现实。常见做法是封装成油猴脚本让浏览器在打开特定页面时自动注入。封装本身不难关键点有三个脚本的匹配规则、执行时机、以及沙箱环境处理。// UserScript // name QZone Mood Batch Deleter // namespace local // version 0.1.0 // match https://user.qzone.qq.com/* // run-at document-end // grant none // /UserScript (function () { use strict; // 把第3章的 collectDelButtons 和 clickDelete 搬进来 // 再定义一个启动开关避免每次进页面都自动删 window.startBatchDelete async function (config) { // 这里填入你需要的参数 }; })();match规则只匹配user.qzone.qq.com下的页面避免脚本在他的其它网页上乱跑。grant none表示不需要访问GM_系列的API这在油猴脚本里意味着运行在页面上下文可以直接访问页面里的全局变量和DOM对我们来说够用。注意一点脚本默认不自动执行只注册一个window.startBatchDelete函数你得在控制台手动调用。这个设计是故意的防止某些人把脚本挂上去之后一进页面就开始删误伤自己没想删的内容。4.2 三个必调参数删除间隔、单轮上限、截止时间封装成插件后参数要能从外部传进来而不是每次改源码。最常见的三个参数是删除间隔、单轮上限、截止时间。前两个和第3章一样第三个是新增的防误删关键参数。// 调用示例只删除2020年之前发布的说说 window.startBatchDelete({ maxDelete: 2000, // 总共最多删2000条 batchSize: 10, // 每轮删10条 interval: 800, // 每次删除间隔800ms scrollWait: 1500, // 滚动后等待1500ms endTime: new Date(2019-12-31 23:59:59).getTime() // 只删这个时间之前的 });实现上关键是在clickDelete之前先判断当前说说的发布时间。说说的DOM里通常有一个时间元素用innerText解析后和endTime比较早于截止时间才删。这个判断必须在“点击删除按钮”之前做因为一旦点了就不可能撤销。我把endTime设计成时间戳数值而不是字符串是因为不同页面显示的时间格式不一样有的显示“昨天”有的显示“2023-05-01”解析成本高。如果你只固定用年份做判断也可以直接把endTime改成2020匹配时间文本里以2020开头的条目。参数按需要调整关键是思路给脚本加一个“只删旧不删新”的过滤器比删完后悔强一百倍。4.3 断点续删用localStorage记录已删的tid批量删除最怕中途断掉。可能是因为网络波动也可能是因为你自己想暂停。如果脚本没有记忆重新执行时又会从头开始已删的说说不会出现在页面里但脚本不知道它只会把当前列表里的按钮当成待删对象。这个逻辑问题会浪费大量时间。解决方式是在删除成功后把该说说的唯一标识tid记到localStorage里。每次删除前先检查tid是否已经存在存在就跳过。function getDeletedTids() { return JSON.parse(localStorage.getItem(deletedMoodTids) || []); } function markDeleted(tid) { const list getDeletedTids(); if (!list.includes(tid)) { list.push(tid); localStorage.setItem(deletedMoodTids, JSON.stringify(list)); } } // 在批量删除循环内拿到当前删除按钮对应的tid后先做检查 async function safeDelete(btn) { const tid btn.closest([class*item]).getAttribute(data-tid) || btn.getAttribute(data-tid); if (!tid) { // 拿不到tid就直接删不做断点保护 return clickDelete(btn); } if (getDeletedTids().includes(tid)) { console.log(跳过已删除的tid:, tid); return; } await clickDelete(btn); markDeleted(tid); }这里有个绕不开的现实问题并不是每条说说的DOM节点都有>// 导出当前页面所有说说的文本、时间和链接到一个JSON文件 function exportMoods() { const items Array.from(document.querySelectorAll([class*mood], [class*feed], [class*item])); const data items.map((el, idx) { const timeEl el.querySelector([class*time]); const contentEl el.querySelector([class*content]); return { index: idx, time: timeEl ? timeEl.innerText.trim() : , content: contentEl ? contentEl.innerText.trim() : , url: location.href }; }).filter((item) item.content || item.time); const blob new Blob([JSON.stringify(data, null, 2)], { type: application/json }); const a document.createElement(a); a.href URL.createObjectURL(blob); a.download mood-backup- Date.now() .json; a.click(); console.log(已导出, data.length, 条说说记录); } // 手动执行 exportMoods();导出函数的实用条件同样依赖说说的容器类名。如果querySelector匹配不到内容先回第2章用Elements面板定位当前页面的实际类名。这份备份虽然不包含图片和视频但文字和发布时间足以找回大部分回忆。批量删除是危险操作备份就是后悔药宁可多导一次也别跳过。5. 批量删除高频踩坑选择器失效、滚动卡顿和风控拦截5.1 现象脚本跑着跑着按钮突然全部点不动了出现的时机很典型脚本跑了三四分钟后控制台不报错但日志卡在某个数字不再增长手动看页面发现说说的删除按钮不见了。原因滚动加载机制把前面已经渲染过的DOM节点从页面中移除了。你脚本里缓存的按钮数组还留着这些节点引用点击事件绑在已经脱离文档的元素上自然毫无效果。解决不要在脚本开头一次性采集所有按钮存数组而是每轮循环都调用collectDelButtons()重新查询当前DOM。我之前第3章的代码就是这么写的每轮都重新采集不会踩这个坑。如果你改代码时图省事把采集移出了循环那就会复现这个问题。5.2 现象点击删除后没有弹窗或者弹窗里找不到“确定”按钮这是2023年改版后最常见的翻车点。老脚本统一走window.confirm () true这条路新版改成div弹窗后这个接管就失效了。弹窗照常出现但脚本找不到确认按钮删除请求根本没发出页面看起来“没反应”。原因改版后确认弹窗是页面内渲染的自定义组件不是浏览器的原生确认框。它的按钮文本可能从“确定”改成了“删除”或“移除”类名则是动态生成的。解决先确认弹窗出现然后在弹窗内部用文本匹配找按钮而不是依赖类名。如果文本不是“确定”把匹配关键词改成弹窗上实际显示的文案。另外可能遇到一种特殊情况弹窗里有两个按钮一个是“取消”一个是“确定”但“取消”按钮在DOM里排在前面。这时要用find而不是filter只取第一个文本匹配的按钮否则会把取消当成确定。5.3 现象连续删除几十条后弹出验证码或者接口直接报错这是风控拦截的特征。具体表现有两种一种是在页面正中弹出滑块验证码另一种是Network面板里删除请求返回的错误码不是成功值。原因短时间内的删除操作频率超过了平台设定的阈值。这个阈值没有公开文档但根据实际操作来看每秒超过2次、或者单轮超过20条时风险会明显升高。解决把删除间隔从800毫秒调到1200毫秒以上每轮上限从10条降到5条并且在每轮之间加一个5秒的暂停。我见过最稳妥的方案是“删5条停10秒”跑一千条大概一小时虽然慢但几乎不会被拦。如果你已经被风控停止一切删除操作等几个小时再用。千万不要尝试绕过验证码那是底线问题账号安全远比删说说重要。5.4 现象手机版页面跑不了脚本选择器全查不到在手机浏览器上打开说说页你会发现同样一套脚本完全失效。原因很直接手机版页面和PC版页面的DOM结构完全不同容器类名、删除按钮位置都不一样而且手机浏览器的控制台很难打开。解决统一在PC端执行。如果你必须用手机可以试试在PC浏览器里开启移动设备模拟模式让PC浏览器按手机版的User-Agent和视口渲染页面但说实话这个方案里选择器还是要单独适配手机版DOM维护成本翻倍。我个人的建议是放弃手机端只在PC上跑这是性价比最高的路径。5.5 现象脚本提示删除成功但刷新后说说还在这条最让人崩溃。控制台日志显示每条删除都触发了页面上的说说也消失了但F5刷新后原来删掉的说说又回来了。原因大多数情况下页面上的删除操作需要接口返回成功后才算数。脚本点击了删除按钮、也点了确定但弹窗里的确认逻辑可能走了别的分支比如接口失败但前端把该条DOM先移除了刷新后重新拉取数据删除没生效就说说明说还在。解决不要只看页面表现要看Network面板里的删除请求返回体。找到删除请求查看它的返回JSON里的code字段成功值是0或者code定义的“成功”值。如果返回值是失败检查请求参数是不是少了关键字段特别是csrf token。另一个常见原因是cookie失效登录态过期后接口会静默失败这时重新刷新页面登录一次再跑。6. 进阶绕过页面按钮直接调删除接口批量删如果你要删的数量在几百条以内第3章的控制台脚本完全够用。但如果要清理上千条、并且你希望整个过程快一点、稳一点下一步就是直接从 Network 面板里把删除接口“挖”出来用脚本循环调。这条路我实际跑下来的速度大概是一分钟删几十条而且不容易被页面DOM改版影响。操作步骤是先在页面上手动删除一条说说同时打开Network面板找到删除请求。这个请求通常是个POST请求名字里带delete或类似关键词。点开它在Payload里能看到几个关键参数hostUin是你的QQ号tid是要删除的说说IDcode和verifyToken是校验字段。前两个直接读就行后两个字段在不同的页面版本里来源不一样有些挂在页面全局变量上有些存在页面localStorage里。你需要做的就是手动删除一次把这些字段的实际值记下来观察它们是不是固定的。如果code在每次删除时都一样说明它不是随机生成的那脚本里直接写死即可。如果每次都变就要在脚本里先从页面某个全局JS对象里读取它。这里没法给出通用的解析代码因为全局对象名也随版本变化我的建议是在Console里输入你怀疑的关键词比如window.code、window.uin看能不能打印出敏感值。简要封装如下// 直接调用删除接口的示例封装 // 注意DELETE_API、hostUin、getCode 都要从你自己的Network请求里替换为实际值 const DELETE_API /cgi-bin/emotion_cgi_delete_v6; // 以你抓包看到的实际地址为准 const hostUin 你的QQ号; async function deleteMoodByApi(tid) { const body new URLSearchParams(); body.append(hostUin, hostUin); body.append(tid, tid); body.append(code, getCode()); // 从页面上下文中提取 body.append(verifyToken, getVerifyToken()); const resp await fetch(DELETE_API, { method: POST, credentials: include, headers: { Content-Type: application/x-www-form-urlencoded }, body: body.toString() }); const json await resp.json(); return json; } // 循环删除示例tidList是待删除的tid数组 async function batchDeleteByApi(tidList) { let success 0; let fail 0; for (const tid of tidList) { const result await deleteMoodByApi(tid); if (result result.code 0) { success; } else { fail; console.error(删除失败:, tid, result); // 失败后停一下避免触发风控 await new Promise((r) setTimeout(r, 2000)); } // 每删一条等500ms await new Promise((r) setTimeout(r, 500)); } console.log(完成成功${success}条失败${fail}条); }接口方案的关键参数是credentials: include删除操作靠cookie鉴权不带cookie这个请求必失败。tidList的来源如果是少量说说可以从当前页面的DOM里把每个style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />