#はじめに
楽天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
gem 'mechanize'
#基本的なやり方
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/
から商品基本情報の関連作品をとってみようと思います。
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 sample2.rb
#成功していれば下記のように出る
ダイヤのA
https://books.rakuten.co.jp/mediamix/XW00153690/?l-id=item-c-mediamix
ルビー単体での実装ではこんなもので大丈夫ではないでしょうか?
では、次からはじめに
で書いた内容を実装していこうと思います。
#はじめにの実装
では実際に実装していきたいと思いますやることとしては
-
rails 内で実行したいのでクラスを作成する。
-
どうやって現在日時から2週間以上のデーターを取ってくるか。(月跨ぎを含む)
-
取ってきた中で漫画の詳細に入る必要がある。
-
商品基本情報内のISBMコードを取得する。
をやっていきたいと思います。 -
rails内で実行したいので、クラスを作成し、言語化してみました。
require 'mechanize'
class Scraping
def self.start
#取得するURLを作成
#ページネーションの 次の30件のURLを取得かつ本の詳細URLを取得
#スクレイピングの実行(ISBNコード取得)
end
end
2.どうやって現在日時から2週間以上のデーターを取ってくるか。(月跨ぎを含む)
どれが正解かは私はわからないですが、楽天のコミックの発売一覧はこのようなURLでした。
https://books.rakuten.co.jp/calendar/001001/weekly/?tid=2020-07-12
おそらくですがこのような感じだと思われます。
/001001/ → 漫画(コミック) 本のジャンル選択
/weekly/ → 期間で絞り込む(1週間単位)
/?tid=2020-07-12/ → 現在日時
これよりアドレスは
"https://books.rakuten.co.jp/calendar/001001/monthly/?tid=#{可変月}"
でいいのではないでしょうか
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件
のリンクを取得させてそれがなくなるまで自動で回収させるところを実装していきます。
ただ、確認のために全てを毎回スクレイピングさせるのも負荷がかかったり時間がかかってしまったりしてしまうので、
指定できるようにしました。
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セレクタの使い分けは正直私には分かりませんが、色々やってみて慣れるのが一番だと思いました。
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
- 商品基本情報内のISBMコードを取得する。
あとはもう簡単です。さくっとeachを回してisbnコードをスクレイピングしていきます。
def self.get_isbn
の中で isbn = ''
をしてわざわざ作っているのはその後に正規表現をつけれるようにしています。
また、スクレイピングでat_css
を使用しているのは、methodのparent
を使う為に使用しています。
at_css
メソッドは引数として指定したCSSセレクタに合致する最初のノードを返すのに対し、
css
メソッドは引数として指定したCSSセレクタに合致する全てのノードを配列を返すようです。
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