1. autumnlike
Changes in body
Source | HTML | Preview
@@ -1,174 +1,174 @@
# CSV#read,CSV#foreachでSJISを読み込みたい
ECなどいまだにSJIS文化がはびこってるシステムとやりとりをするるとどうしてもSJISを入出力しないといけなくなります。RubyでCSVを読み込むには、`CSV`と、`File`をつかって読み込む方法がありますが、どちらにしても文字コードの壁がそびえ立っています。`File`をつかわずに、`CSV`だけで完結する方法をとってみました。
## 文字コードを指定
大概の場合このケースでいけることが多いです。
```ruby:csv_sjis.rb
CSV.foreach("/path/to/file", encoding: "Shift_JIS:UTF-8") do |row|
p row #->UTF-8な日本語
end
```
が。
> Encoding::UndefinedConversionError - "\x87U" from Shift_JIS to UTF-8:
とエラーが出てしまいます。
## 文字コードのオプションを付けてみる
File#openの場合には、
> {encoding: "Shift_JIS:UTF-8", undef: :replace}
というオプションをつけて対応できますが、CSV#foreachなど、読み込み系のメソッドではこのオプションに対応しておらずに、エラーが出てしまいます。
> ArgumentError - Unknown options: undef.:
というわけで、csvモジュールにモンキーパッチを当ててこのオプションが有効になるようにします。
## csv.rbを調査
標準添付のcsv.rbのforeachメソッドをみてみます。
```rb
#@1119
def self.foreach(path, options = Hash.new, &block)
return to_enum(__method__, path, options) unless block
open(path, options) do |csv|
csv.each(&block)
end
end
```
オプションごとopenメソッドに渡しています。
```
#@1249
def self.open(*args)
# find the +options+ Hash
options = if args.last.is_a? Hash then args.pop else Hash.new end
# wrap a File opened with the remaining +args+ with no newline
# decorator
file_opts = {universal_newline: false}.merge(options)
begin
f = File.open(*args, file_opts)
rescue ArgumentError => e
raise unless /needs binmode/ =~ e.message and args.size == 1
args << "rb"
file_opts = {encoding: Encoding.default_external}.merge(file_opts)
retry
end
csv = new(f, options)
...
```
openメソッドでは、Fileメソッド`universal_newline`を追加したオプションにを渡しています。ということはここではエラーは起きずにopenされていることになります。
開いたfileクラスのインスタンスを、再度オプションと一緒にコンストラクタに渡しています。
```rb
#@1486
def initialize(data, options = Hash.new)
# build the options for this read/write
options = DEFAULT_OPTIONS.merge(options)
...
init_separators(options)
init_parsers(options)
init_converters(options)
init_headers(options)
init_comments(options)
@force_encoding = !!(encoding || options.delete(:encoding))
options.delete(:internal_encoding)
options.delete(:external_encoding)
unless options.empty?
unless options.empty?
raise ArgumentError, "Unknown options: #{options.keys.join(', ')}."
end
...
```
ここで、`Unknown options`のエラーをあげているようです。追っていくと、optionに`encoding`、`internal_encoding`、`extranal_encoding`以外のオプションがあるとエラーをあげる仕組みになっています。CSVクラスにとっては、直接関係ない読み込みオプションはエラーを出してるみたいです。
## モンキーパッチ
csv.rbのコンストラクタにモンキーパッチを当てて`undef`と`replace`のオプションを削除すれば動きそうです。というわけで出来たのが以下のパッチです。
```rb:config/initializer/csv.rb
module CSVEncodingExtension
def initialize(data, options = Hash.new)
options.delete(:replace)
options.delete(:undef)
super
end
end
CSV.send(:prepend, CSVEncodingExtension)
```
optionsからCSVクラスに直接不要なオプションを削除することで対応しました。
railsで利用しているのでconfig/initializerに入れています。
## 使い方
開くときにオプションをつけて呼び出せます。
```ruby:csv_sjis.rb
CSV.foreach("/path/to/file", encoding: "Shift_JIS:UTF-8", undef: :replace, replace: "*") do |row|
p row #->UTF-8な日本語
end
```
これでどんなSJISがきても大丈夫(なはず)。
#SJISでCSVを書き出す
SJISであって、UTF-8にない文字コードのためにエラーがおきます。
`String`にメソッドを追加して、他の文字コードに置き換えてしまうのが一般的です。ただ、SJIS→UTF-8→SJISの場合、代替文字である`U+FFFD`をSJISにするとエラーが起きてしまうためにさらにエラーが起きないように対応してあります。
```rb:config/initializer/string.rb
class String
def sjisable
str = self
str = str.exchange("U+301C", "U+FF5E")
str = str.exchange("U+2212", "U+FF0D")
str = str.exchange("U+00A2", "U+FFE0")
str = str.exchange("U+00A3", "U+FFE1")
str = str.exchange("U+00AC", "U+FFE2")
str = str.exchange("U+2014", "U+2015")
str = str.exchange("U+2016", "U+2225")
str = str.exchange("U+FFFD", "U+30FB")
end
def exchange(before_str,after_str)
self.gsub( before_str.to_code.chr('UTF-8'),
after_str.to_code.chr('UTF-8') )
end
def to_code
return $1.to_i(16) if self =~ /U\+(\w+)/
raise ArgumentError, "Invalid argument: #{self}"
end
end
```
#使い方
```rb:sjis_output.rb
sjis_string = "SJISにしたい文字列を入れます"
-p sjis_string.sjiable.encode("cp932")
+p sjis_string.sjisable.encode("cp932")
```
Rails4からview側でロジックかいてあげるとCSVファイルをわざわざつくらなくていいので楽ちんであります。
参考:
[http://esoz.blog.fc2.com/blog-entry-59.html](http://esoz.blog.fc2.com/blog-entry-59.html)
それでは良いSJISライフを!