85
76

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Shift-JISなCSVを読み込む・書き出しするときにエラーを起こさない数少ない方法

Last updated at Posted at 2015-04-10

CSV#read,CSV#foreachでSJISを読み込みたい

ECなどいまだにSJIS文化がはびこってるシステムとやりとりをするるとどうしてもSJISを入出力しないといけなくなります。RubyでCSVを読み込むには、CSVと、Fileをつかって読み込む方法がありますが、どちらにしても文字コードの壁がそびえ立っています。Fileをつかわずに、CSVだけで完結する方法をとってみました。

文字コードを指定

大概の場合このケースでいけることが多いです。

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メソッドをみてみます。

#@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クラスのインスタンスを、再度オプションと一緒にコンストラクタに渡しています。

#@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にencodinginternal_encodingextranal_encoding以外のオプションがあるとエラーをあげる仕組みになっています。CSVクラスにとっては、直接関係ない読み込みオプションはエラーを出してるみたいです。

モンキーパッチ

csv.rbのコンストラクタにモンキーパッチを当ててundefreplaceのオプションを削除すれば動きそうです。というわけで出来たのが以下のパッチです。

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に入れています。

使い方

開くときにオプションをつけて呼び出せます。

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にするとエラーが起きてしまうためにさらにエラーが起きないように対応してあります。

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

#使い方

sjis_output.rb
sjis_string = "SJISにしたい文字列を入れます"
p sjis_string.sjiable.encode("cp932")

Rails4からview側でロジックかいてあげるとCSVファイルをわざわざつくらなくていいので楽ちんであります。

参考:
http://esoz.blog.fc2.com/blog-entry-59.html

それでは良いSJISライフを!

85
76
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
85
76

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?