【保存版】NokogiriでWebスクレイピングを完全攻略!実践的な7つのテクニック

Nokogiriとは?Webスクレイピングの強力な味方

RubyのWebスクレイピングライブラリの定番として選ばれる理由

Nokogiriは、HTMLやXMLを解析するためのRubyライブラリであり、その名前は日本語の「鋸(のこぎり)」に由来します。木を切るように、HTMLやXMLドキュメントを自在に解析できるという意味が込められています。

Nokogiriが多くのRubyエンジニアから選ばれる理由は、以下の特徴にあります:

  1. 高速な解析処理
  • ネイティブのC言語実装により、大量のHTMLやXMLを高速に処理
  • メモリ効率の良い実装により、大規模なドキュメントも扱える
  1. 直感的なAPI設計
   require 'nokogiri'
   require 'open-uri'

   # HTMLの取得と解析
   doc = Nokogiri::HTML(URI.open('https://example.com'))

   # CSSセレクタで要素を取得
   titles = doc.css('h1.title')

   # XPath式での要素取得
   links = doc.xpath('//a[@class="link"]')
  1. 豊富な検索機能
  • CSSセレクタとXPathの両方をサポート
  • 要素の検索、属性の取得、テキストの抽出が容易
  • 階層構造を考慮した柔軟な要素指定が可能
  1. 強力なドキュメント操作
  • 要素の追加、削除、置換が可能
  • 属性の操作や内容の変更が容易
  • DOMツリーの走査機能が充実

Nokogiriがサポートするパーサーとその特徴

Nokogiriは複数のパーサーをサポートしており、用途に応じて適切なものを選択できます:

  1. HTML4パーサー
  • 標準的なHTMLドキュメントの解析に最適
  • 壊れたHTMLも自動修正して解析
  • 最も一般的に使用されるパーサー
   # HTML4パーサーの使用例
   doc = Nokogiri::HTML4(html_content)
  1. HTML5パーサー
  • モダンなHTML5文書の解析に対応
  • より厳密なHTML5仕様に準拠
  • 新しいHTML5要素やセマンティクスをサポート
   # HTML5パーサーの使用例
   doc = Nokogiri::HTML5(html_content)
  1. XMLパーサー
  • 厳密なXML文書の解析に使用
  • 名前空間のサポート
  • DTDやスキーマの検証が可能
   # XMLパーサーの使用例
   doc = Nokogiri::XML(xml_content)
  1. SAXパーサー
  • イベントドリブンな解析が可能
  • 大規模なファイルを効率的に処理
  • メモリ使用量を抑えた処理が可能
   # SAXパーサーの使用例
   class MyHandler < Nokogiri::XML::SAX::Document
     def start_element(name, attrs = [])
       puts "開始要素: #{name}"
     end
   end

   parser = Nokogiri::XML::SAX::Parser.new(MyHandler.new)
   parser.parse(xml_content)

各パーサーは特定のユースケースに最適化されており、プロジェクトの要件に応じて適切なものを選択することで、効率的なスクレイピングを実現できます。特に、一般的なWebスクレイピングではHTML4パーサーが最も使用されますが、より特殊な要件がある場合は他のパーサーの使用を検討することをお勧めします。

環境構築から始めるNokogiri入門

gem installからbundlerでの管理まで

Nokogiriの環境構築には複数の方法がありますが、ここでは最も一般的な手順を解説します。

  1. 直接インストール
   # 最新版のインストール
   gem install nokogiri

   # 特定のバージョンを指定してインストール
   gem install nokogiri -v '1.15.5'
  1. Bundlerを使用したインストール
   # Gemfileに追加
   source 'https://rubygems.org'

   gem 'nokogiri'
   # もしくはバージョンを指定
   gem 'nokogiri', '~> 1.15.5'

   # インストールの実行
   bundle install
  1. プロジェクトでの使用開始
   # Bundlerを使用する場合
   require 'bundler/setup'
   require 'nokogiri'

   # 直接requireする場合
   require 'nokogiri'
  1. 動作確認
   # バージョン確認
   puts Nokogiri::VERSION

   # 簡単な解析テスト
   doc = Nokogiri::HTML('<h1>Hello, Nokogiri!</h1>')
   puts doc.at_css('h1').text  # => "Hello, Nokogiri!"

よくあるインストールエラーとその解決方法

Nokogiriのインストール時には、システムの環境によって様々なエラーが発生する可能性があります。以下に主な問題と解決方法を示します:

  1. ネイティブエクステンション関連のエラー エラーメッセージ例:
   ERROR: Failed to build gem native extension.

解決方法:

   # Ubuntuの場合
   sudo apt-get install build-essential patch ruby-dev zlib1g-dev liblzma-dev

   # macOSの場合
   xcode-select --install

   # インストール後、再度gem installを実行
   gem install nokogiri
  1. libxml2/libxslt関連のエラー エラーメッセージ例:
   ERROR: cannot find library 'libxml2'

解決方法:

   # Ubuntuの場合
   sudo apt-get install libxml2-dev libxslt-dev

   # macOSの場合(Homebrewを使用)
   brew install libxml2 libxslt

   # システムのlibxml2を使用する場合
   gem install nokogiri -- --use-system-libraries
  1. SSL証明書関連のエラー エラーメッセージ例:
   SSL_connect returned=1 errno=0 state=error: certificate verify failed

解決方法:

   # 証明書の更新(RubyGemsの場合)
   gem update --system

   # または環境変数で証明書のパスを指定
   export SSL_CERT_FILE=/path/to/cacert.pem
  1. メモリ不足エラー エラーメッセージ例:
   Failed to allocate memory (NoMemoryError)

解決方法:

   # swapファイルの作成(Linuxの場合)
   sudo dd if=/dev/zero of=/swapfile bs=1M count=2048
   sudo chmod 600 /swapfile
   sudo mkswap /swapfile
   sudo swapon /swapfile

インストールに問題が発生した場合は、以下の手順で対処することをお勧めします:

  1. エラーメッセージを注意深く読む
  2. システムの依存関係を確認
  3. 必要なライブラリをインストール
  4. 環境変数の設定を確認
  5. 必要に応じてシステムを再起動

これらの手順で解決しない場合は、Nokogiriの公式ドキュメントGitHubのIssuesで追加の情報を確認することをお勧めします。

Nokogiri の基本操作マスター

HTML ドキュメントの読み込みと解析

Nokogiriでは、様々な方法でHTMLドキュメントを読み込むことができます。以下に主要な方法を示します:

  1. 文字列からの読み込み
   # HTML文字列からドキュメントを作成
   html = '<html><body><h1>Hello World</h1></body></html>'
   doc = Nokogiri::HTML(html)

   # エンコーディングを指定する場合
   doc = Nokogiri::HTML(html, nil, 'UTF-8')
  1. ファイルからの読み込み
   # ローカルファイルを読み込む
   doc = Nokogiri::HTML(File.open('index.html'))

   # open-uriを使用してWebページを読み込む
   require 'open-uri'
   doc = Nokogiri::HTML(URI.open('https://example.com'))
  1. フラグメントの解析
   # HTMLフラグメントを解析
   fragment = Nokogiri::HTML.fragment('<div>部分的なHTML</div>')

   # フラグメント内の要素を操作
   fragment.css('div').each do |div|
     puts div.content
   end

CSS セレクタを使用した要素の取得テクニック

CSSセレクタを使用すると、直感的に要素を取得できます:

  1. 基本的なセレクタの使用
   # タグ名による取得
   doc.css('h1')               # すべてのh1タグ

   # クラスによる取得
   doc.css('.content')         # contentクラスを持つ要素

   # IDによる取得
   doc.css('#main')           # mainというIDを持つ要素

   # 属性による取得
   doc.css('a[href]')         # href属性を持つすべてのaタグ
   doc.css('img[alt="logo"]') # alt属性が"logo"であるimg要素
  1. 複合セレクタの活用
   # 子孫セレクタ
   doc.css('div.content p')    # contentクラスのdiv内のすべてのp要素

   # 直接の子要素
   doc.css('ul > li')         # ulの直接の子であるli要素

   # 複数条件の組み合わせ
   doc.css('div.content, div.sidebar') # contentクラスまたはsidebarクラスのdiv
  1. 要素の操作
   # テキスト内容の取得
   doc.css('h1').each do |element|
     puts element.text        # テキスト内容を出力
   end

   # 属性値の取得
   doc.css('a').each do |link|
     puts link['href']       # href属性の値を出力
   end

   # 属性の設定
   doc.css('img').each do |img|
     img['loading'] = 'lazy' # loading属性を設定
   end

XPath 式を活用した高度な要素指定

XPath式を使用すると、より細かい要素の指定が可能です:

  1. 基本的なXPath式
   # 絶対パス
   doc.xpath('/html/body/div')  # htmlのbody内のdivを取得

   # 相対パス
   doc.xpath('.//p')            # 現在のノードから見て任意の階層のp要素

   # 属性による指定
   doc.xpath('//div[@class="content"]')  # contentクラスを持つdiv
  1. 高度な条件指定
   # テキスト内容による指定
   doc.xpath('//p[contains(text(), "重要")]')  # "重要"を含むp要素

   # 位置による指定
   doc.xpath('//ul/li[1]')                    # 各ulの最初のli要素
   doc.xpath('//ul/li[last()]')               # 各ulの最後のli要素

   # 複数条件の組み合わせ
   doc.xpath('//div[@class="content" and contains(@id, "main")]')
  1. カスタム関数の活用
   # 名前空間の登録
   doc.xpath('//custom:element', 
     'custom' => 'http://example.com/namespace')

   # カスタムXPath関数の定義
   module MyCustomFunctions
     def filter_by_length(nodes, min_length)
       nodes.find_all { |node| node.content.length >= min_length.to_i }
     end
   end

   # 関数の登録と使用
   Nokogiri::XML::Document.send(:include, MyCustomFunctions)
   doc.xpath('//p[filter_by_length(., 100)]')

これらの基本操作を組み合わせることで、複雑なHTMLドキュメントから必要な情報を効率的に抽出できます。また、CSSセレクタとXPath式は状況に応じて使い分けることで、より柔軟なスクレイピングが可能になります。

実践的なスクレイピング実務テクニック

複数ページの効率的なクローリング方法

大規模なWebサイトのスクレイピングでは、複数ページを効率的に処理する必要があります。以下に実践的なテクニックを紹介します:

  1. ページネーション処理
   require 'nokogiri'
   require 'open-uri'
   require 'uri'

   class PaginationCrawler
     def initialize(base_url)
       @base_url = base_url
       @processed_urls = Set.new
       @results = []
     end

     def crawl(max_pages: 10)
       current_page = 1

       while current_page <= max_pages
         url = "#{@base_url}?page=#{current_page}"
         break unless process_page(url)
         current_page += 1

         # クロール間隔を設定
         sleep(1)
       end

       @results
     end

     private

     def process_page(url)
       return false if @processed_urls.include?(url)

       doc = Nokogiri::HTML(URI.open(url))
       @processed_urls.add(url)

       # ページ内のデータを抽出
       items = doc.css('.item').map do |item|
         {
           title: item.at_css('.title')&.text&.strip,
           price: item.at_css('.price')&.text&.strip
         }
       end

       @results.concat(items)

       # 次のページが存在するか確認
       !!doc.at_css('.next-page')
     rescue OpenURI::HTTPError => e
       puts "ページ取得エラー: #{url} - #{e.message}"
       false
     end
   end
  1. 並行処理による高速化
   require 'parallel'

   def parallel_crawl(urls, max_concurrency: 3)
     Parallel.map(urls, in_processes: max_concurrency) do |url|
       begin
         doc = Nokogiri::HTML(URI.open(url))
         # データ抽出処理
         {
           url: url,
           data: extract_data(doc),
           status: 'success'
         }
       rescue => e
         {
           url: url,
           error: e.message,
           status: 'error'
         }
       end
     end
   end

動的コンテンツへの対応策

JavaScriptで動的に生成されるコンテンツへの対応方法を説明します:

  1. APIエンドポイントの活用
   require 'json'
   require 'net/http'

   def fetch_api_data(api_url, params = {})
     uri = URI(api_url)
     uri.query = URI.encode_www_form(params)

     response = Net::HTTP.get_response(uri)

     if response.is_a?(Net::HTTPSuccess)
       JSON.parse(response.body)
     else
       raise "API呼び出しエラー: #{response.code}"
     end
   end
  1. ヘッドレスブラウザとの連携
   require 'selenium-webdriver'

   def scrape_dynamic_content(url)
     options = Selenium::WebDriver::Chrome::Options.new
     options.add_argument('--headless')

     driver = Selenium::WebDriver.create(:chrome, options: options)

     begin
       driver.get(url)
       # JavaScriptの実行完了を待機
       wait = Selenium::WebDriver::Wait.new(timeout: 10)
       wait.until { driver.execute_script('return document.readyState') == 'complete' }

       # HTML取得とNokogiri解析
       doc = Nokogiri::HTML(driver.page_source)
       extract_data(doc)
     ensure
       driver.quit
     end
   end

バリデーションとエラーハンドリング

堅牢なスクレイピングシステムには、適切なバリデーションとエラー処理が不可欠です:

  1. データバリデーション
   class DataValidator
     def self.validate_item(item)
       errors = []

       errors << "タイトルが空です" if item[:title].nil? || item[:title].empty?
       errors << "価格が不正です" unless valid_price?(item[:price])
       errors << "URLが不正です" unless valid_url?(item[:url])

       {
         valid: errors.empty?,
         errors: errors,
         data: item
       }
     end

     private

     def self.valid_price?(price)
       return false unless price.is_a?(String)
       price.match?(/^\d+,?\d*円$/)
     end

     def self.valid_url?(url)
       uri = URI.parse(url)
       uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
     rescue URI::InvalidURIError
       false
     end
   end
  1. エラー処理とリトライ機能
   class ResilientScraper
     MAX_RETRIES = 3
     RETRY_DELAY = 2 # 秒

     def scrape_with_retry(url)
       retries = 0

       begin
         doc = Nokogiri::HTML(URI.open(url))
         process_document(doc)
       rescue OpenURI::HTTPError => e
         if e.message =~ /429|5\d\d/ && retries < MAX_RETRIES
           retries += 1
           sleep(RETRY_DELAY * retries)
           retry
         else
           raise
         end
       rescue StandardError => e
         log_error(url, e)
         raise
       end
     end

     private

     def log_error(url, error)
       # エラーログの記録
       puts "[#{Time.now}] Error scraping #{url}: #{error.message}"
       puts error.backtrace.join("\n")
     end
   end

これらのテクニックを組み合わせることで、実運用に耐えうる堅牢なスクレイピングシステムを構築できます。特に、エラー処理とバリデーションは本番環境での安定性を確保する上で重要な役割を果たします。

パフォーマンスとスケーラビリティの改善

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

大規模なHTMLドキュメントを処理する際は、メモリ使用量の最適化が重要です。以下に効果的な手法を紹介します:

  1. ストリーム処理の活用
   require 'nokogiri'

   class StreamParser < Nokogiri::XML::SAX::Document
     def initialize
       @current_depth = 0
       @data = []
     end

     def start_element(name, attributes = [])
       @current_depth += 1

       # 特定の要素のみを処理
       if name == 'article' && @current_depth == 2
         @current_article = {}
         attributes.each do |key, value|
           @current_article[key] = value
         end
       end
     end

     def end_element(name)
       if name == 'article' && @current_depth == 2
         process_article(@current_article)
         @current_article = nil
       end
       @current_depth -= 1
     end

     private

     def process_article(article)
       # 必要な処理のみを実行
       @data << article
     end
   end

   # 使用例
   parser = Nokogiri::XML::SAX::Parser.new(StreamParser.new)
   parser.parse(File.open("large_file.xml"))
  1. メモリ解放の明示的制御
   class MemoryEfficientParser
     def process_large_document(file_path)
       batch_size = 1000
       current_batch = []

       File.open(file_path) do |file|
         doc = Nokogiri::HTML(file)

         doc.css('article').each do |article|
           current_batch << extract_data(article)

           if current_batch.size >= batch_size
             process_batch(current_batch)
             current_batch = []
             GC.start # 明示的なガベージコレクション
           end
         end

         # 残りのバッチを処理
         process_batch(current_batch) unless current_batch.empty?
       end
     end

     private

     def extract_data(article)
       # 必要なデータのみ抽出
       {
         title: article.at_css('h1')&.text,
         content: article.at_css('p')&.text
       }
     end

     def process_batch(batch)
       # バッチ処理の実装
       batch.each do |data|
         # データの保存や加工
       end
     end
   end

メモリ処理による高速化の実現

パフォーマンスを最大限に引き出すための高度な最適化テクニックを紹介します:

  1. 並列処理の最適化
   require 'parallel'
   require 'oj' # 高速なJSONパーサー

   class ParallelProcessor
     def initialize(worker_count = 4)
       @worker_count = worker_count
       @mutex = Mutex.new
       @results = []
     end

     def process_documents(urls)
       Parallel.each(urls, in_processes: @worker_count) do |url|
         result = process_single_document(url)

         @mutex.synchronize do
           @results << result
         end
       end

       @results
     end

     private

     def process_single_document(url)
       doc = Nokogiri::HTML(URI.open(url))

       # メモリ効率の良いデータ抽出
       {
         url: url,
         data: extract_minimal_data(doc)
       }
     end

     def extract_minimal_data(doc)
       # 必要最小限のデータのみを抽出
       doc.css('target_element').map do |element|
         element.text.strip
       end
     ensure
       # 明示的にメモリを解放
       doc = nil
       GC.start
     end
   end
  1. キャッシュの活用
   require 'redis'

   class CachedParser
     def initialize
       @redis = Redis.new
       @cache_ttl = 3600 # 1時間
     end

     def parse_with_cache(url)
       cache_key = "parsed_content:#{Digest::MD5.hexdigest(url)}"

       # キャッシュの確認
       if cached = @redis.get(cache_key)
         return Oj.load(cached)
       end

       # 新規パース
       result = parse_fresh_content(url)

       # キャッシュの保存
       @redis.setex(cache_key, @cache_ttl, Oj.dump(result))

       result
     end

     private

     def parse_fresh_content(url)
       doc = Nokogiri::HTML(URI.open(url))

       # メモリ効率を考慮したパース処理
       {
         title: doc.at_css('title')&.text,
         content: doc.at_css('main')&.text,
         timestamp: Time.now.to_i
       }
     end
   end
  1. プロファイリングとモニタリング
   require 'memory_profiler'

   class ProfilingParser
     def self.profile_parsing(url)
       report = MemoryProfiler.report do
         doc = Nokogiri::HTML(URI.open(url))
         yield(doc) if block_given?
       end

       # メモリ使用状況のレポート出力
       report.pretty_print(to_file: 'memory_profile.txt')
     end

     def self.monitor_memory_usage
       initial_memory = GetProcessMem.new.mb

       yield if block_given?

       final_memory = GetProcessMem.new.mb
       memory_difference = final_memory - initial_memory

       puts "メモリ使用量の変化: #{memory_difference.round(2)}MB"
     end
   end

これらの最適化テクニックを適切に組み合わせることで、大規模なスクレイピングプロジェクトでも安定したパフォーマンスを実現できます。特に、メモリ使用量の制御とキャッシュ戦略は、運用環境での安定性を確保する上で重要な要素となります。

セキュリティとマナーの適切事項

正しいリクエスト対策の設定

Webスクレイピングを行う際は、対象サイトへの負荷とセキュリティを考慮した適切な設定が必要です:

  1. リクエストヘッダーの適切な設定
   require 'nokogiri'
   require 'open-uri'

   class ResponsibleScraper
     def initialize
       @headers = {
         'User-Agent' => 'MyBot/1.0 (contact@example.com)',
         'Accept' => 'text/html,application/xhtml+xml,application/xml',
         'Accept-Language' => 'ja,en-US;q=0.9,en;q=0.8'
       }
     end

     def fetch_page(url)
       URI.open(url, @headers) do |f|
         Nokogiri::HTML(f)
       end
     rescue OpenURI::HTTPError => e
       handle_http_error(e, url)
     end

     private

     def handle_http_error(error, url)
       case error.message
       when /429/
         puts "レート制限に達しました: #{url}"
         sleep(300) # 5分待機
       when /403/
         puts "アクセスが拒否されました: #{url}"
       else
         puts "エラーが発生しました: #{error.message}"
       end
       nil
     end
   end
  1. アクセス頻度の制御
   class RateLimiter
     def initialize(requests_per_minute: 20)
       @interval = 60.0 / requests_per_minute
       @last_request = Time.now - @interval
     end

     def throttle
       wait_time = @interval - (Time.now - @last_request)
       sleep(wait_time) if wait_time > 0
       @last_request = Time.now

       yield if block_given?
     end
   end

   # 使用例
   limiter = RateLimiter.new(requests_per_minute: 30)
   urls.each do |url|
     limiter.throttle do
       # スクレイピング処理
     end
   end

robots.txtの尊重とサイトポリシーの確認

Webサイトのポリシーを尊重することは、倫理的なスクレイピングの基本です:

  1. robots.txtの解析と遵守
   require 'robotstxt'

   class PolicyCompliantScraper
     def initialize(base_url)
       @base_url = base_url
       @parser = initialize_robots_parser
     end

     def can_crawl?(url)
       return false unless @parser

       path = URI.parse(url).path
       @parser.allowed?(path, user_agent: 'MyBot/1.0')
     end

     private

     def initialize_robots_parser
       robots_url = URI.join(@base_url, '/robots.txt')
       robots_content = URI.open(robots_url).read
       Robotstxt.parse(robots_content, user_agent: 'MyBot/1.0')
     rescue OpenURI::HTTPError
       puts "robots.txtが見つかりませんでした: #{@base_url}"
       nil
     end
   end

   # 使用例
   scraper = PolicyCompliantScraper.new('https://example.com')
   if scraper.can_crawl?('/articles/123')
     # スクレイピング処理
   end
  1. サイトポリシーの確認と遵守
   class SitePolicy
     def self.check_terms_of_service(url)
       domain = URI.parse(url).host
       tos_paths = ['/terms', '/tos', '/terms-of-service']

       tos_paths.each do |path|
         tos_url = "https://#{domain}#{path}"
         begin
           response = URI.open(tos_url)
           puts "利用規約を確認してください: #{tos_url}"
           return true
         rescue OpenURI::HTTPError
           next
         end
       end

       puts "利用規約が見つかりませんでした。サイト管理者に確認することをお勧めします。"
       false
     end

     def self.validate_content_usage(content)
       restricted_patterns = [
         /confidential/i,
         /private/i,
         /proprietary/i
       ]

       restricted_patterns.each do |pattern|
         if content.match?(pattern)
           raise "制限付きコンテンツが検出されました"
         end
       end
     end
   end
  1. エラー時の適切な対応
   class EthicalScraper
     MAX_RETRIES = 3
     BACKOFF_FACTOR = 2

     def scrape_with_respect(url)
       retries = 0

       begin
         return unless SitePolicy.check_terms_of_service(url)

         doc = ResponsibleScraper.new.fetch_page(url)
         content = extract_content(doc)
         SitePolicy.validate_content_usage(content)

         content
       rescue => e
         retries += 1
         if retries <= MAX_RETRIES
           sleep(BACKOFF_FACTOR ** retries)
           retry
         else
           log_error(url, e)
           nil
         end
       end
     end

     private

     def log_error(url, error)
       File.open('scraping_errors.log', 'a') do |f|
         f.puts "[#{Time.now}] Error scraping #{url}: #{error.message}"
       end
     end
   end

これらの対策を実装することで、対象サイトに負荷をかけずに、かつ倫理的な方法でスクレイピングを実施できます。また、適切なエラー処理とログ記録により、問題が発生した際の対応も容易になります。

トラブルシューティングガイド

エンコード関連の問題解決

Nokogiriでよく遭遇するエンコーディング問題とその解決方法を説明します:

  1. 文字化けへの対処
   require 'nokogiri'
   require 'open-uri'

   # 基本的な文字コード対応
   html = URI.open('https://example.jp').read
   doc = Nokogiri::HTML(html, nil, 'Shift_JIS')

   # 文字コードを明示的に指定する場合
   doc = Nokogiri::HTML(html.force_encoding('Shift_JIS').encode('UTF-8'))

   # メタタグから文字コードを判定する場合
   doc = Nokogiri::HTML(html) do |config|
     config.strict.noent
   end
   charset = doc.at_css('meta[charset]')&.[]('charset') ||
             doc.at_css('meta[http-equiv="Content-Type"]')&.[]('content')&.match(/charset=(.+?)($|;)/i)&.[](1)
  1. エンコーディングエラーの解決例
   def safe_encode(text, from_encoding = 'Shift_JIS')
     text.force_encoding(from_encoding)
         .encode('UTF-8', 
                invalid: :replace, 
                undef: :replace, 
                replace: '?')
   rescue Encoding::InvalidByteSequenceError
     text.force_encoding('UTF-8')
   end

   # 使用例
   html = URI.open('https://example.jp').read
   safe_html = safe_encode(html)
   doc = Nokogiri::HTML(safe_html)

パース失敗時の対処法

HTMLのパースに失敗した際の一般的な対処方法を紹介します:

  1. 壊れたHTMLの修正
   # 不正なタグの処理
   html = html.gsub(/<\/?[^>]*>/) do |tag|
     if tag =~ /<\/?(?:div|p|span|a|img|h[1-6]|ul|ol|li)[\s>]/
       tag
     else
       ''  # 不明なタグを削除
     end
   end

   # 閉じタグの補完
   html = html.gsub(/<(div|p|span)((?!>).)*?>(?!.*?<\/\1>)/) do |match|
     "#{match}</#{$1}>"
   end

   doc = Nokogiri::HTML(html)
  1. 一般的なエラーと対処法
  • 空のドキュメントエラー def parse_with_validation(html) doc = Nokogiri::HTML(html) if doc.css('body').empty? raise "有効なHTML内容が見つかりません" end doc rescue => e puts "パースエラー: #{e.message}" nil end
  • 無効なセレクタエラー def safe_css_select(doc, selector) doc.css(selector) rescue Nokogiri::CSS::SyntaxError => e puts "無効なCSSセレクタ: #{selector}" puts "エラー: #{e.message}" [] # エラー時は空配列を返す end # 使用例 elements = safe_css_select(doc, 'div.content > p')
  1. よくあるトラブルと解決策
  • セレクタが要素を取得できない # 問題のある例 doc.css('.specific-class') # 要素が見つからない # 解決策:階層を確認 puts doc.at_css('.specific-class')&.parent&.to_html # 解決策:クラス名の完全一致を確認 doc.css('[class="specific-class"]')
  • 動的コンテンツが取得できない # JavaScriptで生成される内容は通常のNokogiriでは取得できない # 解決策1: APIを使用 require 'json' require 'net/http' response = Net::HTTP.get(URI('https://api.example.com/data')) data = JSON.parse(response) # 解決策2: Seleniumなどのヘッドレスブラウザを使用 require 'selenium-webdriver' driver = Selenium::WebDriver.for :chrome, options: options driver.get(url) html = driver.page_source doc = Nokogiri::HTML(html)

デバッグのためのヒント:

  1. ドキュメントの構造を確認
   # HTMLの構造を確認
   puts doc.to_html

   # 特定の要素の周辺構造を確認
   element = doc.at_css('.target')
   puts element&.parent&.to_html
  1. エラーの詳細を取得
   begin
     doc = Nokogiri::HTML(html)
     result = doc.css('selector').text
   rescue => e
     puts "エラータイプ: #{e.class}"
     puts "エラーメッセージ: #{e.message}"
     puts "バックトレース: #{e.backtrace.join("\n")}"
   end

これらの対処法を活用することで、多くの一般的なNokogiriのトラブルを解決できます。問題が発生した場合は、まずHTMLの構造とエンコーディングを確認し、その後で適切な対処法を選択することをお勧めします。

実践的なユースケース集

ニュースサイトの記事情報取得

  1. ニュース記事スクレイパーの実装
   class NewsArticleScraper
     def initialize
       @headers = {
         'User-Agent' => 'NewsBot/1.0 (contact@example.com)'
       }
     end

     def scrape_article(url)
       doc = Nokogiri::HTML(URI.open(url, @headers))

       {
         title: extract_title(doc),
         publish_date: extract_date(doc),
         author: extract_author(doc),
         content: extract_content(doc),
         categories: extract_categories(doc)
       }
     end

     private

     def extract_title(doc)
       # 一般的なニュースサイトのタイトル要素パターン
       doc.at_css('h1.article-title, .entry-title, [itemprop="headline"]')&.text&.strip
     end

     def extract_date(doc)
       # 日付の抽出と解析
       date_text = doc.at_css('time, .date, [itemprop="datePublished"]')&.[]('datetime') ||
                  doc.at_css('time, .date, [itemprop="datePublished"]')&.text

       return nil unless date_text
       DateTime.parse(date_text) rescue nil
     end

     def extract_author(doc)
       doc.at_css('[itemprop="author"], .author-name, .writer')&.text&.strip
     end

     def extract_content(doc)
       # 記事本文の抽出(広告や関連記事を除外)
       main_content = doc.css('.article-body p, .entry-content p').map(&:text).join("\n\n")
       main_content.gsub(/\n{3,}/, "\n\n").strip
     end

     def extract_categories(doc)
       doc.css('.category, .tags a').map(&:text).map(&:strip).uniq
     end
   end

   # 使用例
   scraper = NewsArticleScraper.new
   article = scraper.scrape_article('https://example.com/news/123')

Eコマースサイトの商品データ収集

  1. 商品情報スクレイパーの実装
   class ProductScraper
     def initialize
       @rate_limiter = RateLimiter.new(requests_per_minute: 20)
     end

     def scrape_product_listing(url)
       @rate_limiter.throttle do
         doc = Nokogiri::HTML(URI.open(url))

         products = doc.css('.product-item').map do |item|
           {
             name: extract_product_name(item),
             price: extract_price(item),
             availability: extract_availability(item),
             specifications: extract_specifications(item),
             image_url: extract_image_url(item)
           }
         end

         {
           products: products,
           next_page: extract_next_page(doc)
         }
       end
     end

     private

     def extract_product_name(item)
       item.at_css('.product-name, h2')&.text&.strip
     end

     def extract_price(item)
       price_text = item.at_css('.price, [itemprop="price"]')&.text&.strip
       return nil unless price_text

       # 価格のクリーニング
       price_text.gsub(/[^\d]/, '').to_i
     end

     def extract_availability(item)
       status = item.at_css('.stock-status, .availability')&.text&.strip
       case status
       when /在庫あり|在庫有り/
         :in_stock
       when /残りわずか/
         :limited_stock
       else
         :out_of_stock
       end
     end

     def extract_specifications(item)
       item.css('.specifications li, .specs tr').each_with_object({}) do |spec, hash|
         key = spec.at_css('.label, th')&.text&.strip
         value = spec.at_css('.value, td')&.text&.strip
         hash[key] = value if key && value
       end
     end

     def extract_image_url(item)
       item.at_css('img.product-image')&.[]('src')
     end

     def extract_next_page(doc)
       next_link = doc.at_css('.pagination .next a')
       next_link&.[]('href')
     end
   end

SNSプロフィール情報の抽出

  1. プロフィールスクレイパーの実装
   class ProfileScraper
     def initialize
       @cache = {}
     end

     def scrape_profile(url)
       return @cache[url] if @cache[url]

       doc = Nokogiri::HTML(URI.open(url))

       profile = {
         username: extract_username(doc),
         bio: extract_bio(doc),
         followers: extract_followers(doc),
         following: extract_following(doc),
         posts: extract_posts(doc),
         verified: is_verified?(doc)
       }

       @cache[url] = profile
       profile
     end

     private

     def extract_username(doc)
       doc.at_css('.profile-username, .user-name')&.text&.strip
     end

     def extract_bio(doc)
       doc.at_css('.bio, .profile-description')&.text&.strip
     end

     def extract_followers(doc)
       count_text = doc.at_css('.followers-count')&.text&.strip
       parse_count(count_text)
     end

     def extract_following(doc)
       count_text = doc.at_css('.following-count')&.text&.strip
       parse_count(count_text)
     end

     def extract_posts(doc)
       posts = doc.css('.post-item').map do |post|
         {
           content: post.at_css('.post-content')&.text&.strip,
           timestamp: parse_timestamp(post.at_css('.timestamp')&.text),
           likes: parse_count(post.at_css('.likes-count')&.text)
         }
       end

       posts.compact
     end

     def is_verified?(doc)
       !!doc.at_css('.verified-badge, .verified-icon')
     end

     private

     def parse_count(text)
       return 0 unless text

       case text.downcase
       when /k$/
         (text.to_f * 1000).to_i
       when /m$/
         (text.to_f * 1_000_000).to_i
       else
         text.gsub(/[^\d]/, '').to_i
       end
     end

     def parse_timestamp(text)
       return nil unless text

       begin
         case text
         when /(\d+)秒前/
           Time.now - $1.to_i
         when /(\d+)分前/
           Time.now - ($1.to_i * 60)
         when /(\d+)時間前/
           Time.now - ($1.to_i * 3600)
         when /(\d+)日前/
           Time.now - ($1.to_i * 86400)
         else
           Time.parse(text)
         end
       rescue
         nil
       end
     end
   end

これらの実装例は、実際のプロジェクトですぐに活用できる実践的なコードです。ただし、実際の使用時には以下の点に注意してください:

  1. サイトの利用規約とrobots.txtを必ず確認する
  2. 適切なレート制限を設定する
  3. エラー処理を実装する
  4. キャッシュ戦略を検討する
  5. 対象サイトの構造変更に対応できるように設計する

これらのコードは基本的な実装例であり、実際のプロジェクトでは要件に応じて適切にカスタマイズすることをお勧めします。