実例で分かるデザインパターン ~Webスクレイピングツールを例にして~ ref: https://qiita.com/YSRKEN/items/30654cd7f2f628649d6c
require 'open-uri'
require 'nokogiri'
require 'parallel' # 複数のWebサイトを同時にDLするために使用
# サイトデータを事前に構築しておく
site_list = make_site_list()
# スレッド数はお好みで
data_hash = {} #データを蓄えるハッシュ
Parallel.each(site_list, in_threads: 10) do |site|
# ダウンロード・リトライ機能などを内包したダウンローダーのクラスを初期化
crawler = CrawlerFactory.create(site.name)
# 検索結果をダウンロードし、ハッシュに追加する
data_hash.merge!(crawler.execute)
end
# ダウンロード結果を保存する
db = Database.new #架空のデータベース
data_hash.each{|name, info|
db.execute("INSERT INTO data(name, piyo, poyo) VALUES ('#{name}', '#{info[:piyo]}', '#{info[:poyo]}')")
}
# 一番スタンダードなパターン
class CrawlerA < CrawlerBase
# コンストラクタ
def initialize
super
@site_name = "SiteA"
end
# {項目名, 詳細URL}の一覧を取得する
def get_data_hash
doc = download_page("site_a.com/result")
data_hash = {}
doc.css('hoge').each{|node|
data_hash[node.css('h1').inner_text] = node.css(`a`).inner_text
}
return data_hash
end
# 詳細URLから詳細情報を得る
def get_data_info(url)
doc = download_page(url)
data_info = {}
data_info[:piyo] = doc.css(`span > x`).inner_text
data_info[:poyo] = doc.css(`span > y`).inner_text
return data_info
end
end
# executeを書き換えてしまうパターン
class CrawlerB < CrawlerBase
# コンストラクタ
def initialize
super
@site_name = "SiteB"
end
# スクレイピングを実行する
def execute
# {項目名, キーワード}の一覧を取得する
json = @downloader.download_json("site_b.com/result.json")
json.each{|key, value|
data_hash[key] = "#{value}"
}
# {項目名, 詳細情報}の一覧を取得する
detail_data_hash = {}
data_hash.each{|name, keyword|
# APIを叩いて詳細情報を得る
json = @downloader.download_json("site_b.com/#{keyword}/detail.json")
# 一覧に追加
detail_data_hash[name] = json
}
# 結果を返す
return detail_data_hash
end
end
# ファクトリクラス
class CrawlerFactory
# インスタンスを生成する
def self.create(site_name)
case site_name
when "SiteA"
return CrawlerA.new
when "SiteB"
return CrawlerB.new
when "SiteC"
return CrawlerC.new
end
end
end
# 基底クラス
class CrawlerBase
# コンストラクタ(共通部分)
def initialize
# ダウンローダー部分を初期化
@downloader = Downloader.new
end
# スクレイピングを実行する(共通部分)
def execute
# {項目名, 詳細URL}の一覧を取得する
data_hash = get_data_hash()
# {項目名, 詳細情報}の一覧を取得する
detail_data_hash = {}
data_hash.each{|name, url|
# 詳細URLから詳細情報を得る
data_info = get_data_info(url)
# 一覧に追加
detail_data_hash[name] = data_info
}
# 結果を返す
return detail_data_hash
end
# Webページをダウンロード(共通部分)
def download_page(url)
# ページを取得
retry_count = 0
begin
doc = @downloader.download_html(url)
sleep(1000)
return doc
rescue
retry_count += 1
puts "retry...[#{retry_count}/3] #{url}"
sleep(1000)
retry if retry_count < 3
raise # 3回以上リトライ=諦める
end
end
# {項目名, 詳細URL}の一覧を取得する(抽象メソッド)
def get_data_hash
return {}
end
# 詳細URLから詳細情報を得る(抽象メソッド)
def get_data_info(url)
return {}
end
end
# 一見美しいが、要するに個別ファイルに分割しただけ
download_data = {}
case site.name
when "siteA" then download_data_a()
when "siteB" then download_data_b()
else download_data_other(site.name)
end
# 結果をマージする
data_hash.merge!(download_data)
# だいたいこんな風になってしまう例
detail_url_hash = {}
case site.name
# サイトAの場合、JSONを解析してURLを取り出す
when "siteA" then
json = downloader.download_json(site.special_url)
json.each{|key, value|
detail_url_hash[key] = "#{value.url}/detail"
}
# サイトBの場合、記述が特殊なので場合分けを行う
when "siteB" then
doc.css(~).css(~).each{|node|
detail_url_hash[node.css('span').css('name').inner_text] = node.css('h1.name').css('a').inner_text
}
# その他の場合
else
# pattern1~pattern3は、CSSセレクタの文字列をサイト毎に格納したハッシュ
doc.css(pattern1[site.name]).each{|node|
detail_url_hash[node.css(pattern2[site.name]).inner_text] = node.css(pattern3[site.name]).inner_text
}
end
require 'open-uri'
require 'nokogiri'
require 'parallel' # 複数のWebサイトを同時にDLするために使用
# サイトデータを事前に構築しておく
site_list = make_site_list()
# スレッド数はお好みで
data_hash = {} #データを蓄えるハッシュ
Parallel.each(site_list, in_threads: 10) do |site|
# ダウンロード・リトライ機能などを内包したダウンローダーのクラスを初期化
downloader = Downloader.new
# 検索結果をダウンロード・パースする
# (download_htmlメソッドには、リトライ機能や時間待ち機能を内包させておく)
doc = downloader.download_html(site.search_url)
# 検索結果の各項目を分析し、それぞれダウンロードしていく
doc.css('hoge > fuga').each{|node|
# 項目名と詳細URLを分析で読み取る
name = node.inner_text
detail_url = node.css('a').attribute('href').value
# 各URLをダウンロード・分析し、項目名と合わせる
# (RubyにはGILがあるので書き込みにロック処理が必要ない)
doc2 = downloader.download_html(detail_url)
info = {:piyo => doc2.css('piyo').inner_text, :poyo => doc2.css('poyo').inner_text}
data_hash[name] = info
}
end
# ダウンロード結果を保存する
db = Database.new #架空のデータベース
data_hash.each{|name, info|
db.execute("INSERT INTO data(name, piyo, poyo) VALUES ('#{name}', '#{info[:piyo]}', '#{info[:poyo]}')")
}
# Webスクレイピングのサンプル(るりまサーチを例にして)
require 'open-uri' # ダウンロード用のライブラリ
require 'nokogiri' # パース用のライブラリ
Encoding.default_external = "UTF-8" # 内部のエンコーディングをUTF-8にしておく
keyword = "include" # 検索キーワード
# 検索用URLを作成
url = "https://docs.ruby-lang.org/ja/search/query:#{keyword}/"
# ダウンロード処理(charsetに対象サイトのエンコーディングが入る)
charset = nil
html = open(url){|f|charset = f.charset; f.read}
# パース処理
doc = Nokogiri::HTML.parse(html, nil, charset)
# 分析処理(ここでは各メソッド名と返り値の文字列を取り出している)
method_name_list = []
doc.css('dt.entry-name > h3 > span.signature').each{|node|
method_name_list.push(node.inner_text.gsub(/\n +/, ''))
}
# 出力処理
method_name_list.each{|method_name|
puts "・#{method_name}"
}