web-dev-qa-db-ja.com

bashでCSVをJSONに変換する

CSVファイルをJSONに変換しようとしています

以下は2つのサンプル行です。

-21.3214077;55.4851413;Ruizia cordata
-21.3213078;55.4849803;Cossinia pinnata

私は次のようなものを手に入れたいです:

"occurrences": [
                 {
                "position": [-21.3214077, 55.4851413],
                "taxo": {
                    "espece": "Ruizia cordata"
                 },
                 ...
             }]

これが私のスクリプトです:

    echo '"occurences": [ '

cat se.csv | while read -r line
  do
      IFS=';' read -r -a array <<< $line;
      echo -n -e '{ "position": [' ${array[0]}
      echo -n -e ',' ${array[1]} ']'
      echo -e ', "taxo": {"espece":"' ${array[2]} '"'
done
echo "]";

私は本当に奇妙な結果を得ます:

   "occurences": [ 
 ""position": [ -21.3214077, 55.4851413 ], "taxo": {"espece":" Ruizia cordata
 ""position": [ -21.3213078, 55.4849803 ], "taxo": {"espece":" Cossinia pinnata

私のコードの何が問題になっていますか?

18
HydrUra

このジョブに適したツールは jq です。

jq -Rsn '
  {"occurrences":
    [inputs
     | . / "\n"
     | (.[] | select(length > 0) | . / ";") as $input
     | {"position": [$input[0], $input[1]], "taxo": {"espece": $input[2]}}]}
' <se.csv

あなたの入力を与えて、放出します:

{
  "occurences": [
    {
      "position": [
        "-21.3214077",
        "55.4851413"
      ],
      "taxo": {
        "espece": "Ruizia cordata"
      }
    },
    {
      "position": [
        "-21.3213078",
        "55.4849803"
      ],
      "taxo": {
        "espece": "Cossinia pinnata"
      }
    }
  ]
}

ちなみに、元のスクリプトのバグの少ないバージョンは次のようになります。

#!/usr/bin/env bash

items=( )
while IFS=';' read -r lat long pos _; do
  printf -v item '{ "position": [%s, %s], "taxo": {"espece": "%s"}}' "$lat" "$long" "$pos"
  items+=( "$item" )
done <se.csv

IFS=','
printf '{"occurrences": [%s]}\n' "${items[*]}"

注意:

  • catを使用してループにパイプする(そして そうしない正当な理由 );にはまったく意味がありません。したがって、リダイレクト(<)を使用して、ループのstdinとしてファイルを直接開きます。
  • readには、宛先変数のリストを渡すことができます。したがって、配列に読み込む必要はありません(またはfirstから文字列に読み込んでから、継承を生成し、そこから配列に読み込む必要はありません。 )。最後の_は、posに追加されるのではなく、余分な列が(_という名前のダミー変数に入れられることによって)破棄されることを保証します。
  • "${array[*]}"は、arrayの要素をIFSの文字と連結して文字列を生成します。したがって、これを使用して、必要な場合にのみコンマが出力に存在することを確認できます。
  • printf自体の仕様 の「アプリケーションの使用法」セクションで推奨されているように、echoechoよりも優先して使用されます。
  • これは文字列の連結を介してJSONを生成するため、依然として本質的にバグがあります。使用しないでください。
16
Charles Duffy

これは件名に関する記事です: https://infiniteundo.com/post/99336704013/convert-csv-to-json-with-jq

JQも使用しますが、split()map()を使用する少し異なるアプローチです。

jq --Slurp --raw-input \
   'split("\n") | .[1:] | map(split(";")) |
      map({
         "position": [.[0], .[1]],
         "taxo": {
             "espece": .[2]
          }
      })' \
  input.csv > output.json

ただし、セパレータのエスケープは処理しません。

5
Ondra Žižka

受け入れられた回答はjqを使用して入力を解析します。これは機能しますが、jqはエスケープを処理しません。つまり、Excelまたは同様のツールから生成されたCSVからの入力は、次のように引用されます。

foo,"bar,baz",gaz

jqは3ではなく4つのフィールドを参照するため、誤った出力になります。

1つのオプションは、(入力データにタブが含まれていない限り)カンマの代わりにタブで区切られた値を使用することです。

別のオプションは、ツールを組み合わせて、各部分に最適なツールを使用することです。入力を読み取ってJSONに変換するCSVパーサーと、JSONをターゲット形式に変換するjqです。

Pythonベースの csvkit は、CSVをインテリジェントに解析し、CSVをJSONに変換するためのより優れたツールcsvjsonが付属しています。次に、これをjqにパイプして、csvkitによるフラットなJSON出力をターゲットフォームに変換できます。

OPによって提供されるデータを使用して、目的の出力について、これは次のように単純です。

csvjson --no-header-row  |
  jq '.[] | {occurrences: [{ position: [.a, .b], taxo: {espece: .c}}]}'

Csvjsonは;を区切り文字として自動的に検出し、入力にヘッダー行がない場合、jsonキーをab、およびcとして割り当てます。

同じことは、toCSVファイルの書き込みにも当てはまります-csvkitは、JSON配列または改行区切りのJSONを読み取り、インテリジェントに出力できますin2csv経由のCSV。

5
Raman

おかしくなりたい場合は、jqを使用してパーサーを作成できます。これは、@csvフィルターの逆と考えることができる私の実装です。これを.jqファイルにスローします。

def do_if(pred; update):
    if pred then update else . end;
def _parse_delimited($_delim; $_quot; $_nl; $_skip):
    [($_delim, $_quot, $_nl, $_skip)|explode[]] as [$delim, $quot, $nl, $skip] |
    [0,1,2,3,4,5] as [$s_start,$s_next_value,$s_read_value,$s_read_quoted,$s_escape,$s_final] |
    def _append($arr; $value):
        $arr + [$value];
    def _do_start($c):
        if $c == $nl then
            [$s_start, null, null, _append(.[3]; [""])]
        Elif $c == $delim then
            [$s_next_value, null, [""], .[3]]
        Elif $c == $quot then
            [$s_read_quoted, [], [], .[3]]
        else
            [$s_read_value, [$c], [], .[3]]
        end;
    def _do_next_value($c):
        if $c == $nl then
            [$s_start, null, null, _append(.[3]; _append(.[2]; ""))]
        Elif $c == $delim then
            [$s_next_value, null, _append(.[2]; ""), .[3]]
        Elif $c == $quot then
            [$s_read_quoted, [], .[2], .[3]]
        else
            [$s_read_value, [$c], .[2], .[3]]
        end;
    def _do_read_value($c):
        if $c == $nl then
            [$s_start, null, null, _append(.[3]; _append(.[2]; .[1]|implode))]
        Elif $c == $delim then
            [$s_next_value, null, _append(.[2]; .[1]|implode), .[3]]
        else
            [$s_read_value, _append(.[1]; $c), .[2], .[3]]
        end;
    def _do_read_quoted($c):
        if $c == $quot then
            [$s_escape, .[1], .[2], .[3]]
        else
            [$s_read_quoted, _append(.[1]; $c), .[2], .[3]]
        end;
    def _do_escape($c):
        if $c == $nl then
            [$s_start, null, null, _append(.[3]; _append(.[2]; .[1]|implode))]
        Elif $c == $delim then
            [$s_next_value, null, _append(.[2]; .[1]|implode), .[3]]
        else
            [$s_read_quoted, _append(.[1]; $c), .[2], .[3]]
        end;
    def _do_final($c):
        .;
    def _do_finalize:
        if .[0] == $s_start then
            [$s_final, null, null, .[3]]
        Elif .[0] == $s_next_value then
            [$s_final, null, null, _append(.[3]; [""])]
        Elif .[0] == $s_read_value then
            [$s_final, null, null, _append(.[3]; _append(.[2]; .[1]|implode))]
        Elif .[0] == $s_read_quoted then
            [$s_final, null, null, _append(.[3]; _append(.[2]; .[1]|implode))]
        Elif .[0] == $s_escape then
            [$s_final, null, null, _append(.[3]; _append(.[2]; .[1]|implode))]
        else # .[0] == $s_final
            .
        end;
    reduce explode[] as $c (
        [$s_start,null,null,[]];
        do_if($c != $skip;
            if .[0] == $s_start then
                _do_start($c)
            Elif .[0] == $s_next_value then
                _do_next_value($c)
            Elif .[0] == $s_read_value then
                _do_read_value($c)
            Elif .[0] == $s_read_quoted then
                _do_read_quoted($c)
            Elif .[0] == $s_escape then
                _do_escape($c)
            else # .[0] == $s_final
                _do_final($c)
            end
        )
    )
    | _do_finalize[3][];
def parse_delimited($delim; $quot; $nl; $skip):
    _parse_delimited($delim; $quot; $nl; $skip);
def parse_delimited($delim; $quot; $nl):
    parse_delimited($delim; $quot; $nl; "\r");
def parse_delimited($delim; $quot):
    parse_delimited($delim; $quot; "\n");
def parse_delimited($delim):
    parse_delimited($delim; "\"");
def parse_csv:
    parse_delimited(",");

データについては、区切り文字をセミコロンに変更します。

$ cat se.csv
-21.3214077;55.4851413;Ruizia cordata
-21.3213078;55.4849803;Cossinia pinnata
$ jq -R 'parse_delimited(";")' se.csv
[
  "-21.3214077",
  "55.4851413",
  "Ruizia cordata"
]
[
  "-21.3213078",
  "55.4849803",
  "Cossinia pinnata"
]

これは、一度に1行を解析するほとんどの入力で問題なく機能しますが、データにリテラルの改行がある場合は、ファイル全体を文字列として読み取る必要があります。

$ cat input.csv
Year,Make,Model,Description,Price
1997,Ford,E350,"ac, abs, moon",3000.00
1999,Chevy,"Venture ""Extended Edition""","",4900.00
1999,Chevy,"Venture ""Extended Edition, Very Large""",,5000.00
1996,Jeep,Grand Cherokee,"MUST SELL!
air, moon roof, loaded",4799.00
$ jq -Rs 'parse_csv' input.csv
[
  "Year",
  "Make",
  "Model",
  "Description",
  "Price"
]
[
  "1997",
  "Ford",
  "E350",
  "ac, abs, moon",
  "3000.00"
]
[
  "1999",
  "Chevy",
  "Venture \"Extended Edition\"",
  "",
  "4900.00"
]
[
  "1999",
  "Chevy",
  "Venture \"Extended Edition, Very Large\"",
  "",
  "5000.00"
]
[
  "1996",
  "Jeep",
  "Grand Cherokee",
  "MUST SELL!\nair, moon roof, loaded",
  "4799.00"
]
1
Jeff Mercado

完全を期すために、 Xidel といくつかのXQueryマジックを組み合わせてこれを行うこともできます。

xidel -s input.csv --xquery '
  {
    "occurrences":for $x in tokenize($raw,"\n") let $a:=tokenize($x,";") return {
      "position":[
        $a[1],
        $a[2]
      ],
      "taxo":{
        "espece":$a[3]
      }
    }
  }
'
{
  "occurrences": [
    {
      "position": ["-21.3214077", "55.4851413"],
      "taxo": {
        "espece": "Ruizia cordata"
      }
    },
    {
      "position": ["-21.3213078", "55.4849803"],
      "taxo": {
        "espece": "Cossinia pinnata"
      }
    }
  ]
}
1
Reino

一般に、jqにinputs組み込みフィルター(jq 1.5以降で使用可能)がある場合は、-sコマンドラインオプションよりも使用することをお勧めします。

ここでは、いずれの場合もinputsを使用したソリューションです。このソリューションも変数がありません。

{"occurrences":
  [inputs
   | select(length > 0)
   | . / ";"
   | {"position": [.[0], .[1]], 
      "taxo": {"espece": .[2]}} ]}

SSV、CSV、その他すべて

上記はもちろん、ファイルの各行にセミコロンで区切られたフィールドがあり、CSVファイルに関連する複雑な問題がないことを前提としています。

入力に1文字で厳密に区切られたフィールドがある場合、jqはそれを処理するのに問題はありません。それ以外の場合は、jqが直接処理できるTSV(タブ区切り値)形式に確実に変換できるツールを使用するのが最善です。

0
peak

jqソリューションはCSVエスケープ、最初の行の列名、コメント化された行、およびその他の一般的なCSVの「機能」を処理しないため、 CSV Cruncher ツールを拡張して、 CSVを読み取ってJSONとして書き込みます。 「Bash」ではありませんが、jqでもありません。

これは主にCSV-as-SQL処理アプリなので、完全に簡単なわけではありませんが、ここに秘訣があります。

./crunch -in myfile.csv -out output.csv --json -sql 'SELECT * FROM myfile'

また、行ごとのJSONオブジェクトまたは適切なJSON配列として出力することもできます。ドキュメントを参照してください。

ベータ品質なので、フィードバックやプルリクエストは大歓迎です。

0
Ondra Žižka