- 平成24年1月30日(月) 16時14分22秒
-
区分
- Rails
-
報告者:
- tahara
こんにちは tahara です。
デプロイ時の Unicorn リスタートがときどき失敗して悩んでいました。
幸い本番環境では発生せず、ステージング環境と開発環境で発生していました。
リスタートは unicorn-4.1.1/examples/init.sh の upgrade を使っています。
upgrade は sig USR2 && sleep 2 && sig 0 && oldsig QUIT とい一連の流れになっています。
調べてみると sig USR2 で新しい PID ファイルが作成されるのですが、
sig 0 の時点でまだそれができていなくて失敗していました。
本番環境はサーバの性能が高いので sleep 2 で間に合っていましたが、
ステージング環境等では間に合わなかったんですね。
そこで、次のように sig 0 が成功するまで一定期間リトライするようにしました。
upgrade)
echo -n "sig USR2"
if sig USR2
then
sleep 1
n=$UPGRADE_TIMEOUT
while ! sig 0 && test $n -ge 0
do
printf '.' && sleep 1 && n=$(( $n - 1 ))
done
echo
if test $n -lt 0 && ! sig 0
then
echo >&2 "sig SUR2 failed!"
exit 1
fi
echo -n "oldsig QUIT"
if oldsig QUIT
then
n=$TIMEOUT
while test -s $old_pid && test $n -ge 0
do
printf '.' && sleep 1 && n=$(( $n - 1 ))
done
echo
if test $n -lt 0 && test -s $old_pid
then
echo >&2 "$old_pid still exists after $TIMEOUT seconds"
exit 1
fi
echo "ok"
exit 0
fi
fi
echo
echo >&2 "Couldn't upgrade, starting '$CMD' instead"
$CMD
;;
これでうまくリスタートできるようになりました。
ついでに、何かの拍子に古い方のプロセスに QUIT を送れなかった時の対策として、
Unicorn の設定ファイルで QUIT を送るようにしました。
before_fork do |server, worker|
# the following is highly recomended for Rails + "preload_app true"
# as there's no need for the master process to hold a connection
defined?(ActiveRecord::Base) and
ActiveRecord::Base.connection.disconnect!
# oldsig QUIT
old_pid = "#{server.config[:pid]}.oldbin"
if File.exists?(old_pid) && server.pid != old_pid
begin
Process.kill("QUIT", File.read(old_pid).to_i)
rescue Errno::ENOENT, Errno::ESRCH
end
end
end
これなら init.sh の方では sig USR2 だけで、後は Unicorn に古いプロセスの QUIT を
まかせればいいかと思いましたが、
そうすると新しいプロセスのワーカが動き出すまで古いワーカが処理を行ってしまい、
DB のマイグレーションを行った時なんかは悲しいことになりそうです。
上記のように init.sh で古いプロセスを QUIT する場合は、
新しいプロセスのワーカが動き出すまでリクエストは待たされるので、
まだこちらの方がいいんじゃないかしらん、というところです。
>View Comments
このページの上へ戻る
- 平成24年1月24日(火) 23時01分53秒
-
区分
- Rails
-
報告者:
- tahara
こんにちは、tahara です。
突然ですが、弊社では現在エンジニアを募集しています。
仕事内容は情シス業務と自社サービの開発です。
開発は主に Ralis で PHP もときどきあります。
たまぁに Common Lisp もあます(増やしていきたいです)。
詳細はこちらをご覧ください。
それでは本題です。
いまさらではありますが、弊社で運営している http://iko-yo.net を Rails3 に移行しました。
今回はその移行作業について書いていきたいと思います。
rvm で ruby 1.9.3 をインストールし gemsent を作成する。
bash < <(curl -s https://raw.github.com/wayneeseguin/rvm/master/binscripts/rvm-installer)
rvm install ruby-1.9.3
rvm use ruby-1.9.3
gem install bundler
rvm gemset create iko-yo-rails3
rvm --rvmrc --create 1.9.3@iko-yo-rails3
cd ..
cd -
きれいする
rm -r vendor/rails
rm -r vendor/gems
rm -r vendor/plugins/*
gem を入れる
vi Gemfile
assets は使わない。。。
source 'http://rubygems.org'
gem 'rails', '3.1.0'
# Bundle edge Rails instead:
# gem 'rails', :git => 'git://github.com/rails/rails.git'
gem 'mysql2'
gem 'jquery-rails'
gem 'exception_notification'
gem 'geokit-rails3'
gem 'jpmobile'
gem 'nokogiri'
gem 'paperclip'
gem 'restful-authentication'
gem 'ssl_requirement'
gem 'acts_as_taggable_on_steroids'
gem 'acts_as_commentable'
gem 'will_paginate'
gem 'dynamic_form'
gem 'mecab-ruby', :require => 'MeCab'
gem 'twitter'
gem 'oauth'
gem 'garb'
gem 'gdata_19', :require => 'gdata'
gem 'holiday_jp'
gem 'dalli'
gem 'newrelic_rpm'
#;; config.assets.enabled = false
#;; # Gems used only for assets and not required
#;; # in production environments by default.
#;; group :assets do
#;; gem 'sass-rails', " ~> 3.1.0"
#;; gem 'coffee-rails', "~> 3.1.0"
#;; gem 'uglifier'
#;; end
# Use unicorn as the web server
gem 'unicorn'
# Deploy with Capistrano
gem 'capistrano'
gem 'capistrano-ext'
# To use debugger
# gem 'ruby-debug19', :require => 'ruby-debug'
group :test, :development do
# Pretty printed test output
gem 'turn', :require => false
gem 'spork'
gem 'rspec-rails', "~> 2.6"
gem 'capybara'
gem 'ZenTest'
gem 'autotest-stumpwm'
gem 'remarkable_activerecord', '>=4.0.0.alpha4'
gem 'spork'
end
gem 入れて rails3 にする。
bundle install
bundle exec rails new
ソースの編集
config/rootes.rb はがんばる。
メールまわりも完全に書きなおし。
ソースをちまちま書きかえる(以下はイメージです。実際に動作するものではありません)。
config/boot.rb に次を追加
# /home/ancient/.rvm/rubies/ruby-1.9.2-p290/lib/ruby/1.9.1/psych.rb:148:in `parse': couldn't parse YAML at line 18 column 13 (Psych::SyntaxError)
require 'yaml'
YAML::ENGINE.yamler= 'syck'
各ファイルの1行目に次を追加
# -*- coding: utf-8 -*-
helper 系メソッドに .html_safe を付加
あは以下のようなイメージでどんどん書きかえていく。
s/adapter: mysql/adapter: mysql2/ database.yml.release
s/named_scope/scope/
s/RAILS_ROOT/Rails.root/
s/returning/tap/
s/request_uri/fullpath/
s/<% form/<%= form/
s/<%= f.fields_for/<%= f.fields_for/
layout が使われなかったのは ApplicationController#initialize で super を呼んでなかったからだった。
s/(.*).merge_conditions (.*)/where(\1).where(\2)/
s/mobile_filter :hankaku => true/hankaku_filter :input => true/
s/include ActionController::UrlWriter/include Rails.application.routes.url_helpers/
s/.class_name/.name/
s/link_to_remote .*/link_to \1, :remote => true/
:complete, :before 等は js で bind('ajax:complete', ...), bind('ajax:before', ...) にする。
http://www.alfajango.com/blog/rails-3-remote-links-and-forms/
error_messages がなくなったので gem 'dynamic_form'
s/choice/sample/
s/observe_field/ふつうの手書き jQuery/
s/action mailer/スーパークラスは Jpmobile::Mailer::Base で書きなおす/
/self.include_root_in_json = false/d config/initializers/wrap_parameters.rb
s/model.save(false)/model.save(:validate => false)/
s/errors.or\((.*)\)/errors[\1]/
他にもいっぱいあったような気もしますが、だいたいこんな感じです。
次に実行環境まわり。
実行環境
Apache と Passenger だったのを nginx と unicorn にしました。
unicorn についているサンプルをもとに設定しました。
まずは ngix
nginx.conf
user deployer;
worker_processes 2;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
client_max_body_size 50m;
client_header_buffer_size 4k;
include mime.types;
default_type application/octet-stream;
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
#access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 15;
gzip on;
gzip_disable "msie6";
gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
#open_file_cache max=2000 inactive=300s;
#open_file_cache_valid 360s;
#open_file_cache_min_uses 2;
#open_file_cache_errors off;
include /var/www/outing/current/config/unicorn/production/nginx-site.conf;
}
nginx-site.conf
upstream outing {
# for UNIX domain socket setups:
#server unix:/tmp/.outing.sock fail_timeout=0;
# for TCP setups, point these to your backend servers
server 127.0.0.1:8080 fail_timeout=0;
}
server {
listen 80;
root /var/www/outing/current/public;
server_name iko-yo.net;
location / {
#auth_basic "Restricted";
#auth_basic_user_file /etc/nginx/outing-password;
if ($request_uri ~* "\.(jpg|jpeg|gif|css|png|js|ico)\?[0-9]+$") {
expires max;
access_log off;
break;
}
if (-f $request_filename) {
expires 24h;
access_log off;
break;
}
try_files $uri @app;
}
location @app {
# an HTTP header important enough to have its own Wikipedia entry:
# http://en.wikipedia.org/wiki/X-Forwarded-For
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# enable this if and only if you use HTTPS, this helps Rack
# set the proper protocol for doing redirects:
# proxy_set_header X-Forwarded-Proto https;
# pass the Host: header from the client right along so redirects
# can be set properly within the Rack application
proxy_set_header Host $http_host;
# we don't want nginx trying to do something clever with
# redirects, we set the Host: header above already.
proxy_redirect off;
# set "proxy_buffering off" *only* for Rainbows! when doing
# Comet/long-poll/streaming. It's also safe to set if you're using
# only serving fast clients with Unicorn + nginx, but not slow
# clients. You normally want nginx to buffer responses to slow
# clients, even with Rails 3.1 streaming because otherwise a slow
# client can become a bottleneck of Unicorn.
#
# The Rack application may also set "X-Accel-Buffering (yes|no)"
# in the response headers do disable/enable buffering on a
# per-response basis.
# proxy_buffering off;
proxy_pass http://outing;
}
}
# HTTPS server
server {
listen 443;
root /var/www/outing/current/public;
server_name iko-yo.net;
ssl on;
ssl_certificate /etc/ssl/iko-yo.net/iko-yo.net.crt.cer;
ssl_certificate_key /etc/ssl/iko-yo.net/iko-yo.net.key;
ssl_session_timeout 5m;
ssl_protocols SSLv3 TLSv1;
ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv3:+EXP;
ssl_prefer_server_ciphers on;
try_files $uri @app;
location @app {
# an HTTP header important enough to have its own Wikipedia entry:
# http://en.wikipedia.org/wiki/X-Forwarded-For
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# enable this if and only if you use HTTPS, this helps Rack
# set the proper protocol for doing redirects:
proxy_set_header X-Forwarded-Proto https;
# pass the Host: header from the client right along so redirects
# can be set properly within the Rack application
proxy_set_header Host $http_host;
# we don't want nginx trying to do something clever with
# redirects, we set the Host: header above already.
proxy_redirect off;
# set "proxy_buffering off" *only* for Rainbows! when doing
# Comet/long-poll/streaming. It's also safe to set if you're using
# only serving fast clients with Unicorn + nginx, but not slow
# clients. You normally want nginx to buffer responses to slow
# clients, even with Rails 3.1 streaming because otherwise a slow
# client can become a bottleneck of Unicorn.
#
# The Rack application may also set "X-Accel-Buffering (yes|no)"
# in the response headers do disable/enable buffering on a
# per-response basis.
# proxy_buffering off;
proxy_pass http://outing;
}
}
# for munin
server {
listen 127.0.0.1;
server_name localhost;
location /nginx_status {
stub_status on;
access_log off;
allow 127.0.0.1;
deny all;
}
}
# redirect sub domain
server {
listen 80;
server_name *.iko-yo.net;
rewrite ^(.*) http://iko-yo.net$1 permanent;
}
次に unicorn
次の unicorn-init.sh を /etc/init.d/outing へ ln -s します。
#!/bin/sh
### BEGIN INIT INFO
# Provides: outing
# Required-Start: $remote_fs $syslog
# Required-Stop: $remote_fs $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: Example initscript
# Description: This file should be used to construct scripts to be
# placed in /etc/init.d.
### END INIT INFO
# sudo ln -s /var/www/outing/current/config/unicorn/production/unicorn-init.sh /etc/init.d/outing
# sudo update-rc.d outing default
# sudo update-rc.d outing enable
set -e
# Example init script, this can be used with nginx, too,
# since nginx and unicorn accept the same signals
# Feel free to change any of the following variables for your app:
. "/usr/local/rvm/environments/ruby-1.9.3-p0@iko-yo-rails3"
TIMEOUT=${TIMEOUT-60}
APP_ROOT=/var/www/outing/current
PID=$APP_ROOT/tmp/pids/unicorn.pid
CMD="unicorn_rails -E production -D -c $APP_ROOT/config/unicorn/production/unicorn.conf.rb"
INIT_CONF=$APP_ROOT/config/init.conf
action="$1"
set -u
test -f "$INIT_CONF" && . $INIT_CONF
old_pid="$PID.oldbin"
cd $APP_ROOT || exit 1
sig () {
test -s "$PID" && kill -$1 `cat $PID`
}
oldsig () {
test -s $old_pid && kill -$1 `cat $old_pid`
}
case $action in
start)
sig 0 && echo >&2 "Already running" && exit 0
$CMD
;;
stop)
sig QUIT && exit 0
echo >&2 "Not running"
;;
force-stop)
sig TERM && exit 0
echo >&2 "Not running"
;;
restart|reload)
sig HUP && echo reloaded OK && exit 0
echo >&2 "Couldn't reload, starting '$CMD' instead"
$CMD
;;
upgrade)
if sig USR2 && sleep 2 && sig 0 && oldsig QUIT
then
n=$TIMEOUT
while test -s $old_pid && test $n -ge 0
do
printf '.' && sleep 1 && n=$(( $n - 1 ))
done
echo
if test $n -lt 0 && test -s $old_pid
then
echo >&2 "$old_pid still exists after $TIMEOUT seconds"
exit 1
fi
exit 0
fi
echo >&2 "Couldn't upgrade, starting '$CMD' instead"
$CMD
;;
reopen-logs)
sig USR1
;;
*)
echo >&2 "Usage: $0 <start|stop|restart|upgrade|force-stop|reopen-logs>"
exit 1
;;
esac
unicorn.conf.rb
# Sample verbose configuration file for Unicorn (not Rack)
#
# This configuration file documents many features of Unicorn
# that may not be needed for some applications. See
# http://unicorn.bogomips.org/examples/unicorn.conf.minimal.rb
# for a much simpler configuration file.
#
# See http://unicorn.bogomips.org/Unicorn/Configurator.html for complete
# documentation.
# Use at least one worker per core if you're on a dedicated server,
# more will usually help for _short_ waits on databases/caches.
worker_processes 4
# Since Unicorn is never exposed to outside clients, it does not need to
# run on the standard HTTP port (80), there is no reason to start Unicorn
# as root unless it's from system init scripts.
# If running the master process as root and the workers as an unprivileged
# user, do this to switch euid/egid in the workers (also chowns logs):
user "deployer", "deployer"
# Help ensure your application will always spawn in the symlinked
# "current" directory that Capistrano sets up.
working_directory "/var/www/outing/current" # available in 0.94.0+
# listen on both a Unix domain socket and a TCP port,
# we use a shorter backlog for quicker failover when busy
#listen "/tmp/.outing.sock", :backlog => 64
listen 8080, :tcp_nopush => true
# nuke workers after 30 seconds instead of 60 seconds (the default)
timeout 60
# feel free to point this anywhere accessible on the filesystem
pid "/var/www/outing/current/tmp/pids/unicorn.pid"
# By default, the Unicorn logger will write to stderr.
# Additionally, ome applications/frameworks log to stderr or stdout,
# so prevent them from going to /dev/null when daemonized here:
stderr_path "/var/www/outing/current/log/unicorn.stderr.log"
stdout_path "/var/www/outing/current/log/unicorn.stdout.log"
# combine REE with "preload_app true" for memory savings
# http://rubyenterpriseedition.com/faq.html#adapt_apps_for_cow
preload_app true
GC.respond_to?(:copy_on_write_friendly=) and
GC.copy_on_write_friendly = true
before_fork do |server, worker|
# the following is highly recomended for Rails + "preload_app true"
# as there's no need for the master process to hold a connection
defined?(ActiveRecord::Base) and
ActiveRecord::Base.connection.disconnect!
# The following is only recommended for memory/DB-constrained
# installations. It is not needed if your system can house
# twice as many worker_processes as you have configured.
#
# # This allows a new master process to incrementally
# # phase out the old master process with SIGTTOU to avoid a
# # thundering herd (especially in the "preload_app false" case)
# # when doing a transparent upgrade. The last worker spawned
# # will then kill off the old master process with a SIGQUIT.
# old_pid = "#{server.config[:pid]}.oldbin"
# if old_pid != server.pid
# begin
# sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU
# Process.kill(sig, File.read(old_pid).to_i)
# rescue Errno::ENOENT, Errno::ESRCH
# end
# end
#
# Throttle the master from forking too quickly by sleeping. Due
# to the implementation of standard Unix signal handlers, this
# helps (but does not completely) prevent identical, repeated signals
# from being lost when the receiving process is busy.
# sleep 1
end
after_fork do |server, worker|
# per-process listener ports for debugging/admin/migrations
# addr = "127.0.0.1:#{9293 + worker.nr}"
# server.listen(addr, :tries => -1, :delay => 5, :tcp_nopush => true)
# the following is *required* for Rails + "preload_app true",
defined?(ActiveRecord::Base) and
ActiveRecord::Base.establish_connection
# if preload_app is true, then you may also want to check and
# restart any other shared sockets/descriptors such as Memcached,
# and Redis. TokyoCabinet file handles are safe to reuse
# between any number of forked children (assuming your kernel
# correctly implements pread()/pwrite() system calls)
end
問題は unicorn の upgrade (sudo service outing upgrade) で失敗する場合があること。
なぜでしょう。。。
あと unicorn は production 環境ではメモリ使用量が少ないのですが、
development 環境ではどんどんメモリをくっていきます。
これもなぜでしょう。。。
>View Comments
このページの上へ戻る
- 平成22年10月1日(金) 10時11分03秒
-
区分
- Picasa
-
報告者:
- tahara
こんにちは!! tahara です。
Ruby で Picasa の API をたたいてみました。
OAuth でアクセスできる素敵なライブラリをうまく見つけることができなかったので、
Google Data Ruby Utility Library を使って地味に作りました。
Developer's Guide: Protocol - Picasa Web Albums Data API - Google Code
を参照しながらの試行錯誤だったので、いまいち自信ありません。
# -*- coding: utf-8 -*-
require 'oauth/client/net_http'
module Actindi
class Picasa
SITE = "http://photos.googleapis.com"
CONSUMER_KEY = "xxxxxxxxx"
CONSUMER_SECRET = "xxxxxxxxxxx"
PUBLIC_ALBUM = "公開アルバム"
PRIVATE_ALBUM = "プライベートアルバム"
ALBUM_ACCESS_PUBLIC = "public"
ALBUM_ACCESS_PROTECTED = "protected"
def initialize(token, secret)
consumer = OAuth::Consumer.new CONSUMER_KEY, CONSUMER_SECRET, {
:site => SITE,
:signature_method => 'HMAC-SHA1',
:token => OAuth::Token.new(token, secret)
}
@picasa = GData::Client::Photos.new(:http_service => GData::HTTP::OAuthService.new(consumer))
end
def ensure_albums
return if @public_album && @private_album
feed = @picasa.get("#{SITE}/data/feed/api/user/default").to_xml
feed.elements.each("entry") do |entry|
title = entry.elements["title"].text
puts title
if title == PUBLIC_ALBUM
@public_album = entry
elsif title == PRIVATE_ALBUM
@private_album = entry
end
end
return if @public_album && @private_album
unless @public_album
@public_album = create_album(PUBLIC_ALBUM, "public")
end
unless @private_album
@private_album = create_album(PRIVATE_ALBUM, "protected")
end
ensure_albums
end
def user_data
feed = @picasa.get("#{SITE}/data/entry/api/user/default").to_xml
puts feed
feed
end
def create_album(title, access)
entry = <<ENTRY
<entry xmlns='http://www.w3.org/2005/Atom'
xmlns:media='http://search.yahoo.com/mrss/'
xmlns:gphoto='http://schemas.google.com/photos/2007'>
<title type='text'>#{title}</title>
<summary type='text'>あるばむぅ</summary>
<gphoto:access>#{access}</gphoto:access>
<category scheme='http://schemas.google.com/g/2005#kind'
term='http://schemas.google.com/photos/2007#album'></category>
</entry>
ENTRY
@picasa.headers = {}
feed = @picasa.post("#{SITE}/data/feed/api/user/default", entry).to_xml
feed
end
def post_photo(title, summary, photo_file_path, mime_type, access)
album_id = album_id_from_access(access)
entry = <<ENTRY
<entry xmlns='http://www.w3.org/2005/Atom'>
<title>#{title}</title>
<summary>#{summary}</summary>
<category scheme="http://schemas.google.com/g/2005#kind"
term="http://schemas.google.com/photos/2007#photo"/>
</entry>
ENTRY
url = "#{SITE}/data/feed/api/user/default/albumid/#{album_id}"
puts url
puts entry
@picasa.headers = {}
feed = @picasa.post_file(url,
photo_file_path,
mime_type,
entry).to_xml
puts feed
feed.elements["link[@rel='edit']"].attributes['href']
end
def delete_photo(feed)
@picasa.headers = {}
@picasa.delete(feed)
end
def change_album(feed_url, access)
album_id = album_id_from_access(access)
@picasa.headers = {}
entry = @picasa.get(feed_url).to_xml
entry.elements["gphoto:albumid"].text = album_id
edit_url = entry.elements["link[@rel='edit']"].attributes['href']
@picasa.headers = {}
feed = @picasa.put(edit_url, entry.to_s).to_xml
feed.elements["link[@rel='edit']"].attributes['href']
end
def album_id_from_access(access)
ensure_albums
url = if access == ALBUM_ACCESS_PUBLIC
@public_album.elements["id"].text
else
@private_album.elements["id"].text
end
url =~ /([^\/]+$)/
$1
end
class << self
def example
token = "access token"
secret = "access token secret"
picasa = Actindi::Picasa.new(token, secret)
feed = picasa.post_photo("題名", "サマリー", "/tmp/aaa.jpg", "image/jpeg", Actindi::Picasa::ALBUM_ACCESS_PUBLIC)
end
end
end
end
module GData
module HTTP
class OAuthService
def initialize(consumer)
@consumer = consumer
end
def new
self
end
# Take a GData::HTTP::Request, execute the request, and return a
# GData::HTTP::Response object.
def make_request(request)
url = URI.parse(request.url)
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = (url.scheme == 'https')
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
case request.method
when :get
req = Net::HTTP::Get.new(url.request_uri)
when :put
req = Net::HTTP::Put.new(url.request_uri)
when :post
req = Net::HTTP::Post.new(url.request_uri)
when :delete
req = Net::HTTP::Delete.new(url.request_uri)
else
raise ArgumentError, "Unsupported HTTP method specified."
end
case request.body
when String
req.body = request.body
when Hash
req.set_form_data(request.body)
when File
req.body_stream = request.body
request.chunked = true
when GData::HTTP::MimeBody
req.body_stream = request.body
request.chunked = true
else
req.body = request.body.to_s
end
request.headers.each do |key, value|
req[key] = value
end
request.calculate_length!
@consumer.sign!(req)
res = http.request(req)
response = Response.new
response.body = res.body
response.headers = Hash.new
res.each do |key, value|
response.headers[key] = value
end
response.status_code = res.code.to_i
return response
end
end
end
end
>View Comments
このページの上へ戻る
- 平成22年7月9日(金) 11時41分18秒
-
区分
- Google
-
報告者:
- tahara
こんにちは!! tahara です。
Ruby で Google Analytics API をたたいてみました。
といっても Garb を使えば簡単です。
ユーザID(email)とパスワードでも認証ができるのですが、今回は OAuth を使います。
インストール
gem install garb oauth
まずは Google Analytics, OAuth and Ruby. Oh, my. | everburning を参考に OAuth します。
あらかじめ https://www.google.com/accounts/ManageDomains から CONSUMER_KEY と CONSUMER_SECRET を取得しておく必要があります。
# -*- coding: utf-8 -*-
require 'oauth'
CONSUMER_KEY = "xxxxxx"
CONSUMER_SECRET ="xxxxxxxxx"
consumer = OAuth::Consumer.new CONSUMER_KEY, CONSUMER_SECRET, {
:signature_method => 'HMAC-SHA1',
:site => 'https://www.google.com',
:request_token_path => '/accounts/OAuthGetRequestToken',
:authorize_path => '/accounts/OAuthAuthorizeToken',
:access_token_path => '/accounts/OAuthGetAccessToken',
}
request_token = consumer.
get_request_token({}, :scope => "https://www.google.com/analytics/feeds/")
# 次の URL をブラウザでアクセスし、確認コードを取得する。
p request_token.authorize_url
# 取得した確認コード
ACCESS_CODE = "xxxxxxxxx"
# 確認コードからアクセストークンを取得
access_token = request_token.get_access_token(:oauth_verifier => ACCESS_CODE)
# access_token.token と access_token.secret を取得する。
p access_token.token
p access_token.secret
ACCESS_TOKEN = access_token.token
ACCESS_SECRET = access_token.secret
# 次回からは次のようにしてアクセストークンを生成する。
access_token = OAuth::AccessToken.new(consumer, ACCESS_TOKEN, ACCESS_SECRET)
CONSUMER_KEY, CONSUMER_SECRET, ACCESS_TOKEN, ACCESS_SECRET がそろったので準備完了です。
Garb を使ってみます。
次は正規表現 ^/facilities/[0-9]+$ にマッチするページのページビューを取得するコードです。
# -*- coding: utf-8 -*-
=begin
http://github.com/vigetlabs/garb
=end
require "garb"
require "oauth"
CONSUMER_KEY = "xxxxx"
CONSUMER_SECRET ="xxxxxx"
ACCESS_TOKEN = "xxxxx"
ACCESS_SECRET = "xxxxx"
consumer = OAuth::Consumer.new CONSUMER_KEY, CONSUMER_SECRET, {
:signature_method => 'HMAC-SHA1',
:site => 'https://www.google.com',
:request_token_path => '/accounts/OAuthGetRequestToken',
:authorize_path => '/accounts/OAuthAuthorizeToken',
:access_token_path => '/accounts/OAuthGetAccessToken',
}
access_token = OAuth::AccessToken.new(consumer, ACCESS_TOKEN, ACCESS_SECRET)
Garb::Session.access_token = access_token
# プロファイル を指定
profile = Garb::Profile.first('UA-xxxxxxx-x')
class PageView
extend Garb::Resource
# 横に並ぶ項目。複数指定可能
metrics :pageviews
# 縦に並ぶ項目。複数指定可能
dimensions :page_path
# 並び順。複数指定可能。降順は後に .desc をつける。
sort :pageviews.desc
# フィルタ
filters do
# 正規表現で指定可能
# http://code.google.com/intl/ja/apis/analytics/docs/gdata/gdataReferenceDataFeed.html#filters
# http://www.google.com/support/analytics/bin/answer.py?answer=55582
contains(:page_path, '^/facilities/[0-9]+$')
end
end
# OpenStruct の配列で結果を取得。最大 10000 件取得できる。:offset で取得開始位置も指定可能。
res = PageView.results(profile, :start_date => '2010-07-01'.to_date, :end_date => '2010-07-07'.to_date, :limit => 10000)
# => [#<OpenStruct page_path="/facilities/159", pageviews="1237">, #<OpenStruct page_path="/facilities/164", pageviews="1061">, ...]
Data Feed Query Explorer - Google Analytics - Google Code ではブラウザから Analytics Data Export API をたたけるようになっていますので、
このページを参考にしながら、 metrics や dimensions の設定をいろいろかえると面白いことができるかもしれません。
>View Comments
このページの上へ戻る
- 平成22年6月5日(土) 17時15分45秒
-
区分
- Lisp on Rails
-
報告者:
- tahara
こんにちは!! tahara です。
Objective-C づけになり、すっかりこぶさたしておりましたが Lisp on Rails 第9回です!
今回はビューです。
Common Lisp で ERB 相当を実装します。
Common Lisp で実装するからにはリードテーブルを使い、
HTML ファイルを関数にコンパイルしたいと思います。
HTML ファイルを関数にコンパイルソースは
http://github.com/quek/lisp-on-rails/blob/master/action-pack/ecl.lisp です。
ところどころ説明させていただきます。
html-defun-readtable ではビューファイルの最初の1文字をマクロキャラクタにして、
先頭に (in-package :xxxx) を追加し、全体を (defun xxxx () ...) でくるむようにしています。
動的にリーダをカスタマイズしているのです。
これでビューファイルを1つの関数として読み込むことができるようになります。
(defun html-defun-readtable (fname pathspec)
(let ((*readtable* (basic-readtable)))
(set-macro-character
(first-char pathspec)
(let ((in-package t))
(lambda (stream char)
(unread-char char stream)
(print
(if in-package
(progn
(setf in-package nil)
`(in-package ,(package-name action-controller:*app-package*)))
`(defun ,fname ()
,(body-code stream char)))))))
*readtable*))
Rails ではコントローラからビューへの値の受け渡しは @foo のようなインスタンス変数が使われます。
それに対応するためビューファイルの中に @ で始まるシンボルがあれば、
コントローラのスロット値へのアクセスに変換するシンボルマクロを定義します。
(defun body-code (stream char)
(walk-body-code (read-body-code stream char)))
(defun read-body-code (stream char)
(let ((*readtable* (make-html-readtable char)))
(loop for x = (read stream nil stream t)
until (eq x stream)
collect x)))
(defun walk-body-code (code)
`(symbol-macrolet
,(series:collect
(series:mapping
((x (series:choose-if (q:^ q:symbol-head-p _ "@")
(series:scan-lists-of-lists-fringe code))))
`(,x (slot-value action-controller:*controller*
',(intern (subseq (symbol-name x) 1)
action-controller:*app-package*)))))
,@code))
そんなこんなで、なんとかモデル、コントローラ、ビューが繋がりました。
モデル
(in-package :blog)
(def-record post
(:has-many comments))
(def-record comment
(:belongs-to post))
コントローラ。生の defclass です。
(in-package :blog)
(defclass top-controller (application-controller)
((message)
(posts)))
(defmethod index ((self top-controller))
(with-slots (message posts) self
(setf message "まみむめも♪"
posts (all post))))
ビュー。HTML タグと loop が混在するのもまた一興ですね。
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>ブログ</title>
</head>
<body>
<h1><%= @message %></h1>
<h2>投稿を loop で表示する</h2>
<ul><% (loop for post in @posts for comments = (comments-of post) do %>
<li><%= (content-of post) %> -- <%= (name-of post) %></li>
<% if comments do %>
<ul><% (loop for comment in comments do %>
<li><%= (body-of comment) %> -- <%= (commenter-of comment) %></li><% ) %>
</ul><% ) %>
</ul>
</body>
</html>
コントローラは次のように書けるようにすると、それっぽい気もしますが、いまはまだ書けません。
(def-controller top (application)
(def-action index
(setf @message "まみむめも♪"
@posts (all post)))
(def-action foo
(setf @essage "foo")))
以上、まとめますと
「リーダをいじれる言語は素敵ですね!」
でした。
ソースはこちらから http://github.com/quek/lisp-on-rails
第10回につづきます。
>View Comments
このページの上へ戻る
- 平成22年5月14日(金) 10時50分16秒
-
区分
- Apache
-
報告者:
- tahara
こんにちは!! tahara です。
iPhone アプリからのアクセス以外はベーシック認証でブロックしたい、というときのお話です。
この設定で User-Agent に CFNetwork が含まれていない場合だけベーシック認証が必要になります。
<Location />
Satisfy Any
BrowserMatchNoCase CFNetwork is_iPhone=1
Order Deny,Allow
Deny from all
Allow from env=is_iPhone
AuthUserFile /var/www/htpasswd
AuthGroupFile /dev/null
AuthName "Please enter username and password"
AuthType Basic
require valid-user
</Location>
これでステージング環境が Google に補足されることもなくなるはずです。
>View Comments
このページの上へ戻る
- 平成22年4月24日(土) 18時38分25秒
-
区分
- 集合知
-
報告者:
- tahara
こんにちは!! tahara です。
少々事情があってある単語に関連する単語を自動的に取得したくなりました。
『集合知イン・アクション』 を参考に Common Lisp で書いてみました。
Yahoo の Web API を利用させていただきます。
- ウェブ検索とブログ検索で単語に関連するテキストを収集
- 日本語形態素解析で単語に分解
- 単語からタームベクトルを作成
(eval-when (:compile-toplevel :load-toplevel :execute)
(require :drakma)
(require :cxml)
(require :cl-ppcre))
(defparameter *words*
'("アナウンサー" "お医者さん" "イラストレーター" "宇宙飛行士"
"タクシー運転手" "電車運転士" "バス運転士" "映画監督" "絵本作家"
"演奏家" "歌手" "カメラマン" "看護師" "外交官" "画家" "高校の先生"
"小学校の先生" "中学校の先生" "気象予報士" "キャビンアテンダント"
"救急救命士" "銀行員" "警察官" "裁判官" "作詞家" "サッカー監督"
"サッカー選手" "作曲家" "シェフ" "指揮者" "社長" "小説家" "消防士"
"新聞記者" "動物のお医者さん" "政治家" "声優" "船長" "大工" "図書館司書"
"俳優" "花火師" "花屋" "パイロット" "パン屋さん" "美容師"
"ピアノニスト" "プロ野球選手" "弁護士" "幼稚園の先生")
"これらの単語に関連する単語が欲しいのです。")
(defparameter *yahoo-appid*
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
"Yahoo Web API の アプリケーションID")
(defparameter *yahoo-ma-url* "http://jlp.yahooapis.jp/MAService/V1/parse"
"日本語形態素解析")
(defparameter *yahoo-web-search-url*
"http://search.yahooapis.jp/WebSearchService/V1/webSearch"
"ウェブ検索")
(defparameter *yahoo-blog-search-url*
"http://search.yahooapis.jp/BlogSearchService/V1/blogSearch"
"ブログ検索")
(defparameter *occurrence-threshold* 5
"これより少ない出現頻度の単語は無視します。")
(defparameter *stop-words*
'("あれ" "いい" "こんな" "こちら" "こと" "これ" "それ" "ため" "とき" "ない"
"もの" "よく"
"以上" "一覧" "最新"
"amp" "at" "by" "com" "gt" "http" "https" "jp" "lt")
"これらの単語は無視します。")
(defparameter *stop-words-regexps*
(mapcar #'ppcre:create-scanner
'("^[0-90-9]+$" "^.$"))
"これらの正規表現に一致する単語ま無視します。")
;; Drakma の設定
(setf drakma:*drakma-default-external-format* :utf-8)
(pushnew '("application" . "xml") drakma:*text-content-types* :test #'equal)
(defun stop-word-p (word)
(or (find word *stop-words* :test #'string=)
(some (lambda (x) (ppcre:scan x word)) *stop-words-regexps*)))
(defun yahoo-ma-request (text)
(drakma:http-request
*yahoo-ma-url*
:method :post
:parameters `(("appid" . ,*yahoo-appid*)
("filter" . "1|9") ; 形容詞 名詞
("sentence" . ,text))))
(defun text-to-words (text)
(destructuring-bind (result-set
schema-location
(ma-result
_
total-count
filtered-count
word-list))
(cxml:parse (yahoo-ma-request text) (cxml-xmls:make-xmls-builder))
(declare (ignorable result-set schema-location ma-result _
total-count filtered-count))
(loop for (_a _b (_c _d word)) in (cddr word-list)
collect word)))
(defun yahoo-web-search-request (query)
(drakma:http-request
*yahoo-web-search-url*
:method :get
:parameters `(("appid" . ,*yahoo-appid*)
("query" . ,query)
("results" . "50")
("format" . "html"))))
(defun web-search (query)
(destructuring-bind (result-set
pgr . results)
(cxml:parse (remove #\lf (yahoo-web-search-request query))
(cxml-xmls:make-xmls-builder))
(declare (ignorable result-set pgr))
(loop for (result _a (_title _b title) (_summary _c summary)) in results
collect (list title summary))))
(defun yahoo-blog-search-request (query)
(drakma:http-request
*yahoo-blog-search-url*
:method :get
:parameters `(("appid" . ,*yahoo-appid*)
("query" . ,query)
("results" . "50"))))
(defun blog-search (query)
(destructuring-bind (result-set first-result-position . results)
(cxml:parse (remove #\lf (yahoo-blog-search-request query))
(cxml-xmls:make-xmls-builder))
(declare (ignorable result-set first-result-position))
(loop for (result _a id rss-url (_title _b title)
(_description _c description)) in results
collect (list title description))))
(defun word-to-word-list (word)
(remove-if #'stop-word-p
(loop for i in '(web-search blog-search)
append (text-to-words
(format nil "~{~{~a ~a ~}~}" (funcall i word))))))
(defun word-count-alist (word)
(let (alist)
(loop for i in (word-to-word-list word)
if (assoc i alist :test #'string=)
do (incf (cdr (assoc i alist :test #'string=)))
else
do (setf alist (acons i 1 alist)))
(setf alist (sort alist #'(lambda (x y)
(>= (cdr x) (cdr y)))))
(remove-if (lambda (x) (< (cdr x) *occurrence-threshold*)) alist)))
(defun normalize (alist)
"重みづけを、その平方和が 1 とらるように正規化する。"
(loop with factor = (sqrt (loop for i in alist sum (expt (cdr i) 2)))
for (word . magnitude) in alist
collect (cons word (/ magnitude factor))))
(defun all-word-alist ()
(loop for word in *words*
collect (print (cons word (normalize (word-count-alist word))))))
出力は次のとおりです。
("宇宙飛行士" ("宇宙" . 0.7740145) ("飛行士" . 0.5890963) ("山崎" . 0.09774244)
("若田" . 0.06604219) ("野口" . 0.05547544) ("地球" . 0.05547544)
("訓練" . 0.050192066) ("ステーション" . 0.047550377) ("毎日新聞" . 0.044908687)
("日本人" . 0.042267002) ("国際" . 0.042267002) ("日本" . 0.039625313)
("直子" . 0.039625313) ("スペースシャトル" . 0.03434194) ("ニュース" . 0.03434194)
("シャトル" . 0.03170025) ("帰還" . 0.03170025) ("家族" . 0.03170025)
("写真" . 0.029058563) ("ISS" . 0.029058563) ("情報" . 0.026416875)
("光一" . 0.026416875) ("聡一" . 0.026416875) ("JAXA" . 0.023775188)
("活動" . 0.023775188) ("飛行" . 0.023775188) ("紹介" . 0.023775188)
("産経新聞" . 0.023775188) ("映像" . 0.021133501) ("ミッション" . 0.021133501)
("NASA" . 0.021133501) ("交信" . 0.021133501) ("職業" . 0.018491814)
("毛利" . 0.018491814) ("滞在" . 0.018491814) ("撮影" . 0.018491814)
("研究" . 0.018491814) ("女性" . 0.018491814) ("サイト" . 0.018491814)
("搭乗" . 0.015850125) ("ページ" . 0.015850125) ("選抜" . 0.015850125)
("イベント" . 0.015850125) ("実現" . 0.015850125) ("アポロ" . 0.015850125)
("きぼう" . 0.013208438) ("航空" . 0.013208438) ("開発" . 0.013208438)
("機構" . 0.013208438) ("参加" . 0.013208438) ("さいたま市" . 0.013208438)
("試験" . 0.013208438) ("仕事" . 0.013208438) ("最後" . 0.013208438)
("月面" . 0.013208438) ("着陸" . 0.013208438) ("特集" . 0.013208438)
("時事通信" . 0.013208438) ("契約" . 0.013208438) ("サム" . 0.013208438))
さて、この出力を利用することができるかどうかがまた問題です。
>View Comments
このページの上へ戻る
- 平成22年4月10日(土) 18時42分58秒
-
区分
- Lisp on Rails
-
報告者:
- tahara
こんにちは!! tahara です。
Lisp on Rails 第8回です!
今回は ActiveRecord::Base の save, create, update, destroy 等々のメソッドには
beforo_* や after_* というフックメソッドを定義することができます。
ActiveRecord::Callbacks でそのあたりの実装がされています。
これを Common Lisp でやろうとした場合、
(defmethod save :before ((self post)) ...)
で OK と思ったらそうはいきません。
before_* メソッドが false を返した場合はメソッドを呼び出しを中断する必要があります。
Common Lisp の before メソッドは返り値は無視してしまうので、そのまま使うことはできないのです。
仕方ないので自分で新しいメソッドコンビネーションを実装します。
(define-method-combination active-record ()
((around (:around))
(before (:before))
(primary () :required t)
(after (:after)))
"before メソッドが nil を返した場合メソッドの実行を中断する。"
(flet ((call-methods (methods)
(mapcar #'(lambda (method)
`(call-method ,method))
methods))
(call-methods-and (methods)
`(and ,@(mapcar #'(lambda (method)
`(call-method ,method))
methods))))
(let ((form (if (or before after (rest primary))
`(when ,(call-methods-and before)
(multiple-value-prog1
(call-method ,(first primary)
,(rest primary))
,@(call-methods (reverse after))))
`(call-method ,(first primary)))))
(if around
`(call-method ,(first around)
(,@(rest around)
(make-method ,form)))
form))))
あとは defgeneric するときにこのメソッドコンビネーションを指定すれば OK です。
(defgeneric save (record)
(:method-combination active-record)
...)
簡単にメソッドの呼び出し方法を定義できてしまうなんて Common Lisp はいい言語ですね。
ソースはこちらから http://github.com/quek/lisp-on-rails
第9回につづきます。
>View Comments
このページの上へ戻る
- 平成22年3月27日(土) 07時20分34秒
-
区分
- Lisp on Rails
-
報告者:
- tahara
遅くなりました!! tahara です。
Lisp on Rails 第7回です!
今回は ActiveRecord::Base の find メソッドの機能を多少実装してみたいと思います。
ActiveRecord::Base の find メソッドは次の4つの使い方があります。
- id で検索。
引数は
(id, *args), (id1, id2, ..., *args),
([id1, id2, ..., *args]) の3パターン。
該当するレコードがない場合は RecordNotFound が発生する。
- 最初の1件を検索。
引数は
(:first, *args) で、該当がない場合は nil を返す。
Model.first(*args) というショートカットがある。
- 最後の1件を検索。
引数は
(:last, *args) で、該当がない場合は nil を返す。
Model.last(*args) というショートカットがある。
- 該当する全件を検索。
引数は
(:all, &args) で、該当がない場合は nil を返す。
Model.all(*args) というショートカットがある。
上記の4つの使い方全てで次のハッシュオプションが使えます。
- :conditions - いわゆる検索条件。文字列またはリストで指定。
- :order - SQL の ORDEY BY
- :group - SQL の GROUP BY
- :having - SQL の HAVING
- :limit - 最大取得件数。
- :offset - 取得開始位置。
- :joins - SQL の JOIN だけど、普通次の :include を使う。
- :include - 検索時に一緒にとってくるテーブルをアソシエーション名(has_many :xxxs)で指定する。
- :select - 取得カラム。デフォルトは "*"。
- :from - SQL の FROM。ビューから検索するとき等に使える。
- :readonly - 取得結果をリードオンリー指定する。
- :lock - SQL でのロック。"FOR UPDATE" とか。
このような場合 Common Lisp ではマルチディスパッチとキーワード引数を使えばうまくいくはずです。
;; find は CL にあるので select にする
(defgeneric select (class id-or-keyword
&key
conditions
order
group
having
limit
offset
joins
include
select
from
readonly
lock
&allow-other-keys))
(defmethod select ((class symbol) id-or-keyword &rest args)
(apply #'select (find-class class) id-or-keyword args))
(defmethod select ((class active-record-class) (id integer)
&rest args
&key conditions)
(setf conditions (append (list :id id) conditions))
...)
(defmethod select ((class active-record-class) (ids list)
&rest args
&key conditions)
(setf conditions (append (list :id ids) conditions))
...)
(defmethod select ((class active-record-class) (keyword (eql :all))
&rest args)
...)
(defmethod select ((class active-record-class) (keyword (eql :first))
&rest args
&key (order "id"))
...)
(defmethod select ((class active-record-class) (keyword (eql :last))
&rest args
&key (order "id"))
...)
(defun all (&rest args)
(apply #'select (car args) :all (cdr args)))
;; first と last は CL パッケージとかぶる。
といった感じで実装してみました。
joins, include, readonly 等はまだ未実装です。
しかし Common Lisp パッケージとシンボル名(find, first, last)がかぶるのが悩ましいところです。shadowing しようかしらん。
ソースはこちらから http://github.com/quek/lisp-on-rails
第8回につづきます。
>View Comments
このページの上へ戻る
- 平成22年3月19日(金) 12時10分44秒
-
区分
- Lisp on Rails
-
報告者:
- tahara
こんにちは!! tahara です。
Lisp on Rails 第6回です!
has-one を has-many のコピペで書いてしまったので、
ここらでリファクタリングしたいと思います。
has-one のスロット定義
(defclass ar-has-one-slot-mixin ()
((has-one :initarg :has-one
:initform nil
:accessor has-one)
(class-symbol :initarg :class-symbol
:initform nil
:accessor class-symbol)))
(defmethod initialize-instance :after ((self ar-has-one-slot-mixin) &rest args)
(declare (ignore args))
(unless (class-symbol self)
(setf (class-symbol self) (has-one self))))
(defclass ar-has-one-direct-slot-definition (ar-direct-slot-definition
ar-has-one-slot-mixin)
())
(defclass ar-has-one-effective-slot-definition (ar-effective-slot-definition
ar-has-one-slot-mixin)
())
has-many のスロット定義
(defclass ar-has-many-slot-mixin ()
((has-many :initarg :has-many
:initform nil
:accessor has-many)
(class-symbol :initarg :class-symbol
:initform nil
:accessor class-symbol)))
(defmethod initialize-instance :after ((self ar-has-many-slot-mixin) &rest args)
(declare (ignore args))
(unless (class-symbol self)
(setf (class-symbol self)
(sym (singularize (has-many self))))))
(defclass ar-has-many-direct-slot-definition (ar-direct-slot-definition
ar-has-many-slot-mixin)
())
(defclass ar-has-many-effective-slot-definition (ar-effective-slot-definition
ar-has-many-slot-mixin)
())
いやー、ひどいですね。
ほとんど one と many の違いだけです。
さて、これをリファクタリングするのに Common Lisp
にはマクロという手抜きプログラマには必須の機能があります。
普通リファクタリングするとなると、関数、メソッド、スーパークラス等々の
切り出しが必要になりますよね?
でも、マクロなら何ら設計を変更することなくリファクタリングが可能になります。
では、実際にマクロを使ってリファクタリングしてみましょう。
(defmacro def-has-xxx-slot-definition (xxx
default-class-symbol-form)
`(progn
(defclass ,(sym "ar-has-" xxx "-slot-mixin") ()
((,(sym "has-" xxx) :initarg ,(key-sym "has-" xxx)
:initform nil
:accessor ,(sym "has-" xxx))
(class-symbol :initarg :class-symbol
:initform nil
:accessor class-symbol)))
(defmethod initialize-instance :after ((self ,(sym "ar-has-" xxx "-slot-mixin")) &rest args)
(declare (ignore args))
(unless (class-symbol self)
(setf (class-symbol self) ,default-class-symbol-form)))
(defclass ,(sym "ar-has-" xxx "-direct-slot-definition") (ar-direct-slot-definition
,(sym "ar-has-" xxx "-slot-mixin"))
())
(defclass ,(sym "ar-has-" xxx "-effective-slot-definition") (ar-effective-slot-definition
,(sym "ar-has-" xxx "-slot-mixin"))
())
))
(def-has-xxx-slot-definition one (has-one self))
(def-has-xxx-slot-definition many (sym (singularize (has-many self))))
すばらい。
最初のひどい設計を何ら変えることなくリファクタリングできました。
手抜き設計のベタ書きコードそのままで、リファクタリングを可能とするマクロは、
未熟なプログラマにとって、なくてはならない存在です。
ソースはこちらから http://github.com/quek/lisp-on-rails
第7回につづきます。
>View Comments
このページの上へ戻る