WEB スクレーピングとその応用

作成日 : 2012-11-11
最終更新日 :

WEB スクレーピング

WEB スクレーピング(WEB スクレイピング, Web scraping )とは、 WEB 画面に表示される情報を何らかのプログラムを使って評価して必要な情報を得ることである。 誤解のない範囲内で、単にスクレーピング(スクレイピング)と呼ばれることもある。 ここでは、google 検索で指定したキーワードを含むページが何件あるか、 概算を拾って表示するプログラムを開発する。

もともとは、自分の興味で「発音について」を作り始めたとき、 二つの読み方を比較するのに都合がいいプログラムが見当たらず、自分で作らないといけないことから始まった。

プログラムの一例は検索じゃんけんにある。

コマンド編

第1章:欧文決め打ちで表示する

第1節: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 は諦める。

第1節追加:Yahoo

この追加の節は 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件"]

できている。

第2節:msearch

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件ヒット"]

第2章:日本語決め打ちで表示する

この章以降、Google の結果は削除した。

こんどは、検索の単語を日本語まで拡大するとともに、 今回は複数単語に挑戦した。「今川焼き」と「二重焼き」を付け加えた (広島では今川焼きのことを二重焼きという)。

第1節 : msearch

複数単語の対応と日本語のエンコーディングをした修正コードは次の通り。


	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)。

第 2 節 : Yahoo

複数単語の対応と日本語のエンコーディングを上記と同様に行なった。修正コードは次の通り。


	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 件

この結果は妥当だろう。

WEB CGI 編

追って書く予定(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">

Nokogiri を使う

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

URI の存在を確認する

これは 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 のプログラムとして作ってみた。 (2023-03-11)

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 スクレーピングとその応用


MARUYAMA Satosi