In this Article
Web scraping Rust adalah praktik mengambil dan parsing halaman web dengan bahasa pemrograman Rust serta ekosistem crate-nya. Rust menarik bagi developer yang menginginkan performa yang dapat diprediksi dan keamanan memori tanpa garbage collector, sehingga sangat cocok untuk crawler yang berjalan terus-menerus atau dalam volume besar.
Panduan ini adalah pembahasan praktis yang mengutamakan kode, berpusat pada HTTP client reqwest dan crate parsing scraper. Panduan ini membahas penyiapan project dan fetch minimal, CSS selector, model data terstruktur, ekspor JSON dan CSV, pagination, penanganan error dengan retry dan backoff, concurrency async dengan tokio, konfigurasi proxy, serta fallback browser headless untuk situs yang sarat JavaScript. Setiap snippet dirancang untuk dapat di-compile dan dijalankan.
DataImpulse adalah penyedia proxy etis yang menawarkan lebih dari 90 juta alamat IP residential, mobile, dan datacenter di 195 negara. DataImpulse menggunakan model pay-as-you-go mulai dari 1 dolar per GB dengan lalu lintas yang tidak kedaluwarsa, dan digunakan untuk web scraping, verifikasi iklan, pemantauan harga, riset pasar, dan pengelolaan multi-akun.
Fakta Utama
- Stack inti: Web scraping Rust biasanya memadukan HTTP client reqwest dengan crate scraper untuk parsing HTML, ditambah tokio untuk concurrency async dan serde untuk mengekspor data terstruktur.
- Jenis proxy terbaik: proxy residential rotating, yang menggunakan IP konsumen asli untuk menghindari deteksi.
- Harga: mulai dari 1 dolar per GB, pay-as-you-go, dengan lalu lintas yang tidak kedaluwarsa dan tanpa langganan.
- Cakupan: lebih dari 90 juta IP yang diperoleh secara etis di 195 negara.
- Keandalan: tingkat keberhasilan 99,51%, dengan nilai 4,8 dari 5 di G2.
- Protokol dan targeting: HTTP, HTTPS, dan SOCKS5, termasuk country targeting.

Mengapa menggunakan Rust untuk web scraping?
Rust cocok untuk web scraping ketika kecepatan, penggunaan memori rendah, dan keandalan lebih penting daripada pembuatan prototipe cepat. Binary yang telah di-compile berjalan dekat dengan hardware, dan borrow checker menangkap berbagai jenis bug concurrency pada saat compile, bukan saat production.
Karakteristik tersebut bermanfaat dalam beberapa skenario umum:
- Throughput: Crawler Rust yang telah di-compile memproses volume halaman besar dengan overhead CPU dan memori yang lebih rendah daripada bahasa terinterpretasi, sehingga mengurangi biaya infrastruktur pada pekerjaan jangka panjang.
- Concurrency yang aman: Runtime async tokio memungkinkan Anda menjalankan ribuan request secara concurrent, sementara type system mencegah data race di antara task tersebut.
- Penggunaan resource yang dapat diprediksi: Tanpa garbage collector, latency menjadi lebih stabil, yang membantu crawler yang berjalan selama berjam-jam atau berhari-hari.
Konsekuensinya adalah kurva belajar yang lebih terjal dan waktu compile yang lebih lama, sehingga Rust lebih menguntungkan untuk project yang berjalan dalam skala besar daripada script sekali pakai. Apa pun bahasanya, aturan dasarnya sama: patuhi robots.txt, batasi laju request Anda, dan ikuti praktik terbaik web scraping agar crawler Anda tetap bertindak dengan baik.
Bagaimana menyiapkan project scraping Rust?
Siapkan project scraping Rust dengan membuat package Cargo baru dan mendeklarasikan crate yang Anda perlukan di Cargo.toml. Cargo adalah build tool dan package manager Rust, sehingga Cargo menyelesaikan dependency dan meng-compile project untuk Anda.
Jalankan cargo new rust-scraper untuk membuat kerangka project, lalu edit Cargo.toml yang dihasilkan. Kumpulan dependency di bawah ini mencakup semua hal dalam panduan ini: HTTP, parsing, async, serialisasi, output CSV, stream, dan penanganan error yang praktis.
[package]
name = "rust-scraper"
version = "0.1.0"
edition = "2021"
[dependencies]
reqwest = { version = "0.12", features = ["json", "socks"] }
scraper = "0.20"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
csv = "1.3"
futures = "0.3"
anyhow = "1"
thiserror = "1"
Di sini reqwest menangani request (fitur socks mengaktifkan proxy SOCKS5, fitur json menambahkan helper JSON), scraper melakukan parsing HTML dengan CSS selector, tokio menyediakan runtime async, serde dan serde_json melakukan serialisasi data Anda, csv menulis spreadsheet, futures menyediakan stream combinator untuk concurrency, dan anyhow bersama thiserror menyederhanakan error. Jalankan cargo build sekali untuk mengunduh dan meng-compile semuanya sebelum menulis request pertama Anda.
Bagaimana melakukan fetch dan parsing halaman dengan reqwest dan scraper?
Lakukan fetch halaman dengan mengirim request GET melalui reqwest, lalu muat response body ke scraper untuk melakukan query dengan CSS selector. Contoh di bawah berjalan secara asynchronous pada tokio, mengambil halaman, dan mencetak teks setiap elemen H1.
use scraper::{Html, Selector};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let url = "https://example.com";
let body = reqwest::get(url).await?.text().await?;
let document = Html::parse_document(&body);
let heading = Selector::parse("h1").unwrap();
for element in document.select(&heading) {
let text: String = element.text().collect();
println!("{}", text.trim());
}
Ok(())
}
Alurnya selalu sama: reqwest::get mengembalikan response, .text() membaca body-nya sebagai string, dan Html::parse_document mengubah string tersebut menjadi tree yang dapat di-query. Operator ? meneruskan setiap error jaringan atau decoding hingga ke main. Pemanggilan .text() pada elemen yang cocok mengumpulkan text node-nya, lalu Anda rapikan dan gunakan. Pendekatan HTML mentah ini cepat, tetapi perlu diingat bahwa pendekatan ini hanya melihat markup yang dikembalikan server, bukan konten yang nantinya dirender browser dengan JavaScript.
Bagaimana menargetkan elemen dengan CSS selector?
Targetkan elemen dengan memberikan string CSS selector standar ke Selector::parse, sama seperti di console browser. Crate scraper mendukung tag, class, id, kecocokan atribut, path turunan, dan pseudo-class struktural.
use scraper::Selector;
// Match by tag
let links = Selector::parse("a").unwrap();
// Match by class
let prices = Selector::parse(".price").unwrap();
// Match by id
let main = Selector::parse("#main-content").unwrap();
// Match by attribute value
let external = Selector::parse("a[rel=\"nofollow\"]").unwrap();
// Descendant path (a title inside a product card)
let titles = Selector::parse("div.product h2.title").unwrap();
// Direct child plus pseudo-class
let first_row = Selector::parse("table > tbody > tr:first-child").unwrap();
Compile setiap selector satu kali dan gunakan kembali, alih-alih melakukan parsing di dalam loop, karena parsing memiliki sedikit biaya. Ketika selector mungkin tidak cocok, pilih .next() dengan fallback daripada .unwrap() pada elemen agar field yang tidak ada tidak membuat seluruh proses panic. Memeriksa halaman target dengan element picker browser Anda adalah cara tercepat untuk menemukan nama class dan atribut yang stabil untuk dipilih.
Bagaimana mengekspor data hasil scraping ke JSON dan CSV?
Ekspor data hasil scraping dengan memodelkan setiap record sebagai struct yang menggunakan derive Serialize, lalu tulis slice struct tersebut ke JSON dengan serde_json atau ke CSV dengan crate csv. Mendefinisikan model bertipe menjaga extraction dan output tetap selaras serta membuat kode mudah dipahami.
Pertama, modelkan record dan ambil field dari setiap elemen container:
use scraper::{Html, Selector};
use serde::Serialize;
#[derive(Debug, Serialize)]
struct Product {
name: String,
price: String,
url: String,
}
fn parse_products(html: &str) -> Vec<Product> {
let document = Html::parse_document(html);
let card = Selector::parse("div.product").unwrap();
let name_sel = Selector::parse("h2.title").unwrap();
let price_sel = Selector::parse("span.price").unwrap();
let link_sel = Selector::parse("a").unwrap();
let mut products = Vec::new();
for card_el in document.select(&card) {
let name = card_el
.select(&name_sel)
.next()
.map(|e| e.text().collect::<String>().trim().to_string())
.unwrap_or_default();
let price = card_el
.select(&price_sel)
.next()
.map(|e| e.text().collect::<String>().trim().to_string())
.unwrap_or_default();
let url = card_el
.select(&link_sel)
.next()
.and_then(|e| e.value().attr("href"))
.unwrap_or_default()
.to_string();
products.push(Product { name, price, url });
}
products
}
Karena Product menggunakan derive Serialize, struct yang sama dapat digunakan oleh kedua exporter tanpa pekerjaan tambahan:
use std::fs::File;
use std::io::Write;
fn export_json(products: &[Product]) -> anyhow::Result<()> {
let json = serde_json::to_string_pretty(products)?;
let mut file = File::create("products.json")?;
file.write_all(json.as_bytes())?;
Ok(())
}
fn export_csv(products: &[Product]) -> anyhow::Result<()> {
let mut writer = csv::Writer::from_path("products.csv")?;
for product in products {
writer.serialize(product)?;
}
writer.flush()?;
Ok(())
}
Fungsi to_string_pretty dari serde_json menghasilkan JSON yang mudah dibaca manusia, sedangkan fungsi serialize dari crate csv memetakan setiap struct menjadi satu baris dengan header yang berasal dari nama field. Menggunakan kembali satu model bertipe untuk scraping, JSON, dan CSV membuat pipeline Rust mudah dipelihara ketika situs target berubah.
Bagaimana melakukan scraping beberapa halaman dengan loop pagination?
Lakukan scraping beberapa halaman dengan melakukan loop pada nomor halaman atau URL halaman berikutnya, serta gunakan kembali satu client reqwest agar koneksi dipool di seluruh request. Bangun setiap URL, lakukan fetch, parsing dengan fungsi dari bagian sebelumnya, dan berhenti ketika halaman tidak mengembalikan hasil.
use scraper::Html;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = reqwest::Client::new();
let mut all_products = Vec::new();
for page in 1..=10 {
let url = format!("https://example.com/products?page={}", page);
let body = client.get(&url).send().await?.text().await?;
let products = parse_products(&body);
// Stop when a page returns no results
if products.is_empty() {
break;
}
all_products.extend(products);
// Pause between requests to stay polite
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
}
println!("Collected {} products", all_products.len());
Ok(())
}
Membuat client sekali dengan reqwest::Client::new() di luar loop memungkinkannya menggunakan kembali koneksi TCP, yang terasa jauh lebih cepat daripada client baru untuk setiap request. Pemeriksaan hasil kosong mengakhiri loop secara alami saat Anda mencapai halaman terakhir, dan jeda singkat memberi jarak antar-request agar Anda tidak membebani server. Untuk situs yang menggunakan link berikutnya alih-alih halaman bernomor, ekstrak href anchor halaman berikutnya pada setiap iterasi dan ikuti hingga link tersebut hilang.
Bagaimana menangani error dan retry dengan backoff?
Tangani error dengan mengembalikan error bertipe dari fungsi fetch Anda, lalu lakukan retry untuk kegagalan sementara dengan exponential backoff. Jaringan dapat timeout dan server dapat mengembalikan status code sementara, sehingga scraper yang andal menganggap satu kegagalan sebagai hal yang wajar, bukan fatal.
Crate thiserror memungkinkan Anda mendefinisikan enum error ringkas yang otomatis dikonversi dari error reqwest:
use thiserror::Error;
#[derive(Error, Debug)]
enum ScrapeError {
#[error("request failed: {0}")]
Request(#[from] reqwest::Error),
#[error("unexpected status code: {0}")]
Status(reqwest::StatusCode),
#[error("selector matched no elements")]
Empty,
}
async fn fetch_page(client: &reqwest::Client, url: &str) -> Result<String, ScrapeError> {
let response = client.get(url).send().await?;
if !response.status().is_success() {
return Err(ScrapeError::Status(response.status()));
}
Ok(response.text().await?)
}
Atribut #[from] berarti kegagalan reqwest berubah menjadi ScrapeError::Request secara otomatis melalui operator ?. Selain error bertipe, bungkus request dalam loop retry yang waktu tunggunya bertambah setelah setiap percobaan:
use std::time::Duration;
async fn fetch_with_backoff(client: &reqwest::Client, url: &str) -> anyhow::Result<String> {
let max_attempts = 5;
for attempt in 1..=max_attempts {
match client.get(url).send().await {
Ok(resp) if resp.status().is_success() => {
return Ok(resp.text().await?);
}
Ok(resp) => {
eprintln!("attempt {attempt}: status {}", resp.status());
}
Err(err) => {
eprintln!("attempt {attempt}: {err}");
}
}
// Exponential backoff: 0.5s, 1s, 2s, 4s, 8s
let backoff = Duration::from_millis(500 * 2u64.pow(attempt - 1));
tokio::time::sleep(backoff).await;
}
anyhow::bail!("giving up on {url} after {max_attempts} attempts")
}
Exponential backoff (0,5s, 1s, 2s, 4s, 8s di sini) memberi kesempatan kepada server yang terkena rate limit atau kelebihan beban sesaat untuk pulih, alih-alih terus-menerus membebaninya. Menggabungkan retry dengan proxy rotating bahkan lebih efektif: request yang diblokir atau dibatasi pada satu IP dapat berhasil pada percobaan berikutnya dari exit IP yang berbeda.
Bagaimana menjalankan request concurrent dengan tokio?
Jalankan request concurrent dengan mengubah daftar URL Anda menjadi stream async dan menerapkan buffer_unordered, yang membatasi jumlah request yang berjalan sekaligus. Ini adalah faktor peningkatan performa terbesar dalam scraper Rust, dan tokio bersama crate futures membuatnya aman.
use futures::stream::{self, StreamExt};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = reqwest::Client::new();
let urls: Vec<String> = (1..=50)
.map(|n| format!("https://example.com/item/{}", n))
.collect();
let bodies = stream::iter(urls)
.map(|url| {
let client = client.clone();
async move {
let text = client.get(&url).send().await?.text().await?;
Ok::<_, reqwest::Error>((url, text))
}
})
.buffer_unordered(10); // at most 10 requests in flight
bodies
.for_each(|result| async {
match result {
Ok((url, body)) => println!("{}: {} bytes", url, body.len()),
Err(err) => eprintln!("error: {}", err),
}
})
.await;
Ok(())
}
Di sini stream::iter mengubah daftar URL menjadi stream, .map memulai satu request async per URL, dan buffer_unordered(10) mempertahankan paling banyak sepuluh request yang berjalan concurrent dan menghasilkan hasil saat selesai. Cloning client per task murah karena reqwest Client adalah Arc di sekitar connection pool bersama. Sesuaikan batas concurrency untuk menyeimbangkan kecepatan dengan beban yang Anda berikan pada target: sepuluh hingga dua puluh merupakan titik awal yang wajar, dan memadukan concurrency lebih tinggi dengan pool proxy menyebarkan request tersebut ke banyak IP.
Bagaimana merutekan lalu lintas scraping Rust melalui proxy?
Rutekan lalu lintas melalui proxy dengan membuat client reqwest menggunakan reqwest::Proxy dan melampirkan gateway serta kredensial Anda dengan .basic_auth. Mengirim request melalui IP rotating mendistribusikan beban, menjangkau konten khusus geografi, dan mengurangi kemungkinan terkena rate limit. IP rotating adalah salah satu teknik paling efektif untuk scraping tanpa diblokir.
use reqwest::Proxy;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Authenticated gateway; rotates the exit IP on each request
let proxy = Proxy::all("http://gw.dataimpulse.com:823")?
.basic_auth("login", "password");
let client = reqwest::Client::builder()
.proxy(proxy)
.timeout(std::time::Duration::from_secs(30))
.build()?;
let body = client
.get("https://httpbin.org/ip")
.send()
.await?
.text()
.await?;
println!("{}", body);
Ok(())
}
Replace login dan password dengan kredensial Anda dan sesuaikan port untuk jaringan yang Anda perlukan. Jika proxy menolak request Anda dengan status 407, penyebabnya hampir selalu kredensial yang hilang atau salah; panduan tentang error HTTP 407 serta panduan tentang autentikasi proxy menjelaskan cara memperbaikinya. DataImpulse menyediakan proxy residential, proxy datacenter, dan proxy mobile melalui HTTP, HTTPS, dan SOCKS5, dengan sesi rotating dan sticky. Country targeting sudah termasuk, sedangkan targeting state, city, ZIP, dan ASN adalah add-on berbayar.
Bagaimana melakukan scraping halaman yang sarat JavaScript, dan kapan Rust menjadi pilihan tepat?
Lakukan scraping halaman yang sarat JavaScript dengan menjalankan browser headless sungguhan, karena reqwest dan scraper hanya melihat HTML mentah dari server dan tidak pernah menjalankan script. Ketika konten hanya muncul setelah client-side rendering, gunakan crate browser automation seperti thirtyfour, yang menggunakan protokol WebDriver, atau chromiumoxide, yang mengendalikan Chrome melalui DevTools Protocol.
use thirtyfour::prelude::*;
#[tokio::main]
async fn main() -> WebDriverResult<()> {
let caps = DesiredCapabilities::chrome();
let driver = WebDriver::new("http://localhost:9515", caps).await?;
driver.goto("https://example.com").await?;
// Wait for client-side content to render, then read it
let heading = driver.find(By::Css("h1")).await?;
println!("{}", heading.text().await?);
driver.quit().await?;
Ok(())
}
Contoh thirtyfour terhubung ke chromedriver yang berjalan, memuat halaman, menunggu elemen dirender, lalu membaca teksnya. Pendekatan ini jauh lebih berat daripada fetch HTTP biasa, jadi gunakan hanya untuk halaman yang benar-benar membutuhkannya dan pertahankan jalur reqwest yang cepat untuk hal lainnya. Untuk pembahasan lebih dalam mengenai kompromi tersebut, lihat panduan tentang scraping halaman web dinamis dan web scraping dengan JavaScript. Tabel di bawah merangkum bagaimana crate utama saling melengkapi:
| Crate | Peran | Menjalankan JavaScript | Paling sesuai untuk |
|---|---|---|---|
| reqwest | HTTP client | Tidak | Mengambil HTML mentah dan JSON API |
| scraper | Parser HTML | Tidak | Ekstraksi data dengan CSS selector |
| thirtyfour | Client WebDriver | Ya | Browser automation penuh dan formulir |
| chromiumoxide | DevTools Protocol | Ya | Kontrol Chrome headless yang cepat |
Untuk pilihan bahasa, pilih Rust saat Anda memerlukan performa berkelanjutan dan penggunaan resource rendah dalam skala besar, dan pilih Python saat Anda mengutamakan pengembangan cepat serta ekosistem scraping yang lebih luas seperti Scrapy dan Playwright. Pola yang umum adalah membuat prototipe aturan extraction di Python, lalu memindahkan hot path ke Rust setelah logikanya stabil dan volume bertambah. Apa pun pilihan Anda, network layer sama pentingnya dengan parser, dan proxy yang diperoleh secara etis menjaga pekerjaan besar tetap andal.

Pertanyaan yang sering diajukan
Crate Rust mana yang terbaik untuk web scraping?
Kombinasi yang paling umum adalah reqwest untuk request HTTP dan scraper untuk parsing HTML dengan CSS selector. Tambahkan tokio untuk concurrency async, serde untuk mengekspor data, dan crate browser automation seperti thirtyfour atau chromiumoxide saat situs memerlukan rendering JavaScript.
Apakah Rust dapat melakukan scraping halaman yang dirender JavaScript?
Tidak hanya dengan reqwest dan scraper, karena keduanya hanya melihat HTML mentah yang dikembalikan server dan tidak dapat menjalankan script. Untuk melakukan scraping konten yang dirender client, jalankan browser headless melalui crate WebDriver seperti thirtyfour atau crate DevTools Protocol seperti chromiumoxide.
Bagaimana mengekspor data hasil scraping di Rust?
Modelkan setiap record sebagai struct yang menggunakan derive Serialize dari serde, lalu tulis slice struct tersebut ke JSON dengan serde_json atau ke CSV dengan crate csv. Satu model bertipe dapat digunakan untuk kedua format tanpa menduplikasi logika extraction.
Bagaimana menjalankan request concurrent tanpa membebani situs?
Ubah URL Anda menjadi stream async dan terapkan buffer_unordered dengan batas tetap, yang membatasi jumlah request yang berjalan sekaligus. Mulailah dengan sekitar sepuluh hingga dua puluh request concurrent dan padukan concurrency lebih tinggi dengan pool proxy rotating untuk menyebarkan beban ke banyak IP.
Apakah saya memerlukan proxy untuk web scraping Rust?
Tidak untuk pekerjaan kecil, tetapi proxy menjadi penting dalam skala besar untuk mendistribusikan request dan menjangkau konten khusus geografi. Konfigurasikan dengan reqwest::Proxy dan basic_auth, serta gunakan IP residential atau mobile rotating untuk mengurangi rate limit dan pemblokiran.
Kapan DataImpulse bukan pilihan yang tepat?
Jika Anda memerlukan proxy ISP statis, API scraping yang dikelola sepenuhnya, atau akses ke situs perbankan dan pemerintah, DataImpulse bukan solusi yang tepat. DataImpulse berfokus pada proxy residential, mobile, dan datacenter rotating untuk mengumpulkan data publik dan mengakses konten.
Lakukan scraping dengan andal menggunakan proxy etis
Setelah scraper Rust Anda melakukan fetch, parsing, dan ekspor dengan lancar, jaringan proxy yang andal menjaganya tetap berjalan dalam skala besar. DataImpulse menawarkan IP yang diperoleh secara etis dengan pay-as-you-go mulai dari 1 dolar per GB dan lalu lintas yang tidak kedaluwarsa, sehingga Anda dapat membuat akun dan merutekan request pertama Anda dalam hitungan menit.
