#!/usr/local/bin/ruby # cache.rb # ver.0.5 2002.4.7-2004.01.07 # info from/to cache # original; amazon.rb for amazon by not # net情報をcacheで読書する為の雛型 # cachebibを通じて各書店用classに継承させて利用 require 'kconv' #require 'cookie' #require 'webagent' class Cache attr_accessor :info, :code, :id #情報(Hash) attr_reader :cache_dir, :file #cache格納場所, cache-file attr_accessor :expire, :expire_full #cacheリフレッシュ間隔 attr_accessor :hold_keys, :hold_keys_temp, :hold_keys_full #cache永続キー attr_accessor :insystemKcode, :outputKcode, :messageKcode #文字コード attr_accessor :cache_id, :id, :shop_id #ID (cacheの/商品の/店の) attr_accessor :flag_read, :flag_save, :flag_net #フラグ attr_accessor :flag_reset, :flag_overprint #フラグ attr_accessor :flag_init #フラグ attr_accessor :flag #フラグ attr_accessor :stderr #メッセージ出力IO def initialize () #外部から変更可能 @expire = 12 #detail-reload-time (hour) @expire_full = 720 #all-reload-time (hour) (ID-search->detail-parse) @cache_dir = "./" #cache-data-dir @insystemKcode = 3 #sjis #1-jis,2-euc,3-sjis @outputKcode = 2 #euc @messageKcode = 3 #sjis @flag_read = true #cache読許可 @flag_save = true #cache書許可 @flag_net = true #net-download許可 @flag_reset = true #cache消去可、expire時 @flag_init = false #cache無仮定 @flag_overprint = false #info['overprint']:cache上書 @stderr = $stderr #メッセージ出力IO #内部変数 @flag_download = false #cache書込必要確認 @flag = false #cache読込済確認 # @info = Info.new() #Structでデータ保持は無理:各店でデータ書式が異なり過ぎ @info = {} #cache情報 @hold_keys = [] #cache継続情報・自由指定分 @hold_keys_temp = ['id', 'image_local'] #expire_full時には消える分 @hold_keys_full = ['hold_keys'] #expire_full時にも残る分 end #===== ===== main ===== ===== def get_data ( code = nil, mode = nil ) #mode=nil(=code)||:id(=id) return @info if @flag #->already read cache. no-need re-get. set_cache_id(code, mode) if !cache_exist? || @flag_init #cacheが無いならネット取得へ get_info if @flag_net else #cacheが有るならそれを読込 read_cache if @flag_read preset_info #内部変数へ幾つかの情報書込 if !@flag_overprint if expire_full_over? reset_cache( @hold_keys + @hold_keys_full ) if @flag_reset elsif expire_over? reset_cache( @hold_keys + @hold_keys_temp + @hold_keys_full) if @flag_reset end end if expire_over? #リフレッシュ get_info if @flag_net unless download_success? message('WAR; cannot download info. only cache-read.') read_cache if @flag_read end end end # preset_info #内部変数へ幾つかの情報書込 if download_success? #cacheの更新 save_cache if @flag_save end return @info end #===== ===== sub parts ===== ===== def message ( str = '???: message-error.', kcode = @messageKcode ) @stderr.puts str.kconv(kcode) end def set_cache_id ( code, mode = nil ) #mode=nil||:id @cache_id = code @cache_id = "id" + code if mode == :id @file = @cache_dir + @cache_id + '.txt' @info = {} end def cache_exist? return true if File::exist?(@file) return false end def time_over? ( time ) time = time.to_i if time >= 0 #負なら永遠 if time == 0 #0なら常時 return true elsif !File::exist?(@file) return true #存在しないならtrue elsif File::mtime(@file) < Time::now - ( 3600 * time ) return true #過ぎてたらtrue end end return false end def expire_over? return time_over?(@expire) end def expire_full_over? return time_over?(@expire_full) end def reset_cache ( *hold_keys ) info = {} hold_keys.flatten.each{ |key| info[key] = @info[key] unless @info[key].nil? } @info = info @flag = false #cache読込済フラグを消す end #===== ===== get/set info ===== ===== def preset_info #===== preset local-value from @info (from cache) ===== @id = @info["id"] @flag_overprint = true if @info["overprint"] end def get_info #===== get book-info(@info) from url ===== message('CHK; access; ' + @url_site + ' -> ' + @code) #----- get detail-code ----- if !@id agent = WebAgent.new() agent.uri = url_codesearch begin agent.get() rescue message("WAR; access-timeout.") return end body = agent.body.kconv(@insystemKcode) parse_codesearch(body) end #----- get info ----- agent = WebAgent.new() agent.uri = url_info begin agent.get() rescue message("WAR; access-timeout.") return end body = agent.body.kconv(@insystemKcode) parse_info(body) set_info end def set_info #===== set book-info(@info) from local-value ===== #----- set info ----- @flag_download = true #cache書込必要宣言flag @info["id"] = @id #ID @info["code"] = @code #key番号 @info["name"] = @name #名 @info["price"] = @price #価格 @info["desc"] = @desc #内容紹介 @info["stock"] = @stock #在庫 @info["delivery"] = @delivery #配送期日 @info["image"] = @image #画像 @info["image_width"] = @image_width #画像縦 @info["image_height"] = @image_height #画像横 @info["image_local"] = @image_local #download画像 @info["reviewlist"] = @reviewlist #評価リスト @info["review_num"] = @review_num #評数 @info["reviews"] = @reviews if @flag_review #評価情報 @info["hold_keys"] = @hold_keys.join(' ') if @hold_keys.size > 0 @info["overprint"] = true if @flag_overprint #否消去key と 上書固定flag end def url_codesearch( code = @code ) #codesearch or keysearch end def url_info( id = @id ) end def parse_codesearch ( str ) #codesearch or keysearch end def parse_info ( str ) end def download_success? return @flag_download end #===== ===== read/save cache ===== ===== def read_cache ( str = nil ) # ( @file, @flag, @info, @hold_keys ) if @flag #----- already read cache ----- message("CHK; request cache-reread, no-need, cut. FILE; #{@file.to_s}") return end begin #----- \n\n-sepalate ----- texts = [] text = [] str = open(@file).read if str.nil? str.each{ |line| string = (line.chomp.chomp("\r")).kconv(@insystemKcode) if string != "" text << string else texts << text text = [] end } texts << text #----- book-info ----- texts.shift.each{ |line| record = line.split(/\t/) key = record.shift data = record.join("\t") @info[key] = data } #----- review-info ----- if texts.size > 0 reviews = [] texts.each{ |text| reviewinfo = {} text.each { |line| record = line.split(/\t/) key = record.shift data = record.join("\t") reviewinfo[key] = data } reviews << reviewinfo } @info["reviews"] = reviews end #----- hold-keys-info ----- unless @info["hold_keys"].nil? @hold_keys = @info["hold_keys"].split end @flag = true #cache読込済フラグを起す rescue message("CHK; cannot read cahce. FILE; " + @file.to_s) @flag = nil end end def save_cache # ( @file, @flag, @info, @hold_keys ) unless File::directory?(@cache_dir.to_s) message("WAR; no-cache-dir. DIR; " + @cache_dir.to_s) return end begin open(@file, 'w'){ |fp| #----- book-info ----- @info.keys.sort.each{ |key| data = @info[key] if (!data.nil?) && (key != "reviews") key = key.to_s.kconv(@outputKcode) data = data.to_s.kconv(@outputKcode) fp.puts key + "\t" + data end } #----- review-info ----- if !@info["reviews"].nil? @info["reviews"].each{ |review| fp.puts '' review.keys.sort.each{ |key| data = review[key] key = key.to_s.kconv(@outputKcode) data = data.to_s.kconv(@outputKcode) fp.puts key + "\t" + data } } end } @flag = true #cache読込済(=cache書込済)フラグを起す rescue message("WAR; cannot write cache. FILE; " + @file.to_s) end end #===== ===== parts ===== ===== # ToDo: クラス変数・クラスメソッド化・モジュール化の方がいいか? # と同時にオブジェクトメソッドでもありたいのだがその方法は? def cache_dir=( dir ) dir << '/' unless dir[-1,1] == '/' @cache_dir = dir end def merge_info ( list ) # = merge_hash info = {} list.each { |data_info| data_info.each{ |key,data| if data.to_s.size > 0 if info[key].nil? info[key] = data end end } } return info end end =begin :cache-flow-chart: ・キャッシュが有る場合 (1)情報要求 (2)キャッシュ存在確認 (3)キャッシュ読込:cachefile→@info(Hash)に格納 (4)情報提供:@info(Hash)をreturn ・キャッシュが無い場合 (1)情報要求 (2)キャッシュ存在確認 (3)ネット接続・情報解析:@local_valuesに格納 (4)情報取得:@local_values→@info(Hash)に格納 (5)キャッシュ書込:@info(Hash)→cachefileに格納 (6)情報提供:@info(Hash)をreturn ・キャッシュが有るが古い場合 (1)情報要求 (2)キャッシュ存在確認 (3)キャッシュ読込:cachefile→@info(Hash)に格納 (4)情報破棄:選択的に@info(Hash)の数項目を破棄(基本はほぼ全項目破棄) (5)ネット接続・情報解析:@local_valuesに格納 (6)情報取得:@local_values→@info(Hash)に格納 (7)キャッシュ書込:@info(Hash)→cachefileに格納 (8)情報提供:@info(Hash)をreturn ・関数の概形 def get_data set_cache_id if !cache_exist? get_info(+set_info) else read_cache preset_info reset_cache if expire_over? get_info(+set_info) if expire_over? read_cache if !download_success? end save_cache if download_success? return @info end =end =begin :cachefile-format: データ全体は、空行で【ブロック】に分けられる。 【ブロック】ら(データ全体)は [〔商品情報〕,〔評価情報1〕,〔評価情報2〕,…,〔評価情報N〕] となる。 各【ブロック】内は、改行で【ライン】に分けられる。 【ライン】は 〔タグ〕[TAB]〔情報内容〕[RET] ([TAB]はタブを、[RET]は改行を示す) となる。 〔タグ〕は、慣用的な意味で固定されているが、正式には自由である。 (拡張性の為) 〔タグ〕は、「reviews」は禁止(予約)されている。 (〔評価情報〕の為) 〔情報内容〕では、[TAB]が[RET]を意図する。 (〔情報内容〕に改行があった場合、それはタブに置換されて保存されている) 〔情報内容〕に[TAB]は保存できない。 (〔情報内容〕にタブは含める事ができない) 以下に代表的な〔タグ〕を示す id 店内コード code 書籍では基本的にISBN title 題 series シリーズ label レーベル(xx文庫等) author 著者 translator 訳者 publisher 発行元(出版社) pubcode 出版社内番号 pubdate 発行日 pubsize サイズ(A4版等) price 価格 desc 内容紹介等(index=目次を含む時も有) stock 在庫 delivery 配達時間 image 画像URL(書影等) image_width 画像横幅 image_height 画像縦幅 image_local 画像URL(ローカル) reviewlist 評価情報リスト(reviewsが無くこれだけ在る場合も有)様々情報有 review_num 評価情報数(reviewsが無くこれだけ在る場合も有) reviews 評価情報の配列が格納される(別頁の場合多) hold_keys cache更新時も消えない保持項目 overprint cache上書き(一旦消去して再読込でなく)モード指定 関数は情報をHashで返す。 info["hoge"]は〔商品情報〕のhoge〔タグ〕の内容を、 info["reviews"][0]["fuga"]は〔評価情報1〕のfuga〔タグ〕の内容を示す。 ※「info.hoge」や「info.reviews[0].fuga」等の書式による引出にしないのは、 ※・「内部で保持してるデータ」と「実際に出したいデータ」の間に差がある場合もあるだろう(確度が足りないとかの理由で) ※・「p info」等の使用で情報の一覧ができる方がいいだろう ※という理由による。 =end =begin :cachefile-reload-pattern: 〔データ〕には各種商品情報が含まれる。 〔データ〕には必ず〔コード〕(code:書籍では基本的にISBN)と〔キー〕(id:基本的に店内コード)が含まれる。 初動では、〔コード〕で検索して〔キー〕を見つけ、〔キー〕で商品情報を取得する。 以後では、〔キー〕をもとに商品情報を取得する。 cacheの読み書きについての条件は以下の通りである。 flag_read = true #読込許可 flag_save = true #書込許可 flag_net = true #ネット接続許可 flag_reset = true #expire時にcache消去する flag_overprint = true #expire時にcache消去しない(cache上書許可) flag_init = true #cache無と仮定する ※flag_overprint=trueとflag_reset=falseはほぼ等価。常時そうとcacheに記録する(overprint)か、その時だけ行うか(reset)という動作の違い。 cacheの更新については次の2つの時間設定がある。 expire = 12 #12時間毎に〔データ〕は消去、〔キー〕は再取得しない expire_full = 3 * 24 #3日毎に〔データ〕は消去、〔キー〕も再取得する この2つは0で常時取得、負値で取得しなくなる。 ※負値の時は、cacheがあるなら常にそれを利用する。無いとネットから取得を試みる。 ※ネット取得も抑制するにはflag_net=falseを利用する。 〔キー〕を手打ちで入力してる場合(〔コード〕からは検索できない場合)等がある ので、expire_fullでも〔キー〕だけは残すoption〔タグ〕を用意する。 hold_keys = ['id'] #〔キー〕(@info['id'])は消去しない =end