rubyの最近のブログ記事

関西Ruby会議03 - Regional RubyKaigiで、 「LibraHack後の スクレイピングを 考える」というテーマでLTさせていただいてきました。
20101106.pdf wgetScraper.zip


今日は第42回Ruby/Rails勉強会@関西。「実践スクレイピング」というテーマで、電源検索サイトモバイラーズオアシスのデータを集めるために行ったスクレイピングについてお話しさせていただきました。


会場で教えていただいたこと。
  • IRB.conf[:ECHO] = nil でirbの応答がでないように設定可能
  • irb --noecho でも可
okkezさん、cyrossさん、ありがとうございます!


Ruby1.8.6で「嶽」の字をtosjisでShift-JISに変換すると、なんだか意図しない文字(F7 D3 0A)に変換されてしまった。

require 'kconv'  

str="嶽"
print str.tosjis
(utf-8で保存して実行してね。)

Rubyist Magazine - 標準添付ライブラリ紹介 【第 3 回】 Kconv/NKF/Iconvによると、

>先述の Kconv#to* では変換元の文字コードを推測しているため、推測が外れていた場合は変換結果が文字化けしてしまいます。このような危険性を避けるため、変換元の文字コードが分かっている場合は、なるべく文字コードを明示的に指定するようにしましょう。
とのこと。
print str.kconv(Kconv::SJIS, Kconv::UTF8)
でちゃんと変換できた。

"嶽 文字化け"でググっても出てこなかったので、とりあえず記事にしておこう。
ちなみに、「佐渡ケ嶽部屋」もダメだった。「嶽」の字が鬼門らしい。



すっごいいまさらですが、sqlite2(Ruby/DBI)→sqlite3(sqlite-ruby)の移行に着手しています。

sqlite2の時は、アクセスが競合してBusyになった時のリトライ処理が見あたらなかったので、自前でこんなことをしていました。
10.times_retry(DBI::DatabaseError,1) {
  @dbh = DBI.connect(@@db_prefix+@db_filename, "", "" ) unless @dbh
}
(DBに接続を試みて、DBI::DatabaseErrorが発生したら、1秒sleepしたあとリトライ、10回試みて、それでも駄目だったらDBI::DatabaseErrorをraiseします。)
Rubyらしいエレガントなコード♪とか思っていたのですが、プログラムのあちこちにこれが散見される状態というのもなんだか悲しい。

sqlite-rubyでは、busy_handlerという機能が実装され、こういう事はライブラリ内でやってくれるようになりました。ただ、これのサンプルが見あたらない。
あれこれ試行錯誤していちおう動くようになったのですが、なんだか謎の部分も残っているので、ご存じの方、教えていただけると嬉しいです。

片方のシェルで、sqlite3を走らせて、ロック状態にしておきます。
sqlite> BEGIN;
sqlite> insert into fragments (user_id,content,lastmod,bornin) values(0,"test","2009-04-01 12:59:15","2009-04-01 12:59:15");     
(sqlite2の頃は、BEGIN;した時点でロックされていたのですが、今のバージョンでは、実際にロックが必要になった時点でロックするみたいです)

もう片方でirbを走らせて、こんな感じでロックに激突させます。
require 'rubygems'
require 'sqlite3'

dbh = SQLite3::Database.new("test.sqlite3")
#dbh.busy_timeout(10000)
dbh.busy_handler{|data, retries|
   print "in busy_handler data is ",data,"\n"
   print "retries is ",retries,"\n"
   sleep 1;
   (retries<=3)
}
dbh.execute("insert into fragments (user_id,content,lastmod,bornin) values(0,'test','2009-04-01 12:59:15','2009-04-01 12:59:15');")

それいけっ!
irb(main):013:0> dbh.execute("insert into fragments (user_id,content,lastmod,bornin) values(0,'test','2009-04-01 12:59:15','2009-04-01 12:59:15');")
in busy_handler data is nil
retries is 0
in busy_handler data is nil
retries is 1
in busy_handler data is nil
retries is 2
in busy_handler data is nil
retries is 3
/usr/lib/ruby/gems/1.8/gems/sqlite3-ruby-1.2.1/lib/sqlite3/errors.rb:94:in `check': database is locked (SQLite3::BusyException)
        from /usr/lib/ruby/gems/1.8/gems/sqlite3-ruby-1.2.1/lib/sqlite3/resultset.rb:76:in `check'
         :
よしよし。

まとめ:busy_handlerの使い方

  •  DBに接続したあたりで、
    dbh.busy_handler{|data, retries|
    # BUSYだった時の処理
    }
    
    という具合にして宣言する。このコードはすぐ実行されるわけではなく、DBにアクセスしてBUSYだった時に初めて実行される。
  • retriesに試行回数が入っている。
  • (謎)busy_timeoutの挙動が謎。APIリファレンスを見ると、busy_timeout(1000)ってしたら、一秒おきにリトライしそうな物ですが、実際には丸無視しているように見えます
  • しょうがないので、busy_handler内でsleepします
  • (謎)dataって何でしょうね?
  • busy_handlerのブロックの評価結果がtrueであれば、リトライが実施されます。falseであれば、例外が投げられます。

最後の項目、return false って書くわけじゃなくて、ブロックの評価結果が使われるところがポイントかと。
上のサンプルでは、3回まではリトライして、それで駄目なら例外を投げるようなコードになっています。


あけましておめでとうございます。あんまり正月とか関係なしに淡々と開発しております。
年末年始に外出すると混むんだもん。

で、メイドめーるでデフォルト以外のGoogleカレンダーの情報がとれない件。

googlecalendar.PNG Googleカレンダーでは、こうやって複数のカレンダーをまとめて表示させることが出来るのですけど、メイドめーるがメールしてくれるのは一番上のものだけになっております。
なんでかというと、Googleカレンダーにアクセスするのに使っているgcalapiで、カレンダーリストを取得するAPIがみつからなかったからです。

正確に言うと、ユーザー名とパスワードを使ったらとれるのですけど、なぜかAuthSub認証の場合だけ実装されていないのです。
Googleのドキュメントを見たら、取得する方法があるみたいなので、コードを追加してみました。
module GoogleCalendar
  class ServiceAuthSub
    CALENDAR_LIST_PATH = "http://www.google.com/calendar/feeds/default/allcalendars/full"

    # get the list of user's calendars and returns http response object
    def calendar_list
      logger.info("-- get_calendar_list_responce st --") if logger
      auth unless @auth
      uri = URI.parse(CALENDAR_LIST_PATH)
      res = do_get(uri, {})
      logger.info("-- get_calendar_list_responce en(#{res.message}) --") if logger
      res
    end

    alias :calendars :calendar_list
  end  
end
これだけ実装してあげれば、あとは通常認証と同じコードが働くので、こんなふうにして複数のカレンダーを取ってくることが出来ます。
require 'gcalapi'
require 'googlecalendar/service_auth_sub'
require 'googlecalendar/calendar'

srv1 = GoogleCalendar::ServiceAuthSub.new(user.calendarToken)
cal_list1 = GoogleCalendar::Calendar.calendars(srv1)

出来ればこのコード、本家に取り込んでいただけるとみんな幸せになるように思うのですけど。連絡つくかな?


photo by  flyzipper  rails上で自分のドメイン名をつかってナンヤカンヤしたい時というのがあって。

ENV['HOSTNAME']

 これは論外っぽい。

[default] ENV
[default] :{"HOSTNAME"=>"xxxx.mogya.com",

仮想ドメインの設定を無視してホスト名を返してくれます。HOST_NAMEという値ですから、当たり前か。

request.env:['SERVER_NAME']


request.env:{"SERVER_NAME"=>"maidmail.jp",

 ねらい通り、ドメイン名を返してくれます。ただ、このヘッダも、あとrequest.env['HTTP_HOST']も、ちょっとしたことで簡単に偽装されてしまうので、迂闊につかえません。

request.domain


[default] request.domain:maidmail.jp

 こっちを見ると、request.env:['SERVER_NAME']が偽装されてしまうような場面でも、正しいドメイン名がとれました。
ソースを見ると、HTTP_X_FORWARDED_HOSTというヘッダを使うみたいです。

# File vendor/rails/actionpack/lib/action_controller/request.rb
318: def domain(tld_length = 1) 319: return nil unless named_host?(host) 320: 321: host.split('.').last(1 + tld_length).join('.') 322: end 280: def host 281: raw_host_with_port.sub(/:\d+$/, '') 282: end 271: def raw_host_with_port 272: if forwarded = env["HTTP_X_FORWARDED_HOST"] 273: forwarded.split(/,\s?/).last 274: else 275: env['HTTP_HOST'] || env['SERVER_NAME'] || "#{env['SERVER_ADDR']}:#{env['SERVER_PORT']}" 276: end 277: end

HTTP_X_FORWARDED_HOSTがとれなかった場合に限り、env['HTTP_HOST']やenv['SERVER_NAME']を見てくれるので、HTTP_X_FORWARDED_HOST非対応のサーバでも、request.domainを見ておけば悪いようにはしない実装になっているみたいです。

HTTP_X_FORWARDED_HOSTのうまい説明が見つけきれなかったのですが、これってどういう環境変数なのかなぁ?どれくらい信用のおける値なのでしょう?



先日公開したmextractr_webapi.rbが、gem経由でインストールできるようになりました。

こんな具合にして使うことが出来ます。

[daisuke@snares ~]$ sudo gem install mextractr_webapi
Bulk updating Gem source index for: http://gems.rubyforge.org
Successfully installed mextractr_webapi-0.0.1
1 gem installed
[daisuke@snares /var/maidmail]$ script/server
>> require "mextractr_webapi"
>> mextractr = MextractrWebApi.new(API_KEY)
>> res = mextractr.parse("11/29(土) 京都で紅葉を楽しむオフ会を開催します。秋深い京都を撮り歩きましょう。 ")
>> pp res
{"id"=>"20081202_0001",
 "where"=>
  [{"georss:point"=>nil, "valueString"=>"京都"},
   {"georss:point"=>nil, "valueString"=>"京都"}],
 "what"=>[{"valueString"=>"オフ会"}],
 "when"=>[{"valueString"=>"11/29(土)", "startTime"=>"2008-11-29"}],
 "updated"=>"2008-12-02T12:16:50+09:00"
 "content"=>
  "11/29(土) 京都で紅葉を楽しむオフ会を開催します。秋深い京都を撮り歩きましょう。 ",
 "response"=>
  "\n\n  Mextractr WebAPI results\n  2008-12-02T12:16:50+09:00\n  \n    Mextractr\n  \n  20081202_0001\n  \n    \n    20081202_0001\n    extracted event metadata\n    \n      Mextractr WebAPI\n    \n    2008-12-02T12:16:50+09:00\n    11/29(土) 京都で紅葉を楽しむオフ会を開催します。秋深い京都を撮り歩きましょう。 \n    \n    \n    \n    \n  \n\n",
  }

enjoy!


 メイドめーるでは日本語のメールを送信するために、 ヽ( ・∀・)ノくまくまー(07-31[長年日記])で書いていただいている、Iso2022jpMailerを使っています。
クラスを継承するだけでつかえるとてもいいアイデアだったのですが、たった一つ問題が。

IMGP8827.JPG

「~」が文字化けするのです。普通のアプリケーションだったら、まあ横棒でいいか、とか言えるし、実際メイドめーるも今日までそうやってごまかしてきたのですけど。萌え萌えなメイドさんがメールをくれるアプリケーションにおいて「~」がつかえないと言うことは、「おはようございます~」がつかえないことになってしまいます。
「おはようございますー」と「おはようございます~」は断じて違うのです!

とまあそんなわけで、技術的に追求した結果、一応「~」が出せるようになりましたのでご報告です。

IMGP8828.JPG

やり方としては、Iso2022jpMailer の

   @mail.body = NKF::nkf('-j', @mail.body)
の部分を
   @mail.body = NKF::nkf('-Wxm0 --oc=ISO-2022-JP-1', @mail.body)
に置き換えます。
ついでに、タイトルとかに「~」がはいることもあるだろうから、
      text = NKF.nkf('-j -m0', text)
も、
      text = NKF.nkf('-Wxm0 --oc=ISO-2022-JP-1', text)
にしちゃいました。

技術的な話

正直に言うと、なんでこれで化けないのか、最終的な理由は理解できていません。
ただ、あれこれやっているうちに、化ける時と化けない時があって、比較するとこうなっていることが分りました。

wave_on_jis.PNG

上がうまくいく場合で、下が駄目な場合です。日本語メールなのでJISコードでエンコードしてあります。
OKな時は1B 24 42で始まっていて、駄目な時は1B 24 48で始まる...、つまり、JIS X 0208-1983でエンコードしてあればOKで、JIS X 0212-1990だと駄目なことが分ります。
NKFのコマンド詳細を見ながら、irbでいろいろやってみると。

>> NKF::nkf('-jWxm0', "~").each_byte{|b| print b.to_s(16)+","};print "\n"
1b,24,28,44,22,37,1b,28,42,
=> nil
>>NKF::nkf('-Wxm0 --oc=ISO-2022-JP', "~").each_byte{|b| print b.to_s(16)+","};print "\n"
1b,24,28,44,22,37,1b,28,42,
=> nil
>> NKF::nkf('-Wxm0 --oc=ISO-2022-JP-1', "~").each_byte{|b| print b.to_s(16)+","};print "\n"
1b,24,42,21,41,1b,28,42,
=> nil
>> NKF::nkf('-Wxm0 --oc=ISO-2022-JP-3', "~").each_byte{|b| print b.to_s(16)+","};print "\n"
1b,24,28,50,28,37,1b,28,42,
=> nil
--oc=ISO-2022-JP-1の時だけ、ねらい通りの変換をしてくれるようです。ということで、Iso2022jpMailerにも、同じ引数を渡してあげると、「~」が出るようになった次第。
「ISO-2022-JP-1」って、JIS X 0212に対応させるための引数で。むしろ逆のような気がするし、なんでISO-2022-JP-3で駄目なのかも謎です。


Mextractrに関する注目が集まっているので、自分が使っているライブラリを公開します。

mextractr_webapi.rb

#RubyForgeのプロジェクトは今申請中なので、gemはちょっとお待ちくださいませ。

使い方としては、APIキーを取得して、
$ script/console
Loading development environment (Rails 2.0.2)
>> require "mextractr_webapi"	
>> mextractr = MextractrWebApi.new(MEXTRACTR_KEY)
>> res = mextractr.parse("11/27(木)、京都市中京区のはてな京都オフィスでShibuya Perl Mongersテクニカルトークのパブリックビューイングが開催されます。")
>> pp res
{"id"=>"20081127_0001",
 "updated"=>"2008-11-27T10:05:38+09:00"
 "response"=>
  "\n\n  Mextractr WebAPI results\n  2008-11-27T10:05:38+09:00\n  \n    Mextractr\n  \n  20081127_0001\n  \n    \n    20081127_0001\n    extracted event metadata\n    \n      Mextractr WebAPI\n    \n    2008-11-27T10:05:38+09:00\n    11/27(木)、京都市中京区のはてな京都オフィスでShibuya Perl Mongersテクニカルトークのパブリックビューイングが開催されます。\n    \n    \n    \n  \n\n",
 "content"=>
  "11/27(木)、京都市中京区のはてな京都オフィスでShibuya Perl Mongersテクニカルトークのパブリックビューイングが開催されます。",
 "where"=>
  [{"georss:point"=>nil, "valueString"=>"木)、京都市中京区"},
   {"georss:point"=>nil, "valueString"=>"京都"}],
 "when"=>[{"valueString"=>"11/27(木)", "startTime"=>"2008-11-27"}],
 }
という具合です。
res['when'][0]['startTime']とか、res['where'][0]['valueString']という具合で各値を取り出すことが出来ます。

あと、MextractrWebApi.new(MEXTRACTR_KEY)の二つ目の引数にloggerを渡すと、取得してきたatomの内容とかを見ることが出来るので、挙動がおかしい時に犯人を突き止める一助になります。


photo by Martin LaBar

 昨日はすっごいがんばって自前でGoogleAuthSub認証を通すコードを書いたのですが。
今日、続きでカレンダーを扱おうとしたら、gcalapiに同じ内容のコードがあるのを発見してしまいました。
しかもこっちの方が明らかにきれいです。だったら最初からこっちでよかったんじゃん。

再発明しちゃった車輪をもてあそびつつ、gcalapiでAuthSub認証を使う方法を記述しておきます。

Googleの認証ページ用のURLを生成する

require 'gcalapi'
require 'googlecalendar/auth_sub_util'
@uri = GoogleCalendar::AuthSubUtil.build_request_url(
'http://www.example.com/responce',
'http://www.google.com/calendar/feeds/',
false, #use_secure
true #use_session
)

Googleから戻ってきたトークンをつかってセッショントークンをもらう

authsub_token = ''
one_time_token = params[:token]
session_token = GoogleCalendar::AuthSubUtil.exchange_session_token(one_time_token)

戻ってきたページのURLからワンタイムトークンを取り出すためにget_one_time_tokenというメソッドも用意されているのですが、railsであればparams[:token]でとる方がずっと早いですね。

セッショントークンをつかってカレンダーに予定を書き込む

require 'gcalapi'
require 'googlecalendar/auth_sub_util'
require 'googlecalendar/service_auth_sub'

server = GoogleCalendar::ServiceAuthSub.new(session_token)
calendar = GoogleCalendar::Calendar.new(server, 'http://www.google.com/calendar/feeds/default/private/full')
event = calendar.create_event
event.st = Time.parse("2008-09-19 20:00:01")
event.en = Time.parse("2008-09-19 22:00:01")
event.title = "実験!"
event.desc = "こんにちはこんにちは! "
event.save!

あとは基本的に、GoogleCalendar::Service の代わりにGoogleCalendar::ServiceAuthSubを使えばいいらしい。calendar_listが動かないのはしょうがないのかなぁ。



このアーカイブについて

このページには、過去に書かれたブログ記事のうち <$ MTCategoryLabel$> カテゴリに属しているものが含まれています。

前のカテゴリは postfix です。

次のカテゴリは selenium です。

最近のコンテンツは インデックスページ で見られます。過去に書かれたものは アーカイブのページ で見られます。

Powered by
Movable Type