完整扒站实践
Galaxy
|
2026-09-08 10:47
|
258 次浏览 |
技术分享
扒站
笔记
在网络开发与安全研究的实践中,“扒站”往往是许多开发者接触站点架构、资源解耦以及数据采集的第一步。它不仅是一次对前端静态资源的复刻,更是一场涉及网络请求分析、接口逆向解析、资源打包与自动化提取的综合实践。 (注: 本文封面为 Gemini生成)
> **特别声明**:本文涉及的所有抓取与测试均已获得网站所有者(朋友)的明确授权,仅供技术交流与学习使用,未对目标站点造成影响,亦未获取任何敏感数据
这次更新博客时间间隔有点久,本次我使用了我朋友的站点,原站使用经典的nodejs模式 但是我主要需求为他网站的数据 我就不展示扒网站结构了 结构为Ai根据数据直接生成
## 效果展示一下哈
| 源站 | 仿站 |
|-------|-------|
|  | |
|| |
### 扒网站数据
1. 分析网站哪些数据为公开 哪些需要cooike (我已有网站的会员)
我解除登录状态之后发现 除网站收录库提示词 其他都为公开数据

通过网站结构我选择json保存数据 (这里说明原因 应为这个网站基本上 名称-图片-文本-网址 json完全可以处理掉)
2. 接下来就是需要网站每个页面图片 内容 我选择直接F12 分析页面数据
|💨 |图中红框为主要页面js 加载|
|-------|-------|
|通过检查 每个页不会单独分 pages为分页||
|在切换分页时 通过models?page=3&_rsc=xta5k 他rsc这个东西成功引起了我的兴趣 |-------|
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请求|
|-------|-------|
| |  |
可以观察出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 |
|-------|-------|-------|
|  |  |  |
不是我偷懒哈 让ai总结成一个总脚本了 (莫批评😭) 写的太多真整理不过来了

6. 最后网站收录页 /news 解析
还是我们得老朋友F12

打开page-bb7fd8bcbc22172c.js 发现他所有页面数据没走RSC流中 直接把所有除提示词外 数据全部显示出来了 **给大家看一下对比**
| NEW页面 | 其他页面|
|-------|-------|
| |  |
只有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

## 下载json中的图片与视频等外部文件(简略)

大家可以看到有一下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 接口

2. 根据载荷显示 直接去json中 查找 发现为json中的ID

3. 流程
```
1. 读取 data/news.json
↓
2. 找出所有 prompt 为空的条目
↓
3. 随机抽取一条
↓
4. POST 请求 API 获取 prompt
↓
5. 立即写回 JSON(断点续传)
↓
6. 等待 6-12 秒(防限流)
↓
7. 重复直到全部补全
```
由于要有账户所以请求需带cooike

***提取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个数据全部获取完成***
# 结果

以上就是获取到的全部数据 全都把我朋友数据库搬空喽
再次声明:本次扒站以得到我站长朋友的允许 仅作为学习使用
提示:本文最后更新时间为 2026-09-09 00:45,如文中内容素材有错误或者已经失效,请留言告知。
评论 2
fengc 普通用户
9小时前