RubyでHTTPプロキシを使う完全ガイド:Net::HTTP、Typhoeus、ProxyHat SDK

RubyでのHTTPプロキシ実装を網羅的に解説。Net::HTTPの基本からTyphoeusの並列リクエスト、ProxyHat SDKでのIPローテーション、TLS/SSL設定、Rails統合まで、実用的なコード例とベストプラクティスを提供します。

RubyでHTTPプロキシを使う完全ガイド:Net::HTTP、Typhoeus、ProxyHat SDK

Rubyプロジェクトでプロキシを使うべき理由

Webスクレイピングやデータパイプラインを構築する際、プロキシサーバーは単なるオプションではなく必須のインフラです。ターゲットサイトからのIPブロック回避、地理的制限の突破、レート制限の分散処理—allにおいてプロキシが解決策となります。

Rubyエコシステムには複数のHTTPクライアントが存在し、それぞれプロキシ対応のアプローチが異なります。Ruby proxy設定を正しく理解することで、本番環境で安定したスクレイピングシステムを構築できます。

本ガイドでは、標準ライブラリのNet::HTTPから、高機能なTyphoeus、そしてProxyHat SDKまで、実践的なコード例とともに解説します。

Net::HTTPでプロキシを使用する基本

Ruby標準ライブラリのNet::HTTPは、依存関係を追加せずにプロキシ経由でリクエストを送信できます。Net::HTTP proxy設定は、Proxyメソッドを使用してプロキシ接続を確立します。

基本的なプロキシ認証

以下はProxyHatのレジデンシャルプロキシを使用した基本的な例です:

require 'net/http'
require 'uri'

# ProxyHat接続パラメータ
PROXY_HOST = 'gate.proxyhat.com'
PROXY_PORT = 8080
PROXY_USER = 'your_username'
PROXY_PASS = 'your_password'

def fetch_with_proxy(url, proxy_user: PROXY_USER, proxy_pass: PROXY_PASS)
  uri = URI.parse(url)
  
  # プロキシ経由でHTTP接続を確立
  proxy = Net::HTTP::Proxy(PROXY_HOST, PROXY_PORT, proxy_user, proxy_pass)
  http = proxy.new(uri.host, uri.port)
  
  # HTTPSの場合はSSLを有効化
  http.use_ssl = uri.scheme == 'https'
  http.verify_mode = OpenSSL::SSL::VERIFY_PEER
  http.open_timeout = 30
  http.read_timeout = 60
  
  request = Net::HTTP::Get.new(uri.request_uri)
  request['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
  request['Accept'] = 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
  
  response = http.request(request)
  
  {
    status: response.code.to_i,
    headers: response.each_header.to_h,
    body: response.body
  }
end

# 使用例
result = fetch_with_proxy('https://httpbin.org/ip')
puts "Status: #{result[:status]}"
puts "Body: #{result[:body]}"

エラーハンドリングとリトライロジック

本番環境では、ネットワークエラーやタイムアウト、プロキシ接続失敗に対処する必要があります:

require 'net/http'
require 'uri'

class ProxyHTTPClient
  class RetryExhausted < StandardError; end
  class ProxyConnectionError < StandardError; end
  
  MAX_RETRIES = 3
  RETRY_DELAYS = [1, 2, 5] # 指数バックオフ
  
  attr_reader :proxy_host, :proxy_port, :username, :password
  
  def initialize(proxy_host: 'gate.proxyhat.com', 
                 proxy_port: 8080,
                 username: ENV['PROXYHAT_USER'],
                 password: ENV['PROXYHAT_PASS'])
    @proxy_host = proxy_host
    @proxy_port = proxy_port
    @username = username
    @password = password
  end
  
  def get(url, headers: {})
    retries = 0
    
    loop do
      begin
        return perform_request(:get, url, headers: headers)
      rescue Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNREFUSED => e
        retries += 1
        
        if retries >= MAX_RETRIES
          raise RetryExhausted, "Failed after #{MAX_RETRIES} attempts: #{e.message}"
        end
        
        puts "Retry #{retries}/#{MAX_RETRIES} after #{RETRY_DELAYS[retries - 1]}s: #{e.class}"
        sleep RETRY_DELAYS[retries - 1]
      rescue OpenSSL::SSL::SSLError => e
        raise ProxyConnectionError, "SSL error: #{e.message}"
      end
    end
  end
  
  private
  
  def perform_request(method, url, headers: {})
    uri = URI.parse(url)
    proxy_class = Net::HTTP::Proxy(proxy_host, proxy_port, username, password)
    http = proxy_class.new(uri.host, uri.port)
    
    configure_ssl(http, uri)
    http.open_timeout = 30
    http.read_timeout = 60
    
    request_class = case method
                    when :get then Net::HTTP::Get
                    when :post then Net::HTTP::Post
                    else raise ArgumentError, "Unsupported method: #{method}"
                    end
    
    request = request_class.new(uri.request_uri)
    headers.each { |k, v| request[k] = v }
    request['User-Agent'] ||= 'Mozilla/5.0 (compatible; RubyBot/1.0)'
    
    response = http.request(request)
    
    case response
    when Net::HTTPSuccess
      { success: true, status: response.code.to_i, body: response.body }
    when Net::HTTPRedirection
      { success: true, status: response.code.to_i, body: response.body, location: response['location'] }
    else
      { success: false, status: response.code.to_i, body: response.body }
    end
  end
  
  def configure_ssl(http, uri)
    return unless uri.scheme == 'https'
    
    http.use_ssl = true
    http.verify_mode = OpenSSL::SSL::VERIFY_PEER
    http.ca_file = OpenSSL::X509::DEFAULT_CERT_FILE
  end
end

# 使用例
client = ProxyHTTPClient.new
result = client.get('https://httpbin.org/status/200')
puts result.inspect

Typhoeusで並列リクエストを処理する

Typhoeusはlibcurlバインディングを使用し、Hydraによる並列リクエスト処理を実現します。大量のURLを効率的にスクレイピングする場合、Typhoeusは必須のツールです。

単一リクエストとプロキシ設定

require 'typhoeus'

# Typhoeusでプロキシを使用する設定
def create_typhoeus_request(url, proxy_url: nil)
  Typhoeus::Request.new(
    url,
    method: :get,
    proxy: proxy_url,
    headers: {
      'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
      'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
      'Accept-Language' => 'en-US,en;q=0.9'
    },
    timeout: 30,
    connecttimeout: 15,
    followlocation: true,
    ssl_verifypeer: true,
    ssl_verifyhost: 2
  )
end

# ProxyHatプロキシURLを構築
def build_proxy_url(username:, password:, country: nil, city: nil, session: nil)
  user_parts = [username]
  user_parts << "country-#{country}" if country
  user_parts << "city-#{city}" if city
  user_parts << "session-#{session}" if session
  
  "http://#{user_parts.join('-')}:#{password}@gate.proxyhat.com:8080"
end

# 使用例
proxy_url = build_proxy_url(
  username: ENV['PROXYHAT_USER'],
  password: ENV['PROXYHAT_PASS'],
  country: 'US'
)

request = create_typhoeus_request('https://httpbin.org/ip', proxy_url: proxy_url)
response = request.run

if response.success?
  puts "Success: #{response.code}"
  puts "Body: #{response.body}"
elsif response.timed_out?
  puts "Request timed out"
elsif response.code == 0
  puts "Connection failed: #{response.return_message}"
else
  puts "HTTP Error: #{response.code}"
end

Hydraで並列リクエストを実行

Hydraを使用すると、複数のリクエストを同時に実行し、ネットワーク待ち時間を最小化できます:

require 'typhoeus'

class ParallelScraper
  CONCURRENCY = 50  # 同時接続数
  
  attr_reader :proxy_config
  
  def initialize(username:, password:, country: 'US')
    @proxy_config = { username: username, password: password, country: country }
  end
  
  def fetch_all(urls)
    hydra = Typhoeus::Hydra.new(max_concurrency: CONCURRENCY)
    results = Concurrent::Array.new
    mutex = Mutex.new
    
    urls.each_with_index do |url, index|
      # 各リクエストで異なるセッションを使用(IPローテーション)
      session_id = "req_#{index}_#{SecureRandom.hex(4)}"
      proxy_url = build_proxy_url(session: session_id)
      
      request = Typhoeus::Request.new(
        url,
        method: :get,
        proxy: proxy_url,
        headers: default_headers,
        timeout: 30,
        followlocation: true
      )
      
      request.on_complete do |response|
        mutex.synchronize do
          results << {
            url: url,
            status: response.code,
            success: response.success?,
            body: response.body,
            total_time: response.total_time,
            ip: extract_ip_from_response(response.body)
          }
        end
      end
      
      request.on_failure do |response|
        mutex.synchronize do
          results << {
            url: url,
            status: response.code || 0,
            success: false,
            error: response.return_message
          }
        end
      end
      
      hydra.queue(request)
    end
    
    # 全リクエストを実行
    hydra.run
    
    results
  end
  
  private
  
  def build_proxy_url(session:)
    "http://#{@proxy_config[:username]}-country-#{@proxy_config[:country]}-session-#{session}:#{@proxy_config[:password]}@gate.proxyhat.com:8080"
  end
  
  def default_headers
    {
      'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
      'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
    }
  end
  
  def extract_ip_from_response(body)
    JSON.parse(body)['origin'] rescue nil
  end
end

# 使用例
require 'concurrent'
require 'securerandom'

scraper = ParallelScraper.new(
  username: ENV['PROXYHAT_USER'],
  password: ENV['PROXYHAT_PASS'],
  country: 'US'
)

urls = 100.times.map { |i| "https://httpbin.org/delay/#{rand(1..3)}" }
results = scraper.fetch_all(urls)

puts "Completed: #{results.count} requests"
puts "Success rate: #{results.count { |r| r[:success] } / results.count.to_f * 100}%"

ProxyHat Ruby SDKでIPローテーションと地理ターゲティング

Ruby residential proxiesを活用する場合、IPローテーゲーションと地理的ターゲティングが重要です。ProxyHatのユーザー名ベースの設定により、これらを簡単に制御できます。

require 'typhoeus'
require 'securerandom'

# ProxyHat SDKクライアント
class ProxyHatClient
  GATEWAY_HOST = 'gate.proxyhat.com'
  HTTP_PORT = 8080
  SOCKS5_PORT = 1080
  
  attr_reader :username, :password
  
  def initialize(username:, password:)
    @username = username
    @password = password
  end
  
  # ローテーティングプロキシ(リクエストごとに新しいIP)
  def rotating_proxy_url(country: nil, city: nil)
    session = SecureRandom.hex(8)
    build_proxy_url(session: session, country: country, city: city)
  end
  
  # スティッキーセッション(固定IPを維持)
  def sticky_proxy_url(session_id:, country: nil, city: nil, duration: nil)
    build_proxy_url(session: session_id, country: country, city: city, duration: duration)
  end
  
  # 国別ターゲティング
  def country_proxy(country_code)
    rotating_proxy_url(country: country_code)
  end
  
  # 都市別ターゲティング
  def city_proxy(country_code, city_name)
    rotating_proxy_url(country: country_code, city: city_name.downcase.gsub(/\s+/, '-'))
  end
  
  # SOCKS5プロキシURL
  def socks5_proxy_url(session: nil, country: nil)
    user_parts = [username]
    user_parts << "country-#{country}" if country
    user_parts << "session-#{session}" if session
    
    "socks5://#{user_parts.join('-')}:#{password}@#{GATEWAY_HOST}:#{SOCKS5_PORT}"
  end
  
  # リクエスト実行ヘルパー
  def get(url, options = {})
    proxy_url = if options[:sticky]
                  sticky_proxy_url(session_id: options[:session_id] || SecureRandom.hex(4))
                else
                  rotating_proxy_url(country: options[:country], city: options[:city])
                end
    
    request = Typhoeus::Request.new(
      url,
      method: :get,
      proxy: proxy_url,
      headers: options[:headers] || default_headers,
      timeout: options[:timeout] || 30
    )
    
    response = request.run
    
    {
      success: response.success?,
      status: response.code,
      body: response.body,
      proxy_used: proxy_url.gsub(/:[^:@]+@/, ':****@') # パスワードを隠蔽
    }
  end
  
  private
  
  def build_proxy_url(session:, country: nil, city: nil, duration: nil)
    user_parts = [username]
    user_parts << "country-#{country}" if country
    user_parts << "city-#{city}" if city
    user_parts << "session-#{session}" if session
    user_parts << "duration-#{duration}" if duration
    
    "http://#{user_parts.join('-')}:#{password}@#{GATEWAY_HOST}:#{HTTP_PORT}"
  end
  
  def default_headers
    {
      'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
      'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
    }
  end
end

# 使用例
client = ProxyHatClient.new(
  username: ENV['PROXYHAT_USER'],
  password: ENV['PROXYHAT_PASS']
)

# 米国IPでリクエスト
us_result = client.get('https://httpbin.org/ip', country: 'US')
puts "US IP: #{us_result[:body]}"

# ドイツ・ベルリンIPでリクエスト
de_result = client.get('https://httpbin.org/ip', country: 'DE', city: 'berlin')
puts "Berlin IP: #{de_result[:body]}"

# 固定セッション(同じIPを維持)
sticky_result = client.get('https://httpbin.org/ip', sticky: true, session_id: 'my_session_123')
puts "Sticky IP: #{sticky_result[:body]}"

実践例:1000URLを並列スクレイピング

実際の本番環境での使用例として、1000のURLをローテーティングレジデンシャルプロキシ経由で並列取得するシステムを構築します:

require 'typhoeus'
require 'concurrent'
require 'securerandom'
require 'logger'

class ProductionScraper
  include Concurrent::Async
  
  BATCH_SIZE = 100
  MAX_CONCURRENCY = 50
  RETRY_COUNT = 3
  
  attr_reader :client, :logger, :metrics
  
  def initialize(username:, password:)
    @client = ProxyHatClient.new(username: username, password: password)
    @logger = Logger.new(STDOUT)
    @logger.level = Logger::INFO
    @metrics = {
      total: 0,
      success: 0,
      failed: 0,
      retries: 0,
      start_time: nil
    }
    @metrics_mutex = Mutex.new
  end
  
  def scrape_urls(urls, country: 'US')
    @metrics[:start_time] = Time.now
    @metrics[:total] = urls.count
    
    logger.info "Starting scrape of #{urls.count} URLs with country=#{country}"
    
    # URLをバッチに分割
    batches = urls.each_slice(BATCH_SIZE).to_a
    
    results = []
    batches.each_with_index do |batch, batch_idx|
      logger.info "Processing batch #{batch_idx + 1}/#{batches.count}"
      batch_results = process_batch(batch, country)
      results.concat(batch_results)
      
      # バッチ間で少し待機(サーバー負荷軽減)
      sleep 0.5 unless batch_idx == batches.count - 1
    end
    
    log_summary
    results
  end
  
  private
  
  def process_batch(urls, country)
    hydra = Typhoeus::Hydra.new(max_concurrency: MAX_CONCURRENCY)
    results = Concurrent::Array.new
    
    urls.each_with_index do |url, idx|
      session_id = "batch_#{SecureRandom.hex(4)}_#{idx}"
      proxy_url = client.rotating_proxy_url(country: country)
      
      request = build_request(url, proxy_url)
      
      request.on_complete do |response|
        result = handle_response(url, response)
        update_metrics(result)
        results << result
      end
      
      hydra.queue(request)
    end
    
    hydra.run
    results.to_a
  end
  
  def build_request(url, proxy_url)
    Typhoeus::Request.new(
      url,
      method: :get,
      proxy: proxy_url,
      headers: {
        'User-Agent' => random_user_agent,
        'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
        'Accept-Language' => 'en-US,en;q=0.9',
        'Accept-Encoding' => 'gzip, deflate'
      },
      timeout: 30,
      connecttimeout: 15,
      followlocation: true,
      ssl_verifypeer: true
    )
  end
  
  def handle_response(url, response)
    {
      url: url,
      status: response.code,
      success: response.success?,
      body: response.success? ? response.body : nil,
      time: response.total_time,
      error: response.success? ? nil : response.return_message
    }
  end
  
  def update_metrics(result)
    @metrics_mutex.synchronize do
      if result[:success]
        @metrics[:success] += 1
      else
        @metrics[:failed] += 1
      end
    end
  end
  
  def log_summary
    duration = Time.now - @metrics[:start_time]
    rate = @metrics[:total] / duration
    success_rate = (@metrics[:success].to_f / @metrics[:total] * 100).round(2)
    
    logger.info "=" * 50
    logger.info "Scraping Complete"
    logger.info "Total URLs: #{@metrics[:total]}"
    logger.info "Success: #{@metrics[:success]} (#{success_rate}%)"
    logger.info "Failed: #{@metrics[:failed]}"
    logger.info "Duration: #{duration.round(2)}s"
    logger.info "Rate: #{rate.round(2)} req/s"
    logger.info "=" * 50
  end
  
  def random_user_agent
    agents = [
      'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
      'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
      'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0',
      'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15'
    ]
    agents.sample
  end
end

# 実行例
scraper = ProductionScraper.new(
  username: ENV['PROXYHAT_USER'],
  password: ENV['PROXYHAT_PASS']
)

# テスト用URLを生成
test_urls = 1000.times.map do |i|
  "https://httpbin.org/delay/#{rand(1..2)}?id=#{i}"
end

results = scraper.scrape_urls(test_urls, country: 'US')

# 結果をCSVに保存
require 'csv'
CSV.open('scraping_results.csv', 'w') do |csv|
  csv << ['url', 'status', 'success', 'time', 'error']
  results.each do |r|
    csv << [r[:url], r[:status], r[:success], r[:time]&.round(2), r[:error]]
  end
end

TLS/SSL設定と証明書検証

プロキシ経由でHTTPSサイトにアクセスする際、SSL/TLS設定は重要です。自己署名証明書やSNI(Server Name Indication)の問題に対処する方法を解説します。

Net::HTTPでのSSL設定

require 'net/http'
require 'uri'
require 'openssl'

class SSLAwareProxyClient
  attr_reader :proxy_host, :proxy_port, :username, :password
  
  def initialize(proxy_host: 'gate.proxyhat.com', proxy_port: 8080, username:, password:)
    @proxy_host = proxy_host
    @proxy_port = proxy_port
    @username = username
    @password = password
  end
  
  # 標準的なSSL検証
  def fetch_with_ssl_verification(url)
    uri = URI.parse(url)
    proxy = Net::HTTP::Proxy(proxy_host, proxy_port, username, password)
    http = proxy.new(uri.host, uri.port)
    
    if uri.scheme == 'https'
      http.use_ssl = true
      http.verify_mode = OpenSSL::SSL::VERIFY_PEER
      
      # システムのCA証明書ストアを使用
      http.cert_store = OpenSSL::X509::Store.new.tap do |store|
        store.set_default_paths
      end
      
      # SNIを有効化(デフォルトで有効だが明示的に設定)
      http.enable_post_connection_check = true
    end
    
    request = Net::HTTP::Get.new(uri.request_uri)
    http.request(request)
  end
  
  # 自己署名証明書を許可(開発環境のみ)
  def fetch_with_insecure_ssl(url)
    warn "WARNING: SSL verification disabled - use only in development!"
    
    uri = URI.parse(url)
    proxy = Net::HTTP::Proxy(proxy_host, proxy_port, username, password)
    http = proxy.new(uri.host, uri.port)
    
    if uri.scheme == 'https'
      http.use_ssl = true
      http.verify_mode = OpenSSL::SSL::VERIFY_NONE
    end
    
    request = Net::HTTP::Get.new(uri.request_uri)
    http.request(request)
  end
  
  # カスタムCA証明書を使用
  def fetch_with_custom_ca(url, ca_cert_path)
    uri = URI.parse(url)
    proxy = Net::HTTP::Proxy(proxy_host, proxy_port, username, password)
    http = proxy.new(uri.host, uri.port)
    
    if uri.scheme == 'https'
      http.use_ssl = true
      http.verify_mode = OpenSSL::SSL::VERIFY_PEER
      
      http.cert_store = OpenSSL::X509::Store.new.tap do |store|
        store.add_file(ca_cert_path)
      end
    end
    
    request = Net::HTTP::Get.new(uri.request_uri)
    http.request(request)
  end
  
  # クライアント証明書認証
  def fetch_with_client_cert(url, cert_path, key_path)
    uri = URI.parse(url)
    proxy = Net::HTTP::Proxy(proxy_host, proxy_port, username, password)
    http = proxy.new(uri.host, uri.port)
    
    if uri.scheme == 'https'
      http.use_ssl = true
      http.verify_mode = OpenSSL::SSL::VERIFY_PEER
      http.cert = OpenSSL::X509::Certificate.new(File.read(cert_path))
      http.key = OpenSSL::PKey::RSA.new(File.read(key_path))
      
      http.cert_store = OpenSSL::X509::Store.new.tap(&:set_default_paths)
    end
    
    request = Net::HTTP::Get.new(uri.request_uri)
    http.request(request)
  end
end

# 使用例
client = SSLAwareProxyClient.new(
  username: ENV['PROXYHAT_USER'],
  password: ENV['PROXYHAT_PASS']
)

# 標準的なHTTPSリクエスト
response = client.fetch_with_ssl_verification('https://example.com')
puts "Status: #{response.code}"

TyphoeusでのSSL設定

require 'typhoeus'

# TyphoeusでのSSLオプション
request = Typhoeus::Request.new(
  'https://example.com',
  method: :get,
  proxy: 'http://user:pass@gate.proxyhat.com:8080',
  # SSL検証オプション
  ssl_verifypeer: true,      # ピア証明書を検証
  ssl_verifyhost: 2,         # ホスト名を検証(2=厳密、1=緩い、0=無効)
  sslversion: :tlsv1_2,      # TLS 1.2以上を強制
  
  # カスタムCA証明書(オプション)
  # cainfo: '/path/to/ca-bundle.crt',
  
  # クライアント証明書(オプション)
  # sslcert: '/path/to/client.crt',
  # sslkey: '/path/to/client.key',
  
  timeout: 30
)

response = request.run
puts "Status: #{response.code}"

Ruby on Railsでの統合

Railsアプリケーションでプロキシを使用する場合、FaradayミドルウェアやActiveJobとの統合が効果的です。

Faradayミドルウェアでのプロキシ設定

# config/initializers/proxy.rb
require 'faraday'
require 'faraday_middleware'

class ProxyMiddleware < Faraday::Middleware
  def initialize(app, options = {})
    super(app)
    @options = options
  end
  
  def call(env)
    # リクエストごとにローテーティングプロキシURLを生成
    session = SecureRandom.hex(8)
    country = @options[:country] || 'US'
    
    proxy_url = build_proxy_url(session, country)
    env[:proxy] = URI.parse(proxy_url)
    
    @app.call(env)
  end
  
  private
  
  def build_proxy_url(session, country)
    username = ENV['PROXYHAT_USER']
    password = ENV['PROXYHAT_PASS']
    "http://#{username}-country-#{country}-session-#{session}:#{password}@gate.proxyhat.com:8080"
  end
end

# Faradayクライアントの設定
class ApiClient
  attr_reader :connection
  
  def initialize(country: 'US')
    @connection = Faraday.new do |builder|
      builder.use ProxyMiddleware, country: country
      builder.request :url_encoded
      builder.request :retry, max: 3, interval: 1, backoff_factor: 2
      builder.response :json, content_type: /\bjson$/
      builder.adapter :typhoeus
    end
  end
  
  def get(url, params: {})
    response = connection.get(url, params)
    {
      status: response.status,
      body: response.body,
      success: response.success?
    }
  rescue Faraday::Error => e
    { status: 0, body: nil, success: false, error: e.message }
  end
  
  def post(url, body: {})
    response = connection.post(url, body.to_json, 'Content-Type' => 'application/json')
    {
      status: response.status,
      body: response.body,
      success: response.success?
    }
  rescue Faraday::Error => e
    { status: 0, body: nil, success: false, error: e.message }
  end
end

# app/services/web_scraper_service.rb
class WebScraperService
  def initialize(country: 'US')
    @client = ApiClient.new(country: country)
  end
  
  def scrape(url)
    result = @client.get(url)
    return nil unless result[:success]
    
    # HTMLをパースしてデータを抽出
    doc = Nokogiri::HTML(result[:body])
    extract_data(doc)
  end
  
  private
  
  def extract_data(doc)
    {
      title: doc.at('title')&.text,
      h1: doc.at('h1')&.text,
      meta_description: doc.at('meta[name="description"]')&.[]('content')
    }
  end
end

ActiveJobでの並列スクレイピング

# app/jobs/scraping_job.rb
class ScrapingJob < ApplicationJob
  queue_as :scraping
  
  # ジョブ失敗時のリトライ設定
  retry_on ScrapingError, wait: :polynomially_longer, attempts: 3
  discard_on ActiveJob::DeserializationError
  
  def perform(url, country: 'US', session_id: nil)
    @url = url
    @country = country
    @session_id = session_id || SecureRandom.hex(8)
    
    result = fetch_with_proxy
    
    if result[:success]
      process_result(result[:body])
    else
      raise ScrapingError, "Failed to fetch #{@url}: #{result[:error]}"
    end
  end
  
  private
  
  def fetch_with_proxy
    proxy_url = build_proxy_url
    
    request = Typhoeus::Request.new(
      @url,
      method: :get,
      proxy: proxy_url,
      headers: {
        'User-Agent' => random_user_agent,
        'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
      },
      timeout: 30,
      followlocation: true
    )
    
    response = request.run
    
    {
      success: response.success?,
      status: response.code,
      body: response.body,
      error: response.success? ? nil : response.return_message
    }
  end
  
  def build_proxy_url
    username = Rails.application.credentials.proxyhat[:username]
    password = Rails.application.credentials.proxyhat[:password]
    
    "http://#{username}-country-#{@country}-session-#{@session_id}:#{password}@gate.proxyhat.com:8080"
  end
  
  def process_result(html)
    doc = Nokogiri::HTML(html)
    
    # データを抽出して保存
    data = {
      url: @url,
      title: doc.at('title')&.text,
      scraped_at: Time.current
    }
    
    ScrapedPage.create!(data)
  end
  
  def random_user_agent
    [
      'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
      'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
    ].sample
  end
end

# app/jobs/batch_scraping_job.rb
class BatchScrapingJob < ApplicationJob
  queue_as :scraping
  
  def perform(urls, country: 'US')
    # URLを小さなバッチに分割して並列処理
    urls.each_slice(50).with_index do |batch, index|
      batch.each do |url|
        # 各URLを個別のジョブとしてエンキュー
        ScrapingJob.perform_later(url, country, "batch_#{index}_#{SecureRandom.hex(4)}")
      end
      
      # バッチ間で少し待機
      sleep 2 unless index == (urls.count / 50.0).ceil - 1
    end
  end
end

# 使用例
# 大量のURLをバッチでスクレイピング
urls = ['https://example1.com', 'https://example2.com', ...] # 1000 URLs
BatchScrapingJob.perform_later(urls, 'US')

パフォーマンス比較表

各HTTPクライアントとプロキシ構成のパフォーマンスを比較します:

クライアント 並列処理 メモリ使用 推奨用途
Net::HTTP (stdlib) なし(順次) 単純なリクエスト、スクリプト
Net::HTTP + Threads 制限あり 小規模並列処理
Typhoeus Hydra(高性能) 大規模スクレイピング
Faraday + Typhoeus Hydra Rails統合、APIクライアント
ProxyHat + Typhoeus Hydra + IPローテーション 本番スクレイピング

Key Takeaways

  • Net::HTTPは標準ライブラリで依存関係不要、単純なリクエストに最適
  • Typhoeus/Hydraは並列リクエスト処理のベストチョイス
  • ProxyHatのユーザー名ベース設定でIPローテーションと地理ターゲティングを簡単に制御
  • 本番環境では必ずリトライロジック、タイムアウト、エラーハンドリングを実装
  • RailsではFaradayミドルウェアとActiveJobでスケーラブルなアーキテクチャを構築
  • SSL/TLS検証を適切に設定し、セキュリティを維持

Rubyでのプロキシ活用についてさらに詳しく知りたい方は、WebスクレイピングのベストプラクティスSERPトラッキングユースケースも参照してください。ProxyHatのプロキシネットワークは世界中のロケーションで利用可能です。

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

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

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