跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像AI 生图工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Rust算法

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

使用 Rust 结合 reqwest 和 scraper 库构建异步爬虫,抓取 GitHub Trending 页面数据并解析为 JSON。通过 tokio 运行时处理并发,anyhow 管理错误,最终输出包含仓库名、描述及星标数的结构化文件。代码经过优化,增强了 CSS 选择器的稳定性和异常容错能力,适合学习 Rust 网络编程与 HTML 解析实战。

嘘发布于 2026/3/26更新于 2026/7/2030 浏览
基于 Rust 实现 GitHub Trending 热门仓库爬虫

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

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

技术选型

  • HTTP 请求:reqwest(Rust 最流行的异步 HTTP 客户端)
  • HTML 解析:scraper(支持 CSS 选择器,轻量高效)
  • JSON 序列化:serde + serde_json(标准序列化库)
  • 异步运行时:tokio(Rust 异步编程的事实标准)
  • 日志与错误:env_logger + anyhow(简化调试与错误传递)

项目结构

github-trending-crawler/
├── Cargo.toml      # 依赖配置
├── src/
│   └── main.rs     # 核心逻辑
└── trending_repos.json # 输出结果

环境搭建与依赖配置

首先初始化项目并进入目录:

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 序列化/反序列化
 = { version = , features = [] }
 = 

 = { version = , features = [] }

 = 
 = 

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

核心逻辑实现

1. 导入模块与初始化

我们需要引入必要的库来管理日志、HTTP 请求、HTML 解析以及数据序列化。初始化日志有助于在运行过程中观察程序状态。

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;

fn init_logger() {
    env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
}
2. 定义数据结构

为了将网页数据转换为 JSON,我们需要定义一个结构体来映射 GitHub 页面的字段。使用 #[derive(Serialize)] 可以自动生成序列化逻辑。

#[derive(Debug, Serialize)]
struct GithubRepo {
    author: String,
    name: String,
    description: Option<String>,
    stars: String,
    forks: String,
    today_stars: String,
    url: String,
}
3. 获取页面内容

发送 GET 请求时,必须设置 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 提取数据

这是最关键的一步。为了提高鲁棒性,我们尽量使用语义化的属性(如 href 后缀、data 属性)而非易变的类名。对于缺失的数据,使用默认值避免程序崩溃。

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();

        // 拼接完整链接
        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());

        // 提取统计信息,缺失则默认为 "0"
        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 文件,方便后续查看或处理。

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 生态中的成熟库构建了高效的异步爬虫。通过 reqwest 处理网络请求,scraper 解析 HTML,配合 tokio 实现高并发能力。代码特别强化了 CSS 选择器的语义化匹配与异常容错处理,降低了因目标网站改版导致的维护成本。最终输出的结构化 JSON 数据可直接用于数据分析或二次开发。

目录

  1. 基于 Rust 实现爬取 GitHub Trending 热门仓库
  2. 技术选型
  3. 项目结构
  4. 环境搭建与依赖配置
  5. HTTP 客户端(异步)
  6. HTML 解析(CSS 选择器)
  7. JSON 序列化/反序列化
  8. 异步运行时
  9. 日志
  10. 错误处理
  11. 核心逻辑实现
  12. 1. 导入模块与初始化
  13. 2. 定义数据结构
  14. 3. 获取页面内容
  15. 4. 解析 HTML 提取数据
  16. 5. 保存结果
  17. 6. 主函数入口
  18. 运行与验证
  19. 总结
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

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

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

更多推荐文章

查看全部
  • Python 零基础学习经验总结与入门技术指南
  • AI 每日动态:Claude Code 记忆插件、LangChain DeepAgents 与具身智能趋势
  • AI 创作者 xAMA 活动:在技术浪潮中做会发光的造浪者
  • 面向文本图的大语言模型高效微调与推理
  • AI 与存储结合:智能存储实践与挑战
  • AI 应用深度解析:分类、现状、商业化与未来趋势
  • Google 防御史上最大 DDoS 攻击:峰值 3.98 亿 rps
  • Java面试:基础、并发与容器高频考点整理
  • C++ 继承进阶:友元、静态成员与菱形继承底层逻辑
  • 本地部署 Wan2.1 视频生成模型及远程访问指南
  • Spring Boot 数据导入导出与报表生成
  • SVM 核心数据结构详解
  • GTC 2026 前瞻:Rubin 平台与 AI 工厂化趋势
  • AI 产品经理必修:神经网络核心原理与应用解析
  • Rust WebAssembly 开发实战:构建高性能前端应用
  • SpringBoot 从零搭建第一个项目
  • 链表面试基础:快慢指针与哨兵节点的实战应用
  • Self-supervised Learning of Person-specific Facial Dynamics for APR 论文阅读
  • Linux Shell 命令与语法底层执行逻辑
  • GitHub Copilot Plan 模式:核心优势与使用场景解析

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如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