之前我以为这类东西可能不能发,最近看到有人发要下载的插件还收获很多赞,那还不如我这个。。开源给大家用,自己写的用了一年多了
脚本本质上只是:
-
在你自己浏览器里读取当前页面已经渲染出来的文字;
-
自动滚动页面,触发懒加载 / 虚拟列表继续显示楼层;
-
对楼层文本做简单去重;
-
最后在本地导出一个 TXT 文件。
它不登录、不绕权限、不请求后台接口、不上传数据,也不改网页内容。用途主要是长帖备份、全文搜索、丢给 AI 总结、或者把有用讨论保存下来。
想冲钛,所以整理一下发出来。如果版规不合适,版主可删。
使用方法
以 Chrome / Edge 为例。
1. 打开你想导出的帖子页面
先正常打开帖子。如果页面需要登录才能看,先登录。
2. 打开开发者工具
按:
-
Windows:
F12或Ctrl + Shift + J -
Mac:
Cmd + Option + J
切到 Console 标签页。
如果浏览器提示不要粘贴陌生代码,这是正常安全提示。这个脚本只在本地读取页面文字并导出 TXT。你也可以先自己看一遍代码。
3. 粘贴脚本并回车
粘贴下面代码,回车执行。
页面右下角会出现一个小黑框,里面有:
-
开始
-
停止
-
导出TXT
4. 点击"开始"
脚本会从页面顶部开始,边滚动边记录楼层文本。
右下角会显示:
已记录 X 楼 | 最近 Y 轮无新增…
5. 等它完成后点击"导出TXT"
会下载一个类似这样的文件:
thread_export_20260602193422.txt
每个楼层之间用:
----
分隔。
脚本
// ===== 配置(可留空自动探测)=====
const OVERRIDE_POST_SELECTOR = ''; // 例: '.post-item', 'li.reply'
// ===== 轻量 UI =====
(() => {
const id='__grab_txt2_ui__'; if(document.getElementById(id)) return;
const box=document.createElement('div');
box.id=id; box.style.cssText='position:fixed;right:16px;bottom:16px;z-index:2147483647;background:#111;color:#fff;padding:10px 12px;border-radius:10px;font:12px/1.4 monospace;box-shadow:0 2px 12px rgba(0,0,0,.5)';
box.innerHTML=`
<div style="margin-bottom:6px">楼层抓取 → TXT(抗虚拟列表)</div>
<button id="gt2_start">开始</button>
<button id="gt2_stop" disabled>停止</button>
<button id="gt2_export" disabled>导出TXT</button>
<label style="margin-left:6px;color:#bbb"><input id="gt2_autoscroll" type="checkbox" checked/> 自动滚</label>
<div id="gt2_stat" style="margin-top:6px;color:#ccc">待开始…</div>`;
document.body.appendChild(box);
gt2_start.onclick=start; gt2_stop.onclick=()=>abortFlag=true; gt2_export.onclick=exportTXT;
})();
// ===== 工具 =====
const sleep = ms => new Promise(r=>setTimeout(r,ms));
const hash = s => {let h=0,i=0;for(;i<s.length;)h=(h<<5)-h+s.charCodeAt(i++)|0;return h.toString(36);};
const $$ = sel => Array.from(document.querySelectorAll(sel));
const nowStamp = () => new Date().toISOString().replace(/[-:T]/g,'').slice(0,14);
let abortFlag=false;
let seen=new Set(); // 去重
let textBlocks=[]; // 逐层文本
function autodetectSelector(){
if (OVERRIDE_POST_SELECTOR) return OVERRIDE_POST_SELECTOR;
const C=['article','.post','.comment','li[data-post-id]','[data-comment-id]','[role="article"]','.reply','.item','.message','.thread-item'];
const scored=[];
for (const s of C){
const els=$$(s).filter(e=>e.innerText?.trim().length>20);
if(els.length>=5) scored.push({s, n:els.length});
}
scored.sort((a,b)=>b.n-a.n);
return scored[0]?.s || 'article';
}
function extractText(el){
// 如需更干净,可改成:el.querySelector('.content')?.innerText
let t=(el.innerText||'').replace(/\u00A0/g,' ').replace(/\s+\n/g,'\n').trim();
return t.length>=10 ? t : '';
}
function exportTXT(){
const sep = '\n\n----\n\n';
const blob = new Blob([textBlocks.join(sep)], {type:'text/plain;charset=utf-8'});
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `thread_export_${nowStamp()}.txt`;
a.click();
URL.revokeObjectURL(a.href);
}
// ===== 主逻辑:仅以“是否有新楼层”判停 =====
async function start(){
abortFlag=false; seen.clear(); textBlocks.length=0;
const stat = gt2_stat;
const bStart = gt2_start, bStop=gt2_stop, bExp=gt2_export, auto=gt2_autoscroll;
bStart.disabled=true; bStop.disabled=false; bExp.disabled=true;
const POST_SELECTOR = autodetectSelector();
stat.textContent = `选择器: ${POST_SELECTOR} | 抓取中…`;
window.scrollTo({top:0,behavior:'auto'}); await sleep(300);
// 反复滚动;若多轮没有任何新楼层,则认定“到底/加载不再推进”
let noNewRounds=0;
const NO_NEW_LIMIT = 40; // 连续多少轮没有新楼层才停(可调大)
const STEP_BASE = Math.max(600, Math.floor(window.innerHeight*0.85));
// 为了更好触发懒加载:偶尔小幅上/下抖动一下(部分站点用交叉观察器)
let jiggleTick = 0;
while(!abortFlag){
let newInThisRound = 0;
const nodes = $$(POST_SELECTOR).filter(e => e.offsetParent !== null);
for (const el of nodes){
const txt = extractText(el);
if (!txt) continue;
const key = hash(txt.slice(0,2000));
if (seen.has(key)) continue;
seen.add(key);
textBlocks.push(txt);
newInThisRound++;
}
if (newInThisRound === 0) noNewRounds++;
else noNewRounds = 0;
stat.textContent = `已记录 ${textBlocks.length} 楼 | 最近${noNewRounds}轮无新增…`;
// 判停:仅当“连续无新增”达到阈值才停
if (noNewRounds >= NO_NEW_LIMIT) break;
// 自动滚动以触发继续加载
if (auto.checked){
// 交替用不同步长,偶尔上抖,帮助触发懒加载/复用窗口
const step = STEP_BASE + (jiggleTick%5===0 ? 200 : 0);
window.scrollBy(0, step);
// 每 7 轮,轻微上抖 1 次再下滚
if (jiggleTick%7===3){
window.scrollBy(0, -120);
await sleep(120);
window.scrollBy(0, 240);
}
jiggleTick++;
}
await sleep(420 + Math.random()*220);
}
bStop.disabled=true; bExp.disabled=false;
stat.textContent = `完成:共 ${textBlocks.length} 楼。点击“导出TXT”。`;
}
简单说明
我自己主要拿来导出长帖,方便之后搜索和总结。欢迎大家改 selector / 改参数 / 拿去魔改。

