2008-07-23 [長年日記]
_ 今さらながら Hpricot は便利
Hpricot, a fast and delightful HTML parser
なんと言っても
HTML でも XHTML でもそれなりに同じように動く
っていうさじ加減が絶妙すぎる。
XML を対象にした便利なツールは実は結構たくさんある。でも世界の Web 上になんとかソースは存在しているが欲しい形にはなってなくて不便、という類いの情報って実は圧倒的に XHTML でないことが多い。だから XML を対象にした便利ツールの大半は使いものにならない。
ブラウザ上では早くから HTML でも XHTML でも DOM 操作は可能だったし、今は XPath も CSS セレクタも使える、といった具合でどんどん便利になっている。でもサーバ側というか非ブラウザ環境ではゴリゴリ正規表現書いたり、なんだか不便な状況が続いていたように思う。特に PHP は C の wrapper なので XML 関数は充実しているんだけど全然使えないという悲しい状況だった。*1
これがあーた。何この Hpricot の楽さ。これに open-uri を組み合わせればまるで remote include 可能な PHP のように作業を始められる。
require 'open-uri' require 'kconv' require 'rubygems' require 'hpricot' Hpricot( open( URL ).read.toutf8 )
また Hpricot::Elem オブジェクトは inspect を書き換えているので、単に p しただけでどのような要素が取得できているのかチェックできて楽ちん。
気が利くなぁ。気が利くよ。さすが why だよ。抜けるところは手を抜くところもさすがだよ。Syck の to_yaml() も個人的にはすげー困るけど、しょうがないかなぁという気もしてきちゃうよ。
*1 だから個人的には Pear の XML_HTMLSax3 を wrap して stack を用意することで階層構造のチェックが可能なものを使っている。結果を serialize して cache しちゃえばそれなりの速度で動いてくれる。
2008-07-21 [長年日記]
_ 今ごろ scraping に苦労している話
今回、ある HTML の断片を定期的に取得するという要求ができたので喜び勇んで yapra を使えば一発じゃーんとやり始めて恐ろしく苦労したのでその顛末を書いておく。なお、苦労したのは pragger や yapra が問題なのではなく、自分の思い描いていた動作と pragger の思想がズレていたこと及び Hpricot の不備が原因である。pragger ダメじゃんとか言うつもりは毛頭ないです。
まぁバカは要らぬ苦労をするということの記録です。言い直すとやはり道具は使ってみないとクセが分からない。なお、使ったのは yapra の git tip です。*1
yapra の conf はちゃんと書きましょう
実は最初のつまずきの話は超簡単で、
存在しない class based plugin を呼び出そうとしていた。
という実にくだらないことでした。以下その trace
./bin/../lib/yapra/pipeline.rb:76:in `run_class_based_plugin':
LoadError (LoadError)
from ./bin/../lib/yapra/pipeline.rb:52:in `execute_plugin'
from ./bin/../lib/yapra/pipeline.rb:45:in `run'
from ./bin/../lib/yapra/inflector.rb:53:in `inject'
..
はい、そのまんまですね。でもぼくはこのメッセージを素直に受け取れなくなっていた。理由は「class based plugin と legacy plugin を一つずつ呼び出していて、class based plugin は絶対に間違いなく load できているはず。」と思い込んでしまったから。
このメッセージは以下の conf で発生していた。
- module: RSS::Save
実はこれ、こう書いていたつもりだった。
- module: RSS::save
お分かりだろうか。s の大文字小文字を間違っているのだ。これに丸1日気づかなかった。というか作者の yuanying さんの手を煩わすまで気づかなかった。ひどい。ひどすぎるぞ > オレ。
この違い、実は RSS::Save と書くと class based plugin の呼び出し、RSS::save と書くと legacy plugin の呼び出しとして動作する。
詳しくは以下に yuanying さんにメールで教えていただいた内容をもとに現時点での自分の理解を書いておきます。
yapra の plugin の load
何のことやら分からない方と自分のためにもう少し詳細を書いておくと、yapra の plugin には
- pragger オリジナルの plugin(これを legacy plugin と呼ぶ)
- yapra 用で Ruby の標準的な module/class 構造の plugin
の2種類の plugin がある。今回自分は
1 を呼び出しているつもりで間違って 2 を呼び出していたことに気づかず、立ち往生していた
のだ。バカすぎる。ちゃんと 2 の呼び出しに失敗したというメッセージを何回も目にしているのに。で、呼び出しの切り替えはまさに書き間違った大文字、小文字によって行われている。
言い訳すると、Perl や Ruby の常識的には module や package は大文字で始まるじゃないですか。だからぼかぁてっきり plugin の名前は常に大文字で始まるもんだと思い込んじゃってたんですね。
でも違ったのです。pragger の場合は基本的に plugin の名前はメソッドの名前なので、メソッドの名前は当然 Ruby 的には小文字で始まる、結果として pragger 由来の legacy plugin の名前は小文字で始まります。RSS::save の RSS が大文字なのは単にディレクトリ名が大文字だから。そうです、ご想像通りこの plugin の実体は RSS/save.rb に書かれています。
yapra の class based plugin の場合は読んで字のごとく class based であり、class ってことは Ruby 的には定数であり、これは大文字で始まる必要があるわけです。
というわけで legacy plugin の呼び出しと class based plugin の呼び出しを分ける決定的な部分を完全に思い込みでミスっていたので、何が起きているのか正しく読み取れず行き詰まってしまったというわけであります。ちゃんとエラーメッセージはそのまま読まなきゃダメです。はい。
ちなみに yapra の最新版は
rubyforge のページに書かれているように git の方なのだそうです。
yuanying's yapra at master ― GitHub
自分は以前 svn co しておいたコードを持っていたので、今回 svn up しただけで作業を始めてしまいましたが、今後は git の方だけでいくことにします。git の操作がよく分かってませんが、どうせ clone と pull しかしないだろう*2から、悩むことはあるまい。
ところで今回初めて git を使いましたが、clone の速いこと速いこと。changeset が番号にならないのでどうも直感的な感じがしませんが、このスピードは魅力かも。(pull のスピードには特別感動しなかったので、日常的にはそんなに svn up と差が出ないかもしれませんけど。)
話はまだ終わらない
実はこの件の目的はまだ達成できていません。すいません、バカで。そもそもやりたいことは
- HTML の一部の情報を抜き出したい
しか決まってません。
これをどう出力したいのかは曖昧なままでした。この状態で Feed::Custom を使い始めました。これが feed を組み立てるためのものだと気づくのに少々時間が掛かりました。オレの目的は単なる scraping で feed の組み立てじゃない気がするなぁと思いながらも、いやいやイマドキみんな plagger/pragger だよ、何ゆってんだよ wtnabe はバカだなぁ。scrape したら feed にするものなのさ、と思い込み、まともに plugin が動いたことに気をよくして作業を進めます。この時点での conf はこんな感じです。
- module: Feed::Custom config: .. - module: print
print plugin は受け取った data をそのまま p してくれます。これで Feed::Custom で目的の scraping が行えているかどうかを確認します。
しかしまず XPath による切り出しでつまずきます。目的の node は、
- あるclass属性を持っている子nodeを含むもの
なのですが、これの書き方が分かりません。子nodeの指定はできますが、目的は子nodeじゃない。XPather と格闘すること数時間。
親node//descendant::子node[@class='FOO']
という書き方で目的を達成できることに気づき狂喜乱舞! しかし、Hpricot でこの書き方が使えないらしく、print で何も出てきません。*3
イヤになってきました。
Hpricot は便利だけどある程度いい加減な実装であるということは知っていました。でも最初に引っかかってしまうとは。だいたい、何も出力されないんじゃ何が起きているか分からない。
ここでいったん yapra は諦め、生 Hpricot で再チャレンジすることにしました。descendant:: の書き方が使えないってことは
Hpricot::Doc::search().each { |ele|
if ( ele.search() )
..
end
}
だよね、と直感的に思ったからです。そして XPath 一発で書けないルールを Feed::Custom 上で再現する方法は知らない。だったらまずは Hpricot で目的の node を取り出すところまでをどうにか動かしてみなくては、と思いました。
これはなんとかなりました。やはり descendant:: の書き方は通じません。うむうむ。状況は把握できた。ここではたと気づきます。
yapra では grep 掛ければいいんじゃなかろうか。
今回の目的は ある node の繰り返しのうち、class="FOO" が設定されている node を取り出す、というものです。これを XPath 一発で書くには descendant:: が必要だけど、とりあえず class があろうがなかろうが切り出しちゃって、class のないものをあとでフィルタで捨てちゃえばいいじゃん、と。つまり、
- module: Feed::Custom - module: grep - module: print
で、うまいこと書くと目的の node だけを取り出せるんじゃないかということです。
しかし Feed::Custom のことを自分は分かっていなかった。
Yapra で Pixiv -- BONNOH FRACTION 13
だけを頼りにしていたのですが、config: extract_xpath: の中に各 item の個々の要素(link とか)を直接書けることに気づかず、apply_template_after_extraced: の中で組み立て作業を行おうとするもこの時点で渡ってくるのは Hpricot オブジェクトではなく文字列なので、属性値を取り出したい場合は文字列処理が必要です。
「なんかおかしくね?」
と思い始めます。頑張って XPath でやってきて最後は文字列処理なのか?と。せっかくの DOM node, せっかくの Hpricot オブジェクトじゃないのかと。じゃあ extract_xpath の方で頑張れるんじゃないだろうか。
しかし自分の力でできたのはここまででした。
capture: (ry split: (ry title: '//node/text()'
そうです。「内容」の取り方は分かったけど、「属性値」の取り方が分からない。XPath は node の指定のためのものだもん。属性値が取りたければやっぱ Hpricot オブジェクトを直接扱えないとダメだよなぁ。と諦めたのが昨日の夜のことでございます。
とりあえず今は生 Hpricot で書いて切り出しだけはできているんですが、今度はこれを feed まで組み立てるのもだるい。(もはや目的が feed の出力だったかどうかすらどうでもよくなっている。)というか yapra に挫折したという事実を受け止めたくない。feed の組み立てなんて自分で書いちゃダメだ、とさえ思い始めています。とりあえず YAML にでも吐き出しておいて、形はあとで考えるかと思ったら
今度は Syck の to_yaml() は日本語を正しく扱えない。もう完全にイヤになりました。
scraping ムズイっす。
たぶん conf だけで解決しようとせずに plugin を書けばいいんですよね。Hpricot を直接いじって、あとは誰か他の plugin に回す。でもまぁ、とりあえず頭を冷やすために放置します。こんなたった数行の処理すらまともに書けないという事実に対する怒りがさらに頭の回転を鈍くしている感じなので。
あと思ったこと
- WWW::Mechanize はローカルのファイルを読み取れないので scraping 自体のテストをしたいときに不便
自分は今回手元で動かしていた Apache の適当な場所に取得済みの HTML を置いてテストしていたけど、そういう環境を用意できない人はどうすればいいんだろう? 毎回本番のサーバにアクセスしに行くのもイヤだよね。まぁパッチ書いている人もいるけど、パッチである以上は本家の更新で動かなくなる危険性は絶対残っちゃうし。
- Syck の to_yaml() は結構致命的
なんとなく 1.9 による m17n が進むと解決しそうな気もするけど、DBMS とか使わずにお手軽に他のシステム(言語から全部違うとか)と連携したい場合、標準の Ruby + Syck は使いものにならない。
まぁだからこそ XML である feed を使う plagger 系ツールの出番なのかもしれないけど、まだ使いこなせてないし。あうー。
RubyForge: Ya2YAML - An UTF8 safe YAML dumper: Project Info
使えばいいっていう話ではあるんだけど。なんかこう釈然としないものは残る。
とは言え今回は
- git
- yapra
- WWW::Mechanize
- Hpricot
- XPath(完全に初めてではないけど)
と初めて使った道具ばかりで、いい経験になったと言えばいい経験になった。次に繋げよう。というか近日中には目的は達成しないといけないわけだけど。
いちばん早いのは生 Hpricot で進める方向かな…。
2008-07-20 [長年日記]
_ 喫茶店を開拓したい
カフェって言うな。
いや、別に言ってもいいけど。
基本的に出不精だし、別に家でもいいんだけど、お気に入りの喫茶店を確保したいなぁと以前から思っていて。例えば読書やコードいじりに使えるいい感じの喫茶店。
最近のカフェっぽいところにも何カ所か行ってみたんだけど、どこの店も都会の店を参考にしているせいか、この辺の店にしてはテーブルの間隔、椅子の間隔が狭い。これが田舎者にはまずカチンとくるわけ。狭いんだよ、と。都会の人は大変だよね。コンビニとかラーメン屋なんか行くと一発で分かるけど、とにかく密度が高い。オレは狭いのきらいなんだよ。田舎もんだからよ。パーソナルスペースが大きいの。隣のやつがこんなに近かったら絶対落ち着かない。
というわけで最近のカフェ系の店で当たりに出会えた試しがない。まぁ基本的に若い女性向けでそもそもちょっと賑やかなのだ。みんなお喋りしてて、落ち着いて本なんか読める感じじゃない。それなら最近大型書店に併設されているコーヒーショップの方が何倍も落ち着く。席もゆったりだ。具体的には Tully's ですが。
それは分かっているんだけど、なんかね、ちょっとこういう書店併設やショッピングセンター内の Tully's は店全体がちょっと手狭で。あんまり落ち着いて本広げたりパソコン広げたりしちゃいけないような気になってしまうのね。そこが実に惜しい。コーヒーの味的にもスターバックスより好きだし、ちゃんと焼き物のカップだし。でもそもそも大型書店併設やショッピングセンター内ってことはタイミングによっては駐車場に車を止めるだけで一苦労だったり、そこから店まで結構距離があったりする。贅沢ですかそうですか。
この前、御経塚サティ内の Tully's にはホットスポットがあったし、あそこがいいのかなぁ。狭めだけど。というか Tully's のサイトにあの店載ってねーよ。少なくともビーンズの店舗より前からあった気がするんですけど!
無線LANのサービスも意外に増えてるし、網羅してるポイントの多いサービスから選んだ方がいいのかしら。むむむ。知らんことばかりだ。
2008-07-17 [長年日記]
_ REXML で SAX風に id と class を抜き出してみた
こんな風に使います。
ruby class_id_picker.rb FILENAME
当然、HTML は処理できなくて、XHTML でないとダメです。
使い道は、とりあえず付けてある名前を推敲するのに使えるかなーくらい。あとは id が unique かどうかとか? 普通はこんなの要らない気がする。
attrs をチェックする際にわざわざ
//i
で引っ掛けているのは REXML でのパース時に大文字や小文字への正規化は行われないためです。
#! /usr/bin/env ruby
require 'rexml/document'
require 'rexml/streamlistener'
class ClassIdPicker
def initialize
@ids = []
@classes = []
end
attr_reader( :ids, :classes )
def self.parse( io )
obj = self.new
REXML::Document.parse_stream( io, Listener.new( obj.ids, obj.classes ) )
return obj
end
def attrs
return instance_variables.map { |e|
e.sub( /\A@/, '' )
}
end
def browse
puts "=== Classes ==="
puts @classes.sort.uniq.join( "\n" )
puts "=== Ids ==="
puts @ids.sort.uniq.join( "\n" )
end
class Listener
include REXML::StreamListener
def initialize( ids, classes )
@ids = ids
@classes = classes
end
attr_reader( :ids, :classes )
def tag_start( name, attrs )
attrs.each_pair { |name, val|
case name
when /\Aid\z/i
@ids.push( val )
when /\Aclass\z/i
@classes.push( val )
end
}
end
end
end
if ( __FILE__ == $0 )
app = ClassIdPicker.parse( ARGF.file )
# 数を数えながら表示してみる
app.attrs.each { |attr|
p attr
attr_list = app.send( attr ).dup
attr_list.sort.uniq.each { |name|
puts "#{name}\t#{attr_list.grep( name ).size}"
}
}
end
_ Yuanying [Yapra書いてて思ったんですけど、ウェブからスクレイピングするなら、苦労してpragger用conf書いても、Ru..]
_ wtnabe [そうかもしれませんw open-uri + 正規表現だけでも結構なことができるし、これに Hpricot なり R..]