トップ «前の日記(2007-01-31) 最新 次の日記(2007-02-06)» 編集

Route 477

過去の日記


2007-02-05

[ruby] RubyでHTMLとWebを操作するためのライブラリ、HpricotとWWW::Mechanize

今日は、RubyでWebサイトを解析するときに強い味方となるライブラリ、HpricotとWWW::Mechanizeを紹介します。

どちらも非常に強力なので、覚えておいて損はないよ!

以下ではまずHpricotでHTMLを解析・編集する方法について解説します。 次に、「はてなダイアリーの自動更新」を例にWWW::Mechanizeの使い方を解説します。

Hpricot

HpricotはHTMLを解析するためのライブラリです。

例えば「あるページのリンクだけを全部抜き出したい」と思ったとき、どうしますか?scrAPIを使う?でもscrAPIはやっぱり ちょっと使いたいだけなのにパーザ(Scrape)用のクラスを定義するのが面倒なんだよね!

Hpricotなら、たったこれだけで解析完了です。

require 'hpricot'
require 'open-uri'

doc = Hpricot( open("http://www.kmc.gr.jp/").read )

(doc/:a).each do |link|
  puts "#{link.inner_html} → #{link[:href]}"
end

HTMLを読み込むには「Hpricot()」という関数にHTMLデータの文字列を与えればOK。 例ではOpenURIを使ってネットから取得しています。

タグを検索するには、読み込んだデータの「/」というメソッドにシンボルか文字列を渡します。

tableタグを全て探す
doc/:table
<div id="hoge">を全て探す
doc/"div#hoge"
<span class="moge">を全て探す
doc/"span.moge"

CSSに慣れている人なら気付いたと思いますが、2番目、3番目の書き方は「CSSセレクタ」と呼ばれるものです。 Hpricotでは他にもさまざまなCSSセレクタを 使用することができます。*1

<table id="list">の中のtdタグを全て探す
doc/"table#list td"
"index.cgi?"から始まるリンクを全て探す
doc/"a[@href^='index.cgi?']"
テキストに"new"という文字列が入っているリンクを全て探す(※version 0.5以降)
doc/"a[text()*='new']"

上の例はどれも「〜なタグを全て探す」でしたが、条件に当てはまるものを一つだけ取り出したいこともありますよね。 そんな時はdoc.at()が使えます。

一番最初のリンクを探す
doc.at(:a)
ElementsとElem

タグ一覧はHpricot::Elementsのインスタンスになります。これはArrayのサブクラスなので、 firstで最初の要素を取り出したり、eachでループを回したり、find_allで条件に合うものだけ抜き出したりできます。

またそれぞれのタグはHpricot::Elemのインスタンスになります。

Elemには以下のようなメソッドが定義されています。

  • attributes : 属性一覧
  • ["href"] : 属性hrefの値を得る
  • name : タグ名
  • parent : 親要素
  • containers : 子要素(タグのみ)
  • children : 子要素(テキストやコメントも含む)
  • css_path : その要素のCSSパス
  • previous_sibling, next_sibling : 隣の要素(タグのみ)
  • previous_node, next_node : 隣の要素(テキストやコメントも含む)
  • each_child{|elem| ..} : 子要素について繰り返す
  • inner_html, inner_text : 内部のHTML, 内部のテキスト
  • to_html : そのタグも含めたHTML
  • to_original_html : そのタグも含めたHTML(なるべく元のHTMLと同じように出力する)
  • to_plain_text : 読みやすいテキストに整形する

実はElemやElementsにも「/」や「at」が定義されているので、「/」による検索結果に対し「その中をさらに探す」なんてこともできます。

例:

divタグの中のリンクを全て探す
doc/:div/:a
各divタグの中の最初のリンクを探す
(doc/:div).each do |div|
  first_link = div/:a
end
HTMLに変更を加える

Hpricotの凄いところはHTMLへの変更もサポートしている点です。 linkをあるリンクタグだとすると、link.inner_html に文字列を代入すると中身のHTMLが変更されたり、 link[:href] = "foo.html" とするとリンク先が"foo.html"になったりします。

これらの変更は、to_htmlなどが生成するHTMLに反映されます。 例えば「サーバ移転したのでexample.jpへのリンクをexample.orgに直したい」という場合は、

doc = Hpricot(ARGF.read)
(doc/:a).each do |link|
  link[:href].gsub! %r(http://example.jp/), "http://example.org"
end
print doc.to_html

という感じで簡単にフィルタスクリプトを書くことができます。

変更系のメソッドには以下のようなものがあります。

  • ["href"]= : 属性hrefの値をセットする
  • inner_html= : 内部のHTMLをセットする
  • name= : タグ名をセットする
  • children= : 子要素をセットする
余談

「/」とか個性的なメソッド名が多いHpricotですが、実は「もっと普通の名前」も用意されています。 例えば「/」には「search」という別名がありますし、 HTMLの読み込みは Hpricot.parse(html) とも書けます。 でもやっぱり短い方を使っちゃうんだよね…!(便利だから)

WWW::Mechanize

WWW::Mechanizeは、Webサイトへのアクセスを自動化するためのライブラリです。

例えばはてなダイアリーに日記を投稿するスクリプトを書くとしましょう。 Net::HTTPだとまず投稿用のURLを調べて、フォームタグの名前を調べてPOSTして、あれ認証ってどうやるんだっけ…?なんて いろいろなことを考えないといけませんが、WWW::Mechanizeを使えば「普段ブラウザを操作するのと同じような感覚で」 スクリプトを書くことができます。

では、ブラウザから日記を書くときの手順を思い出してみましょう。

  1. http://d.hatena.ne.jp/(ユーザid)/ を開く
  2. おおっと、ログインしてなかった。右上の「ログイン」をクリックする
  3. ユーザー名とパスワードを入力し、「ログイン」を押す
  4. 「自動でページを移動しています(移動しないときはこちらのリンクを…)」
  5. 「日記を書く」をクリック
  6. textareaに日記本文を入力し、「この内容を登録する」を押す

WWW::Mechanizeなら、これをそのままスクリプトに落とすことができます。

(1) http://d.hatena.ne.jp/(ユーザid)/ を開く
 require 'mechanize'
 require 'kconv' #あとでUTF-8を扱うので

 agent = WWW::Mechanize.new
 diary_page = agent.get("http://d.hatena.ne.jp/(自分のはてなid)/")

newでインスタンスを作って、getでWebページを取得しています。簡単ですね。

(2) おおっと、ログインしてなかった。右上の「ログイン」をクリックする
 login_link = diary_page.links.text("ログイン".toeuc)
 login_page = agent.get(login_link.href)

diary_page.linksでページ中のリンク一覧が取得できます。ここでは、全てのリンクの中から テキストが"ログイン"に一致するものを探しています。

login_linkはMechanize::Linkのインスタンスで、login_link.hrefでURLが得られます。agent.getにこのURLを渡してログインページを開きましょう。

(3) ユーザー名とパスワードを入力し、「ログイン」を押す
 login_form = login_page.forms.first
 login_form['key'] = "(ユーザ名)"
 login_form['password'] = "(パスワード)"
 redirect_page = agent.submit(login_form)

login_page.formsでformタグの一覧が取得できます。ログインページにはformが一つしかないので、firstで最初のformを選んでいます。

次の行では、<input name="key" ...> というインプットボックスにユーザ名を入力しています。 パスワードも同様に入力します。

agent.submitにこのフォームを渡すとフォームの内容が送信され、送信結果のページが返ってきます。

(4) 「自動でページを移動しています(移動しないときはこちらのリンクを…)」
 diary_link = redirect_page.links.text("こちら".toutf8)
 diary_page = agent.get(diary_link.href)

ログインすると、おなじみの「移動しないときはこちらのリンクを…」というリダイレクトページになります。 WWW::Mechanizeにはリダイレクトを自動で追跡してくれる機能がある…のですが、このリダイレクトページは200 OKなので 自動追跡が効きません(´・ω・`) 仕方がないので、手動でリンクをクリックしましょう。 またこのページはさっきとちがってUTF-8なので、"こちら"もUTF-8に変換しておきます。

(5) 「日記を書く」をクリック
 edit_link = diary_page.links.text("日記を書く".toeuc)
 edit_page = agent.get(edit_link.href)

ここまで来たらあと一歩です。"日記を書く"のリンクを探し、クリックします。

(6) textareaに日記本文を入力し、「この内容を登録する」を押す
 edit_form = edit_page.forms.name("edit").first
 edit_form["body"] += "\n*Rubyから日記を更新してみるテスト。"
 ok_button = edit_form.buttons.name("edit")
 agent.submit(edit_form, ok_button)

さあ、いよいよ日記の書き込みです。編集画面には複数のフォームがあるので、edit_page.form("edit") で最初の <form name="edit" ...> というタグを見つけています。また、このフォームには「確認する」と「登録する」という複数のsubmitボタンがあるので、 登録ボタンを探してsubmitに渡しています(「こっちのボタンを押してください」という意味です)。

http://d.hatena.ne.jp/(はてなid)/ を見てみてください。新しい日記が書き込まれましたか?:-)

WWW::Mechanizeではこのように、ブラウザを操作するような感覚でスクリプトを書くことができます。

簡易リファレンス

pageには以下のようなメソッドがあります。

  • links : リンク(aタグ)一覧
  • forms : formタグ一覧
  • form("foo") : name="foo"である最初のformタグ
  • title : ページタイトル(titleタグの中身)
  • header : HTTPのレスポンスヘッダ
  • root : ページの内容を表すHpricotドキュメント

linksやformsはMechanize::Listのインスタンスを返します。これはArrayのサブクラスなので、配列のように扱うことができます。 また簡便のため、「hrefが"index.cgi"であるものを全て探す」という操作を links.href("index.cgi") のように書けたり、 「name属性が"hoge"であるものを全て探す」という操作を forms.name("hoge") と書けるようになっています。

linkには以下のようなメソッドがあります。

  • href : リンク先のURL
  • text : aタグの中身のテキスト
  • node : aタグを表すHpricot::Elem
  • click : リンクをクリックし、結果のページを返す (newpage = agent.get(link.href) が、 newpage = link.click のように書ける)

formには以下のようなメソッドがあります。

  • []=("foo", "bar") : name="foo" であるフィールドに値"bar"をセットする
  • submit : フォームをsubmitし、結果のページを返す (newpage = agent.submit(form) が、 newpage = form.submit のように書ける)

より詳細なリファレンスはWWW::Mechanize 日本語リファレンスを参照してください。

インストール

さて、そろそろ実際に使ってみたくなったでしょうか?:-) rubygemsをインストール済みなら、

gem install hpricot
gem install mechanize

と、簡単なコマンドでインストールできます。

  • 前述のとおりmechanizeはhpricotに依存しているので、 mechanizeを入れればhpricotは自動的に入ります。
  • Hpricotはつい最近version 0.5が出たので、昔インストールしたことがある人もアップデートをお勧めします。
  • Hpricotは途中でUnix版を入れる(ruby)かWindows版を入れる(mswin32)か聞かれるので、自分の使っている方を選んでください。

rubygemsを使っていない人はアーカイブをダウンロードし、中に入っているinstall.rbを実行すればOKです(たぶん)

まとめ

本稿では、RubyでWebから情報を得るときに役立つ2つのライブラリ、HpricotとWWW::Mechanizeを紹介しました。

これらを使うことで、HTMLのスクレイプやWebアクセスの自動化など今まで「面倒そうだなぁ」と思っていた処理が非常に簡単に書けるようになります。 Webでの情報収集を自動化したくなったとき、この2つのライブラリのことを思い出してもらえれば幸いです。

*1 さらにHpricotではXPathも使える…のですが、たいていはCSSセレクタで間に合うと思います。

[ruby][hiki] HikiReload

WWW::Mechanizeの使用例として、「テキストファイルが保存されたら自動的にWikiを更新する」というスクリプトを載せておきます。 上の記事もこのスクリプトを使って下書きしましたが、自分のお気に入りのエディタを使えるので非常に便利でした:-)

フォームタグを名前で探しているのでHiki限定ですが、他のWikiエンジンへの対応も難しくないと思います。 ぜひチャレンジしてみてください。

#!/usr/bin/env ruby
require "mechanize"

class HikiReload
  POLL_SECONDS = 3

  def initialize(url)
    @agent = WWW::Mechanize.new
    @url = url
  end

  def poll(path)
    loop do
      mtime = File.mtime(path)
      while File.mtime(path) == mtime
        sleep POLL_SECONDS
      end
      puts "file #{path} changed.."
      submit(File.read(path))
      puts "submitted."
    end
  end

  def submit(str)
    editpage = @agent.get(@url)
    form = editpage.forms.first
    form.contents = str
    result_page = @agent.submit(form, form.buttons.name("save"))
    puts result_page.root.to_html
  end
end

url, file = "http://example.jp/hiki/?c=edit;p=FrontPage", "temp.txt"

HikiReload.new(url).poll(file)

使い方

  1. hikireload.rbという名前で保存(urlは自分のWikiに書き換える)
  2. temp.txtに下書きを書く
  3. ruby hikireload.rbで起動
  4. temp.txtを編集すると、エディタで保存するたびにWikiが更新される

エディタでWikiを編集するためのソフトウェアはemacsのhiki-modeなどいくつかありますが、このアプローチだと エディタを選ばない(emacsでもVimでもxyzzyでも秀丸でもなんでも使える)のが利点だと思います。

本日のツッコミ(全3件) [ツッコミを入れる]
(2008-08-25 13:59)

すごいですね。ruby だんだん好きになっちゃいましたよ。<br>ruby中毒になった感じ

ザッピング運営事務局 (2008-10-27 22:59)

※このコメントは、<br>ザッピングをご利用して頂いているユーザー様を対象にお送りしております。<br>既にタグを変更して頂いている方にもコメントしてしまう場合がございますが、<br>ご容赦くださいませ。<br><br>「ザッピング」をご利用いただきありがとうございます。<br>運営元の(株)ブログウォッチャーと申します。<br><br>この度、「ザッピング」のURLが、弊社側のシステムミスで、<br>http://thatsping.com/ から、 http://thatsping.jp/ へ変更になりました。<br>その関係で、ページに貼って頂いているタグを変更して頂く必要がございます。<br><br>以前のタグのままの状態ですと、<br>検索ワード等の情報を取得できない状況が発生しております。<br>ご迷惑をおかけしまして、大変申し訳ございません。<br><br>誠にお手数なのですが、<br>http://thatsping.jp/tagcode<br>に新たなタグ情報が記載されておりますので、<br>ご参照の上、タグを変更して頂けると幸いです。<br><br>今後とも、「ザッピング」を何卒宜しくお願い致します。

ケムス (2011-05-25 11:04)

良く勉強になりました。<br> ありがとうございます。


過去の日記