1 GB を超えるような csv ファイルを一度に読み込むとメモリエラーが発生する場合がある
さらに処理時間もすごくかかるため、ストリーム処理に変更などして処理の高速化した際の方法を備忘録として残しておく

方法

File#open メソッドと CSV#new メソッドを組み合わせて実現する

環境

  • ruby: 3.0.4
  • rails: 6.1.6.1

実際のコード

csv ファイルの特定の文字列を置換する処理

前提

header 付の csv ファイル

リファクタリング前

class Converter
  def self.convert
    # 読み取り専用でファイルを開く
    read_only_file = File.open("{ファイルパス}" , "r")

    # 保存用バッファ
    buffer = read_only_file.read()

    # バッファの中身を変換
    buffer.gsub!({置換対象文字列}, {置換文字列})

    # ファイルを書き込みモードで開き直す
    file = File.open("{ファイルパス}" , "w")

    # 変更内容を出力する
    file.write(buffer)

    # close
    read_only_file.close()
    file.close()
  end
end

リファクタリング後

require 'csv'
require 'fileutils'

class Converter
  def self.convert
    file_basename = "{拡張子を除いたファイル名}"

    # 読み取り専用でファイルを開く
    File.open("#{file_basename}.csv", "r") do |file|
      break if File.zero?("#{file_basename}.csv")

      # 一時ファイルを書き込みモードで開く
      write_file = File.open("#{file_basename}_tmp.csv", "w")
      # csv の header を書き込む
      write_file.write(file.readline)
      file.rewind

      CSV.new(file, headers: true).each do |row|
        # 読み込んだ行内の文字列を置換する
        write_file.write(row.to_s.gsub({置換対象文字列}, {置換文字列}))
      end

      write_file.close
    end

    FileUtils.move("#{file_basename}_tmp.csv", "#{file_basename}.csv") if File.exist?("#{file_basename}_tmp.csv")
  end
end

ヘッダは force quote なし ボディは force quote ありのようにしたい場合

id,名前
"1","太郎"

CSV#open メソッドを使用する

require 'csv'
require 'fileutils'

class Converter
  def self.convert
    file_basename = "{拡張子を除いたファイル名}"

    # 読み取り専用でファイルを開く
    File.open("#{file_basename}.csv", "r") do |file|
      break if File.zero?("#{file_basename}.csv")

      # 一時ファイルを書き込みモードで開く
      CSV.open("#{file_basename}_tmp.csv", "w") do |csv|
        # csv の header を書き込む
        csv << file.readline.parse_csv
      end
      file.rewind

      # 一時ファイルを追記モードで開く
      CSV.open("#{file_basename}_tmp.csv", "a", force_quotes: true) do |csv|
        CSV.new(file, headers: true).each do |row|
          # 読み込んだ行内の文字列を置換する
          csv << row.to_s.gsub({置換対象文字列}, {置換文字列}).parse_csv
        end
      end
    end

    FileUtils.move("#{file_basename}_tmp.csv", "#{file_basename}.csv") if File.exist?("#{file_basename}_tmp.csv")
  end
end

1.4 GB 程の csv ファイルを処理した際にリファクタリング前は、1 時間 40 分程度だったものが 5 分程度の処理時間に改善した