rubyの最近のブログ記事
会場で教えていただいたこと。
- IRB.conf[:ECHO] = nil でirbの応答がでないように設定可能
- irb --noecho でも可
Ruby1.8.6で「嶽」の字をtosjisでShift-JISに変換すると、なんだか意図しない文字(F7 D3 0A)に変換されてしまった。
(utf-8で保存して実行してね。)require 'kconv' str="嶽" print str.tosjis
Rubyist Magazine - 標準添付ライブラリ紹介 【第 3 回】 Kconv/NKF/Iconvによると、
>先述の Kconv#to* では変換元の文字コードを推測しているため、推測が外れていた場合は変換結果が文字化けしてしまいます。このような危険性を避けるため、変換元の文字コードが分かっている場合は、なるべく文字コードを明示的に指定するようにしましょう。とのこと。
でちゃんと変換できた。print str.kconv(Kconv::SJIS, Kconv::UTF8)
"嶽 文字化け"でググっても出てこなかったので、とりあえず記事にしておこう。
ちなみに、「佐渡ケ嶽部屋」もダメだった。「嶽」の字が鬼門らしい。
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を走らせて、ロック状態にしておきます。
(sqlite2の頃は、BEGIN;した時点でロックされていたのですが、今のバージョンでは、実際にロックが必要になった時点でロックするみたいです)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");
もう片方で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カレンダーの情報がとれない件。
なんでかというと、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)
出来ればこのコード、本家に取り込んでいただけるとみんな幸せになるように思うのですけど。連絡つくかな?
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を使っています。
クラスを継承するだけでつかえるとてもいいアイデアだったのですが、たった一つ問題が。
「~」が文字化けするのです。普通のアプリケーションだったら、まあ横棒でいいか、とか言えるし、実際メイドめーるも今日までそうやってごまかしてきたのですけど。萌え萌えなメイドさんがメールをくれるアプリケーションにおいて「~」がつかえないと言うことは、「おはようございます~」がつかえないことになってしまいます。
「おはようございますー」と「おはようございます~」は断じて違うのです!
とまあそんなわけで、技術的に追求した結果、一応「~」が出せるようになりましたのでご報告です。
やり方としては、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)
にしちゃいました。
技術的な話
正直に言うと、なんでこれで化けないのか、最終的な理由は理解できていません。
ただ、あれこれやっているうちに、化ける時と化けない時があって、比較するとこうなっていることが分りました。
上がうまくいく場合で、下が駄目な場合です。日本語メールなので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_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の内容とかを見ることが出来るので、挙動がおかしい時に犯人を突き止める一助になります。

昨日はすっごいがんばって自前で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が動かないのはしょうがないのかなぁ。
で、そういう時に、「ここにユーザー名とパスワードを入れてね」という危険なやり方じゃなくて、ちゃんとユーザーの同意をとりつつ、アプリケーションにはパスワードを渡さなくていいようにするための認証APIが、GoogleAuthSubです。
実際に使っている例としては、携帯電話でGoogleカレンダーを読み書きできるGoogle Calendar Mobile Gatewayが有名です。
で、そのGoogleAuthSubをRails(というかRuby)で実現するためのgoogle_auth_sub.rbというのを作ってみました。
手元では動いていますが、いろいろ自信がないところがあるので、動いたとか動かなかったとか、このコードはまずいだろ、とか、いろいろフィードバックいただけると嬉しいです。
使い方
GoogleAuthSubについての情報は、この辺にまとまっています。- AuthSub Authentication for Web Applications - Account Authentication API - Google Code(公式ドキュメント)
- API Developer's Guide: The Protocol - Google Calendar APIs and Tools - Google Code(GoogleカレンダーのAPIの説明の一部)
- AuthSub & ruby on rails - Google Accounts API | Google グループ(Google グループでの議論。railsで使うのにどうやったらいいか)
- Google Calendar APIの基礎 : Under Construction, Baby(日本語で説明してくれている人のページ)
流れを見るのであれば最後のサイトを見るのがわかりやすいと思います。
ここでは、google_auth_sub.rbを使うことを前提に説明してみます。
まず、Googleの認証ページ用のURLを生成して、ユーザーさんに踏んでいただきます。
google_auth_sub.rbでは、こんな感じで生成します。
@uri = GoogleAuthSub.getURLForAuthSubRequest(最初の引数はscope、Googleのどのサービスにアクセスしたいかを示すURLです。
"https://www.google.com/calendar/feeds/",
"http://www.example.com/responce"
)
二つ目は、next、ユーザーさんがGoogleのサイトでアクセスを許可した後戻ってくるページのURLです。三番目以下を使いたい人は、Googleのドキュメント見てください:-)
ともあれ、こうやって生成したURLをWEBサイトに表示して、ユーザーさんに踏んでいただきます。
そうするとユーザーさんはGoogleの認証ページに飛んで、
というようなメッセージで、アプリケーションがGoogleカレンダーにアクセスすることへの確認を求められます。
ユーザーさんが「アクセスを許可」を押すと、ユーザーさんは先ほどnextで指定したURLに帰ってきます。この際、URIにToken=XXXX という形でトークンがついてきます。
これは一時トークンといって、一回しか使えませんが、Googleにお願いすると、当分使えるセッショントークンと交換してもらうことができます。
single_use_token = params[:token]
gas = GoogleAuthSub.new()
token = gas.getSessionToken(single_use_token)
セッショントークンがとれたら、あとはGoogleの各種APIにアクセスすることで、情報を取り出すことができます。このさい、セッショントークンをヘッダに入れてアクセスしないといけませんが
gas = GoogleAuthSub.new()とすることで、その辺をラップして情報をとってくることができるようになります。
res = gas.googleHttpGet('https://www.google.com/calendar/feeds/default/private/full',token)
とってきた情報はGCal namespace element referenceというフォーマットで格納されていますが、ここから先はSubAuthじゃなくて各アプリケーションの処理なので、google_auth_sub.rbが面倒見るのはここまでです。
そのほか
GoogleAuthSub.new()の最初の引数で、CA 証明書ファイルのパスを指定します。nilで省略した場合には、HTTPSのサーバ証明書をチェックしなくなるので、セキュリティ上まずいような気がしますが、ライブラリとしては一応動作します。CA 証明書ファイルを渡すと、VERIFY_PEERモードで証明書を検証します。でも、これで安全なのかどうなのかあまり分かっていないので、このあたりつっこみいただけるととても嬉しいです。
GoogleAuthSub.new()の二番目の引数にloggerを渡すと、Googleとのやりとりを見ることができます。
getAuthSubTokenInfo()をつかうと、トークンが有効かどうかを確認することができます。セッショントークンであっても、ユーザーが有効性を取り消すことができるので、時々チェックした方がいいのかも。
ちなみに一時トークンをこの関数に渡して有効性をチェックすることもできますが、チェックしたことでトークンを使い終わってしまうので、あんまり役にも立ちません:-)