正在加载文章...
请稍候

完整扒站实践

| | 258 次浏览 |
技术分享 扒站 笔记
AI文章摘要

在网络开发与安全研究的实践中,“扒站”往往是许多开发者接触站点架构、资源解耦以及数据采集的第一步。它不仅是一次对前端静态资源的复刻,更是一场涉及网络请求分析、接口逆向解析、资源打包与自动化提取的综合实践。 (注: 本文封面为 Gemini生成) > **特别声明**:本文涉及的所有抓取与测试均已获得网站所有者(朋友)的明确授权,仅供技术交流与学习使用,未对目标站点造成影响,亦未获取任何敏感数据 这次更新博客时间间隔有点久,本次我使用了我朋友的站点,原站使用经典的nodejs模式 但是我主要需求为他网站的数据 我就不展示扒网站结构了 结构为Ai根据数据直接生成 ## 效果展示一下哈 | 源站 | 仿站 | |-------|-------| | ![2_compressed_85%.webp](/uploads/posts/2026-09/20260907_233333_6a9ed94d0c3d9.webp) |![4_compressed_65%.webp](/uploads/posts/2026-09/20260907_233747_6a9eda4b34838.webp) | |![3_compressed_65%.webp](/uploads/posts/2026-09/20260907_233448_6a9ed99800417.webp)|![5_compressed_65%.webp](/uploads/posts/2026-09/20260907_233842_6a9eda829f1fc.webp) | ### 扒网站数据 1. 分析网站哪些数据为公开 哪些需要cooike (我已有网站的会员) 我解除登录状态之后发现 除网站收录库提示词 其他都为公开数据 ![6_compressed_65%.webp](/uploads/posts/2026-09/20260907_234214_6a9edb56207b5.webp) 通过网站结构我选择json保存数据 (这里说明原因 应为这个网站基本上 名称-图片-文本-网址 json完全可以处理掉) 2. 接下来就是需要网站每个页面图片 内容 我选择直接F12 分析页面数据 |💨 |图中红框为主要页面js 加载| |-------|-------| |通过检查 每个页不会单独分 pages为分页|![7_compressed_65%.webp](/uploads/posts/2026-09/20260907_235432_6a9ede381ff02.webp)| |在切换分页时 通过models?page=3&_rsc=xta5k 他rsc这个东西成功引起了我的兴趣 |-------![8_compressed_85%.webp](/uploads/posts/2026-09/20260907_235745_6a9edef90a232.webp)| 3. 那么搜索RSC可得 ``` text RSC (React Server Components) 是 Next.js 13+ 使用的服务端组件渲染格式,它是一种特殊的 Flight 协议 数据流。 ``` 那么我看他的请求方式 ``` curl # 普通请求 → 返回 HTML curl https://XXX.cn/fonts # RSC 请求 → 返回 Flight 数据流 curl -H "RSC: 1" https://XXX.cn/fonts ``` | 普通请求| RSC请求| |-------|-------| | ![9_compressed_55%.webp](/uploads/posts/2026-09/20260908_000253_6a9ee02db3947.webp)| ![10_compressed_55%.webp](/uploads/posts/2026-09/20260908_000413_6a9ee07d03a2d.webp) | 可以观察出RSC流是可以读内容 ```格式解析: 行号: + 数据 数据是 JSON 数组 或 JSON 对象 用 $Lxxxx 引用其他行 ``` 4. 通过观察 除网站收录页 /news以外都可以使用RSC流进行获取 那么我们就可以使用 JavaScript 脚本去给他转为 **JSON** 我写了一个***rebuild-all-json-portable.mjs*** 使用node js运行就可以直接抓取 RSC 数据解析 使用正则表达式实现 提取每个数据 ``` javascript function expandRSC(raw) { // 1. 建立引用映射:行号 → 内容 const refMap = {}; for (const line of raw.split(/\n/)) { const i = line.indexOf(':'); refMap[line.slice(0, i).toLowerCase()] = line.slice(i + 1); } // 2. 移除行号,合并所有内容 let joined = lines.map(l => l.slice(l.indexOf(':') + 1)).join('\n'); // 3. 递归替换 $Lxxxx 为实际值 for (let k = 0; k < 4; k++) { joined = joined.replace(/\$L([0-9a-fA-F]{1,4})/g, (m, hex) => { return refMap[hex.toLowerCase()] || m; }); } return joined; } 提取 item 对象: javascript function extractItems(text) { const expanded = expandRSC(text); const items = []; // 扫描所有 "item":{...} let s = 0; while ((idx = expanded.indexOf('"item":{', s)) !== -1) { // 深度匹配大括号,提取完整 JSON const start = idx + 7; let depth = 0; for (let i = start; i < expanded.length; i++) { if (expanded[i] === '{') depth++; else if (expanded[i] === '}') { depth--; if (depth === 0) { const seg = expanded.slice(start, i + 1); const it = JSON.parse(seg); if (it.id && it.title) items.push(it); break; } } } } return items; } ``` 5. 抓取效果 | 整合抓取 | 缓存TXT目录 | JSON | |-------|-------|-------| | ![11_compressed_55%.webp](/uploads/posts/2026-09/20260908_001414_6a9ee2d68c3ab.webp) | ![12_compressed_55%.webp](/uploads/posts/2026-09/20260908_001548_6a9ee33426bce.webp) | ![13_compressed_55%.webp](/uploads/posts/2026-09/20260908_001724_6a9ee3948680c.webp) | 不是我偷懒哈 让ai总结成一个总脚本了 (莫批评😭) 写的太多真整理不过来了 ![17_compressed_80%.webp](/uploads/posts/2026-09/20260908_002655_6a9ee5cfba6a8.webp) 6. 最后网站收录页 /news 解析 还是我们得老朋友F12 ![14_compressed_55%.webp](/uploads/posts/2026-09/20260908_002103_6a9ee46f89f68.webp) 打开page-bb7fd8bcbc22172c.js 发现他所有页面数据没走RSC流中 直接把所有除提示词外 数据全部显示出来了 **给大家看一下对比** | NEW页面 | 其他页面| |-------|-------| | ![15_compressed_80%.webp](/uploads/posts/2026-09/20260908_002408_6a9ee528e7a8b.webp)| ![16_compressed_80%.webp](/uploads/posts/2026-09/20260908_002503_6a9ee55f96881.webp) | 只有new页面全部在这里 7. 提取流程 ``` 1. 请求 /news (普通 HTML,不带 RSC) ↓ 2. HTML 中找 chunk 路径: /_next/static/chunks/app/news/page-.js ↓ 3. 下载该 JS chunk ↓ 4. 在 JS 中找 JSON.parse('巨长字符串') ↓ 5. 对字符串进行 JS 转义还原 (\n, \uXXXX, \\, 等) ↓ 6. JSON.parse 得到数组 ↓ 7. 自动添加 prompt: "" 字段 ↓ 8. 输出 news.json ``` 一. 第一步:获取 HTML,找 chunk ```javascript const html = await fetchText('/news', { asHTML: true, // ← 不发送 RSC: 1,返回 HTML retries: 5 }); // HTML 内容示例(简化): // // // // // // ... // // 正则提取 chunk 路径 const chunkRe = /\/_next\/static\/chunks\/app\/news\/(page-[a-f0-9]+\.js)/; const m = html.match(chunkRe); // m[1] = "page-abc123.js" const chunkPath = '/_next/static/chunks/app/news/' + m[1]; ``` 二. 下载 JS Chunk ```javascript const js = await fetchText(chunkPath, { asHTML: true, retries: 5 }); // 缓存到 _rsc_cache/news-chunk_page-abc123.js writeFileSync(cacheFile, js, 'utf8'); ``` 三. 提取 JSON.parse 参数 ```javascript // 正则匹配 JSON.parse('...') 或 JSON.parse("...") const re = /JSON\.parse\(\s*(['"`])([\s\S]*?)\1\s*\)/g; let candidates = []; while ((rm = re.exec(js)) !== null) { const body = rm[2]; // ← 引号内的内容 // 检查是否像 news 数据 const head = body.slice(0, 120); const looksLikeArr = head.startsWith('[') && /^\[\s*\{\s*"id"\s*:\s*"(sites|sections|apps|backgrounds):/.test(head); if (looksLikeArr || body.length > 10000) { candidates.push({ quote: rm[1], // 引号类型: ' 或 " 或 ` body: body, len: body.length, head: head }); } } ``` > 这里解释一下为何有多个 > JS chunk 中可能有多个 JSON.parse(),需要筛选:优先选择:开头是 [{"id":"sites: 或 sections: 或 apps: 或 backgrounds: 备选:长度 > 10000 字节的 四. JS 字符串转义还原 ```javascript function unescapeJsStringLiteral(s) { let out = ''; for (let i = 0; i < s.length; i++) { const c = s.charAt(i); if (c !== '\\') { out += c; continue; } // 处理转义序列 const n = s.charAt(i + 1); // 常见转义 if (n === 'n') { out += '\n'; i += 1; } else if (n === 'r') { out += '\r'; i += 1; } else if (n === 't') { out += '\t'; i += 1; } else if (n === '\\') { out += '\\'; i += 1; } else if (n === "'") { out += "'"; i += 1; } else if (n === '"') { out += '"'; i += 1; } // 十六进制: \x41 → 'A' else if (n === 'x' && /^[0-9a-fA-F]{2}$/.test(s.slice(i+2,i+4))) { out += String.fromCharCode(parseInt(s.slice(i+2,i+4), 16)); i += 3; } // Unicode: \u4E2D → '中' else if (n === 'u' && /^[0-9a-fA-F]{4}$/.test(s.slice(i+2,i+6))) { out += String.fromCharCode(parseInt(s.slice(i+2,i+6), 16)); i += 5; } // 其他:原样输出 else { out += n; i += 1; } } return out; } ``` 五. JSON 解析 ```javascript // 还原转义 const unescaped = unescapeJsStringLiteral(best.body); // 解析为数组 let arr = JSON.parse(unescaped); // arr = [ // { "id": "sites:1", "title": "站点1", "categoryKey": "sites", ... }, // { "id": "sites:2", "title": "站点2", "categoryKey": "sites", ... }, // { "id": "apps:1", "title": "应用1", "categoryKey": "apps", ... }, // ... // ] ``` 这样就组成了完整的 json ![18.png](/uploads/posts/2026-09/20260908_003600_6a9ee7f058637.png) ## 下载json中的图片与视频等外部文件(简略) ![19_compressed_80%.webp](/uploads/posts/2026-09/20260908_003831_6a9ee887f14ff.webp) 大家可以看到有一下gif webp等等文件我们来转到本地 **每个文件都正则提取就可** ``` const data = JSON.parse(readFileSync('news-full.json', 'utf8')); [ { "id": "sites:1", "title": "某个网站", "media": [ { "thumbnailSrc": "/previews/sites/xxx.webp", // ← 提取这个 "originalSrc": "/images/sites/xxx.webp" } ] } ] const list = []; const seen = new Set(); for (const item of data) { for (const m of item.media) { if (m.thumbnailSrc && m.thumbnailSrc.startsWith('/') && !seen.has(m.thumbnailSrc)) { seen.add(m.thumbnailSrc); list.push(m.thumbnailSrc); } } } // list = ['/previews/sites/a.webp', '/previews/sites/b.webp', ...] ``` # 重点重点 获取AI提示词 **两个坑 我都暴雷了 ** 1. 一分钟不能获取超过7个提示词 2. 一个IP一小时提取提示词不能超过100个 由于原图 没办法展示我就没法贴出来了 使用我多个服务器不同IP 去获取 1. **F12 获取接口** 点击发现出现了/prompt 接口 ![20_compressed_80%.webp](/uploads/posts/2026-09/20260908_004539_6a9eea33f3861.webp) 2. 根据载荷显示 直接去json中 查找 发现为json中的ID ![21_compressed_80%.webp](/uploads/posts/2026-09/20260908_004814_6a9eeaced45af.webp) 3. 流程 ``` 1. 读取 data/news.json ↓ 2. 找出所有 prompt 为空的条目 ↓ 3. 随机抽取一条 ↓ 4. POST 请求 API 获取 prompt ↓ 5. 立即写回 JSON(断点续传) ↓ 6. 等待 6-12 秒(防限流) ↓ 7. 重复直到全部补全 ``` 由于要有账户所以请求需带cooike ![22_compressed_80%.webp](/uploads/posts/2026-09/20260908_005236_6a9eebd4ce9e0.webp) ***提取ID+接口获取 填入*** ```javascript const API_URL = 'https://XXX.cn/XX/XXXX/prompt'; // POST 请求体 const body = JSON.stringify({ id: 'sites:1' }); // 请求头 const headers = { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body), Cookie: COOKIE, // ← 关键!需要登录态 Origin: 'https://xxxx.cn', Referer: 'https:/xxxxxx.cn/news', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' }; ``` ***限流控制*** ```javascript const INTERVAL_MIN = 6; // 最小间隔 6 秒 const INTERVAL_MAX = 12; // 最大间隔 12 秒 // 随机等待 const sec = rand(INTERVAL_MIN, INTERVAL_MAX); await sleep(sec * 1000); // 平均 8.3 次/分钟,低于 10 次/分钟的限制 // 计算:(6+12)/2 = 9 秒 → 60/9 ≈ 6.7 次/分钟 // 实际加上网络请求时间,约 6-7 次/分钟 ``` ***历经3个小时500个数据全部获取完成*** # 结果 ![23_compressed_80%.webp](/uploads/posts/2026-09/20260908_005559_6a9eec9fe2ef6.webp) 以上就是获取到的全部数据 全都把我朋友数据库搬空喽 再次声明:本次扒站以得到我站长朋友的允许 仅作为学习使用

提示:本文最后更新时间为 2026-09-09 00:45,如文中内容素材有错误或者已经失效,请留言告知。
版权声明

本文作者:Galaxy

本文链接: https://lygalaxy.cn/blog.php?id=43

许可协议: CC BY-NC-ND 4.0

协议说明: 允许他人非商业性共享作品,必须注明原作者及来源,不得修改、衍生或用于商业目的

本站所有文章除特别声明外,均采用上述许可协议。转载请注明文章出处!

评论 2

登录 后发表评论
fengc 普通用户
9小时前
这个比较狠啊。
Galaxy 管理员
9小时前
@fengc 有些细节没展示出来 还有proxy没说😶‍🌫️
×