donfanning
8/15/2018 - 12:05 PM

実例で分かるデザインパターン ~Webスクレイピングツールを例にして~ ref: https://qiita.com/YSRKEN/items/30654cd7f2f628649d6c

実例で分かるデザインパターン ~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}"
}