URI::Parser

投稿者 akira 2008-07-29 15:00:00 GMT

URIモジュールをいじくって、RFC的にはダメなんだけどちょっとだけ大目に見てよ、というようなことが出来るようにしてみた。というのは、その種のリクエストを何度か受け取ったことがあるため。リクエストというよりも、バグ報告として受け取ることが多かったと思うけど。

>> p = URI::Parser.new(:ESCAPED=>"(?:%[a-fA-F0-9]{2}|%u[a-fA-F0-9]{4})")
=> #<URI::Parser:0xb7872640>
>> u = p.parse("http://foo.bar.baz/%uABCD")
=> #<URI::HTTP:0xb78cf4f8 URL:http://foo.bar.baz/%uABCD>
>> URI.parse(u.to_s)
URI::InvalidURIError: bad URI(is not URI?): http://foo.bar.baz/%uABCD
	from /.../lib/uri/common.rb:126:in `split'
	from /.../lib/uri/common.rb:144:in `parse'
	from /.../lib/uri/common.rb:592:in `parse'
	from (irb):3
	from /usr/bin/irb1.9:12:in `<main>'
>> s = "http://foo.bar.baz/ABCD"
=> "http://foo.bar.baz/ABCD"
>> u1 = p.parse(s)
=> #<URI::HTTP:0xb78c3220 URL:http://foo.bar.baz/ABCD>
>> u2 = URI.parse(s)
=> #<URI::HTTP:0xb78b6d54 URL:http://foo.bar.baz/ABCD>
>> u1 == u2
=> true
>> u1.eql?(u2)
=> false

あとでruby-devに投げてみよう。

本の発売日をGoogleカレンダーで見られるようにする

投稿者 akira 2008-03-15 15:00:00 GMT

以前、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

投稿者 akira 2008-02-13 15:00:00 GMT

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スクリプトを使った複数サービスの運用あたりまで考えられていること(ちょっとしたスクリプトを提供してくれるだけではあるのだけど)、なんてあたりも重要なポイントなのではないかと思う。

ruby-termios 0.9.5

投稿者 akira 2007-05-30 15:00:00 GMT

田中哲さんに送っていただいたパッチを取り込んでバージョン0.9.5としたものをリリースした。

Ximapdのコードを書いた

投稿者 akira 2007-03-21 15:00:00 GMT

久しぶりにXimapdのコードを書いた。IMAPのタグなし応答がそれなりに送られるような雰囲気のものにしてみたつもり。といいつつテストが追いついていないというのがダメなんだけど。

せっかくの前田さんのコードに対し、どうも私が場当り的に手を入れてしまっているのでここらで少しコードを見直さないとまずそうな気も。一通り終わったらバックエンドをいじるか作るかしないとなー。

amazon.co.jpから注目アイテムの発売日を取り出す

投稿者 akira 2006-08-14 15:00:00 GMT

Ruby/AmazoniCalendarライブラリを使って、キーワード検索の結果から発売スケジュール表を作ってみた。

#!/usr/bin/ruby -Ku
basedir = File.dirname($0)
$LOAD_PATH.unshift File.join(basedir, 'icalendar-0.97/lib')
require 'date'
require 'amazon/search'
require 'icalendar'
require 'time'
 
outfile = '*** filename ***'
devid = '*** devid ***'
keywords = [
  '*** keyword ***',
  '*** keyword ***',
  '*** keyword ***',
]
max_retry = 5
 
products = {}
req = Amazon::Search::Request.new(devid, nil, 'jp')
req.cache = Amazon::Search::Cache.new(File.join(basedir, 'cache'))
retry_count = 0
keywords.each do |keyword|
  (1..99).each do |i|
    begin
      req.keyword_search(keyword, 'books',
                         Amazon::Search::LIGHT, i).products.each do |product|
        h = product.to_h
        products[h['asin']] = h
      end
      retry_count = 0
      sleep 60
    rescue Amazon::Search::Request::SearchError
      break # done
    rescue
      $stderr.puts "#{Time.now.iso8601}: #{$!} (#{keyword})"
      retry_count += 1
      exit(1) if retry_count > max_retry
      sleep 120*retry_count
      retry
    end
  end
end
 
ical = Icalendar::Calendar.new
 
ical.custom_property('X-WR-CALNAME', '注目アイテム')
ical.custom_property('X-WR-CALDESC', '注目アイテム')
ical.custom_property('X-WR-TIMEZONE', 'Asia/Tokyo')
 
ical.timezone do
  tzid 'Asia/Tokyo'
  standard do
    tzoffsetfrom '+0900'
    tzoffsetto   '+0900'
    dtstart      '19700101T000000'
  end
end
 
today = Date.today
products.each do |asin, product|
  begin
    date = Date.parse(product['release_date'])
    next if date - today < -45
    ical.event do
      uid     "X-ASIN-#{product['asin']}"
      dtstart date
      dtend   date.next
      summary product['product_name']
      description "#{product['product_name']} (#{product['manufacturer']})
\n価格: #{product['our_price']} (定価: #{product['list_price']})
\n著者: #{product['authors'].join(', ')}
\n" klass 'PUBLIC' end rescue ArgumentError # 日付が年月日そろっていないので無視 end end File.open(outfile + '.n', 'w') do |o| o.print ical.to_ical end begin File.rename(outfile, outfile + '.o') rescue Errno::ENOENT end File.rename(outfile + '.n', outfile)

もっとましなやり方がありそうな気もする。なお、Ruby/Amazonはdebパッケージでいうところの0.9.0-2でないとリダイレクトが扱えないのでうまくなくて、かつ、sargeのruby1.8だとamazon/search.rbの945行目にある「redirects += 1」をかっこでくくっておかないとならない。というか、Ruby/Amazonの最新は0.9.2なんだな。debは古いままらしい。

あとiCalendar-0.97にはもしかするとバグがあるかも。上のスクリプトを動かすためには以下の変更が必要だった。タイムゾーンのところはコードで使わなければ必要ないがto_icalのほうは必要っぽい。ちなみにタイムゾーンのところはgoogle calendarに読み込んでもらうために試行錯誤した中で付いたもので、実は指定する必要はないのかもしれない(調べていない)。

diff -ruN icalendar-0.97.orig/lib/icalendar/component/timezone.rb icalendar-0.97/lib/icalendar/component/timezone.rb
--- icalendar-0.97.orig/lib/icalendar/component/timezone.rb	2006-04-26 17:57:42.000000000 +0900
+++ icalendar-0.97/lib/icalendar/component/timezone.rb	2006-08-15 09:47:56.000000000 +0900
@@ -20,7 +20,17 @@
   # New York City starting from 1967. Each line represents a description
   # or rule for a particular observance.
   class Timezone < Component
-    ical_component :standard, :daylight
+    def standard(&block)
+      e = Standard.new
+      self.add_component e
+      e.instance_eval &block if block
+      e
+    end
+    def daylight(&block)
+      e = Daylight.new
+      self.add_component e
+      e.instance_eval &block if block
+    end
  
     # Single properties
     ical_property :dtstart, :start
@@ -49,10 +59,12 @@
     # Also need a custom to_ical because typically it iterates over an array
     # of components.
     def to_ical
-      print_component do |s|
+      print_component do
+        s = ''
         @components.each_value do |comp|
           s << comp.to_ical
         end
+        s
       end
     end

ximapdのHyper Estraier対応について

投稿者 akira 2005-08-25 15:00:00 GMT

Journal InTimeから開発メモの記事:

Rastといえば、ximapdをHyper Estraier対応にする実験もやられているみたいだな。実用段階までいくかどうかはわからないけど、コスモポリタニズムというか、ルネサンス的というか、そういうノリは大好きだ。

でも所詮は「比較対象」として、つまり引きたて役の扱いなんだよなぁ。ぶっちゃけ何がいけないんだろう。Hyper Estraierのコンセプトが適合していないのか、設計や実装に気に入らない点があるのか、どこの馬の骨ともわからぬ奴が作っているから信用ならないのか、その全部なのか。ダメ出ししてくれる人ってなかなかいないからわからないんだよなぁ。

[開発メモより引用]

(コメントを入れようと思ったら対応する記事がないというエラーになってしまったので、ここで。)

ximapdのMLで「比較対象くらいには」と書い(てしまっ)たのはHyper Estraierに何か問題があるとかそういうことではなくて、私自身の都合(能力、時間、その他)によってどこまで実装を進められるか分からないのでというような意味合いでした。私自身は、うまく実装が進められたらちゃんと使ってみようと思いつつコードを書いています。もしも気を悪くされたようならごめんなさい。

コメントspamフィルタ更新

投稿者 akira 2005-08-06 15:00:00 GMT

AD-HOCKERY DIARYのパッチをいただきました。URLの最大値を0に設定できるようになる。

あと、最近よく見る変なURLだけのコメントspam。なにしたいんだろなあと思ってたのだけど、どうもブラウザによる補完をあてにしてるっぽい気がしてきた。なのでそのあたりについて少しケアしてみる気分に。パターンが間違っているかもしれないので注意。

コードは前と同じところ

非表示にするのではなくて単に捨てるようにも設定できるようにしたほうが良いかしら。

コメントspamフィルタ更新

投稿者 akira 2005-08-06 15:01:00 GMT

やはり、というかなんというか、パターンに問題があったので修正。

ximapd + Hyper Estraier

投稿者 akira 2005-08-03 15:00:00 GMT

Hyper EstraierのRubyバインディング(SWIGのほう)を使って(少し手を加えて)、ximapdのバックエンドとしてHyper Estraierを使うというのをやってみた。

tests/runner.rbの実行時間がけっこう違うのだけど、こんなものだろうか。何かすっとばしていないかドキドキする。ただ、まあ、精度に関するデフォルトの設定が違っていそうな感じなので、そのあたりが効いてきているのかもなとも思う。パラメータをいじってみたりしたほうが良さそうかな。