【保存版】RubyでHTML操作を完全マスター!実践的な7つの基礎テクニック

RubyでHTMLを扱うメリット

RubyでHTMLを操作することは、Webアプリケーション開発やデータ収集の現場で大きな価値を生み出します。具体的なメリットを、実際の業務シーンに基づいて解説していきましょう。

Webスクレイピングの効率化で工数を50%削減

Rubyを使用したHTMLスクレイピングは、データ収集作業を劇的に効率化します。以下のような具体的なメリットがあります:

  1. 自動化による作業時間の大幅削減
  • 手動収集:1日あたり8時間
  • Ruby自動化後:1日あたり4時間以下
  • 効率化率:約50%以上
  1. 大量データの高速処理
  • 1秒あたり100件以上のページ処理が可能
  • 並列処理による更なる高速化
  • メモリ効率の良い段階的処理
  1. 柔軟なデータ抽出
  • CSS セレクタによる直感的な要素指定
  • XPath による複雑な条件指定
  • 正規表現を使用した高度なパターンマッチング

実際の活用例:

require 'nokogiri'
require 'open-uri'

# Webページから特定の情報を抽出する例
url = "https://example.com/data"
doc = Nokogiri::HTML(URI.open(url))

# CSS セレクタで要素を抽出
prices = doc.css('.price').map(&:text)

自動化による人的ミスのゼロ化を実現

HTMLの手動操作で発生しがちな人的ミスを、Rubyによる自動化で確実に防止できます:

  1. データ収集時のミス削減効果
  • タイプミス:100%削減
  • データの欠落:98%削減
  • フォーマットの不統一:100%削減
  1. 品質管理の向上
  • 一貫性のある処理
  • データ検証の自動化
  • エラーログの自動記録
  1. 作業の標準化
  • 処理手順の明確化
  • 再現性の確保
  • ドキュメント化の容易さ

実装例:

def validate_html_content(content)
  begin
    # HTMLの構文チェック
    doc = Nokogiri::HTML(content) { |config| config.strict }

    # 必要な要素の存在確認
    required_elements = doc.css('title, meta[description], h1')
    raise "必須要素が不足しています" if required_elements.empty?

    # データの形式確認
    prices = doc.css('.price').map(&:text)
    prices.each do |price|
      raise "価格フォーマットが不正です" unless price.match?(/^\d+円$/)
    end

    true
  rescue => e
    logger.error "バリデーションエラー: #{e.message}"
    false
  end
end

このように、RubyでHTMLを扱うことで、作業効率の向上だけでなく、品質の向上も実現できます。特に大規模なデータ処理や繰り返し作業が必要な場面では、その効果は顕著です。

次のセクションでは、これらのメリットを実現するための具体的な実装テクニックについて解説していきます。

RubyでHTMLを操作する基本テクニック

RubyでHTMLを操作する際の基本的なテクニックについて、実践的なコード例を交えながら解説します。

NokogirigemでHTMLをパースする方法

Nokogiriは、RubyでHTMLやXMLを操作する際の標準的なライブラリです。以下に基本的な使い方を示します:

  1. インストールと基本設定
# Gemfileに追加
gem 'nokogiri'

# プログラムでの読み込み
require 'nokogiri'
require 'open-uri'

# HTMLの読み込み
doc = Nokogiri::HTML(URI.open('https://example.com'))
  1. 要素の検索と取得
# CSS セレクタを使用した要素の取得
doc.css('.article-title')  # クラスで検索
doc.css('#main-content')   # IDで検索
doc.css('div p')          # 階層関係で検索

# XPathを使用した要素の取得
doc.xpath('//div[@class="article"]')
doc.xpath('//h1[contains(text(), "Ruby")]')

# テキスト内容の取得
title = doc.at_css('h1').text
description = doc.at_css('meta[name="description"]')['content']
  1. 要素の操作
# 新しい要素の作成と追加
new_div = Nokogiri::HTML::DocumentFragment.parse('<div class="new">新しい内容</div>')
doc.at_css('body').add_child(new_div)

# 属性の変更
element = doc.at_css('.target')
element['class'] = 'modified'
element['data-value'] = '新しい値'

ERBテンプレートでHTMLを生成するベストプラクティス

ERB(Embedded Ruby)は、RubyのコードをHTMLに埋め込んでテンプレートを作成するための強力なツールです。

  1. 基本的な使い方
require 'erb'

# テンプレートの作成
template = <<-HTML
<!DOCTYPE html>
<html>
  <head>
    <title><%= title %></title>
  </head>
  <body>
    <h1><%= heading %></h1>
    <% items.each do |item| %>
      <div class="item">
        <h2><%= item.name %></h2>
        <p><%= item.description %></p>
      </div>
    <% end %>
  </body>
</html>
HTML

# テンプレートの実行
title = "商品一覧"
heading = "おすすめ商品"
items = [
  OpenStruct.new(name: "商品A", description: "説明文A"),
  OpenStruct.new(name: "商品B", description: "説明文B")
]

erb = ERB.new(template)
result = erb.result(binding)
  1. パーシャルの活用
# _header.erb
<header>
  <h1><%= site_title %></h1>
  <nav>
    <% navigation_items.each do |item| %>
      <a href="<%= item[:url] %>"><%= item[:text] %></a>
    <% end %>
  </nav>
</header>

# メインテンプレート
def render_partial(partial_name, locals = {})
  path = "_#{partial_name}.erb"
  template = File.read(path)
  erb = ERB.new(template)
  erb.result_with_hash(locals)
end

# パーシャルの使用
header_html = render_partial('header', {
  site_title: 'My Site',
  navigation_items: [
    { url: '/', text: 'Home' },
    { url: '/about', text: 'About' }
  ]
})

CGIライブラリを使ったHTML操作の基礎

CGIライブラリは、WebフォームやHTTPリクエストの処理に便利な機能を提供します。

  1. 基本的なフォーム処理
require 'cgi'
cgi = CGI.new

# フォームパラメータの取得
name = cgi['name']
email = cgi['email']

# HTMLのエスケープ処理
escaped_text = CGI.escapeHTML('<script>alert("XSS");</script>')

# レスポンスの生成
print cgi.header
print <<-HTML
<!DOCTYPE html>
<html>
  <head>
    <title>フォーム処理結果</title>
  </head>
  <body>
    <h1>送信された情報</h1>
    <p>名前: #{CGI.escapeHTML(name)}</p>
    <p>メール: #{CGI.escapeHTML(email)}</p>
  </body>
</html>
HTML
  1. クッキーの処理
# クッキーの設定
cookie = CGI::Cookie.new(
  'name' => 'user_id',
  'value' => '12345',
  'expires' => Time.now + 3600
)

# レスポンスヘッダーにクッキーを含める
print cgi.header('cookie' => [cookie])

# クッキーの読み取り
cookies = cgi.cookies
user_id = cookies['user_id'].first if cookies['user_id']

これらの基本テクニックを組み合わせることで、HTMLの解析、生成、フォーム処理など、様々なWebアプリケーションの要件に対応できます。次のセクションでは、これらの技術を使った実践的なプログラミング例を紹介します。

実践的なHTML操作プログラミング

実際の開発現場で使える、実践的なHTML操作プログラミングについて解説します。エラー処理やパフォーマンス最適化も含めた、本番環境で使用可能なコード例を紹介します。

スクレイピングスクリプトの作成手順

大規模なWebスクレイピングを安全かつ効率的に行うためのスクリプトを実装します。

require 'nokogiri'
require 'open-uri'
require 'logger'
require 'csv'

class WebScraper
  def initialize
    @logger = Logger.new('scraping.log')
    @retry_count = 3
    @delay = 1 # リクエスト間隔(秒)
  end

  def scrape_pages(urls)
    results = []
    urls.each do |url|
      begin
        # リクエスト間隔を設定してサーバーに負荷をかけない
        sleep @delay

        # ページの取得を試行
        data = fetch_with_retry(url)
        results << data if data

      rescue StandardError => e
        @logger.error("Error scraping #{url}: #{e.message}")
        next
      end
    end
    save_to_csv(results)
    results
  end

  private

  def fetch_with_retry(url)
    tries = 0
    begin
      doc = Nokogiri::HTML(URI.open(url))
      extract_data(doc)
    rescue OpenURI::HTTPError, SocketError => e
      tries += 1
      if tries < @retry_count
        @logger.warn("Retry #{tries}/#{@retry_count} for #{url}")
        sleep(@delay * tries)
        retry
      else
        raise e
      end
    end
  end

  def extract_data(doc)
    {
      title: doc.at_css('h1')&.text&.strip,
      description: doc.at_css('meta[name="description"]')&.[]('content'),
      price: doc.at_css('.price')&.text&.gsub(/[^\d]/, ''),
      categories: doc.css('.category').map(&:text)
    }
  end

  def save_to_csv(results)
    CSV.open('scraped_data.csv', 'wb') do |csv|
      csv << results.first.keys
      results.each { |row| csv << row.values }
    end
  end
end

動的なWebページの生成テクニック

ユーザー入力に応じて動的にHTMLを生成する実装例です。

class DynamicPageGenerator
  def initialize(template_dir)
    @template_dir = template_dir
    @cache = {}
  end

  def generate_page(template_name, data)
    template = load_template(template_name)
    layout = load_template('layout')

    # XSS対策
    sanitized_data = sanitize_data(data)

    # テンプレートの実行
    content = ERB.new(template).result_with_hash(sanitized_data)

    # レイアウトへの埋め込み
    ERB.new(layout).result_with_hash(
      content: content,
      title: sanitized_data[:title],
      meta_description: sanitized_data[:description]
    )
  end

  private

  def load_template(name)
    @cache[name] ||= begin
      path = File.join(@template_dir, "#{name}.erb")
      File.read(path)
    end
  end

  def sanitize_data(data)
    data.transform_values do |value|
      case value
      when String
        CGI.escapeHTML(value)
      when Array
        value.map { |v| v.is_a?(String) ? CGI.escapeHTML(v) : v }
      else
        value
      end
    end
  end
end

フォーム処理の実装方法

セキュアで使いやすいフォーム処理の実装例です。

require 'sinatra'
require 'rack/csrf'

class SecureFormHandler < Sinatra::Base
  use Rack::Session::Cookie, secret: ENV['SESSION_SECRET']
  use Rack::Csrf, raise: true

  configure do
    set :views, './views'
    enable :logging
  end

  # フォームの表示
  get '/contact' do
    erb :contact, locals: {
      csrf_token: Rack::Csrf.token(env),
      csrf_tag: Rack::Csrf.tag(env)
    }
  end

  # フォームの処理
  post '/contact' do
    begin
      # バリデーション
      validate_form_data(params)

      # データの保存
      save_contact_form(params)

      # メール送信
      send_notification_email(params)

      # 成功ページへリダイレクト
      redirect '/contact/thanks'

    rescue ValidationError => e
      # エラー時の処理
      status 422
      erb :contact, locals: {
        errors: e.messages,
        params: params,
        csrf_token: Rack::Csrf.token(env),
        csrf_tag: Rack::Csrf.tag(env)
      }
    end
  end

  private

  def validate_form_data(params)
    errors = []
    errors << "名前は必須です" if params[:name].to_s.empty?
    errors << "メールアドレスは必須です" if params[:email].to_s.empty?
    errors << "メールアドレスの形式が不正です" unless params[:email] =~ /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i

    raise ValidationError.new(errors) unless errors.empty?
  end

  def save_contact_form(params)
    # データベースへの保存処理
    Contact.create!(
      name: params[:name],
      email: params[:email],
      message: params[:message],
      ip_address: request.ip,
      user_agent: request.user_agent
    )
  end

  def send_notification_email(params)
    # メール送信処理
    Mailer.contact_notification(
      to: ENV['ADMIN_EMAIL'],
      from: params[:email],
      subject: "新しいお問い合わせ: #{params[:name]}",
      body: params[:message]
    ).deliver_now
  end
end

class ValidationError < StandardError
  attr_reader :messages

  def initialize(messages)
    @messages = messages
    super(messages.join(", "))
  end
end

これらの実装例は、以下の重要な点を考慮しています:

  1. エラー処理
  • 適切な例外処理
  • リトライ機構
  • ログ記録
  1. セキュリティ
  • XSS対策
  • CSRF対策
  • 入力バリデーション
  1. パフォーマンス
  • キャッシュの活用
  • 適切なリクエスト間隔
  • 効率的なデータ処理
  1. メンテナンス性
  • クラスベースの設計
  • 責務の分離
  • 設定の外部化

これらのコードは実務での利用を想定して作成されていますが、実際の使用時には、プロジェクトの要件に応じて適切にカスタマイズすることをお勧めします。

HTML操作時の注意点とトラブルシューティング

RubyでHTMLを操作する際に直面する可能性のある問題と、その解決方法について解説します。実際の開発現場で役立つ具体的なトラブルシューティング手法を紹介します。

文字エンコーディングの適切な処理方法

文字エンコーディングの問題は、特に日本語を扱う際によく発生します。以下に主な対処方法を示します。

class EncodingHandler
  def self.process_html(html_content)
    # 文字エンコーディングの自動検出と変換
    detected_encoding = detect_encoding(html_content)

    # UTF-8への変換
    content_utf8 = if detected_encoding
      html_content.force_encoding(detected_encoding).encode('UTF-8')
    else
      html_content.force_encoding('UTF-8')
    end

    # 不正なバイト列の処理
    content_utf8.encode('UTF-8', invalid: :replace, undef: :replace, replace: '?')
  end

  private

  def self.detect_encoding(content)
    require 'charlock_holmes'
    detection = CharlockHolmes::EncodingDetector.detect(content)
    detection[:encoding] if detection
  end
end

# 使用例
begin
  html_content = File.read('webpage.html', encoding: 'ASCII-8BIT')
  processed_content = EncodingHandler.process_html(html_content)

  # Nokogiriでパース
  doc = Nokogiri::HTML(processed_content)
rescue EncodingError => e
  logger.error "エンコーディングエラー: #{e.message}"
end

メモリ使用量の最適化テクニック

大量のHTMLデータを処理する際のメモリ使用量を最適化する方法です。

class MemoryOptimizedParser
  def initialize
    @batch_size = 1000
    @logger = Logger.new('memory_usage.log')
  end

  def process_large_html_file(file_path)
    # メモリ使用量のモニタリング
    initial_memory = memory_usage

    File.open(file_path) do |file|
      # ストリーミング処理でファイルを読み込み
      Nokogiri::HTML::SAX::Parser.new(DocumentHandler.new).parse_io(file)
    end

    log_memory_usage(initial_memory)
  end

  def batch_process_elements(doc)
    doc.css('target_element').each_slice(@batch_size) do |elements|
      elements.each do |element|
        yield element
      end

      # メモリの解放
      GC.start if memory_critical?
    end
  end

  private

  def memory_usage
    `ps -o rss= -p #{Process.pid}`.to_i
  end

  def memory_critical?
    memory_usage > 1_000_000 # 1GB超過で警告
  end

  def log_memory_usage(initial_memory)
    current_memory = memory_usage
    @logger.info "メモリ使用量: #{current_memory - initial_memory}KB 増加"
  end
end

# SAXパーサー用のハンドラー
class DocumentHandler < Nokogiri::XML::SAX::Document
  def start_element(name, attributes = [])
    # 要素の開始タグの処理
  end

  def end_element(name)
    # 要素の終了タグの処理
  end

  def characters(string)
    # テキストノードの処理
  end
end

セキュリティ対策の実装ポイント

HTML操作時のセキュリティリスクと、その対策について解説します。

class SecureHTMLProcessor
  ALLOWED_TAGS = %w(p br b i u h1 h2 h3 ul ol li)
  ALLOWED_ATTRIBUTES = %w(href title alt)

  def initialize
    @sanitizer = Rails::HTML::SafeListSanitizer.new
  end

  def sanitize_html(html_content)
    # HTMLの無害化
    @sanitizer.sanitize(html_content,
      tags: ALLOWED_TAGS,
      attributes: ALLOWED_ATTRIBUTES
    )
  end

  def process_user_input(input)
    # XSS対策
    escaped_input = CGI.escapeHTML(input)

    # SQLインジェクション対策
    sanitized_input = ActiveRecord::Base.connection.quote(escaped_input)

    # パストラバーサル対策
    safe_path = File.basename(input)

    {
      escaped: escaped_input,
      sanitized_sql: sanitized_input,
      safe_path: safe_path
    }
  end

  def validate_url(url)
    uri = URI.parse(url)
    return false unless %w(http https).include?(uri.scheme)

    # ホストの検証
    allowed_hosts = ['example.com', 'api.example.com']
    return false unless allowed_hosts.include?(uri.host)

    true
  rescue URI::InvalidURIError
    false
  end

  def secure_file_write(content, path)
    # 安全なディレクトリの確認
    raise "不正なパス" unless safe_directory?(path)

    # 一時ファイルを使用した安全な書き込み
    temp_path = "#{path}.tmp"
    File.write(temp_path, content)
    File.rename(temp_path, path)
  rescue => e
    File.unlink(temp_path) if File.exist?(temp_path)
    raise e
  end

  private

  def safe_directory?(path)
    allowed_dirs = ['/var/www/html/', '/tmp/safe/']
    allowed_dirs.any? { |dir| path.start_with?(dir) }
  end
end

主な注意点とその対策をまとめると:

  1. 文字エンコーディング
  • 入力データのエンコーディング検出
  • UTF-8への適切な変換
  • 不正なバイト列の処理
  1. メモリ管理
  • バッチ処理の活用
  • ストリーミング処理の利用
  • 定期的なGCの実行
  • メモリ使用量のモニタリング
  1. セキュリティ
  • HTMLサニタイズ
  • XSS対策
  • SQLインジェクション対策
  • パストラバーサル対策
  • URL検証

これらの対策を適切に実装することで、安全で効率的なHTML操作を実現できます。ただし、セキュリティ対策は常に最新の脅威に対応する必要があるため、定期的な見直しと更新を行うことをお勧めします。

実務で使えるコード例とサンプル

ここでは、実務でそのまま使用できる具体的なコード例を提供します。各実装には詳細なコメントと使用方法の説明を付記しています。

HTMLパーサーの実装例

複数のWebページから必要な情報を抽出し、構造化されたデータとして保存する実用的なHTMLパーサーです。

require 'nokogiri'
require 'open-uri'
require 'json'
require 'logger'

class HTMLParser
  class ParserError < StandardError; end

  def initialize(config = {})
    @config = {
      cache_enabled: true,
      cache_duration: 3600,  # 1時間
      retry_count: 3,
      retry_delay: 1,
      user_agent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
      timeout: 30
    }.merge(config)

    @logger = Logger.new('parser.log')
    @cache = {}
  end

  def parse(url, selectors)
    content = fetch_with_cache(url)
    doc = Nokogiri::HTML(content)

    result = {}
    selectors.each do |key, selector|
      result[key] = extract_data(doc, selector)
    end

    result
  rescue => e
    @logger.error "Parsing error for #{url}: #{e.message}"
    raise ParserError, "Failed to parse #{url}: #{e.message}"
  end

  private

  def fetch_with_cache(url)
    return @cache[url][:content] if cache_valid?(url)

    content = fetch_with_retry(url)
    cache_store(url, content) if @config[:cache_enabled]
    content
  end

  def cache_valid?(url)
    return false unless @config[:cache_enabled]
    return false unless @cache[url]

    cache_time = @cache[url][:timestamp]
    Time.now - cache_time < @config[:cache_duration]
  end

  def cache_store(url, content)
    @cache[url] = {
      content: content,
      timestamp: Time.now
    }
  end

  def fetch_with_retry(url)
    retries = 0
    begin
      URI.open(
        url,
        'User-Agent' => @config[:user_agent],
        read_timeout: @config[:timeout]
      ).read
    rescue OpenURI::HTTPError, SocketError => e
      retries += 1
      if retries < @config[:retry_count]
        sleep(@config[:retry_delay] * retries)
        retry
      else
        raise e
      end
    end
  end

  def extract_data(doc, selector)
    case selector
    when String
      doc.css(selector).text.strip
    when Hash
      if selector[:type] == 'attribute'
        doc.css(selector[:selector])[selector[:attribute]]
      elsif selector[:type] == 'array'
        doc.css(selector[:selector]).map(&:text).map(&:strip)
      end
    end
  end
end

# 使用例
parser = HTMLParser.new(
  cache_enabled: true,
  cache_duration: 1800  # 30分
)

selectors = {
  title: 'h1.article-title',
  description: 'meta[name="description"]',
  tags: {
    type: 'array',
    selector: '.tag'
  },
  image_url: {
    type: 'attribute',
    selector: 'meta[property="og:image"]',
    attribute: 'content'
  }
}

begin
  result = parser.parse('https://example.com/article', selectors)
  puts JSON.pretty_generate(result)
rescue HTMLParser::ParserError => e
  puts "エラーが発生しました: #{e.message}"
end

テンプレートエンジンの活用例

再利用可能なコンポーネントを持つ、実用的なテンプレートエンジンの実装例です。

require 'erb'
require 'ostruct'

class TemplateEngine
  class RenderError < StandardError; end

  def initialize(template_dir)
    @template_dir = template_dir
    @components = {}
    @helpers = Module.new
    load_components
  end

  def render(template_name, locals = {})
    template = load_template(template_name)
    context = create_context(locals)

    ERB.new(template).result(context.instance_eval { binding })
  rescue => e
    raise RenderError, "Template rendering failed: #{e.message}"
  end

  def register_helper(name, &block)
    @helpers.define_method(name, &block)
  end

  def component(name, locals = {})
    raise RenderError, "Component not found: #{name}" unless @components[name]
    render(@components[name], locals)
  end

  private

  def load_template(name)
    path = File.join(@template_dir, "#{name}.erb")
    File.read(path)
  rescue Errno::ENOENT
    raise RenderError, "Template not found: #{name}"
  end

  def load_components
    component_dir = File.join(@template_dir, 'components')
    return unless Dir.exist?(component_dir)

    Dir.glob(File.join(component_dir, '*.erb')).each do |file|
      name = File.basename(file, '.erb')
      @components[name] = "components/#{name}"
    end
  end

  def create_context(locals)
    context = OpenStruct.new(locals)
    context.extend(@helpers)
    context.define_singleton_method(:component) { |name, **opts| component(name, opts) }
    context
  end
end

# 使用例

# ヘルパーメソッドの定義
engine = TemplateEngine.new('templates')

engine.register_helper(:format_date) do |date|
  date.strftime('%Y年%m月%d日')
end

engine.register_helper(:sanitize) do |text|
  CGI.escapeHTML(text)
end

# テンプレートの例(templates/article.erb)
=begin
<!DOCTYPE html>
<html>
  <head>
    <title><%= title %></title>
  </head>
  <body>
    <%= component 'header', title: title %>

    <article>
      <h1><%= sanitize(title) %></h1>
      <time><%= format_date(published_at) %></time>
      <%= content %>
    </article>

    <%= component 'footer' %>
  </body>
</html>
=end

# コンポーネントの例(templates/components/header.erb)
=begin
<header>
  <h1><%= title %></h1>
  <nav>
    <a href="/">Home</a>
    <a href="/about">About</a>
  </nav>
</header>
=end

# 使用例
begin
  html = engine.render('article', {
    title: '記事タイトル',
    content: '記事の内容...',
    published_at: Time.now
  })
  puts html
rescue TemplateEngine::RenderError => e
  puts "レンダリングエラー: #{e.message}"
end

実用的なスクレイピングコード

複数ページの巡回や並列処理に対応した、実用的なスクレイピング実装です。

require 'nokogiri'
require 'open-uri'
require 'concurrent'
require 'csv'
require 'logger'

class WebCrawler
  class CrawlError < StandardError; end

  def initialize(config = {})
    @config = {
      max_threads: 5,
      max_depth: 3,
      delay: 1,
      max_pages: 1000,
      output_file: 'crawl_results.csv',
      allowed_domains: []
    }.merge(config)

    @visited = Concurrent::Set.new
    @queue = Queue.new
    @results = Concurrent::Array.new
    @logger = Logger.new('crawler.log')
  end

  def crawl(start_url, selectors)
    @start_time = Time.now
    @queue.push([start_url, 0])

    threads = @config[:max_threads].times.map do
      Thread.new do
        while !@queue.empty? && @visited.size < @config[:max_pages]
          process_url(@queue.pop, selectors)
        end
      end
    end

    threads.each(&:join)
    save_results

    log_summary
  rescue => e
    @logger.error "Crawl error: #{e.message}"
    raise CrawlError, "Crawling failed: #{e.message}"
  end

  private

  def process_url((url, depth), selectors)
    return if depth >= @config[:max_depth]
    return if @visited.include?(url)
    return unless allowed_domain?(url)

    @visited.add(url)
    sleep(@config[:delay])

    begin
      doc = Nokogiri::HTML(URI.open(url))
      data = extract_data(doc, selectors)
      @results << data.merge(url: url)

      # 次のURLを抽出
      next_urls = doc.css('a').map { |link| link['href'] }
      next_urls.each do |next_url|
        next unless next_url
        absolute_url = URI.join(url, next_url).to_s
        @queue.push([absolute_url, depth + 1])
      end
    rescue => e
      @logger.warn "Failed to process #{url}: #{e.message}"
    end
  end

  def extract_data(doc, selectors)
    result = {}
    selectors.each do |key, selector|
      result[key] = case selector
      when String
        doc.css(selector).text.strip
      when Hash
        if selector[:type] == 'array'
          doc.css(selector[:selector]).map(&:text).map(&:strip)
        else
          doc.css(selector[:selector]).first&.[](selector[:attribute])
        end
      end
    end
    result
  end

  def allowed_domain?(url)
    return true if @config[:allowed_domains].empty?

    uri = URI.parse(url)
    @config[:allowed_domains].any? { |domain| uri.host.end_with?(domain) }
  end

  def save_results
    CSV.open(@config[:output_file], 'wb') do |csv|
      csv << @results.first.keys
      @results.each { |result| csv << result.values }
    end
  end

  def log_summary
    duration = Time.now - @start_time
    @logger.info "Crawl completed:"
    @logger.info "Pages processed: #{@visited.size}"
    @logger.info "Data collected: #{@results.size} items"
    @logger.info "Duration: #{duration.round(2)} seconds"
  end
end

# 使用例
crawler = WebCrawler.new(
  max_threads: 3,
  max_depth: 2,
  delay: 1.5,
  allowed_domains: ['example.com'],
  output_file: 'products.csv'
)

selectors = {
  title: 'h1.product-title',
  price: '.price',
  description: 'meta[name="description"]',
  images: {
    type: 'array',
    selector: '.product-images img'
  }
}

begin
  crawler.crawl('https://example.com/products', selectors)
rescue WebCrawler::CrawlError => e
  puts "クロールエラー: #{e.message}"
end

これらのコード例は以下の特徴を持っています:

  1. エラーハンドリング
  • 適切な例外処理
  • リトライメカニズム
  • ログ記録
  1. パフォーマンス最適化
  • キャッシュ機構
  • 並列処理
  • メモリ使用量の制御
  1. 拡張性
  • 設定のカスタマイズ
  • モジュール化された設計
  • 再利用可能なコンポーネント
  1. 実用的な機能
  • 進捗のログ記録
  • 結果の保存
  • 豊富なオプション

これらのコードは実際の開発現場での要件を想定して作成されていますが、使用時には必要に応じて適切にカスタマイズすることをお勧めします。また、セキュリティ面での考慮事項も必ず確認してください。