Nokogiriとは?Webスクレイピングの強力な味方
RubyのWebスクレイピングライブラリの定番として選ばれる理由
Nokogiriは、HTMLやXMLを解析するためのRubyライブラリであり、その名前は日本語の「鋸(のこぎり)」に由来します。木を切るように、HTMLやXMLドキュメントを自在に解析できるという意味が込められています。
Nokogiriが多くのRubyエンジニアから選ばれる理由は、以下の特徴にあります:
- 高速な解析処理
- ネイティブのC言語実装により、大量のHTMLやXMLを高速に処理
- メモリ効率の良い実装により、大規模なドキュメントも扱える
- 直感的な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"]')
- 豊富な検索機能
- CSSセレクタとXPathの両方をサポート
- 要素の検索、属性の取得、テキストの抽出が容易
- 階層構造を考慮した柔軟な要素指定が可能
- 強力なドキュメント操作
- 要素の追加、削除、置換が可能
- 属性の操作や内容の変更が容易
- DOMツリーの走査機能が充実
Nokogiriがサポートするパーサーとその特徴
Nokogiriは複数のパーサーをサポートしており、用途に応じて適切なものを選択できます:
- HTML4パーサー
- 標準的なHTMLドキュメントの解析に最適
- 壊れたHTMLも自動修正して解析
- 最も一般的に使用されるパーサー
# HTML4パーサーの使用例 doc = Nokogiri::HTML4(html_content)
- HTML5パーサー
- モダンなHTML5文書の解析に対応
- より厳密なHTML5仕様に準拠
- 新しいHTML5要素やセマンティクスをサポート
# HTML5パーサーの使用例 doc = Nokogiri::HTML5(html_content)
- XMLパーサー
- 厳密なXML文書の解析に使用
- 名前空間のサポート
- DTDやスキーマの検証が可能
# XMLパーサーの使用例 doc = Nokogiri::XML(xml_content)
- 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の環境構築には複数の方法がありますが、ここでは最も一般的な手順を解説します。
- 直接インストール
# 最新版のインストール gem install nokogiri # 特定のバージョンを指定してインストール gem install nokogiri -v '1.15.5'
- Bundlerを使用したインストール
# Gemfileに追加 source 'https://rubygems.org' gem 'nokogiri' # もしくはバージョンを指定 gem 'nokogiri', '~> 1.15.5' # インストールの実行 bundle install
- プロジェクトでの使用開始
# Bundlerを使用する場合 require 'bundler/setup' require 'nokogiri' # 直接requireする場合 require 'nokogiri'
- 動作確認
# バージョン確認
puts Nokogiri::VERSION
# 簡単な解析テスト
doc = Nokogiri::HTML('<h1>Hello, Nokogiri!</h1>')
puts doc.at_css('h1').text # => "Hello, Nokogiri!"
よくあるインストールエラーとその解決方法
Nokogiriのインストール時には、システムの環境によって様々なエラーが発生する可能性があります。以下に主な問題と解決方法を示します:
- ネイティブエクステンション関連のエラー エラーメッセージ例:
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
- 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
- SSL証明書関連のエラー エラーメッセージ例:
SSL_connect returned=1 errno=0 state=error: certificate verify failed
解決方法:
# 証明書の更新(RubyGemsの場合) gem update --system # または環境変数で証明書のパスを指定 export SSL_CERT_FILE=/path/to/cacert.pem
- メモリ不足エラー エラーメッセージ例:
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
インストールに問題が発生した場合は、以下の手順で対処することをお勧めします:
- エラーメッセージを注意深く読む
- システムの依存関係を確認
- 必要なライブラリをインストール
- 環境変数の設定を確認
- 必要に応じてシステムを再起動
これらの手順で解決しない場合は、Nokogiriの公式ドキュメントやGitHubのIssuesで追加の情報を確認することをお勧めします。
Nokogiri の基本操作マスター
HTML ドキュメントの読み込みと解析
Nokogiriでは、様々な方法でHTMLドキュメントを読み込むことができます。以下に主要な方法を示します:
- 文字列からの読み込み
# HTML文字列からドキュメントを作成 html = '<html><body><h1>Hello World</h1></body></html>' doc = Nokogiri::HTML(html) # エンコーディングを指定する場合 doc = Nokogiri::HTML(html, nil, 'UTF-8')
- ファイルからの読み込み
# ローカルファイルを読み込む
doc = Nokogiri::HTML(File.open('index.html'))
# open-uriを使用してWebページを読み込む
require 'open-uri'
doc = Nokogiri::HTML(URI.open('https://example.com'))
- フラグメントの解析
# HTMLフラグメントを解析
fragment = Nokogiri::HTML.fragment('<div>部分的なHTML</div>')
# フラグメント内の要素を操作
fragment.css('div').each do |div|
puts div.content
end
CSS セレクタを使用した要素の取得テクニック
CSSセレクタを使用すると、直感的に要素を取得できます:
- 基本的なセレクタの使用
# タグ名による取得
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要素
- 複合セレクタの活用
# 子孫セレクタ
doc.css('div.content p') # contentクラスのdiv内のすべてのp要素
# 直接の子要素
doc.css('ul > li') # ulの直接の子であるli要素
# 複数条件の組み合わせ
doc.css('div.content, div.sidebar') # contentクラスまたはsidebarクラスのdiv
- 要素の操作
# テキスト内容の取得
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式を使用すると、より細かい要素の指定が可能です:
- 基本的なXPath式
# 絶対パス
doc.xpath('/html/body/div') # htmlのbody内のdivを取得
# 相対パス
doc.xpath('.//p') # 現在のノードから見て任意の階層のp要素
# 属性による指定
doc.xpath('//div[@class="content"]') # contentクラスを持つdiv
- 高度な条件指定
# テキスト内容による指定
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")]')
- カスタム関数の活用
# 名前空間の登録
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サイトのスクレイピングでは、複数ページを効率的に処理する必要があります。以下に実践的なテクニックを紹介します:
- ページネーション処理
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
- 並行処理による高速化
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で動的に生成されるコンテンツへの対応方法を説明します:
- 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
- ヘッドレスブラウザとの連携
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
バリデーションとエラーハンドリング
堅牢なスクレイピングシステムには、適切なバリデーションとエラー処理が不可欠です:
- データバリデーション
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
- エラー処理とリトライ機能
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ドキュメントを処理する際は、メモリ使用量の最適化が重要です。以下に効果的な手法を紹介します:
- ストリーム処理の活用
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"))
- メモリ解放の明示的制御
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
メモリ処理による高速化の実現
パフォーマンスを最大限に引き出すための高度な最適化テクニックを紹介します:
- 並列処理の最適化
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
- キャッシュの活用
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
- プロファイリングとモニタリング
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スクレイピングを行う際は、対象サイトへの負荷とセキュリティを考慮した適切な設定が必要です:
- リクエストヘッダーの適切な設定
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
- アクセス頻度の制御
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サイトのポリシーを尊重することは、倫理的なスクレイピングの基本です:
- 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
- サイトポリシーの確認と遵守
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
- エラー時の適切な対応
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でよく遭遇するエンコーディング問題とその解決方法を説明します:
- 文字化けへの対処
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)
- エンコーディングエラーの解決例
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のパースに失敗した際の一般的な対処方法を紹介します:
- 壊れた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)
- 一般的なエラーと対処法
- 空のドキュメントエラー
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')
- よくあるトラブルと解決策
- セレクタが要素を取得できない
# 問題のある例 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)
デバッグのためのヒント:
- ドキュメントの構造を確認
# HTMLの構造を確認
puts doc.to_html
# 特定の要素の周辺構造を確認
element = doc.at_css('.target')
puts element&.parent&.to_html
- エラーの詳細を取得
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の構造とエンコーディングを確認し、その後で適切な対処法を選択することをお勧めします。
実践的なユースケース集
ニュースサイトの記事情報取得
- ニュース記事スクレイパーの実装
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コマースサイトの商品データ収集
- 商品情報スクレイパーの実装
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プロフィール情報の抽出
- プロフィールスクレイパーの実装
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
これらの実装例は、実際のプロジェクトですぐに活用できる実践的なコードです。ただし、実際の使用時には以下の点に注意してください:
- サイトの利用規約とrobots.txtを必ず確認する
- 適切なレート制限を設定する
- エラー処理を実装する
- キャッシュ戦略を検討する
- 対象サイトの構造変更に対応できるように設計する
これらのコードは基本的な実装例であり、実際のプロジェクトでは要件に応じて適切にカスタマイズすることをお勧めします。