WEB スクレーピング(WEB スクレイピング, Web scraping )とは、 WEB 画面に表示される情報を何らかのプログラムを使って評価して必要な情報を得ることである。 誤解のない範囲内で、単にスクレーピング(スクレイピング)と呼ばれることもある。 ここでは、google 検索で指定したキーワードを含むページが何件あるか、 概算を拾って表示するプログラムを開発する。
もともとは、自分の興味で「発音について」を作り始めたとき、 二つの読み方を比較するのに都合がいいプログラムが見当たらず、自分で作らないといけないことから始まった。
プログラムの一例は検索じゃんけんにある。
キーワードを決め打ちして、そのキーワードが含まれるページ数を表示する。 ここでのキーワードは esperanto である。 ページ数は、「約***件」から抜き取っている。 つたないプログラムだが、気分を理解してもらいたい。
# scraping.rb
require 'open-uri'
url = 'http://www.google.co.jp/search?ie=UTF-8&oe=UTF-8&q=esperanto'
page = open(url)
text = page.read
print text.scan(/約 .* 件/)
コマンド
ruby -Ku scraping.rb
作成日 (2012-11-11) 近くでは次の結果だった。
実行結果
["約 32,400,000 件"]
その後確認すると、2020-03-27 では、実行結果は次の通りである。
実行結果これは、約 *** 件 の部分が、JavaScript か何かの関数から出力されるかたちになっていて、 ソースからは読めなくなったことによる。よって、このスクレーピングは諦めたほうがいいだろう (頑張る手はあるかもしれないが)。
なので、Google は諦める。
この追加の節は 2022-10-18 に書いた。
Google がだめなら他の検索エンジンがあるはずだ、と思って Duckduckgo とか Bing などを調べてみたが、ヒット件数を表示していない。 ひょっとしてと思って Yahoo を見ると検索件数が表示されている。これは使える。
https://search.yahoo.co.jp/search?p=esperanto&ei=UTF-8
上記 URL で検索した結果のソースを見てみると、こんな感じだ。
<div class="Hits"><div class="Hits__contents"><div class="Hits__item">約<!-- -->745,000,000<!-- -->件
同じようにプログラミングしてみた。
# scraping.rb
require 'open-uri'
url = 'https://search.yahoo/search?p=esperanto&ei=UTF-8'
page = URI.open(url)
text = page.read
print text.scan(/約[0-9,]件/)
コマンド
ruby -Ku scraping.rb
作成日 (2022-10-18) では次の結果だった。
実行結果
["約604,400,000件"]
できている。
Google の代わりに私のホームページ上での検索エンジン msearch を例にとって説明する。
やはりキーワードを決め打ちして、そのキーワードが含まれるページ数を表示する。 ここでのキーワードは esperanto である。 ページ数は、「検索し,**件ヒット」から抜き取っている。 つたないプログラムだが、気分を理解してもらいたい。
# scraping.rb
require 'open-uri'
url = 'http://annex.marinkyo.que.jp/cgi/msearch/msearch.cgi?query=Esperanto'
page = open(url)
text = page.read
print text.scan(/検索し, .*件ヒット/)
実行結果
["検索し,62件ヒット"]
この章以降、Google の結果は削除した。
こんどは、検索の単語を日本語まで拡大するとともに、 今回は複数単語に挑戦した。「今川焼き」と「二重焼き」を付け加えた (広島では今川焼きのことを二重焼きという)。
複数単語の対応と日本語のエンコーディングをした修正コードは次の通り。
require 'open-uri'
require 'cgi'
keywords = ['今川焼き', '二重焼き', 'Esperanto']
keywords.each { |a_keyword|
url = "http://annex.marinkyo.que.jp/cgi/msearch/msearch.cgi?query=#{CGI.escape(a_keyword)}"
puts url
text = open(url).read
text.scan(/検索し,([0-9]+)件/)
print "#{a_keyword} : #{$1} 件\n"
}
実行結果は次の通りである。
$ ruby -Ku scraping.rb http://annex.marinkyo.que.jp/cgi/msearch/msearch.cgi?query=%E4%BB%8A%E5%B7%9D%E7%84%BC%E3%81%8D 今川焼き : 1 件 http://annex.marinkyo.que.jp/cgi/msearch/msearch.cgi?query=%E4%BA%8C%E9%87%8D%E7%84%BC%E3%81%8D 二重焼き : 1 件 http://annex.marinkyo.que.jp/cgi/msearch/msearch.cgi?query=Esperanto Esperanto : 62 件
以上、なんとか形になった(2020-03-27)。
複数単語の対応と日本語のエンコーディングを上記と同様に行なった。修正コードは次の通り。
require 'open-uri'
require 'cgi'
keywords = ['今川焼き', '二重焼き', 'Esperanto']
keywords.each { |a_keyword|
url = "https://search.yahoo.co.jp/search?p=#{CGI.escape(a_keyword)}&ie=UTF-8"
puts url
text = URI.open(url).read
text.scan(/約([0-9,]+)件/)
print "#{a_keyword} : #{$1} 件\n"
}
実行結果は次の通りである。
$ ruby -Ku scraping.rb https://search.yahoo.co.jp/search?p=%E4%BB%8A%E5%B7%9D%E7%84%BC%E3%81%8D&ie=UTF-8 今川焼き : 1,640,000 件 https://search.yahoo.co.jp/search?p=%E4%BA%8C%E9%87%8D%E7%84%BC%E3%81%8D&ie=UTF-8 二重焼き : 18,800,000 件 https://search.yahoo.co.jp/search?p=Esperanto&ie=UTF-8 Esperanto : 666,000,000 件
これを見てみると、今川焼きより二重焼きのほうが件数が多いという、考えにくい結果となった。 これは「二重焼き」ではなく、「二重に焼き肉を詰める」のようなものまで検索しているからだろう。 厳密な「二重焼き」のみを対象とするには前後を二重引用符でくくればいい。
require 'open-uri'
require 'cgi'
keywords = ['今川焼き', '二重焼き', 'Esperanto']
keywords.each { |a_keyword|
url = "https://search.yahoo.co.jp/search?p=\"#{CGI.escape(a_keyword)}\"&ie=UTF-8"
puts url
text = URI.open(url).read
text.scan(/約([0-9,]+)件/)
print "#{a_keyword} : #{$1} 件\n"
}
結果は次の通り。
$ ruby -Ku scraping.rb https://search.yahoo.co.jp/search?p="%E4%BB%8A%E5%B7%9D%E7%84%BC%E3%81%8D"&ie=UTF-8 今川焼き : 357,000 件 https://search.yahoo.co.jp/search?p="%E4%BA%8C%E9%87%8D%E7%84%BC%E3%81%8D"&ie=UTF-8 二重焼き : 46,400 件 https://search.yahoo.co.jp/search?p="Esperanto"&ie=UTF-8 Esperanto : 69,200,000 件
この結果は妥当だろう。
追って書く予定(2014-11-30)。 しかし、その「追って」が 5 年たっても実現しなかったので諦めた(2019-12-25)。
erb で書いてみた概略である。(2020-03-28)
<% cgi = CGI.new %> <% p = CGI.escapeHTML(cgi.params["p"][0]) %> <%# CGI.escape('"' + p + '"') %> <% url = 'http://annex.marinkyo.que.jp/cgi/msearch/msearch.cgi?query=' + CGI.escape(p) %> <% text = open(url).read %> <%= text.slice(/検索し,.*?件/) %> <% q = CGI.escapeHTML(cgi.params["q"][0]) %> <% url = 'http://annex.marinkyo.que.jp/cgi/msearch/msearch.cgi?query=' + CGI.escape(q) %> <% text = open(url).read %> <%= text.slice(/検索し,.*?件/) %> <input type="text" name="p" size="40" value="<%= p %>"/> <input type="text" name="q" size="40" value="<%= q %>"/> <input type="submit" value="じゃんけん" accept-charset="UTF-8">
WEB スクレ―ピングには、Nokogiri というライブラリを使うのがよいことがわかった。 以下、少しその応用例を述べてみる。
$ gems install nokogiri
タイトルを取る、といっても将棋の名人位を獲得するなどという話ではない。WEB の HTML のことである。 WEB は HTML で記述されていて、HTML には title タグで文字列が規定されている。 以下は URL から title タグを表示させるスクリプトである。
#! /usr/bin/env ruby -Ku # -- coding: utf-8 require 'nokogiri' require 'open-uri' url = "http://www.ne.jp/asahi/music/marinkyo/index.html.ja" html = open(url).read doc = Nokogiri::HTML.parse(html) p doc.title
スクリプト結果は省略する。
ここで、ヘッダ情報というのは、HTML で h1 とか h2 とかで囲まれているコンテンツを指す。 ここでは仮に、h2 で囲まれているコンテンツを表示させてみよう。 上記の p doc.title の代わりに次のようなループで処理する。
doc.css('h2').each do |link| p link.content end
これは Nokogiri の問題ではないが、指定した URI が存在するか否かを判断する方法について説明する。
たとえば、次のような URL が指定されたとする(この URL は実在しないはずである)。
http://ttt.neesto.zzz/id=k
このように k を変化させて動的なページを作ることは普通におこなわれる。
ここで、k = 1, 2, 3, ..., n まではページはあるが、
その先の k = n + 1, n + 2, ... のページは存在しないものとする。
このとき、存在する URL をたどる方法を考えてみよう。
(この項続く 2019-12-26)
ふつう、WEB ページのクローリングをするソフト(クローラ)は wget を使っている。しかし、ブログを対象にすると、
動的なページ生成をしているために、重複なくクローリングをすることは案外難しい。
ここでは、私が使っている朝日ネットのブログであるアサブロに特化したクローラを Ruby のプログラムとして作ってみた。
(
require 'open-uri' require 'fileutils' host = "http://marinkyo.asablo.jp" uri = host + '/blog/' # 最初はこのページから sio = OpenURI.open_uri(uri) # uri で示された html を読み取り IO ファイルに示す text = sio.read # シリアル IO に渡す %r!a href="(/blog[/\d]+)"!.match(text) # 最新記事の日付のパスを抜き出す <a href="/blog/2023/03/10/9568475">...</a> のようになっている pass = $1 # 複数の記事で一番最初に見つかったのが最新記事の URI のパス while pass != nil uri = host + pass # 分解されているホストとパスを合体 dirname = pass.sub(%r([^/]*$),'') # パスのディレクトリ部を抜き出す /blog/year/month/day filename = pass.sub(%r(^/.*/),'') # パスのファイル部を抜き出す [0-9]+ puts uri # uri を表示 sio = OpenURI.open_uri(uri) # uri で示された html を読み取りシリアル IO に渡す text = sio.read # text に読み込む FileUtils.mkdir_p('.' + dirname) # 再帰的にディレクトリを作っておく( File.open がエラーにならないよう) File.open('.' + pass, "w") {|f| f.write(text)} # ファイルを書き込む %r[class="msg-date">(\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2})].match(text) # 作成日付のパスを抜き出す mtime = Time.parse($1) File.utime(mtime, mtime, '.' + pass) # 更新日時とアクセス日付を書かれたファイルの日時に変更する %r[class="navi-prev" href="([^"]*)"].match(text) # 次のリンクのパスを抜き出す pass = $1 # 次のリンクがない場合は nil end
まりんきょ学問所 > Rubyの浮き輪 > WEB スクレーピングとその応用