【Ruby入門説明書】csvについて詳しく解説
はじめに
CSV(comma-separated values)というのは、文字で構成されているテキストファイル(テキストデータ)です。
ただし、CSVの場合は、データがカンマ「, 」で区切られて列を表し、改行で行を表現している表形式のデータで、ファイルの拡張子が「.csv」になっています。また、1行めに、フィールド名がカンマ区切りで入っているヘッダが含まれている場合もあります。
例えば、次のようなファイルです。
test.csv
name,age,sex,height,weight
ryosuke,21,male,165,65
yuri,22,female,159,45
daiki,24,male,172,68
kei,28,female,163,48
test.csvはデータがカンマ区切りで記載されたテキストファイルで、ファイル拡張子が「.csv」になっていますので、CSVファイルの要件を満たしています。
なお、Rubyでは、テキストファイルの読み書きなどは容易にできるクラスやメソッドが準備されていますので、決まりが分かればCSVデータを解析して扱う(配列に入れたり、ハッシュにしたりなどする)ことはできるでしょう。
しかし、CSVについては、一般的によく使われていることから、Rubyでは安全に扱える専用のクラスが準備されています。
今回は、そんな「CSVクラス」について、詳しく解析していきます。
RubyでCSVファイルを扱う
RubyでCSVを安全に扱うことができるCSVクラスは、残念ながらそのまま何もせずに使えるクラスではありません。 rbファイルの先頭辺りで、以下のようにrequireメソッドを使って読み込む必要があります。
require ‘csv’
requireメソッドは、別ファイルに定義されているクラスなどを読み込むメソッドです。そのため、以上の1文を追加することで、CSVクラスを使うことができるようになります。
では、ここからCSVファイルやCSVデータを扱いながら、CSVクラスのメソッドの中で、使用頻度の高いメソッドを紹介していきましよう。
その前に、冒頭で紹介した「test.csv」を、Rubyのプログラムのファイル(.rbファイル)を保存する場所と同じ場所に保存しておいてください。
以降は、test.csvファイルを使って、説明していきます。
CSVを扱うメソッド一覧
CSVデータは前述しているようにCSV形式でファイルに保存されたデータがほとんどです。
そのため、ファイルからの読み込みや書き込みが必要です。また、CSVはデータを表の形式で持っていますので、行ごとや列ごとに処理を行う場合が多いです。CSVクラスには、そういった一見面倒そうな処理を比較的簡単に扱えるメソッドが揃っています。
そんな、CSVを扱う主なメソッドを一覧で紹介しましょう。
メソッド | 説明 |
---|---|
open | ファイルを開く 読み込みモード、新規書き込みモード、追記モード、読み書きモードなどのモードでファイルを開くことができる。 |
close | ファイルを閉じる |
<<add_row puts | 1行書き込む |
read | CSVファイルの一括読み込み |
foreach | CSVファイルを1行ごとに読み込んで繰り返す |
parse | CSV形式の文字列を2次元配列へ変換する |
parse_line | 1行のCSV形式の文字列を配列へ変換する |
to_csv | 配列をCSV形式の文字列へ変換する |
ファイルを開いて読み書きする(open、<<、add_row、puts)
CSVファイルを開くメソッドには、以下の2つの構文があります。
CSVオブジェクト = CSV.open(CSVファイル, モード)
CSV.open(CSVファイル, モード) do |ブロック引数|
end
「CSVファイル」には、CSVファイルのフォルダとファイル名を記載します。実行するプログラムファイルからの相対位置を指定できますので、プログラムファイルと同じフォルダにある場合は、ファイル名だけで処理可能です。
「モード」には、ファイルの処理方法に合わせて、以下の6種類のいずれかを指定します。なお、デフォルト値として”r”が設定されています。
モード | 内容 |
---|---|
r | ファイルを読み取りモードで開きます ファイルがなければエラーが発生します |
w | ファイルを新規書き込みモードで開きます ファイルがなければ作成します。すでに存在している場合はファイルを空にして開きます |
a | ファイルを追加書き込みモードで開きます ファイルがなければ作成します。すでに存在している場合はそのまま開きます 書き込みの処理を行うと、ファイルの末尾に追記します |
r+ | ファイルを読み書き両用モードで開きます ファイルがなければエラーが発生します 書き込み処理を行うと先頭からから書き込みます |
w+ | ファイルを読み書き両用モードで開きます ファイルがなければ作成します。すでに存在している場合はファイルを空にして開きます 書き込み処理を行うと先頭からから書き込みます |
a+ | ファイルを読み書き両用モードで開きます ファイルがなければ作成します。すでに存在している場合はそのまま開きます 読み込みは先頭から行いますが、書き込みはファイルの末尾に追記します |
1つめの構文を使えば、CSVファイルを開いて、そのデータを含んだCSVオブジェクトを取得できます。
CSVオブジェクトはeachメソッドを使って、繰り返し処理の中でデータを1行ずつ取得できますので、そこで必要な処理を行うことになるでしょう。
ブロックを渡す2つめの構文では、ブロック引数にCSVオブジェクトが渡されますので、1行ずつのデータ処理を行うために、ブロックの中でeachメソッドを使った繰り返しが必要になります。
これは、一見面倒に見えますが、CSVファイルを開いてデータを読み込む(書き込む)処理をブロック内に収めるため、「明示的にファイルを閉じる必要がない」という利点があります。
じつは、1つめの構文を使った場合、明示的にファイルを閉じる処理(closeメソッド)を行わなければ、ファイルが開いたままになってしまいます。ファイルを開ける数は限られていますので、一定の間動かすと新たにファイルを開けなくなるなど、解析の困難な不具合を作り出してしまうことになります。
※ただし、1つや2つのファイルを開いているだけでは、プログラム終了時にRubyがファイルを閉じますので、大きな問題には見えません
そのため、明示的にcloseメソッドを呼び出す必要のない2つめの構文の方が、安全なプログラムだと言えるでしょう。
ファイルの読み込み(open)
まず、ファイルの読み込みについて、それぞれの構文の具体例を確認しておきましょう。
csv00-1.rb
require ‘csv’
# 1つめの構文で読み込む
csv = CSV.open(“test.csv”)
csv.each do |row|
p row
end
csv.close # 必ずファイルを閉じること
puts(“——————–“)
# 2つめの構文で読み込む
CSV.open(“test.csv”) do |csv|
csv.each do |row|
p row
end
end
# ブロックの処理が終わると、自動的にファイルが閉じられる
(結果)
[bash][“name”, “age”, “sex”, “height”, “weight”][“ryosuke”, “21”, “male”, “165”, “65”][“yuri”, “22”, “female”, “159”, “45”][“daiki”, “24”, “male”, “172”, “68”][“kei”, “28”, “female”, “163”, “48”]——————–[“name”, “age”, “sex”, “height”, “weight”][“ryosuke”, “21”, “male”, “165”, “65”][“yuri”, “22”, “female”, “159”, “45”][“daiki”, “24”, “male”, “172”, “68”][“kei”, “28”, “female”, “163”, “48”][/bash]
openメソッドでファイルを開くことで、ファイルのデータを持ったCSVオブジェクトが取得できます。
そして、CSVオブジェクトのeachメソッドを使って、内部のデータを1行ずつ取り出して処理するわけです。
ヘッダありのデータを扱う
CSV形式のデータは表形式のデータです。つまり、1行めのヘッダは、フィールド名が入っているだけでデータではありません。しかし、csv00-1.rbでは、ヘッダもデータと同じように扱われています。
ヘッダをヘッダとして扱うためには、ファイル読み込み時に、以下のようなオプション設定を行わなければいけません。
open(CSVファイル, モード, headers: true)
オプション設定することで、先頭行がヘッダとして扱われます。また、trueの代わりに、カンマ区切りの文字列を、渡すことで、区切られた文字列をフィールド名としたヘッダとして扱われます。
ヘッダを扱えることのメリットは、戻り値が配列ではなくRowオブジェクトになることです。Rowオブジェクトは、ハッシュと同じく、各要素にフィールド名でアクセスできますので、プログラムの可読性が上がることにつながります。
csv01.rb
require “csv”
# 1行めをヘッダとして扱う
CSV.open(“test.csv”, headers: true) do |csv|
csv.each do |row|
p row[“name”]end
end
puts(“——————-“)
# ヘッダを追加する
CSV.open(“test.csv”, headers: “name1,age1,sex1,height1,weight1”) do |csv|
csv.each do |row|
p row[“name1”]end
end
(結果)
[bash]“ryosuke”“yuri”
“daiki”
“kei”
——————-
“name”
“ryosuke”
“yuri”
“daiki”
“kei”
[/bash]
ファイルの書き込み(<<、add_row、puts)
書き込む場合も、openメソッドでCSVオブジェクトを取得するところまでは同じです。
ただ、書き込む場合は、1行書き込みのメソッドを利用することになります。
1行書き込みのメソッドには、3種類(<<、add_row、puts)ありますが、どれも、引数として1行分のデータが格納された配列を渡します。このとき、要素数が多くても少なくても、エラーが出ることなく1行として追加されますので、確認処理を事前に行うなどしておかなければ、意図しない動作をしてしまう可能性がありますので、気を付けましょう。
では、ファイルを”a”で開いて追記をする例を見てみましょう。
csv02.rb
require ‘csv’
data1 = [“hikaru”,”27″,”male”,”161″,”60″]data2 = [“kota”,”28″,”male”,”178″,”69″]
# 1つめの構文で書き込む
csv = CSV.open(“test.csv”,”a”)
csv.add_row(data1) # 1行追加
csv.close # 必ずファイルを閉じること
# 表示して確認する
CSV.open(“test.csv”) do |csv|
csv.each do |row|
p row
end
end
puts(“——————–“)
# 2つめの構文で書き込む
CSV.open(“test.csv”,”a”) do |csv|
csv << data2 # 1行追加
end
# ブロックの処理が終わると、自動的にファイルが閉じられる
# 表示して確認する
CSV.open(“test.csv”) do |csv|
csv.each do |row|
p row
end
end
(結果)
[bash][“name”, “age”, “sex”, “height”, “weight”][“ryosuke”, “21”, “male”, “165”, “65”][“yuri”, “22”, “female”, “159”, “45”][“daiki”, “24”, “male”, “172”, “68”][“kei”, “28”, “female”, “163”, “48”][“hikaru”, “27”, “male”, “161”, “60”]——————–[“name”, “age”, “sex”, “height”, “weight”][“ryosuke”, “21”, “male”, “165”, “65”][“yuri”, “22”, “female”, “159”, “45”][“daiki”, “24”, “male”, “172”, “68”][“kei”, “28”, “female”, “163”, “48”][“hikaru”, “27”, “male”, “161”, “60”][“kota”, “28”, “male”, “178”, “69”] [/bash]
なお、読み込み、書き込みともに、1行ごとの配列を扱う点がポイントです。
配列であれば、様々なメソッドがありますので、データを扱うのにとても重宝することでしょう。
CSVファイルからのその他の読み込み(read、foreach)
読み込みについては、openメソッド以外にもメソッドが用意されています。 openメソッドでは、データへのアクセスにどうしてもCSVオブジェクトが必要になっています。 しかし、直接データへアクセスしたいことも少なくないでしょう。 そんなときには、次の読み込み用のメソッドを使う方がシンプルになります。
一括読み込み
配列 = CSV.read(CSVファイル)
1行ずつ読み込み
CSV.foreach(CSVファイル) do |line|
処理
end
どちらを使ってもCSVオブジェクトを介することなく、まるでファイルから直接配列を読み込んでいるかのように、データを取得できます。なお、readメソッドの戻り値は、行ごとの配列の配列という2次元配列になっていますので、注意しましょう。
csv00-1.rbと同じ処理をこれらのメソッドで実現してみます。
csv00-2.rb
require ‘csv’
# readメソッドで読み込む
csv = CSV.read(“test.csv”)
csv.each do |row|
p row
end
# closeは不要
puts(“——————–“)
# foreachメソッドで読み込む
CSV.foreach(“test.csv”) do |row|
p row
end
# closeは不要
結果は、csv00-1.rbと同じになります。
※ただし、csv02.rbを実行したあとの場合は、csv00-1.rbよりも多く表示されます。
readメソッドはcloseメソッドを使用する必要はありませんので、不具合を作るリスクがありません。また、foreachメソッドでは、ブロック引数に1行ごとのデータが格納されていますので、プログラムが格段にシンプルになっています。
なお、readメソッド、foreachメソッドともに、openメソッドと同じくheadersオプションを使うことが可能です。
CSV形式の文字列を扱う
ここまでに紹介したメソッドは、ファイルからCSVデータを取り出して配列の形にする変換処理とも言えます。
つまり、読み込みがCSV形式から配列への変換、書き込みが配列からCSV形式への変換とも解釈できるわけです。
じつは、CSVクラスを使えば、ファイルを介することなく、CSV形式になっている文字列(”,”と改行で区切られた文字列)と配列の相互変換が可能になっています。
CSV文字列から配列へ(parse、parse_line)
“,”と改行で区切られた文字列(CSV形式の文字列)を配列へ変換するには、parseメソッドを使います。
CSV.parse(CSV文字列)
文字列に改行が含まれている場合、改行までを1つの要素(CSVデータの1行)として、配列へ変換します。
require “csv”
datas = “name,age,sex\nyamada,23,male\nsakurada,28,male”
p(CSV.parse(datas))
(結果)
[bash][[“name”, “age”, “sex”], [“yamada”, “23”, “male”], [“sakurada”, “28”, “male”]][/bash]なお、1行分のCSV形式の文字列でも、parseメソッドは2次元配列を返してきてしまいます。
そのため、1行分のデータだけ変換したい場合は、以下のparse_lineメソッドを使う方が良いでしょう。
CSV.parse_line(CSV文字列)
比較してみましょう。
require “csv”
datas = “ino,30,male”
p(CSV.parse(datas))
p(CSV.parse_line(datas))
(結果)
[bash][[“ino”, “30”, “male”]][“ino”, “30”, “male”][/bash]配列をCSV形式にする(generate)
parseメソッドとは逆に、配列からCSV形式の文字列を作りたい場合は、generateメソッドを使います。
CSV.generate(CSV形式の文字列 = “”) do |ブロック引数|
end
ブロック引数には、引数で渡された文字列をCSVオブジェクトへ変換したものが格納されます。
戻り値としては、ブロック引数のCSVオブジェクトを文字列に変換したものが返されます。
generateメソッドは、引数に渡した「CSV形式の文字列に対して、配列の形式で追加するとCSV形式の文字列で追加してくれる」メソッドだと解釈した方が分かりやすいかもしれません。
ただ、引数のデフォルト値が空文字列であるため、配列からCSV形式の文字列への変換に使うことができるわけです。
require “csv”
# 単純な変換
csv_string = CSV.generate() do |csv|
csv << [“yamada”,”23″,”male”]end
p(csv_string)
puts(“—————–“)
# 追加
csv_string = CSV.generate(csv_string) do |csv|
csv << [“sakurada”,”28″,”male”]end
p(csv_string)
(結果)
[bash]“yamada,23,male\n”—————–
“yamada,23,male\nsakurada,28,male\n”
[/bash]
to_csvメソッドなら直感的に
また、to_csvメソッドを使っても変換することができます。(配列から使いますので、CSVクラスのメソッドではありません)
配列.to_csv
簡単な例を使って、動作を確認しておきます。
require “csv”
arr = [“keito”,”22″,”male”]
p(arr.to_csv)
(結果)
[bash]“keito,22,male\n”[/bash]
なお、2次元配列には対応していませんので、注意してください。
問題「ハッシュをCSVファイルへ保存」
以下のデータを、headerで定義されたヘッダ付きのCSVファイル(q.csv)として保存してください。
pv = {“Apple juice”=>300, “Lemon juice”=>200, “Strawberry juice”=>400, “Orange juice”=>500, “Lemon cookie”=>150, “Peach juice”=>400}
header = [“Item_name”, “price”]
ヒント:ハッシュを配列にするには、to_aメソッドが利用できます。
まとめ
CSVファイルは、表形式のデータをテキストファイルという応用範囲の広いファイル形式で扱うことのできるファイルです。そのため、数多くのソフトウェアやデータベースなどで利用されています。
つまり、CSVファイルを簡単に扱えるCSVクラスを駆使することができれば、Rubyプログラムの応用範囲が格段に広がります。
もちろん、ここで紹介した以外にも、CSVクラスにはメソッドがありますし、応用的な使い方も数多くあります。ぜひ、いろいろと試してみてください。
・CSVはデータがカンマ「, 」で区切られており、ファイルの拡張子が「.csv」になっている(カンマで列、改行で行を表している表形式のデータ)
・CSVクラスを使うには、「require ‘csv’」で定義を読み込む必要がある
・CSVクラスのメソッドでCSVファイルを読み書きするときは、1行ごとの配列として扱う
・openメソッドを使った場合、closeしなければいけない(ブロックを渡す場合は除く)
・headersオプションを使うことで、先頭行をフィールド名の書かれたヘッダとして扱い、フィールド名でアクセスできるRowオブジェクトを取得できる
・to_csvメソッドは2次元配列に対応していない
問題の答え
require “csv”
pv = {“Apple juice”=>300, “Lemon juice”=>200, “Strawberry juice”=>400, “Orange juice”=>500, “Lemon cookie”=>150, “Peach juice”=>400}
header = [“Item_name”, “price”]
CSV.open(“q.csv”,”w”) do |csv|
csv << header
pv.to_a.each do |row|
csv << row
end
end