完全にクリーンではありませんが、解決策は単純であることがわかりました。次のように Mechanize#get() の結果をキャッシュするのは簡単なことです。
class CachingMechanize < Mechanize
def get(uri, parameters = [], referer = nil, headers = {})
WebCache.with_web_cache(uri.to_s) { super }
end
end
... with_web_cache() は YAML を使用して、super から返されたオブジェクトをシリアル化し、キャッシュします。
私の問題は、デフォルトで Mechanize#get() が何らかのラムダ オブジェクトを含む Mechanize::Page オブジェクトを返し、YAML でダンプおよびロードできないことでした。修正は、これらのラムダを削除することでした。これはかなり単純であることが判明しました。完全なコードは次のとおりです。
class CachingMechanize < Mechanize
def initialize(*args)
super
sanitize_scheme_handlers
end
def get(uri, parameters = [], referer = nil, headers = {})
WebCache.with_web_cache(uri.to_s) { super }
end
# private
def sanitize_scheme_handlers
scheme_handlers['http'] = SchemeHandler.new
scheme_handlers['https'] = scheme_handlers['http']
scheme_handlers['relative'] = scheme_handlers['http']
scheme_handlers['file'] = scheme_handlers['http']
end
class SchemeHandler
def call(link, page) ; link ; end
end
end
教訓: lambda または proc を含む YAML.dump および YAML.load オブジェクトを試みないでください
これは、この例だけではありません。次のような YAML エラーが表示された場合:
TypeError: allocator undefined for Proc
シリアル化および逆シリアル化しようとしているオブジェクトにラムダまたはプロシージャがあるかどうかを確認します。(この場合のように) ラムダをオブジェクトへのメソッド呼び出しに置き換えることができれば、問題を回避できるはずです。
これが他の誰かに役立つことを願っています。
アップデート
WebCache の定義に関する @Martin の要求に応じて、次のようになります。
# Simple model for caching pages fetched from the web. Assumes
# a schema like this:
#
# create_table "web_caches", :force => true do |t|
# t.text "key"
# t.text "value"
# t.datetime "expires_at"
# t.datetime "created_at", :null => false
# t.datetime "updated_at", :null => false
# end
# add_index "web_caches", ["key"], :name => "index_web_caches_on_key", :unique => true
#
class WebCache < ActiveRecord::Base
serialize :value
# WebCache.with_web_cache(key) {
# ...body...
# }
#
# Searches the web_caches table for an entry with a matching key. If
# found, and if the entry has not expired, the value for that entry is
# returned. If not found, or if the entry has expired, yield to the
# body and cache the yielded value before returning it.
#
# Options:
# :expires_at sets the expiration date for this entry upon creation.
# Defaults to one year from now.
# :expired_prior_to overrides the value of 'now' when checking for
# expired entries. Mostly useful for unit testing.
#
def self.with_web_cache(key, opts = {})
serialized_key = YAML.dump(key)
expires_at = opts[:expires_at] || 1.year.from_now
expired_prior_to = opts[:expired_prior_to] || Time.zone.now
if (r = self.where(:key => serialized_key).where("expires_at > ?", expired_prior_to)).exists?
# cache hit
r.first.value
else
# cache miss
yield.tap {|value| self.create!(:key => serialized_key, :value => value, :expires_at => expires_at)}
end
end
# Prune expired entries. Typically called by a cron job.
def self.delete_expired_entries(expired_prior_to = Time.zone.now)
self.where("expires_at < ?", expired_prior_to).destroy_all
end
end