マッシュアップ・アプリ「アルバイト占い」のソース

Sun×RECRUIT Mash up Award
http://www.recruit.jp/mashup2006/
アルバイト占い
http://fortune.giovedi.net/

マッシュアップコンテストに応募しちゃった「アルバイト占い」のソースについて、一通りのリファクタリングが完了したので、えいやと公開してみる(ただし占いロジック部分だけは内緒(笑))。


リファクタリングといっても大げさなものではなく、見通しをよくして、共通部分をまとめ、占いロジックをカプセル化しただけ。
さすがにスケルトン的に使えるようなレベルではないが、お手軽に REST 系 API を使ってマッシュアップして遊ぼうという人には参考にしてもらえるかも?


見ればわかると思うが、「アルバイト占い」は ruby CGI で組まれている。
ruby CGI を採用したのは、個人的に一番開発速度が速いからだが(どうせ何百人何千人もアクセスするわけじゃないし)、たったこれだけソースで まがりなりにもマッシュアップアプリケーションしているのだから、開発が早いのも納得だろう(公開した範囲で一番コーディング量が多いのは、おまけの労働時間計算部……)。


ここをこうすればもっとうまく書けるとか、こうしないといけないなどの突っ込み大歓迎。

fortune.rb

#!/usr/bin/ruby -Kw

# アルバイト占い (c)v914AKSTN
# mashup-ed with Amazon Web Services API and FromA API

require "cgiutil.rb"
require "restutil.rb"
require "fortunelogic.rb"

# データディレクトリ(インクルードファイル、テンプレート、占いデータ&ログ)
DATADIR = (ENV['RUBYLIB'] || ".") + "/"

# 定数
AWS_KEY = "Amazon アクセスキー"
FROMA_KEY = "フロムエー アクセスキー"
AWS_RES = "ItemIds,ItemAttributes,EditorialReview,Reviews,SalesRank,Offers"

# CGI 初期化
cgi = CGI.new
print "Content-Type: text/html\n\n"

# 占いロジック
fortune = Fortune.new(DATADIR)

# 入力パラメータチェック
asin = cgi["asin"]
if asin !~ /^[0-9A-Z]{10}$/
	puts eval(readTemplate(DATADIR + "param-error.html"))
	exit 1
end

# Amazon 接続&レスポンス解析
path = "/onca/xml?Service=AWSECommerceService&AWSAccessKeyId=#{AWS_KEY}&Operation=ItemLookup&ItemId=#{asin}&ResponseGroup=#{AWS_RES}&Version=2006-06-07"
doc = getResponseAsDom('webservices.amazon.co.jp', path)
product = doc.elements['ItemLookupResponse/Items/Item'] if doc

if !product
	puts eval(readTemplate(DATADIR + "amazon-error.html"))
	exit 1
end
price = fortune.parseAmazon(product)

# 検索条件構成&フロムエー接続
froma_path = "/s/r/jobSearch.jsp?api_key=#{FROMA_KEY}"
fortune.getCondition.each do |key, value|
	froma_path += "&#{key}=#{value}"
end
doc = getResponseAsDom('xml.froma.com', froma_path)

# 占いの結果として、<Offer> エレメントを得る
result = fortune.divine(doc)
if !result
	puts eval(readTemplate(DATADIR + "param-error.html"))
	exit 1
end

# 労働時間計算
paytext = result.elements['PayText'].text	# 給料
worktime = nil
if paytext =~ /時給.*?([\d0123456789]{3,})円/
	pay = to_i_ex($1)
	fortune.logger("時給: #{pay}")
	if price>0 && pay>0
		day, hour, min, sec = fractionalDate(1.0 * price / pay / 24.0, 24)
		worktime = "#{hour}時間 #{min}分 #{sec}秒"
		worktime = "#{day}日 #{worktime}(寝ないで)" if day > 0
	end
elsif paytext =~ /月給(\d+)円/
	pay = $1.to_i
	fortune.logger("月給: #{pay}")
	if price>0 && pay>0
		day, hour, min, sec = fractionalDate(21.5 * price / pay, 8)
		worktime = "#{day}日 #{hour}時間 #{min}分 #{sec}秒"
	end
end
fortune.logger("#{paytext} : #{worktime}")

# 占い結果出力
puts eval(readTemplate(DATADIR + "view.html"))

fortunelogic.rb

#!/usr/bin/ruby -Kw

# アルバイト占い (c)v914AKSTN
# 占いロジック(abstract。でも一応動くはず。本物はもっとごちゃごちゃしてます)

require 'logger'

class Fortune

	def initialize(datadir)
		@log = Logger.new(datadir + 'fortune.log')
	end

	# Amazon 検索結果から、商品の価格を取得して返す
	def parseAmazon(product)
		_price = product.elements['Offers/Offer/OfferListing/Price/Amount']
		if _price && _price.text then _price.text.to_i else 0 end
	end

	# フロムエー API 検索条件を HashMap で返す
	def getCondition
		{'ksjcd'=>'04', 'shrt_indx_cd'=>'1003'}		# 短期・1週間
	end

	# フロムエー API 検索結果から、占いの結果として <Offer> エレメントを1つ返す
	def divine(doc)
		doc.elements.to_a('OfferList/Offer')[0]
	end

	def logger(st)
		@log.debug(st)
	end
end

restutil.rb

#!/usr/bin/ruby -Kw

# REST型APIへのアクセス

require 'net/http'
require 'rexml/document'
Net::HTTP.version_1_2

# 指定URLにアクセスし、レスポンスを DOM にパースして返す
def getResponseAsDom(host, path)
	doc = nil
	Net::HTTP.start(host) do |http|
		response = http.get(path)
		doc = REXML::Document.new(response.body)
	end
	doc
end

# 指定URLのマップにアクセスし、レスポンスを DOM にパースし、マップに格納して返す。
# RSS取得用。
def getResponseListAsDom(host, pathlist)
	list = Hash.new
	pathlist.each do |key, path|
		list[key] = getResponseAsDom(host, path)
		sleep 1
	end
	list
end

cgiutil.rb

#!/usr/bin/ruby -Kw
require 'cgi'

# html無害化
def h(st)
	CGI::escapeHTML(st)
end

# テンプレートファイル読み込み
# html 内に埋め込み値を #{} で記述、eval するだけの超簡易テンプレート……
def readTemplate(filename)
	"<<HTMLTEMPLATE\n" + File.read(filename) + "\nHTMLTEMPLATE"
end

# 全角半角混じりの数値文字列を整数に変換する
# tr() が2バイト文字にも対応していれば良かったのだが。
def to_i_ex(num)
	num.gsub('0','0').gsub('1','1').gsub('2','2').gsub('3','3').gsub('4','4').gsub('5','5').gsub('6','6').gsub('7','7').gsub('8','8').gsub('9','9').to_i
end

# 日時を表す実数値(1.0=1日)を日・時・分・秒に分解する
def fractionalDate(_date, hours_of_day)
	day = _date.floor
	_tmp = (_date - day) * hours_of_day
	hour = _tmp.floor
	_tmp = (_tmp - hour) * 60
	min = _tmp.floor
	sec = ((_tmp - min)*60).floor

	[day,hour,min,sec]
end