実際のサイトで5分以上動作するパーサーは、早かれ遅かれIPによるブロックに直面します。
Rustとreqwestライブラリは優れたパフォーマンスを提供しますが、
プロキシのローテーションの問題はデフォルトでは解決されていないため、自分で実装する必要があります。
この記事では、SOCKS5およびHTTP(S)プロキシを接続し、クライアントプールを構築し、IPローテーションを整理して、
パーサーが最初の429および403の応答の後にクラッシュしないようにする方法を解説します。
パーサーにプロキシが必要な理由とその不在の危険性
マーケットプレイス、価格集約サイト、ソーシャルメディアのようなサイトは、1つのIPからの異常なアクティビティを監視しています。 秒間数十件のリクエスト、同一のUser-Agent、リクエスト間のクッキーの不在などです。疑わしい行動のカウントが閾値を超えると、 IPはバンリストに載せられます — 一時的または永久的です。パーサーにとってこれは、IPが手動で変更されるまでデータ収集が完全に停止することを意味します。
プロキシは、この問題を解決し、数十または数百のIPアドレス間でリクエストを分散させます。この際、単に「プロキシを設定する」だけでなく、 接続プールとローテーションを適切に整理することが重要です。そうしないと、reqwest自体の制限(1クライアント = 1プール接続)に達するか、 同じプロキシからのリクエストをあまりにも頻繁に送信することになります。次に、これらの問題を順番に解決していきます: まずは基本的な接続、 次にプールのアーキテクチャです。
プロダクションパーサーにとって重要なのは、IPのローテーションだけでなく、アドレス自体の質です。Wildberries、Ozon、Avitoをパースする場合、 データセンターのIPは実際のユーザーのIPよりも早くバンされます — プロキシタイプの選択に関するセクションで詳しく説明します。
reqwestにおけるHTTP/HTTPSプロキシの基本設定
reqwestは、Proxy構造体を通じてプロキシをサポートしており、
これをClientBuilderに渡す必要があります。
Cargo.tomlに依存関係を追加します:
[dependencies]
reqwest = { version = "0.12", features = ["socks", "json"] }
tokio = { version = "1", features = ["full"] }
ログインとパスワードによる認証を必要とするHTTPプロキシの最小例:
use reqwest::{Client, Proxy};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let proxy = Proxy::all("http://gate.proxycove.com:8000")?
.basic_auth("user_login", "user_password");
let client = Client::builder()
.proxy(proxy)
.timeout(std::time::Duration::from_secs(15))
.build()?;
let resp = client
.get("https://httpbin.org/ip")
.send()
.await?
.text()
.await?;
println!("プロキシ経由の応答: {resp}");
Ok(())
}
Proxy::allメソッドは、
プロキシを介してHTTPとHTTPSのトラフィックを両方ともルーティングします。プロトコルを分ける必要がある場合は、
Proxy::http()と
Proxy::https()
を個別に使用します — これは、プロキシプロバイダーが異なるポートで異なるプロトコルをリッスンしている場合に便利です。
SOCKS5プロキシの接続
SOCKS5はHTTPプロキシよりも低いレベルで動作し、リクエストの内容を解析せずにTCP接続をサポートします — これはパーシングにとってより速く安定しています、
特にHTTP(S)だけでなくWebSocketエンドポイントにもアクセスする場合です。SOCKS5を使用する場合は、Cargo.tomlにsocks機能を必ず含めてください(上記で既に指定されています)。接続の例:
use reqwest::{Client, Proxy};
async fn build_socks5_client(
host: &str,
login: &str,
password: &str,
) -> reqwest::Result<Client> {
let proxy_url = format!("socks5h://{host}");
let proxy = Proxy::all(&proxy_url)?.basic_auth(login, password);
Client::builder()
.proxy(proxy)
.timeout(std::time::Duration::from_secs(20))
.build()
}
socks5hスキームに注意してください。
socks5の代わりにです。
最後の「h」は、ドメインのDNS解決がプロキシサーバー側で行われることを意味し、ローカルでは行われません。
これは匿名性にとって重要です: これがないと、実際のDNSリゾルバがプロキシを通さずに要求されたドメインを「漏らす」可能性があります。
プロキシプロバイダーがログイン/パスワードによる認証を必要としない場合(IPホワイトリスト)、basic_authを呼び出さないでください —
プロバイダーのパネルに、リクエストを送信するサーバーのIPアドレスを指定してください。
異なるプロキシ用のreqwestクライアントプール
よくある間違いは、各リクエストごとに新しいClientを作成することです。
これはコストがかかります: reqwestは内部でTCP接続とTLSセッションのプールを保持しており、クライアントを再作成するとkeep-aliveの利点がすべて失われます。
正しいアプローチは、各プロキシごとに1つのクライアントを事前に作成し、共通のプールから再利用することです。
use reqwest::{Client, Proxy};
use std::sync::Arc;
#[derive(Clone)]
struct ProxyEndpoint {
url: String,
login: String,
password: String,
}
struct ClientPool {
clients: Vec<Client>,
}
impl ClientPool {
fn build(endpoints: &[ProxyEndpoint]) -> reqwest::Result<Self> {
let mut clients = Vec::with_capacity(endpoints.len());
for ep in endpoints {
let proxy = Proxy::all(&ep.url)?
.basic_auth(&ep.login, &ep.password);
let client = Client::builder()
.proxy(proxy)
.pool_max_idle_per_host(4)
.timeout(std::time::Duration::from_secs(15))
.build()?;
clients.push(client);
}
Ok(Self { clients })
}
}
このプールは、パーサーの起動時に1回作成されます。クライアントの数は通常、プール内のユニークなプロキシアドレスの数に等しくなります — プロバイダーがゲートウェイレベルでのローテーションを提供している場合は、1〜2クライアントで十分です。なぜなら、IPのローテーションは プロバイダー側で新しいTCP接続ごとに自動的に行われるからです。
高い並行性が必要なタスクの場合は、プールをArcでラップし、
tokioタスク間で共有してください — reqwestクライアントはスレッドセーフであり、競合使用のために設計されています。
IPローテーションの戦略
ローテーションには2つの根本的に異なるアプローチがあります。1つ目はプロバイダー側のローテーションです: 1つのゲートウェイアドレスに接続し、プロバイダーが各リクエストごとに新しいIPを自動的に挿入します。2つ目はあなたの側のローテーションです: 静的プロキシのリストを保持し、次のリクエストに使用するものを自分で選択します。
レジデンシャルプロキシを使用している場合、 ゲートウェイを介した自動ローテーションで、コードは最小限に簡略化されます — 同じクライアントを介してリクエストを送信するだけで、プロバイダーが 自動的に出力IPを変更します。しかし、制御が必要な場合(たとえば、実際のユーザーの行動を模倣するために10分間IPをセッションに固定する場合)は、 手動でローテーションを実装します:
use rand::seq::SliceRandom;
use std::sync::atomic::{AtomicUsize, Ordering};
struct RotatingPool {
clients: Vec<Client>,
counter: AtomicUsize,
}
impl RotatingPool {
// ラウンドロビン: クライアントを順番に回す
fn next_round_robin(&self) -> &Client {
let idx = self.counter.fetch_add(1, Ordering::SeqCst) % self.clients.len();
&self.clients[idx]
}
// ランダム選択 — パターン分析に対して有用
fn next_random(&self) -> &Client {
self.clients.choose(&mut rand::thread_rng()).unwrap()
}
}
ラウンドロビンは、すべてのIPに均等な負荷が重要な場合に適しています。ランダム選択は、ターゲットサイトが 1つのアドレスからのアクセス間の間隔を分析している場合にリクエストのパターンを隠すのに適しています。Ozonでの価格監視やAvitoからの広告収集などのタスクでは、 実際には組み合わせが効果的です: プロキシのランダム選択と同じクライアントからのリクエスト間に1〜3秒のランダムな遅延を加えます。
スティッキーセッションについても言及する価値があります。パーサーが多段階のシナリオを通過する場合(ログイン → ページ遷移 → アクション)、 セッションの途中でIPを変更することはできません — サイトはアクティブなクッキーセッション中にIPが変更されたことに気付き、 アカウントをログアウトさせるか、ブロックします。このようなシナリオでは、特定の「セッション」に対して1つのクライアント(およびそれに応じて1つのIP)を 固定してください。
エラーハンドリングとリトライロジック
プロキシは時折ダウンします: タイムアウト、407(プロキシ認証が必要)、TCP接続の切断などです。 パーサーは、「プロキシが壊れた — プールから除外する」と「サイトが429を返した — ただ待ってIPを変更する」を区別できる必要があります。 こちらは、リトライと死んだプロキシの除外を含む作業パターンです:
use reqwest::StatusCode;
use std::time::Duration;
async fn fetch_with_retry(
pool: &RotatingPool,
url: &str,
max_attempts: u32,
) -> Result<String, String> {
let mut last_error = String::new();
for attempt in 0..max_attempts {
let client = pool.next_random();
match client.get(url).send().await {
Ok(resp) if resp.status() == StatusCode::TOO_MANY_REQUESTS => {
last_error = "429: レート制限、IPを変更します".into();
tokio::time::sleep(Duration::from_millis(500 * (attempt as u64 + 1))).await;
continue;
}
Ok(resp) if resp.status().is_success() => {
return resp.text().await.map_err(|e| e.to_string());
}
Ok(resp) => {
last_error = format!("予期しないステータス: {}", resp.status());
}
Err(e) if e.is_timeout() || e.is_connect() => {
last_error = format!("プロキシの問題: {e}");
continue;
}
Err(e) => {
last_error = e.to_string();
}
}
}
Err(format!("すべての試行が尽きました: {last_error}"))
}
重要なポイント: 指数関数的な遅延(500 * attempt)
は、大量の429応答がある場合にターゲットサイトへの負荷を軽減し、同時にプール全体のプロキシが一度に完全にブロックされるリスクを減少させます。
プロダクションパーサーでは、プール内の各プロキシに対してエラーカウンターを追加し、5回以上連続してエラーを返す場合は自動的にアドレスを除外します —
これにより、すでにバンされたIPを通じて無駄な試行を避けることができます。
パーシングに適したプロキシの種類
プロキシの種類の選択は、ブロックされるまでに成功するリクエストの数に直接影響します。データセンターのIPは最も安価で高速ですが、 有名なホスティングプロバイダーの範囲に属しているため、マーケットプレイスやソーシャルメディアのアンチボットシステムはほぼ瞬時にそれらを検出します。 レジデンシャルおよびモバイルIPは、通常のユーザーのように見えるため、はるかに長持ちします。
| プロキシの種類 | 速度 | ブロックのリスク | いつ使用するか |
|---|---|---|---|
| データセンター | 高い | 高い | オープンAPI、アンチボットのないサイト |
| レジデンシャル | 中程度 | 低い | Wildberries、Ozon、Avito、ソーシャルメディア |
| モバイル | 中程度 | 最小限 | Instagram、TikTok、厳しいアンチボット |
マーケットプレイスを大規模にパースする場合、合理的なバランスはレジデンシャルプロキシです: 彼らはモバイルプロキシほどトラフィックあたりのコストが高くなく、データセンターの範囲を切る「実際のユーザー」レベルのチェックを通過します。 ただし、パーサーがソーシャルメディアのAPIを介して動作するか、モバイルアプリを模倣する場合は、モバイルプロキシが IPに対するCAPTCHAや一時的なバンの最小割合を提供します。
パフォーマンスと負荷に関するアドバイス
Rustは1つのインスタンスで数千の同時リクエストを保持できますが、プロキシプロバイダーとターゲットサイトは 実際のスループットを制限します。プロダクションパーサーのためのいくつかの実用的なルール:
tokio::sync::Semaphoreを使用して並行性を制限します — 1つのプロキシプールあたり同時リクエストは20〜50件を超えないようにします。そうしないと、接続の一部がタイムアウトで切断され始めます。pool_idle_timeoutをClientBuilderで使用して、reqwestが不安定なプロキシを介して死んだ接続を保持しないようにします。- 各プロキシのHTTPステータスと応答時間を別々にログに記録します — これにより、どのアドレスが「腐り始めている」かを迅速に確認し、交換が必要です。
- User-AgentのローテーションとIPのローテーションを同時に行うことで、検出の可能性を大幅に減少させます: 同時に変更し、個別のサイクルではなく一緒に行います。
- 長期間稼働するパーサーにはヘルスチェックを追加します: 数分ごとに、各プロキシを介してhttps://httpbin.org/ipにテストリクエストを送信し、応答しないものを除外します。
TLSフィンガープリンティング(JA3/JA3S)にも特別な注意を払ってください。reqwestはデフォルトでrustlsまたはnativetlsを使用し、
ビルドの機能に応じて異なるTLSハンドシェイクのフィンガープリンティングを持っています。いくつかのアンチボットシステム(たとえば、
大規模なマーケットプレイス)は、IPだけでなくTLSフィンガープリンティングを分析します — したがって、完璧なプロキシでも
クライアントが非標準のTLSハンドシェイクを提供する場合は役に立ちません。クリーンなプロキシでブロックに直面した場合は、
reqwestの機能を確認し、rustlsの代わりにnative-tls機能を試すか、その逆を試してください。
結論
Rust reqwestでのプロキシ設定は迅速に行えます — Proxy::all
またはSOCKS5スキームsocks5h://の数行で。
主な作業はその後に始まります: クライアントプールの構築、ローテーション戦略の選択(ラウンドロビン、ランダム、またはスティッキーセッション)、
そしてエラーハンドリングの適切な実装とプールからの非稼働アドレスの除外です。これがなければ、最も高速なRustパーサーでも
数分の作業でブロックに直面します。
あなたのパーサーがマーケットプレイスやソーシャルメディアで動作する場合、アンチボット保護がリクエストの頻度だけでなく IPの出所も分析する場合は、レジデンシャルプロキシから始めてください — 彼らは価格とセッションの生存性のバランスが最も良いです。InstagramやTikTokのような厳しい目的には、モバイルIPをテストする価値がありますが、 アンチボットのない単純なタスクには、データセンターの高速で安価なプロキシで十分です。