跳到主要内容
极客日志极客日志
首页博客AI提示词GitHub精选代理工具
搜索
|注册
博客列表
Rust算法

基于 Rust 实现 GitHub Trending 热门仓库爬取

使用 Rust 异步框架结合 reqwest 与 scraper 库,实现 GitHub Trending 热门 Rust 仓库信息的自动抓取。项目涵盖依赖配置、HTTP 请求封装、DOM 解析逻辑及 JSON 序列化输出。通过语义化选择器提升代码鲁棒性,利用 anyhow 简化错误处理,最终生成包含作者、名称、描述及星标数的结构化数据文件,适合用于数据分析或监控场景。

暗影行者发布于 2026/3/22更新于 2026/5/44 浏览
基于 Rust 实现 GitHub Trending 热门仓库爬取

基于 Rust 实现 GitHub Trending 热门仓库爬取

本实战项目使用 Rust 构建一个异步爬虫,目标是抓取 GitHub Trending 页面中热门 Rust 仓库的详细信息(包括作者、名称、描述、星标数等),并将结果导出为 JSON 文件。代码重点优化了错误处理机制与 CSS 选择器的稳定性,确保在 GitHub 页面结构微调时仍能稳定运行。

技术栈选型

  • HTTP 请求:reqwest(Rust 生态中最流行的异步 HTTP 客户端)
  • HTML 解析:scraper(基于 selectors 库,支持 CSS 选择器,轻量高效)
  • JSON 序列化:serde + serde_json(Rust 标准的序列化工具链)
  • 异步运行时:tokio(Rust 异步编程的事实标准)
  • 日志调试:env_logger + log
  • 错误处理:anyhow(简化错误传递,避免手动定义复杂的错误枚举)

项目初始化

首先创建一个新的 Cargo 项目并进入目录:

cargo new github-trending-crawler
cd github-trending-crawler

接下来配置 Cargo.toml,添加必要的依赖。注意版本兼容性,建议通过 crates.io 查询最新稳定版:

[package]
name = "github-trending-crawler"
version = "0.1.0"
edition = "2021"
description = "A crawler to fetch GitHub Trending Rust repositories"
license = "MIT"

[dependencies]
# HTTP 客户端(异步)
reqwest = { version = "0.12", features = ["json", "rustls-tls"] }
# HTML 解析(CSS 选择器)
scraper = "0.18"
# JSON 序列化/反序列化
serde = { version = "1.0", features = ["derive"] }
serde_json = 

 = { version = , features = [] }

 = 
 = 

 = 
"1.0"
# 异步运行时
tokio
"1.0"
"full"
# 日志
log
"0.4"
env_logger
"0.10"
# 错误处理
anyhow
"1.0"

核心逻辑实现

1. 导入依赖与日志初始化

在 src/main.rs 中引入必要的模块。这里我们使用 anyhow::Result 统一返回类型,减少样板代码。

use anyhow::{Context, Result};
use log::info;
use reqwest::Client;
use scraper::{Html, Selector};
use serde::Serialize;
use std::fs::File;
use std::path::Path;

// 初始化日志,默认过滤级别设为 info
fn init_logger() {
    env_logger::Builder::from_env(
        env_logger::Env::default().default_filter_or("info")
    ).init();
}

2. 定义数据结构

我们需要一个结构体来存储仓库信息,并实现 Serialize trait 以便输出 JSON。字段设计尽量贴合 GitHub 页面的实际数据。

#[derive(Debug, Serialize)]
struct GithubRepo {
    author: String,
    name: String,
    description: Option<String>,
    stars: String,
    forks: String,
    today_stars: String,
    url: String,
}

3. 构建 HTTP 客户端

发送请求时,伪装 User-Agent 是防止被 GitHub 拦截的关键。同时设置合理的超时时间,避免网络波动导致程序挂起。

async fn fetch_trending_page(client: &Client) -> Result<String> {
    let url = "https://github.com/trending/rust?since=daily";
    info!("Fetching page: {}", url);

    let response = client
        .get(url)
        .header(
            "User-Agent",
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
        )
        .send()
        .await
        .context(format!("Failed to request URL: {}", url))?;

    if !response.status().is_success() {
        return Err(anyhow::anyhow!("Request failed with status: {}", response.status()));
    }

    let html = response.text().await.context("Failed to read response body")?;
    info!("Successfully fetched page (size: {} bytes)", html.len());
    Ok(html)
}

4. 解析 HTML 提取信息

这是最关键的一步。为了提高鲁棒性,我们不再依赖容易变动的类名(class),而是优先使用语义化属性(如 href 后缀或 data-* 属性)。对于缺失的数据,使用 unwrap_or_else 提供默认值,防止程序崩溃。

fn parse_repos(html: &str) -> Result<Vec<GithubRepo>> {
    info!("Starting to parse repositories...");
    let document = Html::parse_document(html);

    // 定义 CSS 选择器,优先使用语义化属性
    let repo_selector = Selector::parse("article.Box-row")
        .map_err(|e| anyhow::anyhow!("Failed to parse repo selector: {}", e))?;

    let author_name_selector = Selector::parse("h2 a")
        .map_err(|e| anyhow::anyhow!("Failed to parse author-name selector: {}", e))?;

    let desc_selector = Selector::parse("p")
        .map_err(|e| anyhow::anyhow!("Failed to parse description selector: {}", e))?;

    let stars_selector = Selector::parse("a[href$='/stargazers']")
        .map_err(|e| anyhow::anyhow!("Failed to parse stars selector: {}", e))?;

    let forks_selector = Selector::parse("a[href$='/forks']")
        .map_err(|e| anyhow::anyhow!("Failed to parse forks selector: {}", e))?;

    let today_stars_selector = Selector::parse("span[data-menu-button-text]")
        .map_err(|e| anyhow::anyhow!("Failed to parse today-stars selector: {}", e))?;

    let mut repos = Vec::new();

    for repo_node in document.select(&repo_selector) {
        // 提取作者和仓库名
        let author_name_element = repo_node
            .select(&author_name_selector)
            .next()
            .context("Missing author/name element")?;

        let author_name_text = author_name_element
            .text()
            .collect::<String>()
            .trim()
            .to_string();

        let (author, name) = author_name_text
            .split_once('/')
            .context(format!("Invalid author/name format: '{}'", author_name_text))?;

        let author = author.trim().to_string();
        let name = name.trim().to_string();

        // 拼接完整 URL
        let url = author_name_element
            .value()
            .attr("href")
            .context("Missing href attribute")?
            .to_string();
        let url = format!("https://github.com{}", url);

        // 提取描述(可选)
        let description = repo_node
            .select(&desc_selector)
            .next()
            .map(|elem| elem.text().collect::<String>().trim().to_string());

        // 提取星标、分支、今日新增星标(带容错)
        let stars = repo_node
            .select(&stars_selector)
            .next()
            .map(|elem| elem.text().collect::<String>().trim().to_string())
            .unwrap_or_else(|| "0".to_string());

        let forks = repo_node
            .select(&forks_selector)
            .next()
            .map(|elem| elem.text().collect::<String>().trim().to_string())
            .unwrap_or_else(|| "0".to_string());

        let today_stars = repo_node
            .select(&today_stars_selector)
            .next()
            .map(|elem| elem.text().collect::<String>().trim().to_string())
            .unwrap_or_else(|| "0".to_string());

        repos.push(GithubRepo {
            author,
            name,
            description,
            stars,
            forks,
            today_stars,
            url,
        });
    }

    info!("Successfully parsed {} repositories", repos.len());
    Ok(repos)
}

5. 保存结果到 JSON

使用 serde_json 将数据序列化为格式化 JSON,方便后续查看或处理。

fn save_repos_to_json(repos: &[GithubRepo], path: &str) -> Result<()> {
    info!("Saving repositories to JSON file: {}", path);
    let file = File::create(Path::new(path))
        .context(format!("Failed to create file: {}", path))?;

    serde_json::to_writer_pretty(file, repos)
        .context("Failed to serialize repos to JSON")?;

    info!("Successfully saved {} repos to {}", repos.len(), path);
    Ok(())
}

6. 主函数入口

整合上述流程,使用 tokio 启动异步运行时。

#[tokio::main]
async fn main() -> Result<()> {
    init_logger();
    info!("Starting GitHub Trending Rust Crawler...");

    let client = Client::builder()
        .connect_timeout(std::time::Duration::from_secs(10))
        .timeout(std::time::Duration::from_secs(15))
        .build()
        .context("Failed to create HTTP client")?;

    let html = fetch_trending_page(&client).await?;
    let repos = parse_repos(&html)?;
    save_repos_to_json(&repos, "trending_repos.json")?;

    info!("Crawler finished successfully! Check 'trending_repos.json' for results.");
    Ok(())
}

运行与验证

直接执行编译后的程序即可:

cargo run

如果需要更详细的调试信息,可以设置环境变量:

RUST_LOG=debug cargo run

运行成功后,项目根目录下会生成 trending_repos.json 文件。示例数据如下:

[
  {
    "author": "YaLTeR",
    "name": "niri",
    "description": "A scrollable-tiling Wayland compositor.",
    "stars": "14,823",
    "forks": "523",
    "today_stars": "0",
    "url": "https://github.com/YaLTeR/niri"
  },
  {
    "author": "librespot-org",
    "name": "librespot",
    "description": "Open Source Spotify client library",
    "stars": "6,131",
    "forks": "773",
    "today_stars": "0",
    "url": "https://github.com/librespot-org/librespot"
  }
]

总结

本项目利用 Rust 的异步特性与强大的类型系统,构建了一个健壮的 GitHub Trending 爬虫。通过 reqwest 处理网络请求,scraper 进行 DOM 解析,配合 serde 完成数据持久化。相较于传统脚本语言,Rust 在内存安全与并发性能上表现优异。优化后的代码采用了语义化 CSS 选择器与完善的错误处理机制,有效降低了因目标网站改版导致的维护成本。最终输出的 JSON 格式清晰,便于集成到自动化工作流中。

目录

  1. 基于 Rust 实现 GitHub Trending 热门仓库爬取
  2. 技术栈选型
  3. 项目初始化
  4. HTTP 客户端(异步)
  5. HTML 解析(CSS 选择器)
  6. JSON 序列化/反序列化
  7. 异步运行时
  8. 日志
  9. 错误处理
  10. 核心逻辑实现
  11. 1. 导入依赖与日志初始化
  12. 2. 定义数据结构
  13. 3. 构建 HTTP 客户端
  14. 4. 解析 HTML 提取信息
  15. 5. 保存结果到 JSON
  16. 6. 主函数入口
  17. 运行与验证
  18. 总结
  • 💰 8折买阿里云服务器限时8折了解详情
  • GPT-5.5 超高智商模型1元抵1刀ChatGPT中转购买
  • 代充Chatgpt Plus/pro 帐号了解详情
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • 基于 LangGraph 构建模块化 Skills 型 AI Agent
  • JavaScript 运算符与流程控制基础解析
  • 后仿真 SDF 反标 Warning 详解与排查指南
  • 星辰 RPA 实现小红书自动发文机器人
  • AI Agent 架构:基础组成模块深度解析
  • 无人机发展简史:从古代传说到现代飞行器设计
  • 五款优秀的免费 Ollama WebUI 客户端推荐
  • Python 商业爬虫三大核心项目全流程落地指南
  • 国内主流大模型盘点:Kimi、通义千问等八大模型对比分析
  • WebMCP:浏览器原生 AI 交互新范式
  • 算法题解:旋转链表(Rotate List)
  • ClawX:OpenClaw 可视化桌面客户端入门指南
  • 基于百度天气与 WebGIS 构建复古风格天气预报系统
  • OpenAI 发布 GPT-5.3 Instant:幻觉率降低及 2026 AI 模型排行
  • OpenClaw 橙皮书与蓝皮书核心内容解析
  • 深入理解哈希表:原理、源码与设计哲学
  • 基于 WebGIS 与百度天气接口的复古天气预报系统构建
  • VS Code 安装 GitHub Copilot 实现 AI 辅助编程
  • 前端 AI Agent 进阶学习路线:从基础到智能体构建
  • 低代码平台后端引擎:元数据驱动、插件化内核与 Java 扩展

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online