読み取るCSVファイルの許容範囲の拡大
今の状態では、エラー無く、データベースに登録できるCSVファイルの書式があまりにも限定されている。ちょっとでも想定外の書式だと、データを見ることが出来なかったり、英字のエラー画面になってしまう...。サーバー側である程度CSVファイルの書式を最適化して、なるべく無難に登録できるようにしたい。
最適化する項目
- テーブル名(ファイル名)、列名(CSVファイルの1行目)について
- 利用可能な文字は、英数字と_に限定してしまった。それ以外の文字は全て_に置き換える。
- 数字で始まる場合は、先頭に_を付加する。
- 列名(フィールド名)
- 改行コード
- CSVファイルの改行コードは、全てLFに変換する。
- 余分なカンマ
- エクセルで作表してCSVファイルとして書き出すと、1行目から最終行まで、余分なカンマが付加されてしまうことがよくある。余分なカンマは削除するようにした。
コードの修正
- モデル
- app/models/csv.rb
class Csv < ActiveRecord::Base validates_uniqueness_of :file_name def validate errors.add(:file_name, "アップロードするファイルを指定してください。") if @file.nil? || @file.size == 0 end # Tempfileオブジェクトをインスタンス変数に保存する。 # new()や、update_attributes()の時、呼び出される。 def tempfile=(file) return if file.nil? || file.size == 0 @file = file self.file_name = file.original_filename self.file_size = file.size # file.read.sizeとやってしまうと、41行目で@file.readした時、なぜかnilが返る。 # 1回アクセスすると、そのデータは消えてしまう? #self.file_type = f.content_type end # テーブルを追加する。 def create_csv_table #execute_sql("CREATE TABLE #{csv_table} (id INTEGER PRIMARY KEY NOT NULL#{csv_columns});") ActiveRecord::Migration.create_table csv_table do |t| csv_columns.each do |f| t.column f, :string end end end # テーブル名を返す。 def csv_table File.basename(self.file_name, ".*").gsub(/^([\d])|\W/, '_\1') end # 列名の配列を返す。 # 同時に不完全なファイル内容を最適化する。 # インポート用の一時ファイルを保存する。 def csv_columns # ファイルを読み込む。"'は描画する時に調整する。 # 文字コードをUTF-8、改行コードをLFに変換して、先頭の余分な改行は削除する。 # 改行で区切った配列を返す。 str = @file.read str = NKF.nkf('-wLu', str).sub(/^\n+/, "\n") lines = str.split("\n") # 配列の先頭を取り出して、小文字に変換、カンマで区切った配列にして、余分な空白を削除する。 # gsubの内容は以下の通り。 # 数字で始まる時は、先頭に_を付ける。 # 英数字または_でない文字は、_に置き換える。 # split(",")は、行末の余分なカンマを無視する。 # 例:"1,2,,3,,,".split(",") >> ["1","2","","3"] line = lines.shift columns = line.downcase.split(",").map!{|n| n.strip.gsub(/^([\d])|\W/, '_\1')} # 列タイトルが重複している場合は、データ配列に戻して、列タイトルは空行にする。 unless columns == columns.uniq lines.unshift(line) columns = [] end # 列タイトルが空行なら、ABCの列名を追加する。 if columns.empty? name = 'A' lines[0].split(",").size.times do columns << name name = name.succ end end # 列タイトルの一部がブランクなら、ブランクをABCの列名に置き換える。 name = 'A' columns.map! do |column| column = name if column.empty? name = name.succ column end # 行末の余分なカンマを削除する。 n = lines[0].gsub(/[^,]/, '').size - columns.size + 1 lines.map! {|line| line.sub(/,{#{n}}$/, '')} if n > 0 # id列が存在しなければ追加する。 if columns.index('id').nil? id = 0 lines.map! {|line| id +=1; "#{id},#{line}"} end # path列が存在しなければ追加する。 if columns.index('path').nil? columns << "path" lines.map! {|line| "#{line},"} end # インポート用の一時ファイルを保存する。 File.delete("public/temp.csv") rescue nil File.open("public/temp.csv", "wb") do |f| f.write(lines.join("\n")) end # 最初のidフィールドは、読み飛ばす。 columns.shift if columns.first == 'id' columns end # csvデータをインポートする。 # インポートを高速化するため、sqlite3のインポート命令を、直接コマンド実行する。 def import_csv env = ENV['RAILS_ENV'] || 'development' system("sqlite3", "-separator", ",", "db/#{env}.sqlite3", ".import public/temp.csv #{csv_table}") end # テーブルを削除する。 def drop_csv_table ActiveRecord::Migration.drop_table(csv_table) end end
試してみる
以下のようなCSVファイルであっても...
組織,,経費,,
"100",12576,7349,,
"101",7550,6260,,
営業1課計,20126,13609,,
'200,15709,6083,,
'201,6833,6068,,
'202,13691,7130,,
営業2課計,36233,19281,,
'300,15227,9769,,
営業3課計,15227,9769,,
営業1部計,71586,42659,,
'400,15362,5035,,
'401,11746,7910,,
営業1課計,27108,12945,,
'500,12530,8355,,
'501,10017,5519,,
営業2課計,22547,13874,,
営業2部計,49655,26819,,
計,121241,69478,,