2012-02-11
■[Ruby] マルチバイト文字を含む文字列の表示桁数を合わせる
マルチバイト文字を含む文字列の表示桁数を合わせる方法についてです。
printf や sprintf を使用して文字列の桁数を合わせようとすると、マルチバイト文字が含まれている場合に意図したとおりに表示桁数を合わせることができません。
例えば、以下のようにマルチバイト文字を含む文字列を 7 桁に揃えるコードを書いたとしても、表示桁数は不揃いになってしまいます。
puts '[' + sprintf('%7s', 'a') + ']' puts '[' + sprintf('%7s', 'aB') + ']' puts '[' + sprintf('%7s', 'aBc') + ']' puts '[' + sprintf('%7s', 'aBcD') + ']' puts '[' + sprintf('%7s', 'aBcDe') + ']' puts '[' + sprintf('%7s', 'aBcDeF') + ']'
出力結果:
[ a] [ aB] [ aBc] [ aBcD] [ aBcDe] [ aBcDeF]
Ruby 1.8 では文字列のバイト数、Ruby 1.9 では文字列の文字数を基準に桁数が調整されます。つまり、コンソール等に表示されるときの表示幅は考慮されません。表示幅はフォントによって異なるので、sprintf の動作は正しいです。
とはいえ、コンソール等に出力するときに表示桁数を合わせたい場合もあります。そこで、以下のような表示幅を求めるメソッドを定義し、独自にパディングを行うことで対応します。
# 文字列の表示幅を求める. def print_size(string) string.each_char.map{|c| c.bytesize == 1 ? 1 : 2}.reduce(0, &:+) end # 指定された表示幅に合うようにパディングする. def pad_to_print_size(string, size) # パディングサイズを求める. padding_size = size - print_size(string) # string の表示幅が size より大きい場合はパディングサイズは 0 とする. padding_size = 0 if size < 0 # パディングする. '.' * padding_size + string end
上記のメソッドを実際に使用します(違いが分かりやすいように sprintf の結果も表示します)。
# Ruby 1.8 の場合は $KCODE を設定する. if RUBY_VERSION < '1.9' $KCODE = 'UTF-8' end puts '---- sprintf ----' puts '[' + sprintf('%7s', 'a') + ']' puts '[' + sprintf('%7s', 'aB') + ']' puts '[' + sprintf('%7s', 'aBc') + ']' puts '[' + sprintf('%7s', 'aBcD') + ']' puts '[' + sprintf('%7s', 'aBcDe') + ']' puts '[' + sprintf('%7s', 'aBcDeF') + ']' puts '---- pad_to_print_size ----' puts '[' + pad_to_print_size('a', 7) + ']' puts '[' + pad_to_print_size('aB', 7) + ']' puts '[' + pad_to_print_size('aBc', 7) + ']' puts '[' + pad_to_print_size('aBcD', 7) + ']' puts '[' + pad_to_print_size('aBcDe', 7) + ']' puts '[' + pad_to_print_size('aBcDeF', 7) + ']'
出力は以下の通りです。
---- sprintf ---- [ a] [ aB] [ aBc] [aBcD] [aBcDe] [aBcDeF] ---- pad_to_print_size ---- [ a] [ aB] [ aBc] [ aBcD] [aBcDe] [aBcDeF]
2012-01-21
■[Python] Python でのエンコード指定
Python で非 ASCII 文字列を扱う場合に、エンコード指定が必要な場合があります。
例として、非 ASCII 文字列をファイルに保存する以下のコードを考えます。
# coding: utf-8 output = open("a.txt", "w") output.write(u"非 ASCII 文字列") output.close()
上記コードを実行すると、次のようなエラーが発生する場合があります。
> python a.py
Traceback (most recent call last):
File "a.py", line 4, in <module>
output.write(u"非 ASCII 文字列")
UnicodeEncodeError: 'ascii' codec can't encode character u'\u975e' in position 0: ordinal not in range(128)
この場合、エンコーディングを明示的に指定/変換する必要があります。
(1) str.encode() でエンコードする
ファイルに出力する前に str.encode() でエンコード変換を行う方法です。
# coding: utf-8 output = open("a.txt", "w") output.write(u"非 ASCII 文字列".encode("utf-8")) output.close()
(2) codecs.open() でエンコードを指定してファイルを開く
codecs.open() でファイルを開き、そのときにエンコーディングを指定する方法です。事前に codecs を import する必要があります。
# coding: utf-8 import codecs output = codecs.open("a.txt", "w", "utf-8") output.write(u"非 ASCII 文字列") output.close()
(3) システムのデフォルトエンコーディングを変更する
sys.setdefaultencoding() でシステムのデフォルトのエンコーディングを変更する方法です。全体に影響する方法のため、できるだけ上記の (1) または (2) の方法で対応すると良いかと思います。
# coding: utf-8 import sys reload(sys) sys.setdefaultencoding("utf-8") output = open("a.txt", "w") output.write(u"非 ASCII 文字列") output.close()
2012-01-16
■[R] R でコマンドライン引数を取得する方法
R のスクリプトを作成するときに「集計対象のデータファイルはコマンドライン引数で指定したい」という場合があります。
それを実現するには、R コマンドの --args オプションを使用します。--args オプション以降は R コマンド自体のオプションとはみなされなくなります。
R --file=summary.r --args data.csv
そして、--args で渡された引数は commandArgs() 関数で取得することができます。
commandArgs(trailingOnly=TRUE)[1] # => "data.csv"
ポイントは trailingOnly=TRUE を指定することです。trailingOnly=TRUE を指定しない場合は、以下のように --args より前に指定したオプションを取得してしまいます。指定したオプションの数によって添え字がずれるため、例えば commandArgs()[3] と書いた場合に取得できる値は、R コマンドのオプションを書いた順番に依存します。
commandArgs()[1] # => "/usr/lib/R/bin/exec/R" commandArgs()[2] # => "--file=summary.r" commandArgs()[3] # => "--args" commandArgs()[4] # => "data.csv"
■[R] R の read.csv で欠損値を NA に変換しながらデータを取り込む方法
以下の成績データ(data.csv)があるとします。データ中の「XX」及び「-1」は欠損値を表します。
名前,国語,算数,理科,社会 太郎,95,70,75,80 次郎,90,80,80,85 花子,80,90,80,70 A子,XX,70,70,70 B子,70,70,70,70 C子,80,80,80,80 D子,80,-1,80,80 E子,90,90,90,90 F子,90,XX,90,-1
ファイルを読み込むときに欠損値を NA に置き換えたい場合は、以下のように na.strings で NA に変換したい値を指定します。
read.csv("data.csv", header=TRUE, na.strings=c("XX",-1))
結果は以下の通りです。
名前 国語 算数 理科 社会 1 太郎 95 70 75 80 2 次郎 90 80 80 85 3 花子 80 90 80 70 4 A子 NA 70 70 70 5 B子 70 70 70 70 6 C子 80 80 80 80 7 D子 80 NA 80 80 8 E子 90 90 90 90 9 F子 90 NA 90 NA
■[R] R で欠損値を除外して集計する
以下の成績データ(data.csv)があるとします。
名前,国語,算数,理科,社会 太郎,95,70,75,80 次郎,90,80,80,85 花子,80,90,80,70 A子,XX,70,70,70 B子,70,70,70,70 C子,80,80,80,80 D子,80,-1,80,80 E子,90,90,90,90 F子,90,XX,90,-1
各教科の平均点を求めるために以下の R スクリプトを実行すると、データに NA が含まれるため、思い通りの結果は得られません。
data <- read.csv("data.csv", header=TRUE, na.strings=c("XX",-1))
cat("国語平均:", mean(data["国語"]), "\n")
cat("算数平均:", mean(data["算数"]), "\n")
cat("理科平均:", mean(data["理科"]), "\n")
cat("社会平均:", mean(data["社会"]), "\n")
結果は以下の通りです。
国語平均: NA 算数平均: NA 理科平均: 79.44444 社会平均: NA
NA を除外して集計するには、na.rm=TRUE を指定します(他の関数でも使用可能です)。
data <- read.csv("data.csv", header=TRUE, na.strings=c("XX",-1))
cat("国語平均:", mean(data["国語"], na.rm=TRUE), "\n")
cat("算数平均:", mean(data["算数"], na.rm=TRUE), "\n")
cat("理科平均:", mean(data["理科"], na.rm=TRUE), "\n")
cat("社会平均:", mean(data["社会"], na.rm=TRUE), "\n")
結果は以下の通りです。
国語平均: 84.375 算数平均: 78.57143 理科平均: 79.44444 社会平均: 78.125
上記の例では列ごと(国語、算数、理科、社会)に見て NA を除外した上で平均を取る、という処理を行っていますが、行ごとにみて NA を含む行を集計対象外としたい場合もあります。その場合は na.omit() 関数で NA を含む行を削除することができます。
data <- read.csv(file, header=TRUE, na.strings=c("XX",-1))
na.omit(data)
結果は以下の通りです。
名前 国語 算数 理科 社会 1 太郎 95 70 75 80 2 次郎 90 80 80 85 3 花子 80 90 80 70 5 B子 70 70 70 70 6 C子 80 80 80 80 8 E子 90 90 90 90
各集計関数のオプションで na.rm=TRUE を指定するのか、それとも na.omit() 関数を使用するのかはケースバイケースです。全教科の得点が分かっている人のみ集計したい場合は na.omit() を使う、教科ごとの平均点が分かれば良いという場合は na.rm=TRUE を指定する、という具合に用途に応じて使い分けます。
2012-01-15
■[Ruby on Rails]Thinプロセスを順番に再起動する方法
「thin --onebyone restart」のように「--onebyone」オプションを付けておくと、プロセスを一つずつ再起動するようになることを知ったのでメモしておきます。
Rails アプリケーションを動作させるときに Thin を使うことがあります。再起動するときは「thin -C webapp.yml restart」などとしますが、このときに全プロセスが終了後、新しいプロセスが起動されます。一時的に全プロセスが停止した状態となるため、どうしても瞬断するタイミングが出来てしまい不便に感じていました。
瞬断を発生させずに再起動するために、設定ファイルを二つに分け、「thin -C webapp1.yml restart」「thin -C webapp2.yml restart」という具合に半分ずつ再起動するようにしていました。
thin のヘルプを見ていて気づいたのですが、「--onebyone」というオプションを指定すると、プロセスが一つずつ再起動されることを知りました。つまり、以下のようなどうさになります。
プロセスAを終了
↓
プロセスAを起動
↓
プロセスBを終了
↓
プロセスBを起動
:
「--onebyone」オプションを指定すれば、瞬断を避けるために設定ファイルを二つに分ける必要はなくなります。
2011-12-24
■[Ruby on Rails] Fixture 間の関連を ID ではなく名前で設定する
O'Reilly のサイトで Rails 3 in a Nutshell という本の内容が公開されていました。その中の 11. Testing の「Defining relationships between fixtures」で解説されていたのですが、Fixture 間の関連は ID ではなく名前で設定することができるそうです。
例として、ユーザ(User)が記事(Article)を投稿する機能があるとします。
ユーザテーブル (users) のカラムには「名前(name)」と「メールアドレス(email)」があり、モデルの定義は以下のようになります。
# app/models/user.rb class User < ActiveRecord::Base validates :name, :presence => true validates :email, :presence => true end
記事テーブル(articles)のカラムには「投稿ユーザ ID(user_id)」と「タイトル(title)」、「本文(body)」があり、モデルではユーザへの関連(belongs_to :user)を記述しておきます。
# app/models/article.rb class Article < ActiveRecord::Base belongs_to :user validates :title, :presence => true validates :body, :presence => true end
ユーザ(User)の Fixture では「taro」及び「jiro」を定義します。
# test/fixtures/users.yml taro: name: たろう email: taro@example.com jiro: name: じろう email: jiro@example.com
記事(Article)の Fixture では「taro」及び「jiro」の記事データを登録します。その際、上で定義した「taro」や「jiro」を使用して関連を設定することができます。
# test/fixtures/articles.yml taro_greeting: user: taro # users.yml で定義した名前で指定する。 title: 自己紹介 body: たろうです。 jiro_greeting: user: jiro # users.yml で定義した名前で指定する。 title: 自己紹介 body: じろうです。
2011-12-19
■[Ruby on Rails] テーブルに存在しない属性は型変換が行われない
Rails でデータベースにカラムが存在しない属性を扱いたい場合、通常は attr_accessor で属性を定義すると思います。しかし、attr_accessor で定義した属性は、テーブルにカラムが存在する属性のように、代入時の型変換が行われません。
例えば、以下のような会員クラスに利用規約の同意フラグを保持する属性を attr_accessor :accepted で定義したとします。
# 会員. class User < ActiveRecord::Base # 利用規約の同意フラグ. attr_accessor :accepted end
会員登録フォームの View ファイルでは、利用規約に同意する場合はチェックボックスにチェックするものとします。
<%= form_for(@user) do |form| %> ... 利用規約に同意しますか? <%= form.check_box(:accepted) %>はい ... <% end %>
利用規約に同意されなかった場合はエラーとしたいのですが、会員登録を行うコントローラで以下のように記述しても正しく動作しません(常に同意したものとして処理されてしまいます)。
def create @user = User.new(params[:user]) unless @user.accepted # 利用規約に同意していないのでエラー end end
原因は、accepted は attr_accessor で定義されているためです。利用規約のチェックボックスがチェックされなかった場合にクエストパラメータとして '0' が送られてくるのですが、accepted には文字列としての '0' がそのまま保持されます。Ruby では '0' も真として扱われるため、チェックしたかしないかによらず unless @user.accepted の内部には入りません。本当は true/false に型変換されると嬉しいのですが。
■[Ruby on Rails] テーブルに存在しない属性でも型変換を行う方法
テーブルに存在しない属性でも型変換を行う方法を考えます。
Rails では、カラムの情報は ActiveRecord::ConnectionAdapters::Column クラスが保持しています。そして、代入時の型変換は ActiveRecord::ConnectionAdapters::Column#type_cast メソッドで行われます。
# ActiveRecord::ConnectionAdapters::Column.new(name, default, sql_type = nil, null = true) column = ActiveRecord::ConnectionAdapters::Column.new(:accepted, false, :boolean) column.type_cast('1') # => true column.type_cast('0') # => false
そこで、attr_accessor の変わりに、代入時に ActiveRecord::ConnectionAdapters::Column#type_cast で型変換を行うメソッドを定義します。
def self.column(name, type, default=nil)
ActiveRecord::ConnectionAdapters::Column.new(name, default, type).tap do |column|
define_method("#{name}=") do |value|
instance_variable_set("@#{name}", column.type_cast(value))
end
define_method(name) do
unless instance_variable_defined?("@#{name}")
instance_variable_set("@#{name}", column.default)
end
instance_variable_get("@#{name}")
end
end
end
end
そして、上記の column メソッドでカラムがテーブルに存在しない属性を定義します。
# 会員. class User < ActiveRecord::Base # 利用規約の同意フラグ. column :accepted, :boolan, false end
これで代入時に型変換が行われるようになりました。
user = User.new(:accepted => '1') user.accepted # => true user = User.new(:accepted => '0') user.accepted # => false
2011-12-18
■[Ruby][Ruby on Rails]ハッシュで初期化可能な構造体
Ruby には Struct という構造体を作成するクラスがあります。
例えばユーザ情報(名前とメールアドレス)を保持する構造体を作成する場合は以下のように記述します。
User = Struct.new(:name, :email) user = User.new('taro', 'taro@example.com') user.name # => 'taro' user.email # => 'taro@example.com'
ところで Rails を使用していると、ハッシュをキーワード引数のように使用するシーンが多々あります。例えばモデルクラスを初期化するときや、メソッドの引数などです。これに慣れてくると、構造体も以下のようにインスタンス化できれば便利だと感じます。
# このように書きたい. user = User.new(:name => 'taro', :email => 'taro@example.com')
そこで、ハッシュで初期化可能な構造体を考えます。
まずはハッシュで初期化するための initialize メソッドを提供するモジュールを作成します。
module HashInitializable def initialize(attributes={}) attributes.each do |name, value| send("#{name}=", value) end end end
この HashInitializable を Struct.new で作成した構造体クラスに include すれば、ハッシュで初期化可能な構造体が出来上がります。
User = Struct.new(:name, :email) User.send(:include, HashInitializable) user = User.new(:name => 'taro', :email => 'taro@example.com') user.name # => 'taro' user.email # => 'taro@example.com'
HashInitializable は単なるモジュールなので、Struct 以外にも使用可能です。
上記の User を通常のクラスとして以下のように定義することもできます。
class User attr_accessor :name, :email include HashInitializable end user = User.new(:name => 'taro', :email => 'taro@example.com') user.name # => 'taro' user.email # => 'taro@example.com'
それでは最後に、HashInitializable を使用して、ハッシュで初期化可能な構造体を作成します。
class HashInitializableStruct def senf.new(*arguments) struct = Struct.new(*arguments) struct.send(:include, HashInitializable) struct end end
これで以下のように記述することができるようになりました。
User = HashInitializableStruct.new(:name, :email) user = User.new(:name => 'taro', :email => 'taro@example.com') user.name # => 'taro' user.email # => 'taro@example.com'
2011-12-17
■[Ruby] 外部コマンド呼び出しにおいて、標準出力、標準エラーを取得する方法
Ruby の標準添付ライブラリである open3 を使用すると、外部コマンド呼び出しにおいて、標準出力と標準エラーを別々に取得することができます。
例えば、Open3.capture3(command) メソッドは、渡された command を実行し、戻り値として [標準出力, 標準エラー, 終了ステータス] という配列を返します。
# coding: utf-8 require 'open3' # ruby コマンドで「puts 'Hello'」を実行する (puts は標準出力に出力する). command = %q(ruby -e "puts 'Hello'") Open3.capture3(command) # => ["Hello\n", "", #<Process::Status: pid 6154 exit 0>] # ruby コマンドで「warn 'Hello'」を実行する (warn は標準エラーに出力する). command = %q(ruby -e "warn 'Hello'") Open3.capture3(command) # => ["", "Hello\n", #<Process::Status: pid 6161 exit 0>]
外部コマンドに対して、標準入力から何らかの入力を行いたい場合は Open3.popen3(command) を使用します。
# coding: utf-8 require 'open3' Open3.popen3('cat') do |stdin, stdout, stderr, thread| stdin.puts 'Hello' puts stdout.readline end
上記コードは、外部コマンドとして cat を実行し、cat の標準入力に 'Hello' を渡します。そして、cat の標準出力を読み取り表示します。
2011-12-11
■[Ruby on Rails] rake タスクを再定義する方法
rake タスクを定義するときに、既に同じ名前のタスクが存在する場合は、処理内容が「再定義(上書き)」されるのではなく、定義した順に処理が実行されます。
例えば、以下のように hello というタスクを 2 回定義しているとします。
# lib/tasks/hello.rake desc 'Hello1' task :hello do puts 'Hello1' end desc 'Hello2' task :hello do puts 'Hello2' end
「rake hello」で hello タスクを実行すると、タスクを定義した順に実行されることが分かります。
$ rake hello Hello1 Hello2
「rake -T hello」で hello タスクの情報を表示すると、定義した内容がそれぞれ実行されることが分かります(desc で指定した説明が '/' で区切られて表示されます)。
$ rake -T hello rake hello # Hello1 / Hello2
定義済みの内容に独自の処理を追加したい場合は、単純に同じ名前のタスクを定義すれば良いです。
反対に、定義済みの処理内容を再定義(上書き)したい場合は、以下のように定義済みのタスク情報をクリアする必要があります。
# lib/tasks/hello.rake desc 'Hello1' task :hello do puts 'Hello1' end # 定義済みタスクの情報をクリアする. task = Rake.application.lookup('hello') task.clear task.instance_variable_set('@full_comment', nil) desc 'Hello2' task :hello do puts 'Hello2' end
上記のように、Rake.application.lookup で指定した名前のタスクを取得することができます。
そして、task.clear でタスクの処理をクリアします。また、インスタンス変数の @full_comment を nil にリセットすることで、「rake -T」で出力されるタスクの説明をクリアすることができます。
「rake hello」で hello タスクを実行すると、以下のように最後に定義した処理だけが実行されることが分かります。
$ rake hello Hello2
「rake -T hello」でタスクの情報を表示しても、最後に定義したものだけが出力されることを確認できます。
$ rake -T hello rake hello # Hello2
2011-12-08
■[Ruby on Rails] テーブルごとに初期データを登録する
Rails でテーブルごとに初期データを登録できるように改良する方法を考えます。
Rails では db/seeds.rb に初期データを登録するスクリプトを記述します。
db/seeds.rb は rake の db:seed タスクにより実行されます。
rake db:seed
この仕組みを改良し、テーブル単位で初期データを登録できるようにします。
アプローチは色々考えられますが、ここでは以下のように改良する方法を考えます。
- db/seeds.rb をテーブルごとに分割して作成する。
- 分割したファイルを個別に実行する rake タスクを定義する。
db/seeds.rb をテーブルごとに分割する
まずは db/seeds.rb をテーブルごとに分割します。
分割したファイルは db/seeds というディレクトリを作成し、その中に保存することにします。
具体的には、以下のようになります。
db/seeds/shops.rb db/seeds/products.rb db/seeds/prefecture.rb :
rake タスクを定義する
次に、db/seeds/*.rb を個別に実行する rake タスクを定義します。
独自の rake タスクを定義するには、lib/tasks ディレクトリに .rake という拡張子のファイルを作成します。
ここでは以下の内容で lib/tasks/seed.rake ファイルを作成します。
# db/seeds/*.rb にマッチするファイルごとにタスクを定義する. Dir.glob(File.join(Rails.root, 'db', 'seeds', '*.rb')).each do |file| desc "Load the seed data from db/seeds/#{File.basename(file)}." task "db:seed:#{File.basename(file).gsub(/\..+$/, '')}" => :environment do load(file) end end
以下のコマンドで定義したタスクが有効になっていることを確認します。
rake -T db:seed
結果は以下のようになります。
rake db:seed # Load the seed data from db/seeds.rb. rake db:seed:shops # Load the seed data from db/seeds/shops.rb. rake db:seed:products # Load the seed data from db/seeds/products.rb. rake db:seed:prefectures # Load the seed data from db/seeds/prefectures.rb. :
db/seeds.rb を修正する
最後に db/seeds.rb に db/seeds/*.rb を読み込むコードを記述します。
Dir.glob(File.join(Rails.root, 'db', 'seeds', '*.rb')) do |file| load(file) end
これで「rake db:seed」とすれば全ての初期データが登録され、「rake db:seed:shops」などとすれば個別のテーブルごとに初期データを登録することができるようになりました。
2011-10-02 Rails3 でのメール送信
Rails3 でのメール送信処理について調べました。
■[Ruby on Rails] Rails3 のメール送信で本文を 8bit で送信する
というわけで困ってたんですが、さっきspecを見たりしながら調べたら、どうもMail::Message#transport_encodingを8bitに設定すればよいようです。
Rails3で送信するメールの本文をbase64ではなく8bitにする方法 - 思っているよりもずっとずっと人生は短い。
Rails3 で何も設定を行わずにメール送信を行うと、本文が base64 エンコーディングされたのですが、以下のように transport_encoding = '8bit' を指定すれば UTF-8 のまま送信されるようです。
mail = Mailer.notice('お知らせです') mail.transport_encoding = '8bit' mail.deliver
メールを送信する全ての箇所を上記のように書き換えるのは手間なので、次のように mail メソッドを置き換えて、デフォルトで transport_encoding = '8bit' を設定するようにしました。*1
# app/mailers/mailer.rb class Mailer < ActionMailer::Base def notice(body) @body = body mail(:to => 'xxx@example.com') end protected def mail_with_default_settings(headers={}, &block) mail_without_default_settings(headers, &block).tap do |mail| mail.transport_encoding = '8bit' end end alias_method_chain :mail, :default_settings end
■ [Ruby on Rails] Ruby1.9 + Rails3 でメール送信時に出力される警告の対応
Ruby1.9 + Rails3 でメール送信を行ったときに、以下のような警告が出力されました。
(省略)/ruby-1.9.2-p136/lib/ruby/1.9.1/net/protocol.rb:305: warning: regexp match /.../n against to UTF-8 string
原因は net/protocol.rb の 305 行目において、エンコーディングが UTF-8 である(ASCII-8BIT ではない)文字列に対して n オプションを指定した正規表現でマッチさせているため。正規表現の n オプションを指定するとバイト列として扱われますが、マルチバイト文字に対して n オプションを指定した正規表現を使用しているため、上記警告が出力されます。
対処法としては次のアプローチが考えられますが、n オプションを指定した正規表現でマッチさせる側で対象の文字列のエンコーディングを ASCII-8BIT にするべきだと考えたので、(A) の対応を行いました。
- (A) 警告が出ている箇所の直前で文字列を ASCII-8BIT に変更するように net/protocol.rb を修正するパッチを作成する。
- (B) ASCII-8BIT にエンコーディングを設定した文字列を渡すように ActionMailer を修正するパッチを作成する。
作成したパッチ:
# config/initializers/net_protocol_patch.rb require 'net/protocol' class Net::InternetMessageIO private def each_crlf_line_with_force_encoding(src) buffer_filling(@wbuf, src) do @wbuf.force_encoding('ASCII-8BIT') while line = @wbuf.slice!(/\A.*(?:\n|\r\n|\r(?!\z))/n) yield line.chomp("\n") + "\r\n" end end end alias_method_chain :each_crlf_line, :force_encoding end
*1:PC 向けのメール送信と携帯向けのメール送信で設定を変えたい、という場合もあるはずなので、ActionMailer::Base#mail を直接変更するのではなく、ActionMailer::Base の派生クラスごとに対応することにしました。
2010-08-01
■[Ruby on Rails] Model クラスの一覧を取得する方法
Model クラスの一覧を取得するには、以下のアプローチが考えられます。
- テーブル名からクラス名を求める方法
- 全クラスから ActiveRecord::Base を継承するクラスを抽出する方法
テーブル名からクラス名を求める方法
方法
tables = ActiveRecord::Base.connection.tables
=> ["schema_migrations", "users", "photos"]
models = tables.map{|table| Object.const_get(table.classify) rescue nil}.compact
=> User(id: integer, email: string, created_at: datetime), Photo(id: integer, us
er_id: integer, file_name: string, created_at: datetime)]
問題
以下のように table_name でテーブル名を設定している(クラス名がテーブル名の単数形になっていない)クラスを取得することが出来ません。
class Image self.table_name = 'photos' end
対策
クラス名とテーブル名を Rails の規約に沿うように決めることで、上記の問題は発生しなくなります。
全クラスから ActiveRecord::Base を継承するクラスを抽出する方法
方法
constants = Object.constants.map{|name| Object.const_get(name)}
=> ["2.3.5", ApplicationController, OpenSSL, Rational, Signal, "i686-linux", Pre
ttyPrint, TSort, MonitorMixin, StringScanner::Error, Module, NKF, Socket, Except
... (省略)
constants.select{|c| c.class == Class and c < ActiveRecord::Base and !c.abstract_class?}
=> [Photo(id: integer, user_id: integer, file_name: string, created_at: datetime
), User(id: integer, email: string, created_at: datetime)]
問題
ロードされていないクラスを取得することができません。例えば、development モードで実行しているとき *1 はリクエストごとに Model クラスがキャッシュされないため、そのリクエストでアクセスされなかった Model クラスを取得することが出来ません。
対策
$RAILS_ROOT/app/models/ 以下の *.rb ファイルを事前に require すれば回避できます。
*1:正確には config.cache_classes = false が設定されているとき
2010-07-03 CSV 形式で出力する
■[PostgreSQL] CSV 形式で出力する
COPY コマンドを使用すればテーブルの内容を CSV 形式で出力することができます。
例えば、商品テーブル(products)を CSV 形式で出力するには以下ようにします。
COPY products TO STDOUT CSV;
以下、いくつかバリエーションを示します。
# ヘッダ行を含める場合. COPY products TO STDOUT CSV HEADER; # 出力するカラムを指定する場合. COPY products(name, remarks) TO STDOUT CSV; # ファイルに出力する場合. COPY products TO '/tmp/products.csv' CSV; # SQL の結果を出力する場合. COPY (SELECT name, remarks FROM products WHERE price > 1000) TO STDOUT CSV; # Shift_JIS で出力する場合. \encoding Shift_JIS COPY products TO '/tmp/products.csv' CSV HEADER;
COPY コマンドの詳細は「\h COPY」で確認することが出来ます。
postgres=# \h COPY
Command: COPY
Description: copy data between a file and a table
Syntax:
COPY tablename [ ( column [, ...] ) ]
FROM { 'filename' | STDIN }
[ [ WITH ]
[ BINARY ]
[ OIDS ]
[ DELIMITER [ AS ] 'delimiter' ]
[ NULL [ AS ] 'null string' ]
[ CSV [ HEADER ]
[ QUOTE [ AS ] 'quote' ]
[ ESCAPE [ AS ] 'escape' ]
[ FORCE NOT NULL column [, ...] ]
COPY { tablename [ ( column [, ...] ) ] | ( query ) }
TO { 'filename' | STDOUT }
[ [ WITH ]
[ BINARY ]
[ OIDS ]
[ DELIMITER [ AS ] 'delimiter' ]
[ NULL [ AS ] 'null string' ]
[ CSV [ HEADER ]
[ QUOTE [ AS ] 'quote' ]
[ ESCAPE [ AS ] 'escape' ]
[ FORCE QUOTE column [, ...] ]
2009-11-03 ページごとに <title> の値を変更する方法
■[Ruby on Rails] ページごとに <title> の値を変更する方法
Rails でページごとに <title> の値を変更する方法についてまとめます。
通常、ヘッダやフッタなどの共通部分は app/views/layouts に置かれるレイアウトファイルに記述し、ボディ部分を個々の View ファイルに記述します。
このとき問題となるのが、レイアウトファイルに共通部分として記述されている内容の一部を変更するにはどうするか、ということです。例えば、「ページごとに <title> で指定するページのタイトルをどのように変えたい」という場合です。
■[Ruby on Rails] ページごとに <title> の値を変更する方法(content_for を使用する)
content_for を使用する
ページごとに <title> の値を変更するには content_for メソッドを使用することができます。
例として以下の構成を考えます。
- app/views/layout/default.html.erb(レイアウト用のファイル)
- app/views/users/index.html(ユーザ一覧ページ)
- ...(他のページ)
この場合、app/views/layout/default.html.erb(レイアウト用のファイル)の内容は以下のようになります。
<!-- app/views/layouts/default.html.erb --> <html> <head> <!-- この部分に「content_for :title」の内容が挿入される --> <title><%= yield :title %></title> </head> <body> <%= yield %> </body>
そして、app/views/users/index.html(ユーザ一覧ページ)の内容は以下のようになります。
<!-- app/views/users/index.html --> <!-- default.html.erb の <%= yield :title %> の部分に挿入される --> <% content_for :title do %> ユーザ一覧 <% end %> <!-- 以下、default.html.erb の <%= yield %> の部分に挿入される --> ...
結果として、以下のような HTML が生成されます。
<html> <head> <title>ユーザ一覧</title> </head> <body> ... </body>
同様に、他のページにも「content_for :title」を追加すれば、ページごとに <title> の値を変更することができます。
content_for を使用する方法の問題点
content_for を使用する方法でやりたいことは実現できるのですが、以下の点に不満が残ります。
- ページのタイトル文字列が個々の View ファイルに分散してしまう。
- 個々の View ファイルで「content_for :title」を忘れた場合に気付きにくい(「yield :title」の部分が空白になるだけ)。
■[Ruby on Rails] ページごとに <title> の値を変更する方法(page_title メソッドを実装する)
やりたいこと
やりたいことをまとめると以下の通りです。
- ページのタイトル文字列は一箇所で管理したい。
- 個々の View ファイルに特別な処理は書きたくない。
方針
やりたいことを実現するために、以下の方針を採ることにします。
- 各ページのタイトルは config/locales/ja.yml に記述する*1。
- 現在表示しているページを判別し、それに対応する値を config/locales/ja.yml から取得する。
具体的には config/locales/ja.yml には以下のように記述します。
# config/locales/ja.yml
ja:
# このセクションに各ページのタイトル文字列を記述することにします
title:
users:
index: ユーザ一覧
show: ユーザ詳細
new: ユーザ登録
create: ユーザ登録
edit: ユーザ編集
update: ユーザ編集
...
そして、app/views/layout/default.html.erb(レイアウト用のファイル)は以下のように記述できる仕組みを作ります。
<!-- app/views/layout/default.html.erb --> <html> <head> <!-- page_title は現在表示しているページのタイトル文字列を返すメソッドとします --> <title><%= page_title %></title> </head> <body> <%= yield %> </body>
page_title メソッドを実装する
現在表示しているページのタイトル文字列を返す page_title メソッドを ApplicationHelper に実装します。
config/locales/ja.yml に記述したタイトル文字列の取得は、以下のように I18n.t メソッドで行うことが出来ます。
I18n.t('title.users.index') # => 'ユーザ一覧'
ですので、これから作成する page_title メソッドでは「I18n.t('title.XXX.YYY')」の「XXX」と「YYY」の部分に指定する値(コントローラ名とアクション名)を準備すれば良いことになります。
現在アクセスされているページのコントローラ名とアクション名はそれぞれ「controller_name」および「action_name」で取得することができる*2ので、page_title メソッドの実装は以下のようになります。
# app/helpers/application_helper.rb module ApplicationHelper # 現在のページのタイトルを取得する def page_title t("title.#{controller_name}.#{action_name}") end end
page_title メソッドの問題点
ここで実装した page_title メソッドにはいくつかの問題点(制限)があります。それは、config/locales/ja.yml の以下の部分です。
# config/locales/ja.yml
ja:
title:
users:
index: ユーザ一覧
show: ユーザ詳細
new: ユーザ登録
create: ユーザ登録 # new と同じ値を指定する必要がある
edit: ユーザ編集
update: ユーザ編集 # edit と同じ値を指定する必要がある
...
コメントに書いた通り、「# new と同じ値を指定する必要がある」と「# edit と同じ値を指定する必要がある」というのが制限になります。
理由は、render メソッドで描画するテンプレートを明示的に指定された場合に対応できていないためです。以下のコードを見てください。
class UsersController < ActionController::Base def new @user = User.new end def create @user = User.new(params[:user]) if @user.save redirect_to :action => index else render :action => :new # 登録画面が表示されるが action_name の値はあくまで 'create' end end ... end
コメントに書いたとおり、「render :action => :new」としても「action_name」の値はあくまで 'create' となるため、先ほど作成した page_title メソッドでは「ja: > :title > :users > :create」の値が返されます。
そのため、config/locales/ja.yml の「new と create」および「edit と update」に異なる値を指定すると、validation エラーが発生したときだけページのタイトル文字列が変わってしまうという問題が発生します。
page_title メソッドの問題点 (2)
今度は「登録画面」の次に「確認画面」を入れる場合を考えます。
config/locales/ja.yml の内容は以下のようになります。
# config/locales/ja.yml
ja:
title:
users:
index: ユーザ一覧
show: ユーザ詳細
new: ユーザ登録
confirm_new: ユーザ登録確認 # この場合はどうしよう?
create: ユーザ登録
edit: ユーザ編集
confirm_edit: ユーザ編集確認 # この場合はどうしよう?
update: ユーザ編集
...
すると、「new と confirm_new に同じ値を指定する」というアプローチでは苦しくなります。登録確認画面のタイトルは「ユーザ登録」ではなく「ユーザ登録確認」としたいのですが、そうしてしまうと登録画面で validation エラーが発生した場合に、ページのタイトルが「ユーザ登録確認」となってしまいます。
■[Ruby on Rails] ページごとに <title> の値を変更する方法(page_title メソッドを改良する)
方針
render メソッドでテンプレート名が明示的に指定された場合に問題があることが分かったので、以下の方針で対応します。
- render メソッドをオーバーライドしてレンダリング対象のテンプレート名を @current_render_target に保持する。
- page_title メソッドでは @current_render_target の値からページのタイトル文字列を取得する。
render メソッドをオーバーライドする
render メソッドは ApplicationController でオーバーライドします。
render メソッドが呼び出されるパターンと @current_render_target に設定する値をまとめます。
- 引数がハッシュの場合
- (A) 「render :action => :new」のようにテンプレート名が指定される場合は指定されたテンプレート名を設定する。
- (B) テンプレート名が指定されない場合は action_name の値を設定する。
- 引数がハッシュではない場合
- (C) 「render :new」のようにテンプレート名が指定される場合は指定されたテンプレート名を設定する。
- (D) テンプレート名が指定されない場合(明示的に render メソッドが呼ばれない場合)は action_name の値を設定する。
これを実装すると以下のようになります。
# app/controllers/application_controller.rb class ApplicationController < ActionController::Base def render(options = nil, extra_options = {}, &block) # レンダリング対象のテンプレート名を取得する if render_options.kind_of?(Hash) # (A) または (B) の場合 @current_render_target = render_options[:action] || action_name else # (C) または (D) の場合 @current_render_target = render_options || action_name end # レンダリング処理自体は ActionController::Base.render に任せる super end ... end
page_title メソッドを改良する
ApplicationHelper に作成した page_title メソッドは以下のようになります。
# app/helpers/application_helper.rb module ApplicationHelper # 現在のページのタイトルを取得する def page_title # 変更前 # t("title.#{controller_name}.#{action_name}") # 変更後 t("title.#{controller_name}.#{@current_render_target}") end end
これで render メソッドでテンプレート名が指定された場合にも正しく動作するようになりました。
page_title メソッドをもう少し改良する
page_title メソッドをもう少し改良して、
- 引数で指定されたページのタイトルを返す
- 引数が省略された場合は現在のページのタイトルを返す
とすれば、より便利になります。
実装は以下のようになります。
# app/helpers/application_helper.rb module ApplicationHelper # 現在のページのタイトルを取得する def page_title(controller=controller_name, action=@current_render_target) t("title.#{controller}.#{action}") end end
■[Ruby on Rails] ページごとに <title> の値を変更する方法(まとめ)
長くなりましたので、最終的に行ったことをまとめます。
メッセージリソース(config/locales/ja.yml)
メッセージリソース(config/locales/ja.yml)には各ページのタイトル文字列を記述します。
# config/locales/ja.yml
ja:
# 各ページのタイトル文字列
title:
users:
index: ユーザ一覧
show: ユーザ詳細
new: ユーザ登録
create: ユーザ登録
edit: ユーザ編集
update: ユーザ編集
...
レイアウト用のファイル(app/views/layout/default.html.erb)
レイアウト用のファイル(app/views/layout/default.html.erb)では page_title メソッドで取得した値を <title> に指定します。
<!-- app/views/layout/default.html.erb --> <html> <head> <!-- page_title は現在表示しているページのタイトル文字列を返すメソッドとします --> <title><%= page_title %></title> </head> <body> <%= yield %> </body>
ApplicationController(app/controllers/application_controller.rb)
ApplicationController では render メソッドをオーバーライドしてレンダリング対象のテンプレート名を @current_render_target に保持します。
# app/controllers/application_controller.rb class ApplicationController < ActionController::Base def render(options = nil, extra_options = {}, &block) # レンダリング対象のテンプレート名を取得する if render_options.kind_of?(Hash) @current_render_target = render_options[:action] || action_name else @current_render_target = render_options || action_name end # レンダリング処理自体は ActionController::Base.render に任せる super end ... end
ApplicationHelper(app/helpers/application_helper.rb)
ApplicationHelper では @current_render_target に保持されているレンダリング対象のテンプレート(または引数で指定されたページ)のタイトル文字列を取得します。
# app/helpers/application_helper.rb module ApplicationHelper # 現在のページのタイトルを取得する def page_title(controller=controller_name, action=@current_render_target) t("title.#{controller}.#{action}") end end
2008-07-20
■[本] 50代からの選択
50代からの選択―ビジネスマンは人生の後半にどう備えるべきか (集英社文庫 お 66-1)
- 作者: 大前研一
- 出版社/メーカー: 集英社
- 発売日: 2008/02/20
- メディア: 文庫
- 購入: 1人 クリック: 14回
- この商品を含むブログ (15件) を見る
50代からの選択―ビジネスマンは人生の後半にどう備えるべきか (集英社文庫 お 66-1) を読んだ。社会人となって最初の10年くらいで覚えるべきことは全てマスターする。そうすると35歳くらいが知識も体力もベストな状態となる。そこからしばらくは出世の順番を待つ「魔の15年」。そして50代。50代はその後の人生の展開を見つめなおすべき重要な時期。
自分の会社での将来はどうなりそうか。
- 上位のポストが与えられる。
- 上位のポストが与えられないまま定年を迎える。
- 肩をポンと叩かれてリストラされる。
人生の後半に向けての準備のまえに、現状をしっかり認識しておくことが重要ということが分かった。出世できるから人生の成功というわけではなく、現状を認識した上で人生の後半に向けて備えましょう、それが50代です、という話。
本書を読んでもう一つ分かったことは、年金制度は「少数の弱者(若者)と多数の強者(老人)」という構造になっていること(「弱者が少数」という現象はあまりないらしい)。選挙では若者の票よりも老人の票の方が多い。そのため政治家は老人から票がもらいやすい政策を多く掲げることになる。結果として、若者は払ったお金の半分しか年金がもらえないが、老人はしっかり年金が貰える、ということになる。老人は裕福であり、若者は貧乏(また、その傾向が強まる)ということになる。
2008-07-19
■[本] モリログ・アカデミィ 8
MORI LOG ACADEMY〈8〉レースにかける青春 (ダ・ヴィンチブックス)
- 作者: 森博嗣
- 出版社/メーカー: メディアファクトリー
- 発売日: 2007/12
- メディア: 文庫
- クリック: 9回
- この商品を含むブログ (19件) を見る
MORI LOG ACADEMY〈8〉レースにかける青春 (ダ・ヴィンチブックス) を読んだ。内容は ダ・ヴィンチ電子ナビ - 電子書籍も紙の本も、見つかる、買える、楽しめる! を文庫化したもの。Web だと最新データを無料で閲覧できるが、本は携帯して隙間時間に読むことができる。本だと写真が全て白黒印刷されてしまうが、Web はカラーで写真を見ることができる。帯に長し襷に短し、である(言葉も意味も同時に間違えてみた)。
以下を読んでブログを書こうと思った(むしろ自分のブログがあったことを思い出した)ので久しぶりに書くことにした。
2007年08月25日(土曜日) 【HR】 ブログを毎日書くには
毎日毎日よくも書き続けられるものだ、と言われることが多いが、毎日長文のブログを書いている人はけっこういる(しかも無報酬で)。ようするに、考えていれば、考えていることを書けば良いし、行動していれば、それについて書けば良い。考えてもいないし、行動もしていないと、ちょっと書きにくい。あと、毎日同じことしか考えなかったり、同じ行動しかしなかったりすると、やっぱり書きにくくなる。だから、そうしないようにすると、続くかも。
2008-02-10 Greasemonkey スクリプト作成入門
2007-11-18 Windows Vista の VirtualStore ファイルの罠
■[Vista] Windows Vista の VirtualStore ファイルの罠 - 設定ファイルを編集したが反映されない問題
Windows Vista にある VirtualStore ファイルという機能が原因で
- Windows Vista に Apache をインストールする
- 設定ファイル(httpd.conf)を編集する
- Apache を再起動する
- なぜか変更した設定内容が反映されない
という問題が発生しました。
VirtualStore ファイル機能は「設定ファイルはユーザごとに保存する」という目的で作られた機能のようです。編集しようとした設定ファイルに対する書き込み権限が無い場合に、自動的にそのユーザ用の設定ファイルが作成される、というものです。
今回の場合、
- Apache を管理者権限でインストール
- 管理者ではないユーザで設定ファイル(httpd.conf)を編集する
- 設定ファイルに対する書き込み権限が無いので、VirtualStore ファイルが作成される
- Apache を再起動する(Apache は管理者権限で実行)
- Apache が読み込む設定ファイル(httpd.conf)は編集した設定ファイルではない
となっていたため、いくら設定ファイルを変更しても反映されない、という問題になっていました。
エディタで開いたときに特にエラーが発生することも無く普通に保存できてしまうので、なかなか気づきにくい問題でした。
この問題の対応策は「設定ファイルを管理者権限で編集する」ことになります。エクスプローラ上で実行ファイル(エディタ)のアイコンを [Shift] キーを押しながら [右クリック] し、ポップアップメニューの「管理者として実行」を選択すれば、アプリケーションを管理者権限で実行することができます。
2007-10-28 クイックサーチ機能の活用:よく見るサイトにはキーワードを設定する
2007-05-26 Apache Derby のプロパティ
2007-04-30 Cygwin 導入時に遭遇した問題と解決のメモ
2007-03-11 Ruby の導入(Windows 編)
2007-02-21 Apache Derby における XML データの扱い
2007-02-06 関数ポインタの比較とコンパイラの最適化
2007-01-27 Apache Derby の導入
2007-01-20 ajaxslt で XSLT を実行する
■[JavaScript][Ajax][ajaxslt] ajaxslt で XSLT を実行する
ajaxslt(JavaScript による XSLT の実装)を用いて XSLT を適用するサンプル・コードです。
2007-01-19 Google Gadgets API 入門 (9)
■[Google Gadgets] Google Gadgets API 入門 (9)
ユーザ・プリファレンスのデータ型に新しく "list" 型が追加されました。(Google Group での投稿 Google Gadgets: New Features and Updates! でアナウンスが行われました)。今回は、"list" 型のユーザ・プリファレンスの解説を行います。
2007-01-18 Google Gadgets スタイルシート指定の注意点
■[Google Gadgets] Google Gadgets スタイルシート指定の注意点
Google Gadgets で、ワイルドカードを使用してスタイルを指定すると、うまく反映されない場合がある、という話です。
2007-01-16 ajaxslt で XPath を使用する
■[JavaScript][Ajax][ajaxslt] ajaxslt で XPath を使用する
ajaxslt(JavaScript による XSLT の実装)を用いて XPath 式を評価するサンプル・コードです。
2007-01-13 雑記三本立て
■[その他] Google News for Blog
Google News と同じアプローチをブログに対して行うことを考える。ブログにかかれる情報の多くは断片的なものなので、それを自動的にまとまった情報として再構成することには価値があると思う。世のブログでどんなことが多く語られているのか、ということも知ることができるのでブログ検索とは違った便利さも生まれると思う。
■[その他] ブログを書くのは大変だ
ブログを書くことの心理的な負担が大きいと感じてしまうのですが、一般のブログ書きの人達はどういう感覚を持っているのだろうか。新しい記事を投稿すると、それをトリガとして、RSS が生成されたり、ブログ・サービスの新着日記一覧に反映されたりする。「ブログで誰でもカンタンに情報発信」というけれど、手軽に情報発信するたびに、その裏側で色々な処理が行われていることを慮る(おもんばかる:あれこれ思いをめぐらせる)とブログの「投稿ボタン」を押す指が重くなってしまう。全くそんなことを気にする必要は無いのだが…。そう感じる原因はブログを始めて日が浅いからだと解釈しておく。
ブログを書くことの心理的な負担、その2。普通のホームページでは「ここで完成」という区切りを意識することはほとんどなく、ヒューリスティックにニョキニョキ成長させていくイメージで取り組める。完成という区切りのようなものはなく、ちょっとずつ進化させていく、より良い方向にのんびり修正していく、という取り組み方ができる(かなり気持ちの問題だが)。一方のブログは、完成した記事を毎回投稿する、という印象。「投稿」という操作を「この記事は完成品です」という決意表明だと考えてしまう。金輪際編集しません、みたいな。ちょっと書くたびに「完成」を迫られているような気がしてしまう。
■[その他] すこぶるトラブルPPT
Microsoft Power Point を使っていたら、突然強制終了してしまった。「パワーポイント」と書くか「Microsoft Power Point」と書くか迷ったので、最初だけ「Microsoft Power Point」、以降は「パワーポイント」と表記することにした。突然強制終了したパワーポイントは、すかさず再起動して復活。「ファイルを修復しますか?」と尋ねてくる。
「そんなことしてる暇があったらバグ直せ」と言われそうな機能ではあるけれど、信頼性を向上させる良い機能だと思う。プログラムにバグは付き物なので、予期せぬ不具合が発生したときにどうするか、きちんと対処されていると評価すべきだと思う。ちなみに頻繁に落ちるわけではない。しばしばパワーポイントを使い、しばしば落ちる。
この死んだら復活する機能。実はタスクバーにも備わっている。OS ぐるみで死んだら復活するのである(誇張)。しかもプログラム的にタスクバーが復活したことを検出することができる。復活の呪文は RegisterWindowMessage("TaskbarCreated")。タスクバーが死んでしまうと、タスクトレイに追加されているアイコンも消えてしまう。そのため、常駐プログラムなんかを作るときにはタスクバーが復活を検出して、タスクトレイにアイコンを登録しなおす、という対処を行う。そうしないと、タスクトレイのアイコンが消えたままになってしまうので、ユーザがプログラムを終了する手段を失ってしまうことになる(タスクマネージャなどがあるにはあるが)。
2007-01-12 Google Gadgets API 入門 (8)
■[Google Gadgets] Google Gadgets API 入門 (8)
今回は Feature-specific ライブラリから、タブ機能を提供する MiniMessage ライブラリについて解説します。