初学者がスクレイピングに興味を持っていろいろハマった話

#はじめに
楽天Bookから最低2週間以上先の漫画の発売日情報を入手し、DBに登録していくということをしようとしました。
そこで、スクレイピングという単語すら知らなかった私が、色々ドツボにハマってしまいましたので、実行手順等を残そうと思います。
（正規表現や、DB登録については省略しようと思います。）

# 実行環境または使用Gem
Ruby '2.6.5'
Ruby on Rails '5.2.4'
Gem 'mechanize'

#Mechanizeとは
スクレイピングを簡単に行ってくれるGemです。他にも'nokogiri'というGemがあるのですが、個人的にはこの'nokogiri'の機能を搭載していて、他のGemを入れずにできて便利だったという感想です。違うようだったら教えてください。

#gemの追加
gemファイルに下記を記載して、 `bundle install`

```ruby
gem 'mechanize'
```

#基本的なやり方

```ruby:sample.rb
require 'mechanize'

agent = Mechanize.new
page = agent.get("スクレイピングしたいURL")
elements = page.search('XpathかCSSセレクタ')

# urlの取得は　.attribute('href').value を後ろにつける。
# 内容だけ欲しい時は　.inner_text を後ろにつける。

puts elements  
```

Xpathについては下記のQiita記事が参考になりました。
https://qiita.com/rllllho/items/cb1187cec0fb17fc650a

基本的には検証モードにして欲しい要素を`右クリック`→`Copy`→`Copy Xpath` で大丈夫だと思います。
（個人的には　XpathやCSSセレクタの感覚がわかるとjsonの分解が楽になる気がしました。）


# 試しに使ってみる
試しに　https://books.rakuten.co.jp/rb/16371179/
から商品基本情報の関連作品をとってみようと思います。

```ruby:sample2.rb
require 'mechanize'

agent = Mechanize.new
page = agent.get("https://books.rakuten.co.jp/rb/16371179/")

#商品基本情報内の　'ダイヤのA'　及び　リンクURLを取得してみる
title = page.search('//*[@id="productDetailedDescription"]/div/ul/li[3]/span[2]/a')
title_name = title.inner_text
title_link = title.attribute('href').value

puts title_name
puts title_link
```
そして実行

```ruby
ruby sample2.rb

#成功していれば下記のように出る
ダイヤのA
https://books.rakuten.co.jp/mediamix/XW00153690/?l-id=item-c-mediamix
```
ルビー単体での実装ではこんなもので大丈夫ではないでしょうか？
では、次から`はじめに`で書いた内容を実装していこうと思います。

#はじめにの実装
では実際に実装していきたいと思いますやることとしては
1. rails 内で実行したいのでクラスを作成する。
2. どうやって現在日時から２週間以上のデーターを取ってくるか。（月跨ぎを含む）
3. 取ってきた中で漫画の詳細に入る必要がある。
4. 商品基本情報内のISBMコードを取得する。
をやっていきたいと思います。

1. rails内で実行したいので、クラスを作成し、言語化してみました。

```ruby:scraping.rb
require 'mechanize'

class Scraping
  def self.start
  #取得するURLを作成
  #ページネーションの　次の30件のURLを取得かつ本の詳細URLを取得
  #スクレイピングの実行(ISBNコード取得)
  end
end  
```
2.どうやって現在日時から２週間以上のデーターを取ってくるか。（月跨ぎを含む）

どれが正解かは私はわからないですが、楽天のコミックの発売一覧はこのようなURLでした。
https://books.rakuten.co.jp/calendar/001001/weekly/?tid=2020-07-12
おそらくですがこのような感じだと思われます。
/001001/ →　漫画(コミック）　　　本のジャンル選択
/weekly/ →　期間で絞り込む(１週間単位）　
/?tid=2020-07-12/ →　現在日時

これよりアドレスは

```ruby
"https://books.rakuten.co.jp/calendar/001001/monthly/?tid=#{可変月}"　
```
でいいのではないでしょうか

```ruby:scraping.rb
require 'mechanize'
require 'date'
(略)
  def self.start
    content_urls = create_content_url
    #ページネーションの　次の30件のURLを取得かつ本の詳細URLを取得
    #スクレイピングの実行（ISBNコード取得)
  end

  def self.create_content_url
    target_date = Date.today
    search_days = ["#{target_date.year}-#{format("%02d", target_date.month)}-01"]
    #もし14日以降であれば次の月もスクレイピングする範囲対象にする
    if Date.today.day > 14
      target_date2 = target_date >> 1
      search_days.push("#{target_date2.year}-#{format("%02d", target_date2.month)}-01")
    end
  
    #URLの作成 
    search_days.map do |search_day|
      "https://books.rakuten.co.jp/calendar/001001/monthly/?tid=#{search_day}"
    end
  end
end
```

3.取ってきた中で漫画の詳細に入る必要がある。

まず、https://books.rakuten.co.jp/calendar/001001/monthly/?tid=2020-07-12
ここでは、月の発売の漫画を一覧には出していますが、ページネーションをしている為、全ての漫画詳細のURLを確保できません。
ですので、`次の30件`のリンクを取得させてそれがなくなるまで自動で回収させるところを実装していきます。
ただ、確認のために全てを毎回スクレイピングさせるのも負荷がかかったり時間がかかってしまったりしてしまうので、
指定できるようにしました。

```ruby:scraping.rb

require 'mechanize'
require 'date'

class Scraping
PAGE_NUM = 2.freeze #最終実装時には100にする（次の30件を実行する限界値）
NEXT_URL_XPATH = '//*[@id="main-container"]/div[6]/div/div[2]/a'.freeze

  def self.start
    content_urls = create_content_url
    detail_pages = get_detail_pages(content_urls)
    #スクレイピングの実行（ISBNコード取得)
  end

 (中略)

  def self.get_detail_pages(content_urls)
    #detail_pagesの前回の内容を保持しないように生成させる
    detail_pages = []
    content_urls.each do |content_url|
      url = content_url
      #動作確認時に毎回最後の'次の30件'のリンクを全て押さないように指定できるようにする
      PAGE_NUM.times do
        agent = Mechanize.new
        index_page = agent.get(url)
        #detail_pages に　urlと確認用にタイトルをいれる
        #次のページのURLを取得
        url = get_next_url(index_page)
        #次のページのURLがない場合終了させる
        break if url.nil?
      end
    end
    detail_pages
  end

  def self.get_next_url(index_page)
    index_page.search(NEXT_URL_XPATH)[0].attribute('href').value
  end  
end
```

次に、肝心の漫画の詳細のリンクと確認のためにもタイトルをスクレイピングで回収していきましょう。
XpathとCSSセレクタの使い分けは正直私には分かりませんが、色々やってみて慣れるのが一番だと思いました。

```ruby:scraping.rb

require 'mechanize'
require 'date'

class Scraping
PAGE_NUM = 2.freeze #最終実装時には100にする（次へのリンクをおす限界値）
SEARCH_TITLE = 'div.item-title > a'.freeze
NEXT_URL_XPATH = '//*[@id="main-container"]/div[6]/div/div[2]/a'.freeze

  def self.start
    content_urls = create_content_url
    detail_pages = get_detail_pages(content_urls)
    #ページネーションの　次の30件のURLを取得かつ本の詳細URLを取得
    #スクレイピングの実行（ISBNコード取得)
  end

 (中略)

  def self.get_detail_pages(content_urls)
    #detail_pagesの前回の内容を保持しないように生成させる
    detail_pages = []
    content_urls.each do |content_url|
      url = content_url
      #動作確認時に毎回最後の次へのリンクを全て押さないように指定できるようにする
      PAGE_NUM.times do
        agent = Mechanize.new
        index_page = agent.get(url)
        #detail_pages に　urlと確認用にタイトルをいれる
        detail_pages.push(get_scrape_detail_page(index_page))
        #次のページのURLを取得
        url = get_next_url(index_page)
        #次のページのURLがない場合終了させる
        break if url.nil?
      end
    end
    detail_pages
  end

  def self.get_scrape_detail_page(index_page)
    index_page.search(SEARCH_TITLE).map do |index_page_by_title|
      {title: index_page_by_title.inner_text, url: index_page_by_title.attribute('href')value}
    end
  end 

  def self.get_next_url(index_page)
    index_page.search(NEXT_URL_XPATH)[0].attribute('href').value
  end  
end
```

4. 商品基本情報内のISBMコードを取得する。

あとはもう簡単です。さくっとeachを回してisbnコードをスクレイピングしていきます。
`def self.get_isbn`の中で　`isbn = ''` をしてわざわざ作っているのはその後に正規表現をつけれるようにしています。
また、スクレイピングで`at_css`を使用しているのは、methodの`parent`を使う為に使用しています。
`at_css`メソッドは引数として指定したCSSセレクタに合致する最初のノードを返すのに対し、
`css`メソッドは引数として指定したCSSセレクタに合致する全てのノードを配列を返すようです。


```ruby:scraping.rb
require 'mechanize'
require 'date'

class Scraping
PAGE_NUM = 2.freeze #最終実装時には100にする（次へのリンクをおす限界値）
SEARCH_TITLE = 'div.item-title > a'.freeze
NEXT_URL_XPATH = '//*[@id="main-container"]/div[6]/div/div[2]/a'.freeze
SEARCH_DETAIL_XPATH  = '//*[@id="productDetailedDescription"]/div/ul'.freeze

  def self.start
    content_urls = create_content_url
    detail_pages = get_detail_pages(content_urls)
    details = get_isbn(detail_pages)
    puts details
  end

  def self.create_content_url
    target_date = Date.today
    search_days = ["#{target_date.year}-#{format("%02d", target_date.month)}-01"]
    #もし14日以降であれば次の月もスクレイピングする範囲対象にする
    if Date.today.day > 14
      target_date2 = target_date >> 1
      search_days.push("#{target_date2.year}-#{format("%02d", target_date2.month)}-01")
    end

    #URLの作成 
    search_days.map do |search_day|
      "https://books.rakuten.co.jp/calendar/001001/monthly/?tid=#{search_day}"
    end
  end

  def self.get_detail_pages(content_urls)
    #detail_pagesの前回の内容を保持しないように生成させる
    detail_pages = []
    content_urls.each do |content_url|
      url = content_url
      #動作確認時に毎回最後の次へのリンクを全て押さないように指定できるようにする
      PAGE_NUM.times do
        index_page = read_html(url)
        #detail_pages に　urlと確認用にタイトルをいれる
        detail_pages.push(get_scrape_detail_page(index_page))
        #次のページのURLを取得
        url = get_next_url(index_page)
        #次のページのURLがない場合終了させる
        break if url.nil?
      end
    end
    detail_pages
  end

  def self.get_scrape_detail_page(index_page)
    index_page.search(SEARCH_TITLE).map do |index_page_by_title|
      {title: index_page_by_title.inner_text, url: index_page_by_title.attribute('href').value}
    end
  end 

  def self.get_next_url(index_page)
    index_page.search(NEXT_URL_XPATH)[0].attribute('href').value
  end  

  def self.get_isbn(detail_pages)
    isbn_data = []
    detail_pages.each do |details_30|
      isbn = details_30.map do |details|
        isbn = ''
        url = details[:url]
        title = details[:title]

        detail = read_html(url)
        elements = detail.search(SEARCH_DETAIL_XPATH)

        unless elements.at_css("li/span:contains('ISBNコード')").nil?
          isbn = elements.at_css("li/span:contains('ISBNコード')").parent.search('span[2]').inner_text
        end

        {title: title, isbn: isbn}
      end
      isbn_data.push(isbn)
    end
    isbn_data
  end

  def self.read_html(url)
    agent = Mechanize.new
    agent.get(url)
  end
    
end
```

一様これで完成になります。

```
rails c

Scraping.start
```
とすれば配列として出てくれると思います。

#あとがき
完成はしましたが、時間がかかってしまうのでgem 'pararel' で並列処理を行なったり、
正規表現等でISBNコードではなくタイトルなどを加工したり、
DBに登録したり、gem 'whenever' でバッチ処理を自動で行ったりなど、
色々やれることは多いと思います。

また、改めて自分で書き直したことで色々発見できて個人的に良かったと思いました。

稚拙なものではありますが、読んでくださった方ありがとうございました。

#参考文献
https://qiita.com/rllllho/items/cb1187cec0fb17fc650a
https://qiita.com/ippomihosanpo/items/da9ce52122ddcedc5841
https://gist.github.com/jescalan/1572289
https://github.com/sparklemotion/nokogiri/wiki
http://docs.seattlerb.org/mechanize/GUIDE_rdoc.html