Typhoeus

Ruby向けの並列HTTPリクエストライブラリ。libcurl基盤により高性能な非同期HTTP通信を実現。複数リクエストの並列実行、キューイング、コールバック処理に特化。大量のHTTPリクエストを効率的に処理する用途に最適化。

HTTPクライアントRuby並列処理libcurl高性能

GitHub概要

typhoeus/typhoeus

Typhoeus wraps libcurl in order to make fast and reliable requests.

スター4,110
ウォッチ54
フォーク441
作成日:2009年2月18日
言語:Ruby
ライセンス:MIT License

トピックス

なし

スター履歴

typhoeus/typhoeus Star History
データ取得日時: 2025/10/22 09:54

ライブラリ

Typhoeus

概要

Typhoeus は「libcurl をラップして高速で信頼性の高いリクエストを作成する」Ruby用の高性能HTTPクライアントライブラリです。C言語で実装されたlibcurlの性能を活用し、同期・非同期両方のHTTPリクエストに対応。独自のHydraシステムによる並列リクエスト処理が最大の特徴で、複数のHTTPリクエストを効率的に同時実行可能。Ruby エコシステムにおいて高性能なHTTP通信が求められる場面で重宝される、企業レベルのアプリケーション開発に適したライブラリです。

詳細

Typhoeus 2025年版は libcurl の安定した基盤の上に構築された実証済みの高性能HTTPクライアントとして確固たる地位を維持しています。C言語のlibcurlが提供する高速性と信頼性をRubyから簡単に利用でき、特に大量のHTTPリクエストを効率的に処理することに長けています。Hydraシステムによる並列処理機能は他のRuby HTTPライブラリにはない独自の強みで、数十〜数百のHTTPリクエストを同時に実行して処理時間を大幅に短縮可能。Faradayアダプターとしても利用でき、既存のアプリケーションへの統合も容易です。

主な特徴

  • Hydra並列処理: 複数HTTPリクエストの効率的な同時実行
  • libcurl基盤: C言語libcurlによる高速で安定したHTTP通信
  • Faraday統合: Faradayアダプターとしての利用が可能
  • 豊富な認証サポート: Basic認証、プロキシ認証、カスタム認証対応
  • ストリーミング処理: 大容量ファイルの効率的なダウンロード対応
  • 高度な設定: SSL/TLS、Cookie、圧縮、タイムアウト等の詳細設定

メリット・デメリット

メリット

  • libcurlベースによる圧倒的な性能と安定性
  • Hydraによる並列処理で大量リクエストの効率的な実行が可能
  • 豊富なHTTP機能(認証、プロキシ、SSL、圧縮等)の包括的サポート
  • Faradayエコシステムとの優れた統合性
  • エンタープライズレベルの信頼性と実績
  • RubyコミュニティでのScrapingやAPI統合での実績

デメリット

  • libcurlへのネイティブ依存によるセットアップの複雑さ
  • 学習コストが高く簡単なHTTPリクエストには過剰
  • Windows環境でのインストールに課題がある場合
  • メモリ使用量が純Ruby実装より多い傾向
  • デバッグ時にRubyレベルでの詳細制御が困難
  • 軽量なHTTPクライアントが求められる場面には不向き

参考ページ

書き方の例

インストールと基本セットアップ

# Gemfileに追加
gem 'typhoeus'

# Bundlerでインストール
bundle add typhoeus

# 直接インストール
gem install typhoeus

# libcurlバージョン確認
curl --version

# Rubyでの確認
ruby -e "require 'typhoeus'; puts Typhoeus::VERSION"

基本的なリクエスト(GET/POST/PUT/DELETE)

require 'typhoeus'

# 基本的なGETリクエスト
response = Typhoeus.get('https://api.example.com/users')
puts response.code       # 200
puts response.body       # レスポンスボディ
puts response.headers    # レスポンスヘッダー
puts response.total_time # 実行時間(秒)

# パラメータ付きGETリクエスト
response = Typhoeus.get(
  'https://api.example.com/users',
  params: { page: 1, limit: 10, sort: 'created_at' }
)
puts response.effective_url  # 実際のリクエストURL

# POSTリクエスト(JSON送信)
user_data = {
  name: '田中太郎',
  email: '[email protected]',
  age: 30
}

response = Typhoeus.post(
  'https://api.example.com/users',
  body: user_data.to_json,
  headers: {
    'Content-Type' => 'application/json',
    'Authorization' => 'Bearer your-token',
    'Accept' => 'application/json'
  }
)

if response.success?
  created_user = JSON.parse(response.body)
  puts "ユーザー作成完了: ID=#{created_user['id']}"
else
  puts "エラー: #{response.code} - #{response.body}"
end

# POSTリクエスト(フォームデータ送信)
response = Typhoeus.post(
  'https://api.example.com/login',
  body: { username: 'testuser', password: 'secret123' }
)

# PUTリクエスト(データ更新)
updated_data = { name: '田中次郎', email: '[email protected]' }
response = Typhoeus.put(
  'https://api.example.com/users/123',
  body: updated_data.to_json,
  headers: {
    'Content-Type' => 'application/json',
    'Authorization' => 'Bearer your-token'
  }
)

# DELETEリクエスト
response = Typhoeus.delete(
  'https://api.example.com/users/123',
  headers: { 'Authorization' => 'Bearer your-token' }
)

puts "削除完了" if response.code == 204

# HEADリクエスト(ヘッダー情報のみ取得)
response = Typhoeus.head('https://api.example.com/users/123')
puts "Content-Length: #{response.headers['Content-Length']}"
puts "Last-Modified: #{response.headers['Last-Modified']}"

# OPTIONSリクエスト(対応メソッド確認)
response = Typhoeus.options('https://api.example.com/users')
puts "Allow: #{response.headers['Allow']}"

高度な設定とカスタマイズ(認証、プロキシ、SSL等)

require 'typhoeus'

# カスタムヘッダー設定
headers = {
  'User-Agent' => 'MyApp/1.0 (Ruby Typhoeus)',
  'Accept' => 'application/json',
  'Accept-Language' => 'ja-JP,en-US',
  'X-API-Version' => 'v2',
  'X-Request-ID' => SecureRandom.uuid
}

response = Typhoeus.get('https://api.example.com/data', headers: headers)

# Basic認証
response = Typhoeus.get(
  'https://api.example.com/private',
  userpwd: 'username:password'
)

# Bearer Token認証
response = Typhoeus.get(
  'https://api.example.com/protected',
  headers: { 'Authorization' => 'Bearer your-jwt-token' }
)

# タイムアウト設定
response = Typhoeus.get(
  'https://api.example.com/slow',
  timeout: 30,           # 全体タイムアウト(秒)
  connecttimeout: 10     # 接続タイムアウト(秒)
)

# リダイレクト設定
response = Typhoeus.get(
  'https://api.example.com/redirect',
  followlocation: true,  # リダイレクト自動追従
  maxredirs: 5          # 最大リダイレクト回数
)

# プロキシ設定
response = Typhoeus.get(
  'https://api.example.com/data',
  proxy: 'http://proxy.example.com:8080'
)

# 認証付きプロキシ
response = Typhoeus.get(
  'https://api.example.com/data',
  proxy: 'http://proxy.example.com:8080',
  proxyuserpwd: 'proxy_user:proxy_password'
)

# SSL/TLS設定
response = Typhoeus.get(
  'https://secure-api.example.com/data',
  ssl_verifypeer: true,  # SSL証明書検証
  ssl_verifyhost: 2,     # ホスト名検証
  cainfo: '/path/to/ca-bundle.crt'  # CA証明書バンドル
)

# SSL検証無効化(開発環境のみ)
response = Typhoeus.get(
  'https://self-signed.example.com/',
  ssl_verifypeer: false,
  ssl_verifyhost: 0
)

# Cookie設定
response = Typhoeus.get(
  'https://api.example.com/user-data',
  cookiefile: '/tmp/cookies.txt',
  cookiejar: '/tmp/cookies.txt'
)

# 圧縮有効化
response = Typhoeus.get(
  'https://api.example.com/large-data',
  accept_encoding: 'gzip'
)

# カスタムUser-Agent設定(グローバル)
Typhoeus::Config.user_agent = 'MyApp/1.0 (Custom Agent)'

# Verbose出力有効化(デバッグ用)
Typhoeus::Config.verbose = true

エラーハンドリングとリトライ機能

require 'typhoeus'

# 包括的なエラーハンドリング
def safe_request(url, options = {})
  request = Typhoeus::Request.new(url, options)
  
  request.on_complete do |response|
    if response.success?
      puts "成功: #{response.code}"
      return response
    elsif response.timed_out?
      puts "タイムアウトエラー: リクエストがタイムアウトしました"
    elsif response.code == 0
      puts "接続エラー: #{response.return_message}"
    else
      puts "HTTPエラー: #{response.code} - #{response.body}"
    end
  end
  
  response = request.run
  response
end

# 使用例
response = safe_request('https://api.example.com/data', timeout: 10)

# リトライ機能付きリクエスト
def request_with_retry(url, options = {}, max_retries = 3)
  retries = 0
  
  begin
    response = Typhoeus.get(url, options)
    
    if response.success?
      return response
    elsif response.timed_out? || response.code >= 500
      raise StandardError, "Retryable error: #{response.code}"
    else
      raise StandardError, "Non-retryable error: #{response.code}"
    end
    
  rescue StandardError => e
    retries += 1
    
    if retries <= max_retries
      wait_time = 2 ** retries  # 指数バックオフ
      puts "リトライ #{retries}/#{max_retries}: #{wait_time}秒後に再試行"
      sleep(wait_time)
      retry
    else
      puts "最大リトライ回数に達しました: #{e.message}"
      raise
    end
  end
end

# 使用例
begin
  response = request_with_retry(
    'https://api.example.com/unstable',
    { timeout: 10 },
    3
  )
  puts "成功: #{response.body}"
rescue StandardError => e
  puts "最終的に失敗: #{e.message}"
end

# ステータスコード別処理
response = Typhoeus.get('https://api.example.com/status-check')

case response.code
when 200
  puts "正常: #{JSON.parse(response.body)}"
when 401
  puts "認証エラー: トークンを確認してください"
when 403
  puts "権限エラー: アクセス権限がありません"
when 404
  puts "見つかりません: リソースが存在しません"
when 429
  puts "レート制限: しばらく待ってから再試行してください"
when 500..599
  puts "サーバーエラー: #{response.code} - #{response.body}"
else
  puts "予期しないステータス: #{response.code}"
end

# タイムアウトチェック
response = Typhoeus.get('https://api.example.com/slow', timeout: 5)
if response.timed_out?
  puts "リクエストがタイムアウトしました"
end

Hydra並列処理とバッチリクエスト

require 'typhoeus'

# 基本的な並列処理
hydra = Typhoeus::Hydra.new
urls = [
  'https://api.example.com/users',
  'https://api.example.com/posts',
  'https://api.example.com/comments',
  'https://api.example.com/categories'
]

# リクエストをキューに追加
requests = urls.map do |url|
  request = Typhoeus::Request.new(url, followlocation: true)
  hydra.queue(request)
  request
end

# 並列実行
hydra.run

# 結果を収集
responses = requests.map do |request|
  {
    url: request.url,
    status: request.response.code,
    body: request.response.body,
    time: request.response.total_time
  }
end

responses.each do |response|
  puts "#{response[:url]}: #{response[:status]} (#{response[:time]}s)"
end

# コールバック付き並列処理
hydra = Typhoeus::Hydra.new
10.times do |i|
  request = Typhoeus::Request.new("https://api.example.com/items/#{i}")
  
  request.on_complete do |response|
    if response.success?
      data = JSON.parse(response.body)
      puts "アイテム #{i}: #{data['name']}"
    else
      puts "エラー アイテム #{i}: #{response.code}"
    end
  end
  
  hydra.queue(request)
end

hydra.run
puts "全てのリクエスト完了"

# 並行数制限付き並列処理
hydra = Typhoeus::Hydra.new(max_concurrency: 5)

50.times do |i|
  request = Typhoeus::Request.new("https://api.example.com/data/#{i}")
  
  request.on_complete do |response|
    puts "処理完了: #{i} - ステータス: #{response.code}"
  end
  
  hydra.queue(request)
end

hydra.run

# 動的リクエスト追加
hydra = Typhoeus::Hydra.new

first_request = Typhoeus::Request.new('https://api.example.com/posts/1')
first_request.on_complete do |response|
  if response.success?
    data = JSON.parse(response.body)
    
    # レスポンスに基づいて追加リクエストを生成
    data['related_urls'].each do |url|
      related_request = Typhoeus::Request.new(url)
      hydra.queue(related_request)
    end
  end
end

hydra.queue(first_request)
hydra.run

# メモ化設定(キャッシュ)
Typhoeus::Config.memoize = true

hydra = Typhoeus::Hydra.new
2.times do
  hydra.queue(Typhoeus::Request.new('https://api.example.com/cache-test'))
end

hydra.run  # 2回目は1回目の結果を再利用

# メモ化無効化
Typhoeus::Config.memoize = false

ストリーミング処理とファイル操作

require 'typhoeus'

# 大容量ファイルのストリーミングダウンロード
downloaded_file = File.open('large_file.zip', 'wb')
request = Typhoeus::Request.new('https://api.example.com/files/large.zip')

request.on_headers do |response|
  if response.code != 200
    downloaded_file.close
    File.delete('large_file.zip')
    raise "ダウンロード失敗: #{response.code}"
  end
  
  content_length = response.headers['Content-Length']
  puts "ファイルサイズ: #{content_length}バイト" if content_length
end

total_downloaded = 0
request.on_body do |chunk|
  downloaded_file.write(chunk)
  total_downloaded += chunk.bytesize
  
  # 進捗表示
  print "\rダウンロード中: #{total_downloaded}バイト"
  
  # 条件によるダウンロード中止
  if total_downloaded > 100 * 1024 * 1024  # 100MB制限
    puts "\nファイルサイズ制限によりダウンロード中止"
    :abort
  end
end

request.on_complete do |response|
  downloaded_file.close
  puts "\nダウンロード完了" if response.success?
end

request.run

# ファイルアップロード
file_path = '/path/to/upload.pdf'
response = Typhoeus.post(
  'https://api.example.com/upload',
  body: {
    title: 'アップロードファイル',
    description: 'テストファイルです',
    file: File.open(file_path, 'rb')
  },
  headers: {
    'Authorization' => 'Bearer your-token'
  }
)

if response.success?
  upload_result = JSON.parse(response.body)
  puts "アップロード完了: #{upload_result['file_id']}"
else
  puts "アップロード失敗: #{response.code}"
end

# マルチパート形式での複数ファイルアップロード
response = Typhoeus.post(
  'https://api.example.com/upload-multiple',
  body: {
    document1: File.open('file1.pdf', 'rb'),
    document2: File.open('file2.docx', 'rb'),
    metadata: { category: 'documents', public: false }.to_json
  },
  headers: {
    'Authorization' => 'Bearer your-token'
  }
)

# レスポンスストリーミング処理
buffer = StringIO.new
request = Typhoeus::Request.new('https://api.example.com/stream-data')

request.on_body do |chunk|
  buffer.write(chunk)
  
  # チャンクごとの処理
  lines = buffer.string.split("\n")
  
  # 完全な行のみ処理
  complete_lines = lines[0..-2]
  buffer.string = lines.last || ""
  
  complete_lines.each do |line|
    begin
      data = JSON.parse(line)
      puts "受信データ: #{data['timestamp']} - #{data['message']}"
    rescue JSON::ParserError
      # JSON以外の行は無視
    end
  end
end

request.run

Faraday統合と実用的な活用例

require 'faraday'
require 'typhoeus'
require 'typhoeus/adapters/faraday'

# Faraday with Typhoeus adapter
conn = Faraday.new(url: 'https://api.example.com') do |builder|
  builder.request :url_encoded
  builder.response :json
  builder.adapter :typhoeus
end

# 単一リクエスト
response = conn.get('/users/123')
puts response.body

# Faradayでの並列処理
conn.in_parallel do
  @user_response = conn.get('/users/123')
  @posts_response = conn.get('/users/123/posts')
  @comments_response = conn.get('/users/123/comments')
end

# 並列処理完了後にレスポンス利用可能
puts "ユーザー: #{@user_response.body['name']}"
puts "投稿数: #{@posts_response.body.size}"
puts "コメント数: #{@comments_response.body.size}"

# API クライアントクラス
class APIClient
  def initialize(base_url, token = nil)
    @conn = Faraday.new(url: base_url) do |builder|
      builder.request :json
      builder.response :json
      builder.adapter :typhoeus
    end
    
    @conn.headers['Authorization'] = "Bearer #{token}" if token
    @conn.headers['User-Agent'] = 'APIClient/1.0'
  end
  
  def get(path, params = {})
    @conn.get(path, params)
  end
  
  def post(path, data = {})
    @conn.post(path, data)
  end
  
  def batch_requests(&block)
    @conn.in_parallel(&block)
  end
end

# 使用例
client = APIClient.new('https://api.example.com', 'your-token')

# バッチリクエスト
client.batch_requests do
  @users = client.get('/users')
  @categories = client.get('/categories')
  @settings = client.get('/settings')
end

# WebスクレイピングでのTyphoeus活用
class WebScraper
  def initialize
    @hydra = Typhoeus::Hydra.new(max_concurrency: 10)
  end
  
  def scrape_urls(urls)
    results = []
    
    urls.each do |url|
      request = Typhoeus::Request.new(url, 
        followlocation: true,
        timeout: 30,
        headers: {
          'User-Agent' => 'Mozilla/5.0 (compatible; WebScraper/1.0)'
        }
      )
      
      request.on_complete do |response|
        if response.success?
          results << {
            url: url,
            title: extract_title(response.body),
            content: extract_content(response.body),
            status: response.code
          }
        else
          puts "スクレイピング失敗: #{url} - #{response.code}"
        end
      end
      
      @hydra.queue(request)
    end
    
    @hydra.run
    results
  end
  
  private
  
  def extract_title(html)
    html.match(/<title>(.*?)<\/title>/i)&.captures&.first&.strip
  end
  
  def extract_content(html)
    # 簡単なコンテンツ抽出例
    html.gsub(/<[^>]*>/, '').strip[0..200]
  end
end

# 使用例
scraper = WebScraper.new
urls = [
  'https://example.com/page1',
  'https://example.com/page2',
  'https://example.com/page3'
]

results = scraper.scrape_urls(urls)
results.each do |result|
  puts "#{result[:title]} - #{result[:url]}"
end