2007-02-06
■ [prog] C#2.0時代のゲームプログラミング(49) 〜 delegateを用いたcontinuation
C#の無名delegate(クロージャっぽいやつ)とCPS変換の話。
11月ごろから書こうと思いつつ放置しているネタに「call/ccを使ってRPGのイベントを実装する(した)」という話が あるんだけど、そうか、実は
- call/ccでやる
- yieldでやる
- CPS変換でやる
という3つの似た選択肢があるんだな。2とか3だとどういうコードになるんだろ…。あとで考える。
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を使えば「普段ブラウザを操作するのと同じような感覚で」 スクリプトを書くことができます。
では、ブラウザから日記を書くときの手順を思い出してみましょう。
- http://d.hatena.ne.jp/(ユーザid)/ を開く
- おおっと、ログインしてなかった。右上の「ログイン」をクリックする
- ユーザー名とパスワードを入力し、「ログイン」を押す
- 「自動でページを移動しています(移動しないときはこちらのリンクを…)」
- 「日記を書く」をクリック
- 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です(たぶん)
- Hpricot : http://code.whytheluckystiff.net/dist/
- WWW::Mechanize : http://rubyforge.org/projects/mechanize
まとめ
本稿では、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)
使い方
- hikireload.rbという名前で保存(urlは自分のWikiに書き換える)
- temp.txtに下書きを書く
- ruby hikireload.rbで起動
- temp.txtを編集すると、エディタで保存するたびにWikiが更新される
エディタでWikiを編集するためのソフトウェアはemacsのhiki-modeなどいくつかありますが、このアプローチだと エディタを選ばない(emacsでもVimでもxyzzyでも秀丸でもなんでも使える)のが利点だと思います。
2007-01-31
■ [prog] anarchy golf始まったな
おおお格好いい!
というわけでテストも兼ねて、Hello, wolrd!の投稿されてない 言語をひととおり埋めてみました。
最初の方は真面目にやってたんですが…Adaあたりからだんだん分からなくなって…、 なんというかその、Wikipediaは便利ですね(おい!)
あと残り4つ。
- Io : なんかtimeoutになってしまう ("Hello, world!" print であってますよね?)
- whitespace : なんか何も出力されなくてfailedになる
- sed : わかんないんです(><)
- x86(exe) : アセンブラ書けないんです(><)*1
(2/2追記:ioとwhitespace(とbefunge)については修正が入ったようです。ありがとうございます。)
*1 サイズが10KBを越えてはいけないのである
2007-01-29
2007-01-21
■ [kmc] おおさと杯
部内の若者向けプログラミングコンテスト、通称「おおさと杯」の第3回が行われました。
基本的にICPCのような感じ(風船マークもあるよ!)で3時間6問なんですが、言語はC系だけではなく、RubyとHaskellも 使えるという充実ぶり。
僕はせっかくなのでRubyで参加しましたが、問題を読み間違ったりなんだりで結局3問しか解けず…駄目駄目ですね。 とはいえ、Rubyでコンテスト的なプログラムを書くのもなかなか新鮮で楽しかったです。次回はHaskellか!?(入力が一番大変そうだ…^^;)
■ [spoj] Sphere Online Judge
で、Rubyとかが使えるOnline Judgeとかないよねーという話をしたら、Sphere Online Judge(SPOJ) というサイトがあると教えてもらいました。
uvaやPKUのような感じで、使用可能な言語は なんと驚きの35種類!
C (gcc 4.0.0-8) C99 strict (gcc 4.0.0-8) C++ (g++ 4.0.0-8) Pascal (gpc v20030830) Pascal (fpc 2.0.4) Java (j2se jdk 5.0) Nice (nicec 0.9.6) JAR (j2se jdk 5.0) C# (mcs 1.0.1) Nemerle (ncc 0.2.1) Smalltalk (gst 2.1.7) Assembler (nasm 0.98.38) Algol (a60 0.20a-4) Fortran (g77 3.3.3) ADA 95 (gnat 3.15p) Bash (bash 2.05b-15) Perl (perl 5.8.3) Python (python 2.4) Ruby (ruby 1.8.1) Lua (lua 5.0.2) Icon (iconc 9.4.2) Pike (pike 7.4.35) PHP (php 4.3.8-9) Scheme (guile 1.6) Scheme (qobi 0.9+0.10a) Common Lisp (gcl 2.6.5) Common Lisp (clisp 2.33.2) Haskell (ghc 6.4.1) Ocaml (ocaml 3.08.1) Clips (clips 6.21) Prolog (swipl 5.2.7) Whitespace (wspace 0.3) Brainf**k (bf2c) Intercal (ick 0.24) Text (pure text)
C/C++/C#/Java等の基本的なところからPerl/Python/Ruby/Pike/Luaのようなスクリプト言語、Lisp,Scheme/Haskell/Ocamlのような関数型言語から Bash/nasm/Whitespace/Brainf**kのような変り種まで幅広く取り揃えられています。
…しかし最後の「Text」ってのはどういう言語なんでしょうか? 期待されるoutputをすべてそのまま予想したらAccept?
■ [spoj] SPOJをネタにRaccを使ってみるテスト
1. TEST
こりゃー簡単ですよね。インタプリタ走らすまでもなく解けるぜ!
…と思ったら改行の処理を忘れて1miss _|‾|○
2. PRIME1
require 'mathn' してPrime#each使ったら一発じゃね!? と思ったのですが、TLE。もうちょっと真面目にやらないと駄目なようです。 ああ、難易度順に並んでるというわけではないのね。
3. SBSTR1
文字列Aに文字列Bが含まれているかを判定する問題。これこそString#include?一発では?と思ったら…
ちょwwwPlease note, that the solution may only be submitted in the following languages: Brainf**k, Whitespace and Intercal.
4. ONP
四則演算+αの式を逆ポーランド記法に直す問題。要するにパーサ書けばいい、んですが…
せっかくだから俺はこの青のRaccを使うぜ!
いや、いいんですよ!Raccの練習だから!
gotokenさんの記事 (今は亡きCマガ…;涙) を見つつ、onp.y を書いてみる。
# exp = exp (+,-,*,/,^) exp | # '(' exp ')' | # = IDENT class OnpParser prechigh left '^' left '/' left '*' left '-' left '+' preclow rule exp : exp '+' exp { result += "#{val[2]}+" } | exp '-' exp { result += "#{val[2]}-" } | exp '*' exp { result += "#{val[2]}*" } | exp '/' exp { result += "#{val[2]}/" } | exp '^' exp { result += "#{val[2]}^" } | '(' exp ')' { result = val[1] } | IDENT ; end
こんな感じかね?
あとスキャナ部分も書かんといかんらしいので、
require 'onp.tab.rb' class OnpParser def parse(str) scan(str) do_parse end private def scan(str) @q = [] until str.empty? case str when /\A\s+/ # skip when /\A([a-z])/ @q.push [:IDENT, $1] when /\A./ # +-*/^() c = str[0].chr @q.push [c, c] end str = $' end @q.push [false, '$end'] end def next_token @q.shift end end gets while line=gets puts OnpParser.new.parse(line.chomp) end
こんな感じで。要するにパーザクラスのnext_tokenメソッドで [トークンの種類、トークンの中身] という配列をひとつずつ返せばいいらしい。 上では記事の例と同じように、scanメソッドで一気に全てのトークンを切り出しています。
というわけでONPは解けた、と思う、んですが…
submitしたらNZEC(non-zero exit codeらしい)だったよ!
require 'racc/parser' だけでもNZECになるので、どうもraccは使えない予感。
…ですよねー(´・∀・`)
2007-01-19
2007-01-18
■ [softs] Xうごいた
(前回までのあらすじ) scim-anthyのためにVMWare Player上のDebianをtestingに上げたyharaだったが、etchではXがx.orgに 変わっているという罠が!xinitしてもエラーで起動しないXをyharaはなんとかできるのか!?
結論から言うと
apt-get install xserver-xorg-input-vmmouse apt-get install xserver-xorg-video-vmware
して、dpkg-reconfigure xserver-xorgしたらなんか動いた。選択肢を忘れちゃったので何の参考にもならん。すいません…。 基本的にわかんないとこはスルー(Enter)もしくは"vmware"とか。
参考:
- debian + vmware player http://d.hatena.ne.jp/papa33/
- ネットワーク http://mkosaki.blog46.fc2.com/blog-entry-32.html
- ネットワークの概要 http://www.ukkii.com/vmware/VMnet_Tips/VMnet_Tips.html
ちなみにscimの起動にはまだ成功していません^^;
2007-01-17
■ [tDiary] 褒めてはいけない
同じパターンのコメントスパムが一杯来るので、設定画面で「(Good|Cool) site. Thank(s| you)!!」みたいなのをはじくことにした。
具体的には
[\w\s]+ site\. Thank[\w\s]+
というパターンがコメント本文に含まれていると投稿できません。これ以降、このサイトを褒めたいときは別の表現を使用してください :-)
■ [rskit] 褒められた!
Ruby は...、なんだろう?とりあえず今弄ってる奴の元になったサンプルが素晴らしく理解しやすい&弄りやすかったというべきだろうか?
[http://d.hatena.ne.jp/isshiki/20070109/p4より引用]
実はあのサンプルの300行に3日くらいかけてたりするので^^; 非常に嬉しいです。 サンプルコードはこれから製作されるゲームに再利用される可能性が高いので気が抜けないですね。 部内でもDown!!のソースとかたまにコピペされてたりするし(※今から参考にするなら断然apple catcherの方がお奨めです)。
まぁプログラミング経験者にとって理解しやすいのと非経験者にとって理解しやすいのはまた違うかも知れません…ということで、 より初心者向けなソース解説をるびまに記事として書かせてもらえることになりました。 1ヵ月後くらいには掲載されると思うので乞うご期待。
あと 他言語から来た人のためのRubyメモ *1は Stringクラスの便利なメソッドについても書こうと思ってたんですが…、放置している間にRuby初心者で無くなってしまった…。 何を書こうと思ってたんだっけなぁ。
鉄は熱いうちに打たんと。
*1 最初は「Ruby初心者のためのTips」という名前だったが、あんまり適切じゃない気がして改名
□ 権 [すごいですね。ruby だんだん好きになっちゃいましたよ。 ruby中毒になった感じ]
□ ザッピング運営事務局 [※このコメントは、 ザッピングをご利用して頂いているユーザー様を対象にお送りしております。 既にタグを変更して頂いて..]
□ ケムス [良く勉強になりました。 ありがとうございます。]