本の発売日をGoogleカレンダーで見られるようにする
以前、Ruby/Amazonを使って書いたスクリプトをamazon-ecs 0.5.1で書き換えた。ついでにiCalendarを1.0.2に更新した。
amazon-ecsは、Ruby/Amazonのような抽象化はしてくれない。パラメータからREST的URLを構成するのと、レスポンスからHprictoのDocオブジェクトを生成するのを助けてくれる程度でしかない。いくつかのメソッドを追加提供してくれてもいるのだが、網羅的ではないので使い分けに困るようなところもある。よって、スクリプトとしてはHpricotによるXMLドキュメントの操作を繰り返すような内容になりがちである。
require 'amazon/ecs'
access_key_id = 'アクセスキーID'
search_keys = YAML.load_file('words.yml') # {type => [検索語, ...], ...}というハッシュ
date_limit = Date.today - 7
Amazon::Ecs.options = { # グローバルなオプション設定
:AWS_access_key_id => access_key_id,
:country => :jp,
:response_group => ['ItemAttributes', 'Images'],
}
# イメージ情報を取り出す
def get_image_info(doc, tag)
tmp = doc/tag
return nil unless tmp
url = tmp/'url'
height = tmp/'height'
width = tmp/'width'
return nil unless url && height && width
{:url =>url.first.inner_text,
:height => height.first.inner_text.to_i,
:width => width.first.inner_text.to_i}
end
# 検索実行
def ecs_search(word, config, date_limit)
products = []
opts = config[:options].dup
page = 1
loop do
opts[:item_page] = page # ページめくりのため
res = Amazon::Ecs.item_search(word, opts)
if res.has_error?
(res.doc/'errors/error').each do |e|
code = (e/'code').inner_text
if code == 'AWS.ECommerceService.NoExactMatches'
# not found
else
raise "#{code}: #{(e/'message').inner_text}"
end
end
end
break if res.total_pages == 0
res.items.each do |item|
prod = {}
attrs = item.search_and_convert('itemattributes')
prod[:url] = item.get('detailpageurl')
prod[:asin] = item.get('asin')
prod[:title] = attrs.get('title')
prod[:small_image] = get_image_info(item, 'smallimage')
prod[:medium_image] = get_image_info(item, 'mediumimage')
prod[:large_image] = get_image_info(item, 'largeimage')
rel_date = attrs.get(config[:date_field])
next if rel_date.nil?
case rel_date
when /^\d{4}$/
if rel_date.to_i < date_limit.year
# date_limitより古くなったら中断
page = res.total_pages
break
end
next
when /^\d{4}-\d{2}$/
rel_date << '-28' if /^\d{4}-\d{2}$/ =~ rel_date
when /^\d{4}-\d{2}-\d{2}$/
# noop
else
next
end
prod[:release] = Date.parse(rel_date)
if prod[:release] < date_limit
# date_limitより古くなったら中断
page = res.total_pages
break
end
products << prod
end
break if products.empty?
break if res.total_pages == page
page += 1
end
products
end
products = {}
{ # 検索対象とその条件
'author' => {
:options => {
:search_index => 'Books',
:type => :author,
:sort => 'daterank', # 日付順ソートを指定する
},
:date_field => 'publicationdate',
},
# (略)
'actor' => {
:options => {
:search_index => 'DVD',
:type => :actor,
:sort => '-releasedate',
},
:date_field => 'releasedate',
},
}.each_pair do |type, config|
(search_keys[type] || []).each do |word|
result = ecs_search(word, config, date_limit)
next if result.empty?
result.each do |prod|
products[prod[:asin]] = prod
end
end
end
検索を実行する部分については、もう少しマシなやり方がありそうに思う。たとえば対象が書籍についてはpower検索を使ったほうが効率が良くなるように思う。
iCalender 1.0.2についてだが、これは0.98のころとあまり状況が変わっていない。図書館で借りた本の返却日をGoogle Calendarに反映する その3で解説されているようなやり方で、VTIMEZONEを追加する必要がある。
ical = Icalendar::Calendar.new
ical.custom_property('X-WR-CALNAME', 'タイトル')
ical.custom_property('X-WR-CALDESC', 'タイトル')
ical.custom_property('X-WR-TIMEZONE', 'Asia/Tokyo')
std_cmp = Icalendar::Component.new('STANDARD')
std_cmp.custom_property('DTSTART', '19700101T000000')
std_cmp.custom_property('TZOFFSETFROM', '+0900')
std_cmp.custom_property('TZOFFSETTO', '+0900')
tvz_cmp = Icalendar::Component.new('VTIMEZONE')
tvz_cmp.custom_property('TZID', 'Asia/Tokyo')
tvz_cmp.add(std_cmp)
ical.add(tvz_cmp)
また、新たな問題もある。1.0.2ではiCalenderテキストの折り返しをきちんと(?)行うようになっているようで、マルチバイト文字はシーケンスの途中でも切り分けられてしまう。Googleカレンダーは、英数字ならばうまく連結してくれるようなのだが、マルチバイト文字は化けたままになるようだ。さらにこの折り返し処理は上書き可能な書き方になっていないため、最終的な出力を行った後で折り返し処理をやり直す形くらいでしか手を入れられない。
ical_text = ''
# 折り返された行を再連結し、折り返えし処理をやり直す
# (テキスト中の改行文字「"\n"」はiCalendarテキスト上では
# 「'\n'」に変換されているためこの処理の影響を受けない)
ical.to_ical.gsub(/\r\n /, '').strip.each_line do |l|
ical_text << utf8_fold(l, 70).join("\r\n ")
end
File.open(output_file + '.n', 'w') do |o|
o.print ical_text
end
新たな試みとしてPublish web content events in iCalに従って、商品イメージを表示できるようにしてみることにした。これは最初、VEVENTにイメージを添付するようなものかと思っていたのだが、実際には天気予報を表示する仕組みのことで、X-GOOGLE-CALENDAR-CONTENT-*が付いたVEVENTについては、他のカレンダーにコピーするなどの操作が(直接的には)出来なくなってしまうようだ。これはうれしくないので、イメージ表示用のエントリを別に作るという形で様子を見ている。
products.each do |asin, product|
date = product[:release]
desc = "...."
ical.event do
uid "X-ASIN-#{product[:asin]}"
dtstart date
dtend date.next
summary product[:short_title]
description desc
klass 'PUBLIC'
end
if image = product[:medium_image]
ical.event do
uid "X-ASIN-#{product[:asin]}-EVENT"
dtstart date
dtend date.next
summary product[:short_title]
custom_property 'X-GOOGLE-CALENDAR-CONTENT-TITLE', product[:title]
custom_property 'X-GOOGLE-CALENDAR-CONTENT-ICON', 'カレンダー上で表示する16x16のイメージのURL'
custom_property 'X-GOOGLE-CALENDAR-CONTENT-URL', image[:url]
custom_property 'X-GOOGLE-CALENDAR-CONTENT-TYPE', 'image/jpeg'
custom_property 'X-GOOGLE-CALENDAR-CONTENT-WIDTH', image[:width].to_s
custom_property 'X-GOOGLE-CALENDAR-CONTENT-HEIGHT', image[:height].to_s
klass 'PUBLIC'
end
end
end
便利かどうかはまだなんとも言えない(あまり便利ではないかも)。イメージを直接埋め込むのではなく、適切なリンクを仕込んだHTMLを生成して、それを埋め込むようにするとよいのかもしれないと思ったが、そこまではやってみていない(すでにありそう……)。
CGI環境、Webアプリケーション環境としてのRack
Thin(0.6.3)のことを調べていたらRack(0.2.0)が出てきて、Thin自体よりもむしろRackに興味が移っていった。Ruby 1.9系ではCGI環境の刷新が議論されている(はずだと思う)が、このRackを検討するのもおもしろいのではないだろうか。
Rackがどういうものかを考える前に、Mongrel-Railsの構成を少しだけかみくだいて見てみると[ブラウザ]-(HTTP)-[Mongrel]=[Rails]-(Rails規約)-[アプリケーション]という接続になっているといえる。これがThis-Rack-Rails構成でどうなるかというとブラウザ-(HTTP)-[Thin HTTPサーバ(Thinハンドラ]=[Rack]-(Rack仕様)-[Railsアダプタ]=[Rails]-(Rails規約)-[アプリケーション]のようになる。ここでRackの前後について、HTTP寄りについてはハンドラという繋ぎ役を作ることで拡張でき、アプリケーション寄りについてはアダプタという繋ぎ役を作ることで拡張できる。あるいは、Rack仕様はとてもシンプルなのでアプリケーションが直接Rackに接続することも考えられる。小さなアプリケーションならそれでも十分だろうと思う。
そのようなわけで、ThinとRackの組み合わせによれば、たとえばMongrel-Rack-Railsというパスを作れるはず。ちょっと作ってみるかという気分になったりしたのだが、あまり変なものは出てこなさそうな気配がしたので止めた。なお、Rack自体の使い方についてはCGI から Mongrel まで、Rack で Web アプリを Web サーバから抽象化するという記事で詳しく説明されている。
Thin自体はMongrel以降のRails向けHTTPサーバ実装として、とくにそのスピードを中心に取り上げられることが多いのかなという印象なのだけど、Rackを採用したこと、Rack仕様に従ったRails adapterを提供しているということ、initスクリプトを使った複数サービスの運用あたりまで考えられていること(ちょっとしたスクリプトを提供してくれるだけではあるのだけど)、なんてあたりも重要なポイントなのではないかと思う。
ruby1.9パッケージ準備中
Ruby 1.9.0のリリースにそなえてruby1.9パッケージを作ってみている。今ところの予定ではrakeもrubygemsもruby1.9.debとlibruby1.9.debに含めるつもりでいる。何か要望が出てきたらパッケージを分けるなどするかもしれない。
残る課題はパッケージバージョンの決定。1.9.1がリリースされるのを前提にしていたために、すでに1.9.0+YYYYMMDD-Xというバージョンを付けてしまっている。まさか1.9.0がリリースされるとは、というわけなのだが、kmutoさん案の1.9.0+rel-Xにしようかと考えているところ。
パッケージを作っていて気付いた10月頃からの変化はというと:
- rakeとrubygemsが加わった
- soapがなくなった
- net/ftptls.r、とnet/telnets.rb、その他いくつかのライブラリもなくなった
$LOAD_PATHに埋め込まれているバージョンが1.9から1.9.0になった$LOAD_PATHにvendor_rubyというパスが加わった- make golfが加わった
言語的な変化ももう一度チェックしなおさないとな。
追記(2007-12-26-1): 1.9.0がリリースされたみたい。Ruby 1.9.x以降のRubyについてはバージョンの三桁目(teeny)が0のときは開発版、1以上になったら安定版というルールになった(はず)なので、今回のはまだ開発版ということになるのだと思う。このため、少なくとも1.9.1がリリースされるまでは、これまで通り、1.8.xがDebainのデフォルトのRubyであり続ける。実際には1.9.1よりもリリースが進んで、かつ、何らかの(たとえばメンテナ間での)意見を聞いてからということになると思う。なお、DebianのデフォルトのRubyはruby-defaultsパッケージが提供するrubyパッケージで決められている(主メンテナはukaiさん)。
追記(2007-12-26-2): Vine Linuxのrubyパッケージでも同様にRuby 1.9.1がリリースされた後、適当なタイミングで移行(するかどうかなど)を検討することになるのだと思う。
追記(2007-12-26-3): tarballはruby-1.9.0-0なのね。-1とか出るんだろうか。
Capistrano 2.1とRuby 1.8.6
Capistranoをいじってみようとしたところ、以下のようになってしまった。
$ bin/cap You are running Ruby 1.8.6, which has a bug in its threading implementation. You are liable to encounter deadlocks running Capistrano, unless you install the fastthread library, which is available as a gem: gem install fastthread Please specify at least one action to execute. Usage: cap [options] action ...
CapistranoのMLやレポジトリのログにあたっても具体的な影響についての記述はないのだが、少なくともCapistrano自身のテストでデッドロックが発生するということらしい。というわけで、実際どうなのよと試してみたところ、1.8.6-2では現象が起きることが確認できた。
$ rake [...] Loaded suite /usr/lib/ruby/1.8/rake/rake_test_loader Started ... [...] ...deadlock 0xb76afb14: sleep:- - ./test/../lib/capistrano/gateway.rb:57 deadlock 0xb7c3a700: sleep:- (main) - ./test/../lib/capistrano/gateway.rb:64 ./test/../lib/capistrano/gateway.rb:64:in `wait': Thread(0xb7c3a700): deadlock (fatal)
また1.8.6.36-1や1.8.6.111-2では現象が起きないことも確認できた。というわけで、1.8.6.36以降ならfastthreadがなくても問題なさそうに思える。毎回メッセージが出てくるのはわずらわしいが、どうしたものだろうな。
ruby1.8_1.8.6.36-1
1.8.6-p36にバグ修正(C++な拡張ライブラリでコンパイルエラーになるというきびしめのやつ)のパッチを加えてパッケージングしたものをuploadした。パッチ具合いは大丈夫かな。
ruby-prof 0.5系のオブジェクトアロケーションプロファイルに対応させるためのパッチ(ruby-Bugs-11497)を入れちゃおうかどうしようか迷ったんだけど、今回は入れないままにした。まあ、それ以前にruby_1_8にしようかと、けっこう悩んだりもした。手がまわらなくなりそうだから止めたけど。
rpmはdebでの様子を見てからにしようかと。
Ruby 1.9の非互換
Ruby 1.9での新機能や変更はChanges in Ruby 1.9に詳しくまとめられているのだが、最近の更新がないようで少し古くなってきているみたい。ChangeLogを追っかけて続きを作…… るというのは難しいので、テスト内容の違い、RDocエントリの違い、おぼろげな記憶などをたりよりに少しだけ探ってみた。あー、非互換のところだけ。
- 文法上ダメになったこと
if、elsif、unless、whenの条件に続けて:を書き、それ続けて実行文を書いてしまう- ブロックパラメータにインスタンス変数を指定する
- ブロックパラメータに同じ変数を何度も指定する
- 挙動が変わったこと
Object#{id,type}なんかがなくなったKernel#{chomp,chop,sub,gsub}{,!}やKernel#{scan,split}などがなくなったException#to_strがなくなったThread.{critical,critical=}がサポート外になり、Thread.exclusiveがなくなった(YARVの制限?)evalによるローカル変数の定義ができない(YARVの制限?)eval("foo = 1"); eval("foo") #=> NameError- 多重代入が整理された結果、一部の挙動が変わった
x = 1; a = *x; a #=> 1.8では1、1.9では[1] x = []; a = *x; a #=> 1.8ではnil、1.9では[] x = [[]]; a = *x; a #=> 1.8では[]、1.9では[[]] x = []; *a = x; a #=> 1.8では[[]]、1.9では[]
- ブロックパラメータでも似たような状況がある(もう少し複雑な条件?)
- クラス変数への値の設定の際、上位クラスを見なくなった(このあたり)
attrがattr_readerのaliasになる(予定? 今は第二引原がtrueかfalseの場合に特別扱いしてくれているが、そのうちダメになる?)- ブロックパラメータがブロック内でローカルになった(外側の変数と重なっても例外にはならない)
- ブロックローカル変数ができた(外側の変数と重なっても例外にはならないが、そのうち例外が上がるようになるのかな?)
foo {|a; b, c| } # bとcがそれ Array#[n,m]へのnilの代入でnilが代入されるようになった?cが文字列を返すprintやputsがnilを「nil」と出力せず空文字列を出力するようになった
とりあえずこのくらいは分かった。見落としがあるかも(ありそう)。それとも意外と少ない? (Threadまわりとか動作上の変化はないのかな?)
そうそう、Changes in Ruby 1.9にあるfoo[]{|x|}というのは一時期たしかにできてたみたいなんだけど、それはたまたまそうなっただけらしく、今(r12207あたりから)は再びできなくなっている。
ruby-termios 0.9.5
田中哲さんに送っていただいたパッチを取り込んでバージョン0.9.5としたものをリリースした。


