Torihaji's Growth Diary

Little by little, no hurry.

Ruby の csvにおいて、取得したい列番号を指定して値を取り出す

はじめに

どうも、torihaziです

Rubycsvを扱う時があり、かつ "ある列の値を取り出したい"ということがありました。

どうやるんだとなったので、色々調べてやってみました。

version

ruby -v
ruby 3.2.0 (2022-12-25 revision a528908271) [aarch64-linux]

rails cで

困ったらrails cで色々実験です。

rubycsvと言ったら

docs.ruby-lang.org

なのでこいつをrequireします

の前にヒアドキュメントを使って、文字列を作ります。

app(dev)" users = <<CSV
app(dev)" id,first name,last name,age
app(dev)" 1,taro,tanaka,20
app(dev)" 2,jiro,suzuki,18
app(dev)" 3,ami,sato,19
app(dev)" 4,yumi,adachi,21
app(dev)> CSV

次にrequireします。

この文字列をCSV.parseを使ってパースすることで加工することができます

 c = CSV.parse(users)
=> [["id", "first name", "last name", "age"], ["1", "taro", "tanaka", "20"], ["2", "jiro", "suzuki", "18"], ["3", "ami", "sato", "19"], ["4", "yumi", "adachi", "21"]]

となって、このように配列の配列で返されます。

今回やりたいことは "取得したい列番号を指定して値を取り出す"ことです。

列番号を指定して値を扱うためには CSV::Tableクラスにすると便利です。

そうするとcsv["header"]やcsv[1]とすれば

名前がheaderの列の値や2列目の値を取り出すことができます。

parseを使ってCSV::Tableを作る

c = CSV.parse(users, headers: true)
=>
#<CSV::Table mode:col_or_row row_count:5>
...

headers optionをtrueにすると CSV::Tableクラスになります。

もう少し。

厄介なことにCSV::Tableクラスは モードという概念があり、defaultでは

:col_or_row というモードです。これは ヘッダーの名前でしか指定できません。

docs.ruby-lang.org

:col_or_row デフォルトはこのモードです。このマニュアル内ではミックスモードと呼んでいます。行単位でアクセスするか列単位でアクセスするか自動的に判断します。

:row ロウモード。テーブルに行単位でアクセスします。

:column カラムモード。テーブルに列単位でアクセスします。

現状のモードは CSV::Tableクラスのインスタンスメソッド modeを使って確認できます。

app(dev)> c = CSV.parse(users, headers: true)
=>
#<CSV::Table mode:col_or_row row_count:5>
...
app(dev)> c.mode
=> :col_or_row

これで2通りの方法で列の値を見てみると

app(dev)> c["id"]
=> ["1", "2", "3", "4"]
app(dev)> c[0]
=> #<CSV::Row "id":"1" "first name":"taro" "last name":"tanaka" "age":"20">

今は ミックスモードなので、1列目を [0]とindex指定でアクセスする事はできません。

docs.ruby-lang.org

indexで指定するとrowのデータになってしまいます。

そこでmodeを変換します。

app(dev)> c["id"]
=> ["1", "2", "3", "4"]
app(dev)> c[0]
=> #<CSV::Row "id":"1" "first name":"taro" "last name":"tanaka" "age":"20">
app(dev)>
app(dev)>
app(dev)>
app(dev)> c.by_col!
=>
#<CSV::Table mode:col row_count:5>
id,first name,last name,age
1,taro,tanaka,20
2,jiro,suzuki,18
3,ami,sato,19
4,yumi,adachi,21

app(dev)> c[0]
=> ["1", "2", "3", "4"]

こうすると先ほど 0 でアクセスしたら rowになっていたものが rowとならずに取得することができました。

これであれば、穴あきのheaderであってもアクセスできそうです。

例えば

app(dev)" users = <<CSV
app(dev)" ,first name,last name,age
app(dev)" 1,taro,tanaka,20
app(dev)" 2,jiro,suzuki,18
app(dev)" 3,ami,sato,19
app(dev)" 4,yumi,adachi,21
app(dev)> CSV

先ほどのidを消したものです

これで1列目を指定したいですが、ヘッダーの名前では指定できそうにありません。

そこでmode変えてできるかを試してみます。

app(dev)> c.by_col!
=>
#<CSV::Table mode:col row_count:5>
,first name,last name,age
1,taro,tanaka,20
2,jiro,suzuki,18
3,ami,sato,19
4,yumi,adachi,21

app(dev)>
app(dev)>
app(dev)> c[0]
=> ["1", "2", "3", "4"]

変わらず行けましたね、納得です。

終わりに

実務で穴あきのheaderを持つcsvを扱うことになりそうだったので予習です。

なんとかなりそう