🎯 Trình duyệt đám mây tùy chỉnh, chống phát hiện được hỗ trợ bởi Chromium tự phát triển, thiết kế dành cho trình thu thập dữ liệu webtác nhân AI. 👉Dùng thử ngay
Quay lại blog

Lập Trình Web Scraping Bằng Rust Với reqwest và scraper: Hướng Dẫn Thực Tế

Ava Wilson
Ava Wilson

Expert in Web Scraping Technologies

21-Jul-2026

Tóm tắt:

  • Việc lấy dữ liệu từ web bằng Rust vào năm 2026 dựa trên hai gói: reqwest cho yêu cầu HTTP và scraper cho phân tích cú pháp CSS-selector, với tokio điều khiển runtime bất đồng bộ.
  • Trình biên dịch buộc bạn phải xử lý mọi trường hợp lỗi, vì vậy một trình lấy dữ liệu Rust có thể biên dịch thường là một trình lấy dữ liệu sống sót trước markup sai định dạng và các phản hồi không phải 200.
  • tokio::spawn biến một crawler trang thành một crawler đồng thời chỉ trong khoảng năm dòng mã, và hướng dẫn này thực hiện nó trên năm trang cho 50 bản ghi.
  • Cả reqwestscraper đều không thực thi JavaScript, vì vậy một trang được render ở phía khách sẽ trả về markup mà khi phân tích sẽ không có bản ghi nào.
  • API Lấy Dữ Liệu Toàn Cầu Không Có Rác (Scrapeless Universal Scraping API) render trang đầu tiên và trả về HTML mà mã scraper không thay đổi phân tích thành 10 bản ghi.
  • Bắt đầu trên kế hoạch miễn phí của Scrapeless và chạy ví dụ pivot với mục tiêu của bạn.

Rust xuất hiện trong công việc lấy dữ liệu vì một lý do cụ thể: crawler thường là phần của pipeline dữ liệu chạy không giám sát trong nhiều giờ đối với markup mà không ai kiểm soát. Một nhị phân đã qua kiểm tra mượn với không có bộ thu gom rác và việc xử lý Result rõ ràng tại mọi ranh giới là lựa chọn phù hợp cho công việc đó.

Hướng dẫn này xây dựng một trình lấy dữ liệu hoạt động với các phiên bản gói hiện tại, chạy nó, và sau đó chỉ ra chính xác nơi mà ngăn xếp thuần Rust dừng lại - với các con số được đo lường thay vì một cảnh báo.

Những gì bạn cần

Việc lấy dữ liệu web bằng Rust cần ba gói và một bộ công cụ ổn định. Các phiên bản dưới đây là các phát hành công bố hiện tại trên đăng ký gói cộng đồng Rust tại thời điểm viết, và mọi ví dụ ở đây đều được biên dịch và chạy trên các phiên bản này:

Gói Phiên bản Công việc
reqwest 0.13.4 Khách hàng HTTP
scraper 0.27.0 Phân tích HTML và CSS selectors
tokio 1.53.0 Runtime bất đồng bộ
serde_json 1 Xử lý JSON cho phản hồi API

Gói scraper bao bọc html5ever, cùng một engine phân tích được sử dụng trong Servo, vì vậy nó tuân theo tiêu chuẩn phân tích HTML thay vì coi markup như văn bản để regex qua. Điều này quan trọng trên các trang thực, nơi mà các thẻ không được đóng là điều bình thường.

Cài đặt

Tạo dự án và thêm các phụ thuộc:

bash Copy
cargo new rust-scraper
cd rust-scraper

Sau đó khai báo khối phụ thuộc trong Cargo.toml:

toml Copy
[package]
name = "rust-scraper"
version = "0.1.0"
edition = "2021"

[dependencies]
reqwest = { version = "0.13.4", features = ["json"] }
scraper = "0.27.0"
tokio = { version = "1.53.0", features = ["macros", "rt-multi-thread"] }
serde_json = "1"

Tính năng json trên reqwest đưa vào việc tuần tự hóa yêu cầu và phản hồi. Các tính năng macrosrt-multi-thread trên tokio là những gì làm cho thuộc tính #[tokio::main] hoạt động.

Lấy và Phân tích Một Trang

Một trình lấy dữ liệu hoàn chỉnh bằng Rust ngắn hơn so với danh tiếng của nó. Trình này lấy một trang được render từ máy chủ, chọn từng container của trích dẫn và lấy hai trường từ mỗi container:

rust Copy
use scraper::{Html, Selector};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let body = reqwest::get("https://quotes.toscrape.com/")
        .await?
        .error_for_status()?
        .text()
        .await?;

    let document = Html::parse_document(&body);
    let quote_sel = Selector::parse("div.quote").unwrap();
    let text_sel = Selector::parse("span.text").unwrap();
    let author_sel = Selector::parse("small.author").unwrap();

    let mut count = 0;
    for quote in document.select(&quote_sel) {
        let text = quote.select(&text_sel).next().map(|e| e.inner_html());
        let author = quote.select(&author_sel).next().map(|e| e.inner_html());
        if let (Some(t), Some(a)) = (text, author) {
            if count == 0 {
                println!("trích dẫn đầu tiên: {} — {}", t, a);
            }
            count += 1;
        }
    }
    println!("số trích dẫn đã phân tích: {}", count);
    Ok(())
}

Chạy nó sẽ in ra:

text Copy
trích dẫn đầu tiên: “Thế giới mà chúng ta đã tạo ra là một quá trình của suy nghĩ của chúng ta. Nó không thể được thay đổi mà không thay đổi suy nghĩ của chúng ta.” — Albert Einstein
số trích dẫn đã phân tích: 10

Ba chi tiết trong đoạn mã đó mang lại hầu hết sức nặng.

error_for_status() chuyển đổi phản hồi 4xx hoặc 5xx thành một Err thay vì để bạn phân tích một trang lỗi như thể nó là dữ liệu. Nếu không có nó, nội dung body của mã 403 sẽ trở thành không có kết quả nào và trông giống hệt như một tập hợp kết quả trống rỗng. Sự phân biệt giữa các lớp trạng thái này được định nghĩa trong tiêu chuẩn ngữ nghĩa HTTP.
Selector::parse là một hàm có thể bị lỗi vì chuỗi selector được biên dịch, không được giải thích tại thời điểm khớp. Biên dịch các selector một lần ngoài vòng lặp — thay vì cho từng phần tử — là lý do tại sao các cuộc gọi select lồng nhau vẫn rẻ. Cú pháp tuân theo các thông số kỹ thuật Selectors Level 4 của W3C.

Toán tử ? lan truyền ra khỏi main là điều biến Box<dyn std::error::Error> thành một kiểu trả về thực tế. Mỗi cuộc gọi có thể bị lỗi đều quay về cùng một nơi, và quá trình kết thúc với giá trị không bằng không khi thất bại — hữu ích khi trình thu thập dữ liệu chạy từ một trình lập lịch.

Thu thập trang một cách đồng thời

Câu chuyện về sự đồng thời của Rust là lý do chính để sử dụng nó ở đây, và tokio::spawn là nơi nó xuất hiện. Mỗi trang trở thành một tác vụ độc lập, tất cả chúng chia sẻ một khách hàng kết nối được quản lý, và các kết quả được thu thập theo thứ tự:

rust Copy
use scraper::{Html, Selector};

async fn page_quote_count(client: &reqwest::Client, page: u32) -> Result<usize, reqwest::Error> {
    let url = format!("https://quotes.toscrape.com/page/{page}/");
    let body = client.get(&url).send().await?.error_for_status()?.text().await?;
    let quote_sel = Selector::parse("div.quote").unwrap();
    Ok(Html::parse_document(&body).select(&quote_sel).count())
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = reqwest::Client::new();

    let tasks: Vec<_> = (1..=5)
        .map(|page| {
            let client = client.clone();
            tokio::spawn(async move { (page, page_quote_count(&client, page).await) })
        })
        .collect();

    let mut total = 0;
    for task in tasks {
        let (page, result) = task.await?;
        let count = result?;
        println!("page {page}: {count} quotes");
        total += count;
    }
    println!("total quotes across 5 pages: {total}");
    Ok(())
}

Chạy:

text Copy
page 1: 10 quotes
page 2: 10 quotes
page 3: 10 quotes
page 4: 10 quotes
page 5: 10 quotes
total quotes across 5 pages: 50

reqwest::Client dễ sao chép vì bản sao chia sẻ hồ bơi kết nối bên dưới. Tạo một khách hàng mới cho mỗi tác vụ sẽ mở một hồ bơi mới mỗi lần và bỏ lỡ lợi ích. Giữ cho khoảng trang bị giới hạn và được định kích thước để điều mà mục tiêu có thể phục vụ một cách thoải mái — đồng thời là một công cụ để hoàn thành một khối lượng công việc đã biết, không phải cho việc phát hành nhiều yêu cầu đồng thời nhất có thể mà runtime cho phép.

Sẵn sàng để chỉ vào một mục tiêu render bên server? Tạo một tài khoản Scrapeless miễn phí và giữ nguyên mã phân tích mà bạn đã có.

Nơi reqwest và scraper dừng lại

Không crate nào thực thi JavaScript. reqwest trả về các byte mà server gửi, và scraper phân tích chính xác những byte đó — vì vậy, trên một trang xây dựng nội dung của nó trong trình duyệt, các selector không khớp với gì cả.

Điều này có thể đo được chứ không phải lý thuyết. Trang web sử dụng ở trên công bố một phiên bản render client của cùng dữ liệu tại /js/. Chỉ việc chỉ đạo logic phân tích giống hệt vào nó:

rust Copy
use scraper::{Html, Selector};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let body = reqwest::get("https://quotes.toscrape.com/js/")
        .await?
        .error_for_status()?
        .text()
        .await?;

    let document = Html::parse_document(&body);
    let quote_sel = Selector::parse("div.quote").unwrap();
    let count = document.select(&quote_sel).count();

    println!("html bytes: {}", body.len());
    println!("quotes parsed: {}", count);
    Ok(())
}
text Copy
html bytes: 5808
quotes parsed: 0

Yêu cầu thành công. Trạng thái là 200. Bộ phân tích đã hoạt động chính xác trên 5,808 byte HTML hợp lệ mà đơn giản không chứa bất kỳ phần tử nào của quote — chúng được viết vào DOM sau khi một script chạy. Một trình thu thập dữ liệu chỉ kiểm tra lỗi HTTP coi đây là một trang trống thay vì một khả năng bị thiếu, đó là lý do tại sao cuộc gọi error_for_status() trước đó là cần thiết nhưng không đủ.

Render trang bằng Universal Scraping API

API Universal Scraping Scrapeless đóng lỗ hổng đó bằng cách render trang trong trình duyệt đám mây và trả về HTML kết quả, điều này có nghĩa là bên Rust vẫn là một cuộc gọi HTTP đơn giản. Đặt js_render thành true và nội dung phản hồi bao gồm DOM sau script.

Xác thực bằng khóa của bạn từ môi trường:

bash Copy
export SCRAPELESS_API_KEY="your_api_key_here"

Sau đó chỉ cần thay thế lớp fetch — mã selector bên dưới giống hệt với ví dụ đầu tiên:

rust Copy
use scraper::{Html, Selector};
use serde_json::json;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let api_key = std::env::var("SCRAPELESS_API_KEY")?;

    let payload = json!({

"diễn viên": "unlocker.webunlocker",
"đầu vào": {
"url": "https://quotes.toscrape.com/js/",
"js_render": true,
"headless": true
}
});

Copy
let phong_bì: serde_json::Value = reqwest::Client::new()
    .post("https://api.scrapeless.com/api/v2/unlocker/request")
    .header("x-api-token", api_key)
    .json(&payload)
    .send()
    .await?
    .error_for_status()?
    .json()
    .await?;

let nội_dung = phong_bì["data"].as_str().unwrap_or_default();

let tài_liệu = Html::parse_document(nội_dung);
let chọn_trích_dẫn = Selector::parse("div.quote").unwrap();
let chọn_văn_bản = Selector::parse("span.text").unwrap();
let chọn_tác_giả = Selector::parse("small.author").unwrap();

let mut đếm = 0;
for trích_dẫn in tài_liệu.select(&chọn_trích_dẫn) {
    let văn_bản = trích_dẫn.select(&chọn_văn_bản).next().map(|e| e.inner_html());
    let tác_giả = trích_dẫn.select(&chọn_tác_giả).next().map(|e| e.inner_html());
    if let (Some(t), Some(a)) = (văn_bản, tác_giả) {
        if đếm == 0 {
            println!("trích dẫn đầu tiên: {} — {}", t, a);
        }
        đếm += 1;
    }
}
println!("byte html: {}", nội_dung.len());
println!("trích dẫn đã phân tích: {}", đếm);
Ok(())

}

Copy
```text
trích dẫn đầu tiên: “Thế giới mà chúng ta đã tạo ra là một quá trình của tư duy của chúng ta. Nó không thể thay đổi nếu không thay đổi tư duy của chúng ta.” — Albert Einstein
byte html: 8985
trích dẫn đã phân tích: 10

Cùng trang, cùng bộ chọn, cùng phiên bản crate. Sự thay đổi duy nhất là lớp nào đã lấy HTML, và số lượng bản ghi từ 0 thành 10 trong khi payload tăng từ 5,808 lên 8,985 byte — sự khác biệt là markup trích dẫn mà script đã ghi vào DOM.

Phản hồi đến dưới dạng một phong bì JSON với tài liệu được tạo ra trong data dưới dạng chuỗi, đó là lý do mà ví dụ này phân tích thành serde_json::Value trước và đưa data cho Html::parse_document. Chi tiết về các tùy chọn render nằm trong tài liệu Scrapeless, và cùng một hành vi js_render được đề cập sâu hơn trong hướng dẫn render JS.

Khắc phục sự cố

Các bộ chọn không khớp với bất cứ điều gì trên một trang mà bạn có thể thấy trong trình duyệt. In chiều dài phản hồi trước, như ví dụ JS ở trên. Một vài nghìn byte với không có sự khớp nào thường có nghĩa là nội dung được render trên khách hàng, không phải là bộ chọn sai. Xem mã nguồn của trang thay vì trình kiểm tra — trình kiểm tra hiển thị DOM sau khi các script đã chạy, điều này không phải là những gì reqwest đã nhận.

Selector::parse gặp sự cố trong quá trình khởi động. Chuỗi bộ chọn không phải là CSS hợp lệ. Các pseudo-elements và một số phần mở rộng kiểu jQuery không phải là một phần của đặc tả và sẽ không biên dịch được.

Việc xây dựng gặp lỗi trong backend TLS. reqwest biên dịch một stack TLS; trên một container tối thiểu, hệ thống cần một chuỗi công cụ C và CMake có sẵn, hoặc bạn có thể chuyển sang backend rustls thuần túy - Rust thông qua các cờ tính năng.

inner_html() trả về markup thay vì văn bản. Phương thức đó trả về tất cả mọi thứ bên trong phần tử, bao gồm cả thẻ. Sử dụng text() và thu thập các đoạn khi một trường có thể chứa các phần tử lồng nhau.

Trước khi chỉ trích điều này vào một mục tiêu trực tuyến, hãy kiểm tra các điều khoản của trang web và các chỉ thị /robots.txt, theo tiêu chuẩn Giao thức Loại trừ Robot. Giữ việc thu thập dữ liệu ở khía cạnh công khai và một khối lượng mà mục tiêu có thể phục vụ một cách thoải mái.

Kết luận

Rust mang đến cho bạn một scraper xử lý thất bại một cách rõ ràng và song song mà không cần nhiều nghi thức — reqwest cho việc vận chuyển, scraper cho các bộ chọn, tokio cho tính đồng thời, và một trình biên dịch sẽ không để bạn phớt lờ một Result. Đống này hoàn toàn bao quát các trang được render trên máy chủ.

Điều nó không làm là chạy JavaScript, và chi phí của khoảng trống đó là một không mãi mãi chứ không phải là một lỗi. Đo lường nó là thói quen hữu ích: 10 bản ghi trên trang được render trên máy chủ, 0 trên bản sao được render từ phía khách, 10 một lần nữa khi một thứ gì đó render trang trước khi Rust phân tích nó.

Bắt đầu với kế hoạch miễn phí của Scrapeless để thực hiện bước render đối với các mục tiêu của riêng bạn, và xem lại giá cả Scrapeless hiện tại khi bạn xác định kích thước của một công việc.

Câu hỏi thường gặp

Hỏi: Crate Rust nào là tốt nhất cho việc web scraping?
reqwest phối hợp với scraper bao phủ hầu hết công việc. reqwest xử lý HTTP với API ưu tiên bất đồng bộ, và scraper cung cấp truy vấn CSS-selector trên cây phân tích html5ever. Chỉ nên tìm đến một crate trình duyệt không đầu hoặc một API rendu khi trang xây dựng nội dung của nó ở phía client.

H: Rust có tốt cho việc thu thập dữ liệu web so với Python không?

Rust là một lựa chọn tốt khi trình thu thập dữ liệu chạy lâu, chạy đồng thời hoặc chạy không người giám sát, vì không có khoảng dừng thu gom rác và trình biên dịch buộc phải xử lý mọi đường dẫn lỗi. Python vẫn chiếm ưu thế về độ phong phú của hệ sinh thái và tốc độ lặp lại cho việc trích xuất một lần. Các khái niệm phân tích chuyển giao trực tiếp giữa chúng.

H: reqwest có thể thực thi JavaScript không?

Không. reqwest là một client HTTP và trả lại các byte mà máy chủ đã gửi. Trên một trang được render bởi client, điều đó có nghĩa là HTML hợp lệ mà không có nội dung nào trong đó — ví dụ trên phân tích 5,808 byte thành không có bản ghi nào. Việc render phải diễn ra ở đâu đó khác, hoặc trong một trình duyệt không đầu hoặc thông qua một API trả về DOM đã được render.

H: Tôi có cần async và tokio cho một trình thu thập dữ liệu đơn giản không?

Không nhất thiết — reqwest cung cấp một client blocking dưới dạng một cờ tính năng, điều này phù hợp cho một yêu cầu tuần tự đơn lẻ. Client bất đồng bộ xứng đáng với sự phụ thuộc vào tokio ngay khi bạn lấy hơn một trang, vì đó là lúc tokio::spawn biến việc lấy tuần tự thành đồng thời.

H: Làm thế nào để tôi giữ cho trình thu thập dữ liệu Rust không bị hỏng khi trang web thay đổi?

Xác nhận cấu trúc thay vì tin tưởng vào nó. Kiểm tra rằng bộ chọn container đã khớp với một số lượng phần tử hợp lý trước khi trích xuất các trường, và coi một con số đột ngột bằng không là một thất bại thay vì một kết quả trống. Biên dịch các bộ chọn một lần tại thời điểm khởi động cũng giúp phát hiện CSS không hợp lệ ngay lập tức thay vì trong quá trình thu thập.

Tại Scrapless, chúng tôi chỉ truy cập dữ liệu có sẵn công khai trong khi tuân thủ nghiêm ngặt các luật, quy định và chính sách bảo mật trang web hiện hành. Nội dung trong blog này chỉ nhằm mục đích trình diễn và không liên quan đến bất kỳ hoạt động bất hợp pháp hoặc vi phạm nào. Chúng tôi không đảm bảo và từ chối mọi trách nhiệm đối với việc sử dụng thông tin từ blog này hoặc các liên kết của bên thứ ba. Trước khi tham gia vào bất kỳ hoạt động cạo nào, hãy tham khảo ý kiến ​​cố vấn pháp lý của bạn và xem xét các điều khoản dịch vụ của trang web mục tiêu hoặc có được các quyền cần thiết.

Bài viết phổ biến nhất

Danh mục