行列屋さんの作業ログ

行列まわりで色々やってたエンジニアの作業メモ&国内外旅行記ブログ

一行ごとのJSONをRで読み書きする.

久しぶりに旅行記以外のものを執筆します.

データ入出力の形式として現在はJSONがとにかく広く使われている気がします.私のプロジェクトでもJSONを頻繁に使うのですが,1行毎にJSONのオブジェクトが格納され末尾にカンマがないという,ちょっと特殊なJSON(一行JSONと勝手に呼んでます)をRで読み書きする機会があったのでそのやり方をまとめておきます.

同様のことをpythonでやっている人は発見しました.
qiita.com
また,jq コマンドというjson操作に特化したコマンドを使用して処理する方法もありました.
qiita.com
ただjqコマンドを入れられない環境もあったので,使わない方法でアプローチします.


よくあるJSONはこんな感じ.

[
{
        "foo" : 10,
        "bar" : 20,
        "arr" :["a","b"]
},
{
        "foo" : 30,
        "bar" : 40,
        "arr" :["c","d"]
}
]

Rではjsonliteパッケージで読み込めます.

df <- jsonlite::fromJSON(txt='./sample.json')
print(df)
| foo| bar|arr  |
|  10|  20|a, b |
|  30|  40|c, d |

一方で今回読み書きしたいJSON(一行JSON)は次のような形式です.

{"foo" : 10,"bar" : 20,"arr" :["a","b"]}
{"foo" : 30,"bar" : 40,"arr" :["c","d"]}

この形式をアプリケーションのログファイル等で使用すると便利なのですが,(少なくともjsonliteでは)想定されていない形式のためそのまま読み書きすることは出来ません.
具体的には,

  • ファイルの先頭,末尾に[]がない
  • 各行末尾に「,」がない

という違いがあります.
そこでシェルスクリプトを噛ませてjsonliteで対応可能な形式に変換を行います.

読み込む

{"foo" : 10,"bar" : 20,"arr" :["a","b"]}
{"foo" : 30,"bar" : 40,"arr" :["c","d"]}

上のようなデータを次のシェルスクリプトで変形します.

#!/bin/bash
#convertJSON.sh
LF=$(printf '\\\012_')
LF=${LF%_}
sed '/^$/d' $1 | sed -e 's/}{/}'"$LF"'{/g' | sed "s/$/,/g" | sed "1s/^/[/" | sed '$s/},/}]/'

$1が読み込みたいデータのファイルパスで,sed '/^$/d'で空行を削除,sed -e 's/}{/}'"$LF"'{/g'で一行に複数データが書き込まれていた際データ毎に改行させます.
sed "s/$/,/g" | sed "1s/^/[/" | sed '$s/},/}]/'でファイル先頭,末尾への[]の挿入と,各行末尾への「,」の挿入を行っています.
(/bin/shでなく/bin/bashを呼び出してるのは改行が上手くいかなかったから...のはずですが再検証したらshでも動いた.)

JSON入力の全操作をRで完結させるため,上のスクリプト(convertJSON.shとします)を使い関数readMyJSONを作成しました.

readMyJSON <- function(jsonFile){
	jsonTmp <- tempfile()
	system(paste0('./convertJSON.sh ',jsonFile,'> ',jsonTmp))
	df <- fromJSON(jsonTmp)
	try({file.remove(jsonTmp)})
	return(df)
}

この関数を使うと一行JSONのファイルを普通のJSONに変換,jsonliteで読み込みデータフレームとして返してくれます.

書き込む

データフレームを一行JSON形式で書き込むためには,jsonliteで一度普通のJSONで書き込んだ後,シェルスクリプトで整形します.
以下が使用した関数writeMyJSONとシェルスクリプトwriteJSON.shです.

writeMyJSON <- function(df,filename){ 
	jsonData <- toJSON(df)
	jsonTmp <- tempfile()
	write(jsonData,file=jsonTmp)
	system(paste0('./writeJSON.sh ',jsonTmp,' > ',filename))
	try({file.remove(jsonTmp)})
	return(TRUE)
}
#!/bin/bash
#writeJSON.sh
LF=$(printf '\\\012_')
LF=${LF%_}
sed -e 's/},{/}'"$LF"'{/g' $1 | sed "1s/\[//" | sed '$s/}]/}/' 

これで一行JSONの書き込みも出来ました.

ただtempfileに一時的にファイルを書き込んでいるため,巨大なファイルを扱うときのパフォーマンスはよくありません.私が扱うのは大きくとも数100MB程度のJSONファイルなのでなんとかなってはいますが.(でもそれ以上に大きなデータはJSONでは扱わないきもする.)