RubyでWebスクレイピング 2022年(結局 Mechanize と Nokogiri)
Webスクレイピングに関する記事は散々出尽くしていますが、2022年現在 Ruby で Webスクレイピングをしたい場合の技術選定ってどんな状況だろう?という疑問のもと調べ直したのでまとめたいと思います。
結論、特に新しいものではなく典型的な Mechanize および Nokogiri による構成に落ち着きました。
Nokogiri は直接操作するのが初めてだったので、使ってみた感想にも触れていきたいと思います。
この記事で説明すること
- Ruby での Webサイトスクレイピング手法の実装の一例を説明します。
- ログインが必要なページでの Mechanize の実装例について
- そして記事の最後ではなぜ Ruby なのかを個人的な目線で説明します。
説明しないこと
- Ruby の環境構築方法は説明しません。
ruby -v
で何かしらの結果が出力されることを想定しています。 - Bundler の有無はどちらでも構わないのですが、この記事では使っています。
Webサイトスクレイピングはルールを守らないとコンテンツ提供主に迷惑をかけるほか、著作権・肖像権等の法的な問題にふれる可能性もあります。利用に際しては各個人の目的に沿って適切に利用してください。
法律に関することなので、インターネット上の記事を参考にする際には法律事務所が執筆している記事がいいでしょう。非専門家の記事は誤っている可能性が高くなりますので注意しましょう。
そこで、著作権法は以下のような例外を認めています。
(2)「情報解析を目的とした記録または翻案」
コンピュータによって情報を解析することが目的である場合には、例外的に著作権者の同意を得ることなく、スクレイピングによって取得した他社情報などを記録媒体に記録したり翻案することができます。
スクレイピングは違法?3つの法律問題と対応策を弁護士が5分で解説 ― https://topcourt-law.com/internet_security/scraping-illegal
適切な目的に沿って利用する限りは問題ないといえます。(注)この引用は部分抜粋ですので、局所的に解釈することのないよう記事全文を参照するようにしてください。記事にある通り、利用規約での記述内容や同意の有無、サーバーへの過負荷による業務妨害など、考慮すべき事項は多数あります。
あわせて、文化庁の記載を引用しておきます。
情報解析のための複製等(第47条の7)
コンピュータ等を用いて情報解析(※)を行うことを目的とする場合には,必要と認められる限度において記録媒体に著作物を複製・翻案することができる。
ただし,情報解析用に広く提供されているデータベースの著作物については,この制限規定は適用されない。
※情報解析とは,大量の情報から言語,音,映像等を抽出し,比較,分類等の統計的な解析を行うことをいう。著作物が自由に使える場合 | 文化庁 ― https://www.bunka.go.jp/seisaku/chosakuken/seidokaisetsu/gaiyo/chosakubutsu_jiyu.html
開発のポイント
- Webサイトに負荷をかけないように気を遣うこと
- 特に開発の初期段階って何度もコードを実行してエラーを修正するトライ・アンド・エラーのスタイルになりがちですが、実行するたびにインターネット上の先方のサイトにアクセスをしていては無駄に負担をかけてしまいます
- 「Webサイト(HTML)にアクセスして取得する部分」と「取得してきたHTMLを解析する部分」でロジック(メソッド)を分けておくと開発しやすいです
- (欲張ると)テストコード起点のスタイルにすること
- Webスクレイピングは自分自身が一方的に取得しにいく行為であり、ぶっちゃけ先方にとってはそんな事情は知ったこっちゃありません。Webサイト(HTMLの構造)は先方の都合でどんどん変わっていきます。となると、「ある日いきなりスクリプトが動かなくなった」ということが起こりがちです。その変化に追従しやすいように、HTML解析結果が期待した内容であるかを確認するテストコードを書いておくとメンテナンスがしやすいです
- 加えると、自分が書いたコードって数カ月後にはもう忘れていますからね。そういう忘却からもコードの品質を守ってくれるのがテストコードです
実装例
架空のサイト定義
以下コード例を記載しますが、サンプルコードの前提となるサイト(架空)を説明します。
- 対象サイトはログイン制の情報発信ページである
- ログインページ (
/login
) の挙動- ログイン前は、ログイン用フォームが表示される
- ログイン済でのアクセスは、「マイページ」という名の自分用ページに遷移する(ログインフォームは表示されない)
- 解析したい対象は、会員のプロフィール情報とする
- プロフィール情報には、名前(
name
)、自己紹介(description
)、ホームページURL(homepage
)を取り出したいとする
- プロフィール情報には、名前(
架空としていますが、実際に利用しているコードから説明用に変更と抜粋をしたものなので、妄想のサイトではありません。
コード例
ライブラリは /lib
ディレクトリに保存し、それらを呼び出す main.rb
が上位階層にあるものとします。次のようなイメージです。
Gemfile
main.rb
lib/
client.rb
parser.rb
spec/ ・・・今回は省略(※)
client_text.rb
parser_text.rb
README.md
※テストコードを書くと説明が長くなってしますので今回は割愛します。
Gemfile
以下の内容で bungle install
します。
いろいろ書いてありますが真に必要なのは
- mechanize (Webページアクセス用)
- nokogiri (HTMLの構造解析用)
の2つです。Ruby on Rails で bundle install する時に nokogoiri のコンパイルで時間を要しているので名前は目に付きがちですね。
# frozen_string_literal: true
source 'https://rubygems.org'
git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
gem 'mechanize'
gem 'nokogiri'
# テスト環境用
gem 'rubocop'
gem 'rspec'
gem 'guard-rspec', require: false
# 同じページを何度もアクセスしなくていいようにページのキャッシュ用
gem 'activerecord'
gem 'sqlite3'
gem 'rake'
lib/client.rb
Mechanize に関する部分です。ここではあくまでもHTTPクライアントとしての仕事をしていて、HTML解析はまだ登場しません。
require 'mechanize'
require 'yaml'
class Client
def initialize()
@cookie = 'cookiejar.yml'
end
def agent
@agent ||= Mechanize.new do |m|
m.user_agent = 'Mozilla/5.0 ... 任意の User Agent'
m.cookie_jar.load(@cookie) if File.exist?(@cookie)
end
end
def save_cookie
agent.cookie_jar.save_as(@cookie)
end
def login
user = ENV['USERNAME']
pass = ENV['PASSWORD']
raise StandardError, 'User or Password does not set.' if !user || !pass
agent.get('https://解析対象ページのサイト/login') do |page|
if page.title =~ /マイページ/
puts '[Info] Already logged in. skip login'
return true
end
# 注 Formに一意の名前がついている場合は page.form_with で特定した方が確実
form = page.forms.first
form.field_with(name: 'username').value = user
form.field_with(name: 'password').value = pass
puts '[Info] Login form submit ...'
response = form.submit
raise StandardError, 'Loggin failed' unless response.title =~ /マイページ/
puts '[Info] Login successfully'
return response
end
end
def get_page(url)
# 取得したHTMLボディを返す。本当はエラーハンドリングとかいろいろ書くべき
agent.get(url).body
end
end
- Cookie を保存できます。対象サイトの仕様によりますが、Cookieにより「一定時間のログイン継続」としている場合は、この実装で次回ログイン時には認証用の POST 通信を省略することができます
- この実装例では、ログイン処理をしようとした時に「マイページ」という名のログイン後の画面だった場合には省略する判定としています
- ユーザー名やパスワードは、ハードコーディングをすると Git での共有時などに事故になるので環境変数から取得するようにします
- コード内のコメントにも記載していますが、この例では「ログインフォームに一意の名前がついていない場合に、最初に表示されたフォームをログイン用のフォームとする」実装にしています。名前顔t苦丁可能な場合は
form_with
メソッドを用いた方が確実です get_page
メソッドでは指定したURLから本文を取得する処理としています。ここでは Mechanize で.get
して本文.body
を返すだけの単純処理にしていますが、場合によっては條件分岐などもここに出てくるでしょう。やりたいことによって変更してください。
lib/parser.rb
続いてHTML解析の部分です。前述の Mechanize(クライアント)とは違って、ここではHTTPクライアントとしての仕事は一切登場しません。あくまでHTMLのテキストを受けとって、HTMLを解析してほしい情報を抽出することに徹しています。
require 'nokogiri'
class Parser
def self.profile(page)
doc = Nokogiri::HTML.parse(page)
nbsp = Nokogiri::HTML(' ').text
{
name: doc.css('.name')[0].text,
description: doc.css('.description')[0].text.gsub(nbsp, ' '),
homepage: doc.css('.website a')[0].get_attribute('href'),
}
end
end
このように Parser HTML解析部分を分離しておくと、HTML文字列を与えるだけで実装の確認ができますので、動作確認や開発段階でいちいちHTTPアクセスをする必要がありません。 先方のサーバーに優しいわけです。
この例では profile
としてクラスメソッドを1個だけ用意していますが、解析したいページの種類だけ任意のクラスメソッドを作成してください。例えば、ツイート的なものがあるならば Parser.tweets
みたいな感じです。以下、一部補足です。
Nokogiri::HTML.parse
で得たオブジェクトには.css
メソッドを用いて、CSSセレクタでの要素特定ができます。- CSSセレクタで抽出した要素は配列になっていますので、
[0]
でアクセスしています。.text
でテキスト文字列を抽出できます.get_attribute('href')
とすることでアンカータグ(a
)からhref
要素(=URL部分)を抽出できます。href 以外にも任意の要素が指定出来ます。例えば<img src=
ならば.get_attribute('src')
- Rspec でテストコードを書いていた時に半角スペースがうまく一致しない問題が置きました。具体的には、空白スペースを意味する
要素が Rubyコード上の半角スペースと一致しないのです。よって、上記の通りNokogiri::HTML(' ').text
として Nokogiri における nbsp の空白文字を作って置換することにしました。
Mechanize本体にも CSS や XPath でのHTML要素セレクターがあるので、シンプルに済むならば Nokogiri を使う必要はありませんが、テストコードの書きやすさの都合でこのように役割を分離しています。
CSSセレクターに限らず xpath も使えます。好みに応じて選びましょう。なお以下の例のように、ログイン処理が不要な場合は open-uri で Get するのがシンプルで手っ取り早いです。
#! /usr/bin/env ruby
require 'nokogiri'
require 'open-uri'
# Fetch and parse HTML document
doc = Nokogiri::HTML(URI.open('https://nokogiri.org/tutorials/installing_nokogiri.html'))
# Search for nodes by css
doc.css('nav ul.menu li a', 'article h2').each do |link|
puts link.content
end
# Search for nodes by xpath
doc.xpath('//nav//ul//li/a', '//article//h2').each do |link|
puts link.content
end
# Or mix and match
doc.search('nav ul.menu li a', '//article//h2').each do |link|
puts link.content
end
Nokogiri ― https://nokogiri.org/
main.rb
各ライブラリで分離しておくと、呼び出し元スクリプトはシンプルに済みます。
require 'rubygems'
require_relative '../lib/client'
require_relative '../lib/parser'
@client = Client.new
@client.login
html_body = @client.get_page('https://解析対象のページ')
result = Parser.profile(html_body)
# Cookie 保存
@client.save_cookie
# HTMLから抽出した情報が含まれる。あとはよしなに
pp result
実装を終えて(および昔話)
Mechanize は随分昔からあるライブラリです。2000年代に登場したものですが、未だにメンテナンスが続いているのはよいですね。 https://rubygems.org/gems/mechanize/versions
2000年代にもこの Ruby実装を使ったことがあるのですが、その当時は Perl コミュニティの勢いを見ていて楽しかったこともあり、Perlの Mechanize と Web::Scraper を用いたスクレイピングを主としていました。
- WWW-Mechanize-2.06 - Handy web browsing in a Perl object - metacpan.org ― https://metacpan.org/dist/WWW-Mechanize
- Web::Scraper - Web Scraping Toolkit using HTML and CSS Selectors or XPath expressions - metacpan.org ― https://metacpan.org/pod/Web::Scraper
今回、昔に書いた Perl のコードを改めて実行しようとしたところ、今更 Perl 環境(特にCPAN)を再構築するのが億劫だったので Ruby で一通り書き換えることにしました。
何故Rubyなのか
個人的にテスト実装のお作法(Rspec)に慣れていただけです。手っ取り早く簡単な処理を作りたい場合は、慣れている言語で作ってしまうのが一番です。
他言語のライブラリ整理
Mechanize は様々が言語で実装されていますので、どの言語でやるかはさほど問題にならないでしょう。各言語の細部までは比較していませんが、おそらくインターフェース的にはそれぞれが全く別物で、ただ単にWebスクレイピングライブラリの代表格である Mechanize という冠を被った異なる設計・実装という認識です(間違っていたらすみません)。
Lang | ライブラリ | ドキュメント |
---|---|---|
Python実装 | python-mechanize/mechanize: The official source code for the python-mechanize project ― https://github.com/python-mechanize/mechanize |
mechanize — mechanize 0.4.7 documentation ―https://mechanize.readthedocs.io/en/latest/ |
Ruby実装 | sparklemotion/mechanize: Mechanize is a ruby library that makes automated web interaction easy. ― https://github.com/sparklemotion/mechanize |
mechanize/GUIDE.rdoc at main · sparklemotion/mechanize ― https://github.com/sparklemotion/mechanize/blob/main/GUIDE.rdoc |
Perl実装 | libwww-perl/WWW-Mechanize ― https://github.com/libwww-perl/WWW-Mechanize |
libwww-perl/WWW-Mechanize ― https://github.com/libwww-perl/WWW-Mechanize |
HTMLクライアント(Mechanize)という目線では上記の通りですが、HTML Parser との関係をまとめると、以下の通りです。
Lang | HTMLクライアント | HTMLパーサー |
---|---|---|
Python実装 | Mechanize または 標準HTTPクライアント | Beautiful Soup - https://www.crummy.com/software/BeautifulSoup/ |
Ruby実装 | Mechanize または 標準HTTPクライアント | Nokogiri - https://nokogiri.org/ |
Perl実装 | Mechanize または 標準HTTPクライアント | Web::Scraper - https://metacpan.org/pod/Web::Scraper |
Node (JavaScript) になってくると肌感が分からないのですが、cheerio (https://cheerio.js.org/) あたりが選択肢なのでしょうか。ただ、JavaScript って日頃使い慣れていないと非同期処理(Promise や await / async)で混乱することがあるので、Webスクレイピング用途では避けたい印象です。
本格的なスクレイピング向けプロダクト
一方で、超本格的なWebスクレイピングをしたい場合は、クローリング&スクレイピングフレームワークを用いた方がいいです。Python の Scrapy が非常に高機能なので良かったです。
フレームワークなのでお作法を理解する必要がありますが、ちょっと練習すれば圧倒的な高品質クローラーを手に入れることができます。
つまるところ、
- 上限が見えている小規模な数ページをスクレイピングしたい場合 →Mechanizeなどで軽く実装
- 本格的な大規模クローリング&スクレイピングをシたい場合 →フレームワークを利用
という使い分けがひとつの目安になるでしょう。
Ruby でもスクレイピングフレームワークとしての実装があるようですね。コード例を見た感じ、上記Python の Scrapy の設計思想に影響を受けているようにも見えました。
その他、API形式で利用するアプローチもあるようです。こちらは詳細は調べていません。
まとめ
Webスクレイピングをしたいという目的に対して、Ruby の Mechanize に関する説明と設計・実装例をまとめました。また、補足的な説明として Python や Perl での Mechanize、および Python の Webスクレイピングフレームワーク Scrapy について簡単に紹介しました。
自分が仮に、使い慣れた言語が全くのない初学者だった場合は Python を選択すると思います。
くれぐれも法律違反・規約違反には注意した上で適切に利用しましょう。