RubyでJSONからTSV、TSVからJSONへ変換

#はじめに
###やりたいこと
TSVファイルをJSON形式に、またJSONをTSVファイルに変換

###環境
Ruby 2.6.5
Mac OS 10.15.5

###この記事内で使用するサンプル
~~~meibo.json
[{"name":"john","gender":"m","age":"18"},
 {"name":"paul","gender":"m","age":"20"},
 {"name":"alice","gender":"f","age":"15"},
 {"name":"dabid","gender":"m","age":"17"},
 {"name":"jasmin","gender":"f","age":"17"}]
~~~

~~~meibo.txt
Name	gender	age
john	m	18
paul	m	20
alice	f	15
dabid	m	17
jasmin	f	17
~~~

#JSONからTSVへ変換する方法
~~~ruby
require "json" #JSONを扱うために記述

File.open("meibo.json") do |json|　#JSONファイルを開く
  File.open("meibo.txt", "w") do |txt|　#出力先のTSVファイルを開く
    array = JSON.load(json)　#JSONの読み込み
    column = ["name","gender","age"]　#ヘッダー
    txt.puts(column.join("\t"))　#ヘッダを入れる
    array.each do |line|　#JSON配列の各要素をTSVファイルの各行に入れる
      attr = [line["name"],line["gender"],line["age"]]
      txt.puts(attr.join("\t"))
    end
  end
end
~~~

まずは、JSONファイルを開きます。出力先のTSVファイルも書き込みモードで開いておきます。
次に、JSONファイルを読み込んでいきます。下記のように、JSONは`JSON.load([Fileオブジェクト])`でJSONファイルを読み込んで、ハッシュを要素とする配列で返してくれます。

~~~ruby
require "json"

File.open("meibo.json") do |json|
  array = JSON.load(json)
  p array
end

#=>
[{"name"=>"john", "gender"=>"m", "age"=>"18"}, {"name"=>"paul", "gender"=>"m", "age"=>"20"}, {"name"=>"alice", "gender"=>"f", "age"=>"15"}, {"name"=>"dabid", "gender"=>"m", "age"=>"17"}, {"name"=>"jasmin", "gender"=>"f", "age"=>"17"}]
~~~

返ってきた配列を`each`メソッドで回し、それぞれのハッシュをTSVでの一行一行にしていくイメージです。
TSVの各行に入れていくテクニックとしては、`each`で取り出した各ハッシュのバリューを、キーを直接指定するかたちで取り出し、それらを一度、配列に入れ直します。そして、`join`メソッドを用いてタブ区切りで配列を結合し、その文字列をTSVのファイルの各行に挿入していきます。

出力結果は以下の通りです。

~~~meibo.txt
name	gender	age
john	m	18
paul	m	20
alice	f	15
dabid	m	17
jasmin	f	17
~~~

###ハマったところ

最初は以下のように、`JSON.load`したあとの配列を`each`処理に回す際、各オブジェクトのバリューの並び順通りに、TSVへ格納していきましたが、
**JSONのメンバー（キーとバリューのセット）の並び順は関係ない**とのこと。

つまり`{"name":"john","gender":"m","age":"18"} `と`{"name":"john","age":"18","gender":"m"}`は区別しません。

よって、今回はたまたま、JSONのメンバーの並び順通りでTSVに変換しても、問題ないですが、メンバーの並び順がバラバラのJSONだと、TSVに変換した時おかしくなってしまうことがわかりました。

なので、面倒ですが一度、ハッシュのキーを指定する形でバリューを取り出し、それをカラムの並び順通りに配列にいれてから、TSVへ入れ込むと、確実です。

~~~ruby
require "json" 

File.open("meibo.json") do |json|
  File.open("meibo.txt", "w") do |txt|
    array = JSON.load(json)
    column = ["name","gender","age"]
    txt.puts(column.join("\t"))
    array.each do |line|
      txt.puts(line.values.join("\t")) #JSONの各オブジェクトのバリューの並び通りにTSVへ入力
    end
  end
end
~~~

#TSVからJSONへ変換する方法

~~~ruby
require "json"
require "csv"

hash_ary = []

File.open("meibo.json","w") do |json|
  CSV.foreach("meibo.txt",col_sep: "\t", headers: true) do |line|
    h = {name: line[0], gender: line[1], age: line[2]}
    hash_ary << h
  end
  JSON.dump(hash_ary,json) #to_jsonを使用
end
~~~

まず、書き込みモードでJSONを書くためのファイルを用意します。

TSVファイルはCSVクラスで`foreach`メソッドを使って各行をハッシュに変換していきます。変換したハッシュは一度配列（ここでは、`hash_ary`）にいれて、全ての行が入れ終わったあとに、`JSON.dump`でハッシュからJSONへの変換を行い、ファイルに書き込みます。

出力結果は以下の通り。

~~~meibo.json
[{"name":"john","gender":"m","age":"18"},{"name":"paul","gender":"m","age":"20"},{"name":"alice","gender":"f","age":"15"},{"name":"dabid","gender":"m","age":"17"},{"name":"jasmin","gender":"f","age":"17"}]
~~~

###ハマったところ
~~~ruby
require "json"
require "csv"

hash_ary = []

File.open("meibo.json","w") do |json|
  CSV.foreach("meibo.txt",col_sep: "\t", headers: true) do |line|
    h = {name: line[0], gender: line[1], age: line[2]}
    hash_ary << h.to_json
  end
  json << hash_ary
end
~~~
最初は、上記のように書いてましたが、このようにすると。。

~~~meibo.json
["{\"name\":\"john\",\"gender\":\"m\",\"age\":\"18\"}", "{\"name\":\"paul\",\"gender\":\"m\",\"age\":\"20\"}", "{\"name\":\"alice\",\"gender\":\"f\",\"age\":\"15\"}", "{\"name\":\"dabid\",\"gender\":\"m\",\"age\":\"17\"}", "{\"name\":\"jasmin\",\"gender\":\"f\",\"age\":\"17\"}"]
~~~

このように`"`の前に`\`が入ってうまく変換できませんでした。

`to_json`メソッドで返ってきた文字列をそのままファイルに書き込むのではなく、`JSON.dump`を使用したら、解決しました。