Rでプロキシを使用する完全ガイド: httr2とrvestでデータ収集を自動化

httr2のreq_proxy()とrvestを組み合わせ、Rでプロキシ経由のWebスクレイピングを実装する方法を解説。地理ターゲティング、スティッキーセッション、SOCKS5、JSレンダリングページ対応まで網羅した開発者向けガイドです。

Using Proxies in R: A Code-First Guide with httr2 and rvest

Rでデータ収集を行うデータサイエンティストやアナリストにとって、Rでプロキシを使用することは単なるオプションではなく、スクレイピングの成功率と信頼性を左右する必須スキルです。本記事では、モダンなHTTPクライアントである httr2req_proxy() と、HTMLパーサー rvest を組み合わせ、ProxyHatのレジデンシャルプロキシを活用した実践的なコード例を多数紹介します。

対象読者は、Rで分析パイプラインを構築しているデータサイエンティスト、アナリスト、R開発者です。PythonやNode.jsのバックグラウンドがある方にも、Rエコシステムでのプロキシ利用パターンがそのまま応用できます。

Rプロキシの基礎: なぜhttr2とrvestでプロキシが必要なのか

Webスクレイピングでは、対象サイトがリクエスト元IPを制限することが一般的です。特にデータセンターIPは、AWS、GCP、AzureなどのクラウドプロバイダーIP帯域として広く識別されており、多くのサイトがHTTP 429や403で即座にブロックします。

一方、レジデンシャルプロキシは一般家庭のISPに属するIPアドレスを使用するため、通常のユーザーからのアクセスと区別が困難です。これにより、地理的制限(geo-wall)のあるコンテンツへのアクセスや、IPベースのレート制限の回避が可能になります。

Rプロキシの利用場面には以下が含まれます:

  • SERPデータの収集(検索結果ページの定期的な取得)
  • eコマースサイトの価格モニタリング
  • 地理的に制限された公開データへのアクセス
  • APIを提供していないサイトからの構造化データ抽出

Rの httr2 パッケージは、パイプベースのAPIでモダンなHTTPリクエストを構築でき、req_proxy() 関数でHTTP/SOCKS5プロキシを簡単に設定できます。従来の httr パッケージと異なり、リクエストオブジェクトを段階的に組み立てる設計のため、プロキシ設定、リトライ、スロットリングを宣言的に追加できます。

httr2でreq_proxy()を設定する基本パターン

ProxyHatのゲートウェイは gate.proxyhat.com で提供され、HTTPプロキシはポート 8080、SOCKS5プロキシはポート 1080 で接続します。ユーザー名にジオターゲティングやセッション指定のフラグを埋め込むのが基本パターンです。

HTTPプロキシの基本例

library(httr2)
library(rvest)

# ProxyHat接続情報
proxy_host <- "gate.proxyhat.com"
proxy_port <- 8080
proxy_user <- "user-country-US"
proxy_pass <- "your_password"

# リクエストの構築
req <- request("https://httpbin.org/ip") |>
  req_proxy(proxy_host, proxy_port, username = proxy_user, password = proxy_pass) |>
  req_user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") |>
  req_timeout(seconds = 30) |>
  req_retry(max_tries = 3, backoff = ~ 2^x)

resp <- req |> req_perform()
status <- resp_status(resp)
body <- resp |> resp_body_json()
print(body)
# $origin
# [1] "198.51.100.42"  # プロキシの出口IP

req_proxy() は接続先ホスト、ポート、ユーザー名、パスワードを引数に取ります。この例では req_retry() で最大3回のリトライと指数バックオフを設定し、req_timeout() で30秒のタイムアウトを指定しています。

地理ターゲティングとスティッキーセッション

ProxyHatでは、ユーザー名フィールドにフラグを追加することで、出口IPの国・都市を指定できます。また、セッションIDを固定することで、一連のリクエストで同じIPを使い続ける「スティッキーセッション」が実現できます。

# イギリス・ロンドンのIPを指定
proxy_user_gb <- "user-country-GB-city-london"

# セッションIDを固定(スティッキーセッション)
session_id <- paste0("sess-", as.integer(Sys.time()))
proxy_user_sticky <- paste0("user-country-GB-session-", session_id)

req <- request("https://httpbin.org/headers") |>
  req_proxy(proxy_host, proxy_port,
           username = proxy_user_sticky,
           password = proxy_pass) |>
  req_user_agent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)")

resp <- req |> req_perform()
json <- resp |> resp_body_json()
print(json$headers)

スティッキーセッションは、ログイン後のページ遷移や、ページネーションを跨ぐスクレイピングで特に重要です。セッションIDが同じ限り、同じ出口IPが割り当てられ、サーバー側でセッションが切断されるのを防ぎます。

SOCKS5プロキシの使用

SOCKS5プロキシはポート 1080 で利用できます。SOCKS5はトランスポート層で動作するため、HTTPヘッダーの改変がなく、より低いレイヤーでのプロキシ通信が可能です。

# SOCKS5プロキシの設定
req_socks <- request("https://httpbin.org/ip") |>
  req_proxy("gate.proxyhat.com", 1080,
           username = "user-country-DE",
           password = proxy_pass)

resp <- req_socks |> req_perform()
print(resp |> resp_body_json())

レジデンシャル vs データセンタープロキシ: 実用的な比較

プロキシの選択は、スクレイピングの成功率に直結します。以下の表は、主要なプロキシタイプの特徴を比較したものです。

特徴 レジデンシャルプロキシ データセンタープロキシ モバイルプロキシ
IPソース ISP(一般家庭) クラウドプロバイダー 携帯キャリア
検知難易度 高(検知困難) 低(容易に検知) 非常に高
成功率(参考) 90%以上 40〜60% 95%以上
レイテンシ 200〜800ms 50〜150ms 300〜1200ms
価格帯 中〜高

データセンタープロキシは高速で低コストですが、多くのWebサイトがプロキシ検知技術で容易に識別できます。レジデンシャルプロキシは、実在するISPのIPアドレスを使用するため、通常のユーザートラフィックと区別がつきにくく、ブロックされる確率が大幅に下がります。

実践例: ページネーション付きテーブルをtidyデータフレームに変換

ここでは、複数ページにわたるHTMLテーブルをスクレイピングし、tidyなデータフレームにまとめる実践的な例を紹介します。purrr::map でページを反復処理し、各ページで異なるセッションIDを使ってIPローテーションを行います。

library(httr2)
library(rvest)
library(purrr)
library(dplyr)
library(tibble)

# ProxyHat設定
proxy_host <- "gate.proxyhat.com"
proxy_port <- 8080
proxy_pass <- "your_password"
base_user <- "user-country-US"

# 1ページ分のテーブルを取得する関数
fetch_table_page <- function(page_num) {
  session_id <- paste0("p", page_num, "-", as.integer(Sys.time()))
  proxy_user <- paste0(base_user, "-session-", session_id)

  url <- sprintf("https://example.com/table?page=%d", page_num)

  req <- request(url) |>
    req_proxy(proxy_host, proxy_port,
             username = proxy_user,
             password = proxy_pass) |>
    req_user_agent(
      "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
    ) |>
    req_headers("Accept" = "text/html,application/xhtml+xml",
                "Accept-Language" = "en-US,en;q=0.9") |>
    req_timeout(seconds = 30) |>
    req_retry(max_tries = 3, backoff = ~ 2^x) |>
    req_throttle(1 / 2)  # 2秒に1リクエスト

  tryCatch({
    resp <- req |> req_perform()
    html <- resp |> resp_body_string() |> read_html()

    table_df <- html |>
      html_elements("table.data-table") |>
      html_table()

    if (length(table_df) > 0) {
      table_df[[1]] <- table_df[[1]] |>
        mutate(source_page = page_num)
      return(table_df[[1]])
    }
    return(NULL)
  }, error = function(e) {
    message(sprintf("Page %d failed: %s", page_num, e$message))
    return(NULL)
  })
}

# 10ページ分を取得して結合
pages <- 1:10
results <- map(pages, fetch_table_page)

# NULLを除外して結合
combined <- results |>
  keep(~ !is.null(.x)) |>
  bind_rows()

print(sprintf("取得行数: %d", nrow(combined)))
glimpse(combined)

この例のポイントは以下の通りです:

  • セッションIDのページ別ローテーション: 各ページで異なるセッションIDを生成し、IPをローテーション
  • req_retry(): 最大3回のリトライと指数バックオフで一時的な障害に対応
  • req_throttle(1 / 2): 2秒に1リクエストのレート制限で対象サーバーに配慮
  • tryCatch(): エラーハンドリングで一部ページの失敗が全体を止めないよう設計
  • mutate(source_page = page_num): 出所ページ番号を列として保持し、トレーサビリティを確保

curlを使った同等のプロキシ設定

R以外のツールとの連携やデバッグ用途では、curlコマンドで同じプロキシ設定を検証できます。

# HTTPプロキシ経由でアクセス
curl -x http://user-country-US-session-test01:your_password@gate.proxyhat.com:8080 \
  -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)" \
  -H "Accept: text/html" \
  "https://httpbin.org/ip"

# SOCKS5プロキシ経由
curl -x socks5://user-country-DE:your_password@gate.proxyhat.com:1080 \
  "https://httpbin.org/ip"

JSレンダリングページへの対応: read_html_live()とchromote

多くのモダンなWebサイトは、JavaScriptでコンテンツを動的にレンダリングします。rvestread_html() は静的HTMLのみを解析するため、JSレンダリングが必要なページでは read_html_live() を使用します。この関数は内部で chromote パッケージを利用し、ヘッドレスChromeを駆動します。

library(rvest)
library(httr2)

# read_html_live()でJSレンダリングページを取得
# プロキシはChromeの起動引数で指定
live_html <- read_html_live(
  "https://example.com/dynamic-content",
  chrome_args = c(
    "--proxy-server=http://gate.proxyhat.com:8080",
    "--proxy-auth=user-country-GB:your_password",
    "--no-sandbox",
    "--disable-gpu"
  )
)

# 動的にレンダリングされた要素を抽出
dynamic_text <- live_html |>
  html_elements(".dynamic-content") |>
  html_text2()

print(dynamic_text)

read_html_live() は、Chromeの --proxy-server 起動引数でプロキシを指定します。HTTPプロキシの場合は http://gate.proxyhat.com:8080、SOCKS5の場合は socks5://gate.proxyhat.com:1080 を指定します。ただし、Chromeのプロキシ認証は --proxy-auth フラグでは直接サポートされていない場合があるため、ProxyHatのIP許可リスト機能を併用するか、拡張機能経由で認証ヘッダーを注入するアプローチが実用的です。

現実的なUser-Agentとヘッダーの設定

プロキシを使用する際、User-AgentやHTTPヘッダーがプロキシの出口IPの地理的位置と整合していることが重要です。例えば、イギリスのIPを使用しているのに日本語のAccept-Languageを送信すると、ボット検知システムにフラグが立つ可能性があります。

# 地理的に整合したヘッダーを設定
req_uk <- request("https://example.co.uk/data") |>
  req_proxy("gate.proxyhat.com", 8080,
           username = "user-country-GB-city-london",
           password = "your_password") |>
  req_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"
  ) |>
  req_headers(
    "Accept-Language" = "en-GB,en;q=0.9",
    "Accept" = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    "Sec-Ch-Ua" = '"Not_A Brand";v="8", "Chromium";v="120"'
  ) |>
  req_timeout(seconds = 30) |>
  req_retry(max_tries = 3, backoff = ~ 2^x)

resp <- req_uk |> req_perform()

エラー処理とプロダクション運用のベストプラクティス

スクレイピングを本番環境で運用する場合、以下の対策が重要です。

サーキットブレーカーパターンの実装

library(httr2)
library(purrr)

# サーキットブレーカー: 連続失敗が閾値を超えたら停止
circuit_breaker_fetch <- function(urls, max_consecutive_failures = 5) {
  failures <- 0
  results <- list()

  for (i in seq_along(urls)) {
    if (failures >= max_consecutive_failures) {
      message(sprintf("Circuit breaker tripped at URL %d", i))
      break
    }

    session_id <- paste0("cb-", i, "-", as.integer(Sys.time()))
    req <- request(urls[i]) |>
      req_proxy("gate.proxyhat.com", 8080,
               username = paste0("user-country-US-session-", session_id),
               password = "your_password") |>
      req_user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64)") |>
      req_timeout(seconds = 20) |>
      req_retry(max_tries = 2, backoff = ~ 1.5^x)

    result <- tryCatch({
      resp <- req |> req_perform()
      if (resp_status(resp) == 200) {
        failures <<- 0
        resp |> resp_body_string() |> read_html()
      } else {
        failures <<- failures + 1
        NULL
      }
    }, error = function(e) {
      failures <<- failures + 1
      message(sprintf("Error on URL %d: %s", i, e$message))
      NULL
    })

    results[[i]] <- result
    Sys.sleep(1.5)  # リクエスト間隔を確保
  }

  results
}

ログ記録とモニタリング

library(httr2)

# ログ付きリクエスト関数
logged_fetch <- function(url, proxy_user, proxy_pass, log_file = "scrape_log.csv") {
  start_time <- Sys.time()

  req <- request(url) |>
    req_proxy("gate.proxyhat.com", 8080,
             username = proxy_user,
             password = proxy_pass) |>
    req_user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64)") |>
    req_timeout(seconds = 30)

  result <- tryCatch({
    resp <- req |> req_perform()
    elapsed <- as.numeric(difftime(Sys.time(), start_time, units = "secs"))
    status <- resp_status(resp)
    log_entry <- data.frame(
      timestamp = start_time,
      url = url,
      status = status,
      elapsed_sec = round(elapsed, 2),
      success = status == 200,
      stringsAsFactors = FALSE
    )
    write.table(log_entry, log_file, append = TRUE,
                sep = ",", row.names = FALSE, col.names = FALSE)
    resp
  }, error = function(e) {
    elapsed <- as.numeric(difftime(Sys.time(), start_time, units = "secs"))
    log_entry <- data.frame(
      timestamp = start_time,
      url = url,
      status = 0,
      elapsed_sec = round(elapsed, 2),
      success = FALSE,
      stringsAsFactors = FALSE
    )
    write.table(log_entry, log_file, append = TRUE,
                sep = ",", row.names = FALSE, col.names = FALSE)
    stop(e)
  })

  result
}

倫理とコンプライアンス

スクレイピングを行う際は、以下の原則を遵守することが重要です:

  • 公開データのみを対象とする: ログイン背後のデータや、明示的に公開されていないデータは収集しない
  • robots.txtの尊重: RFC 9309で標準化されたrobots.txtの指示に従う
  • 利用規約の確認: 対象サイトのTerms of Serviceを確認し、スクレイピングが禁止されていないか確認する
  • GDPR対応: EUのデータ主体に関する個人データを収集する場合、GDPRの要件に準拠する
  • 公式APIを優先: 公式APIが提供されている場合は、スクレイピングよりAPIを使用する
  • 適切なレート制限: 対象サーバーに過度な負荷をかけないリクエスト間隔を設定する

Rでプロキシを使用する場合でも、これらの原則は技術的な実装と同様に重要です。ProxyHatの利用規約も併せて確認し、公式ドキュメントを参照してください。

ProxyHatの設定とSDK連携

ProxyHatのレジデンシャルプロキシは、100以上の国と地域で利用可能で、都市レベルのジオターゲティングに対応しています。詳細は料金ページで確認できます。

RパイプラインとPython/Node.jsパイプラインを混在させる場合、ProxyHatのSDKが同じ接続パターンを提供するため、プロキシ設定の一貫性が保たれます。例えば、Pythonの requests ライブラリでも同じゲートウェイとユーザー名フォーマットを使用します:

# Python側でも同じパターン
import requests

proxies = {
    "http": "http://user-country-US-session-abc123:your_password@gate.proxyhat.com:8080",
    "https": "http://user-country-US-session-abc123:your_password@gate.proxyhat.com:8080"
}
resp = requests.get("https://httpbin.org/ip", proxies=proxies, timeout=30)
print(resp.json())

この一貫性により、Rで前処理を行いPythonで機械学習モデルに投入するパイプラインでも、同じプロキシインフラをシームレスに利用できます。WebスクレイピングのユースケースSERPトラッキングの詳細も参照してください。

Key Takeaways

Rでのプロキシ活用の要点:

  • httr2::req_proxy() でProxyHatのゲートウェイ gate.proxyhat.com:8080(HTTP)または :1080(SOCKS5)を設定
  • ユーザー名に country-UScity-londonsession-xxx フラグを埋め込んでジオターゲティングとスティッキーセッションを制御
  • req_retry() + req_throttle() でリトライとレート制限を宣言的に設定
  • purrr::map でページ反復処理、各ページでセッションIDをローテーション
  • JSレンダリングページは read_html_live() + Chrome起動引数でプロキシ指定
  • User-AgentとAccept-Languageを出口IPの地理と整合させる
  • 公開データ、robots.txt、GDPR、利用規約を遵守し、公式APIを優先

Rでプロキシを使用することは、データ収集の信頼性を飛躍的に向上させます。httr2のパイプベースAPIとrvestの直感的なパーサーを組み合わせれば、プロダクション品質のスクレイピングパイプラインをRで構築できます。まずは少規模でテストし、成功率とレイテンシを測定した上で、本格的なデータ収集に移行してください。

始める準備はできましたか?

AIフィルタリングで148か国以上、5,000万以上のレジデンシャルIPにアクセス。

料金を見るレジデンシャルプロキシ
← ブログに戻る