Spider Rust 实战:构建站点页面与资源清单
使用 Spider 的页面订阅流保存 HTML 和 JSONL 清单,记录状态与页面声明的资源链接,理解抓取范围、消费滞后和任务结束。
做文档迁移或站点盘点时,你需要的不只是一个 URL 集合,还需要知道每个地址返回什么状态、保存在哪个文件,以及页面引用了哪些资源。Spider 可以把页面结果以订阅方式交给 Rust 程序,边遍历边整理清单。
本文建立一轮最多二十页、深度受限的盘点,保存 HTML 与 JSONL,额外提取页面声明的图片、脚本和样式链接。它不下载这些资源,也不把二十页结果当作整站完整镜像。
准备输入地址
先在浏览器找到下列页面并确认内容,再从地址栏复制完整地址。服务或仓库地址从相应账户设置、官方项目的接入说明或克隆菜单取得。不要从示例输出中反推真实地址。
BOOKS_LIST_URL:Books to Scrape 首页地址。
在执行后续命令的同一个 Bash/WSL 终端中设置:
export BOOKS_LIST_URL='<paste-the-actual-books-list-url>'下文 JSON 或任务参数中的 <变量名> 是待替换标记,提交前填写刚复制的实际值;JSON 不会自动展开环境变量。结果示例中的 example.16yun.cn 只表示来源字段的形状,不是本文部署的演示站。
创建项目与依赖
准备稳定版 Rust/Cargo,按照 Rust 安装说明 建好环境。执行:
cargo new site-inventory
cd site-inventory
cargo add spider@=2.52.4 --features sync
cargo add serde_json scraper url保留 Cargo.lock。Spider 在本文中负责遍历与页面订阅,scraper 解析已取得的 HTML,url 负责相对地址解析。不要把不同语言的 Spider SDK 示例拼进 Rust API。接口可查 Spider 文档。
先确定清单范围
示例从 Books to Scrape 开始,限制深度一、页面预算二十。深度不是分类数,页面预算也不是商品数;入口上的分类链接和详情链接都可能被发现。
本轮清单说明“在这组配置下实际取得的页面”,不是证明站点总共只有这些页面。正式盘点应保存入口、范围与预算,并对未遍历范围给出解释。
完整的订阅与保存代码
将 src/main.rs 替换为:
use std::{collections::BTreeSet, error::Error, fs, io::Write};
use scraper::{Html, Selector};
use serde_json::json;
use spider::{tokio, traits::PageData, website::Website};
use url::Url;
type AnyError = Box<dyn Error + Send + Sync>;
#[tokio::main]
async fn main() -> Result<(), AnyError> {
fs::create_dir("output")?;
fs::create_dir("output/pages")?;
let entry = std::env::var("BOOKS_LIST_URL")?;
let mut site = Website::new(&entry);
site.with_limit(20).with_depth(1).with_delay(500);
site.with_respect_robots_txt(true);
let mut pages = site.subscribe(64);
let consumer = tokio::spawn(async move {
let selector = Selector::parse("img[src], script[src], link[href]")
.map_err(|_| "invalid resource selector")?;
let mut index = fs::File::create("output/inventory.jsonl.part")?;
let mut count = 0usize;
loop {
let page = match pages.recv().await {
Ok(page) => page,
Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => {
return Err::<usize, AnyError>(
format!("page consumer missed {n} messages").into()
);
}
};
let source = page.get_url().to_string();
let html = page.get_html();
let path = format!("output/pages/{count:04}.html");
fs::write(&path, &html)?;
let base = Url::parse(&source)?;
let document = Html::parse_document(&html);
let mut resources = BTreeSet::new();
for element in document.select(&selector) {
let value = element.value().attr("src")
.or_else(|| element.value().attr("href"));
if let Some(value) = value {
if let Ok(resolved) = base.join(value) {
if matches!(resolved.scheme(), "http" | "https") {
resources.insert(resolved.to_string());
}
}
}
}
let record = json!({
"url": source,
"status": page.status_code().to_string(),
"html_file": path,
"html_bytes": html.len(),
"declared_resource_urls": resources,
});
writeln!(index, "{record}")?;
count += 1;
}
index.flush()?;
Ok::<usize, AnyError>(count)
});
site.crawl().await;
site.unsubscribe();
let count = consumer.await??;
fs::rename("output/inventory.jsonl.part", "output/inventory.jsonl")?;
println!("saved_pages={count}");
Ok(())
}运行:
cargo run --releaseoutput 必须是新目录;重复运行时为新批次使用另一个目录或先移动旧结果。代码不自动删除旧盘点,避免一次失败覆盖先前材料。
为什么先订阅,再 crawl,最后 unsubscribe
先建立订阅才能接收本轮页面。若等 crawl 完成后才订阅,不应期待补回全部历史事件。
遍历与消费并行进行。订阅缓冲不是无限容量;消费者落后时会收到 Lagged。示例明确报错并保留 .part 文件,不把“循环结束”误认为完整成功。这比用 while let Ok 静默退出更容易发现丢失。
crawl 返回后解除订阅,让消费端能结束,再等待其写入完成,最后把索引改为正式文件名。遍历完成、消息消费完成和文件写入完成是不同阶段。
queue 用于追加 URL,与订阅页面结果不是同一个通道。本文没有混入额外队列生产者,因此不用把“丢弃一个 sender”当成遍历终止条件。
怎样读页面与资源字段
JSONL 每行一条页面记录,示例结构如下:
{"url":"https://example.16yun.cn/docs/","status":"200 OK","html_file":"output/pages/0000.html","html_bytes":1234,"declared_resource_urls":["https://example.16yun.cn/style.css"]}数值与状态文本是示意,实际状态的字符串表示以库返回为准。HTML 的字节数不是可读正文字数,也不是网络传输压缩大小。
资源清单来自 img、script、link 元素,不包含 CSS 内部引用、JavaScript 动态发起的全部请求或浏览器实际加载成功状态。重复 URL 在单页内去重;跨页共享资源仍会出现在各自记录中,便于保留关联。
如果需要验证每个资源是否存在,另建受限下载检查并记录响应,而不是把声明链接标成“下载成功”。如果需要完整浏览器网络清单,则应选择相应的浏览器观测路径。
状态、范围与动态页面怎样处理
优先检查非成功响应和空 HTML,回到保存的文件确认是否为预期页面。即使状态 200,仍可能是访问提示页,不能自动当作业务文档。
默认 HTTP 盘点不会凭空执行所有动态页面行为。需要浏览器渲染时,根据官方对应 feature 与方法重新配置,并重新考虑资源、就绪条件和预算。不能只启用某个 feature 就宣布所有 SPA 已完整盘点。
常见问题与修复
编译找不到状态方法。 本文使用 PageData trait 的 status_code,不存在这里需要的 get_status_code 方法;确认已导入 trait,并且依赖版本一致。
程序只生成 .part。 检查错误输出,可能是消费滞后、解析或磁盘写入失败。保留部分材料用于排查,不把它直接改名为完整结果。
没有二十条。 上限不是最低条数。检查范围、robots、可发现链接与响应,不能以不足预算判断库失效。
盘点少了某些资源。 判断它们是否由 CSS 或运行时生成;当前选择器只覆盖 HTML 显式声明的部分。
写入拖慢导致 Lagged。 减小抓取速度或改变消费与持久化设计。单纯无限增加缓冲只是推迟问题,需要明确内存与吞吐代价。
再次运行提示目录存在。 这是避免混批的有意限制。为新一轮准备独立输出位置,再按来源与配置比较结果。
一个可信的站点清单应同时说明已经取得的材料和范围限制。流式保存解决内存与衔接问题,范围与结果核对仍需要应用明确管理。
需要企业代理方案?
我们可根据目标站点、并发规模与稳定性目标提供定制方案。