【问题标题】:Converting CSV to JSON in bash在 bash 中将 CSV 转换为 JSON
【发布时间】:2017-11-30 12:51:29
【问题描述】:

尝试将 CSV 文件转换为 JSON

这是两个示例行:

-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

我的代码有什么问题?

【问题讨论】:

  • 您是否有理由不使用jq 或其他支持JSON 的工具?使用字符串连接生成 JSON(或其他结构化数据序列化)是......充其量是容易出错的。
  • 除此之外,这里还有很多 的不良做法。您遗漏了一堆基本引用,这样如果您的一个值包含空格包围的 * ,它将被替换为文件名列表。您不必要地使用了-e,因此如果您的JSON 数据包含\n 序列(在JSON 中,它应该完全表示为这些字符),它将被替换为文字换行符; 并且由于-e,您的代码将无法在带有POSIX: standard echo 的shell 上正常工作。
  • re:顺便说一下,缺少的引用 - 考虑养成通过 shellcheck.net 运行代码的习惯。

标签: json bash csv unix jq


【解决方案1】:

适合这项工作的工具是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 管道进入循环(和good reasons not to);因此,我们使用重定向 (&lt;) 将文件直接作为循环的标准输入打开。
  • read 可以传递目标变量列表;因此无需读入数组(或 首先 读入字符串,然后生成异端并将其读入数组)。最后的_ 确保丢弃额外的列(通过将它们放入名为_ 的虚拟变量中)而不是附加到pos
  • "${array[*]}" 通过将array 的元素与IFS 中的字符连接起来生成一个字符串;因此,我们可以使用它来确保逗号仅在需要时出现在输出中。
  • printf 优先于 echo 使用,如 the specification for echo itself 的“应用使用”部分所述。
  • 这仍然存在固有的错误,因为它通过字符串连接生成 JSON。不要使用它。

【讨论】:

  • 谢谢,不知道 jq。但我无法弄清楚如何输入我的 CSV。行尾的 $s 是什么?
  • 哦——那是从字符串而不是文件中读取的。抱歉,在测试中留下了它。
  • 实际上,我在不久前将其编辑了 -- 您能否刷新一下以确保您看到的是当前版本的答案?
  • 那是一些漂亮的东西,谢谢...我喜欢在那里使用输入。
【解决方案2】:

这是一个可以解决问题的 python 单行/脚本:

cat my.csv | python -c 'import csv, json, sys; print(json.dumps([dict(r) for r in csv.DictReader(sys.stdin)]))'

【讨论】:

  • 这应该是公认的解决方案——它适用于所有有效负载。 jq 版本是一次性的,需要煞费苦心地匹配架构
  • 我喜欢这个主意。我稍作修改以写入文件...cat in.csv | python -c 'import csv, json, sys; f = open("out.json", "x"); f.write(json.dumps([dict(r) for r in csv.DictReader(sys.stdin)])); f.close()'
  • 要将其写入文件只需在末尾添加| &gt; filename.json。像这样:cat my.csv | python -c 'import csv, json, sys; print(json.dumps([dict(r) for r in csv.DictReader(sys.stdin)]))' | &gt; my.json
  • @btk,不,它不应该......它根本没有解决对 json 输出中命名实体的需求。如果 CVS 已命名标题,它可能会做正确的事情(我没有数据或时间来创建它来验证它是否会这样做)。
  • @tink imo 向 .csv 文件添加标头比使用复杂的 jq 查询更容易
【解决方案3】:

接受的答案使用jq 来解析输入。这可行,但 jq 不处理转义,即来自 Excel 或类似工具生成的 CSV 的输入引用如下:

foo,"bar,baz",gaz

将导致错误的输出,因为 jq 将看到 4 个字段,而不是 3 个。

一种选择是使用制表符分隔值而不是逗号(只要您的输入数据不包含制表符!),以及接受的答案。

另一种选择是组合您的工具,并为每个部分使用最佳工具:CSV 解析器用于读取输入并将其转换为 JSON,jq 用于将 JSON 转换为目标格式。

基于 python 的csvkit 将智能地解析 CSV,并附带一个工具csvjson,它可以更好地将 CSV 转换为 JSON。然后可以通过 jq 管道将 csvkit 的平面 JSON 输出转换为目标形式。

使用 OP 提供的数据,对于所需的输出,这很简单:

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

请注意,csvjson 会自动将 ; 检测为分隔符,并且在输入中没有标题行,将 json 键分配为 abc

这同样适用于写入 CSV 文件——csvkit 可以读取 JSON 数组或换行分隔的 JSON,并通过in2csv 智能输出 CSV。

【讨论】:

  • 完美,谢谢!我知道必须有比使用 jq 解析 CSV 更好的方法!
【解决方案4】:

这是一篇关于这个主题的文章: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

不过,它不处理分隔符转义。

【讨论】:

  • 这是一个很好的通用方法!也许您可以编辑以使用 OP 的数据结构? (或者是否欢迎第三方编辑这样做?)
  • @CharlesDuffy,我试了一下,但未经测试 - 随时修复/改进。
  • 需要一些小的调整——从 ',' 更改为 ';'作为分隔符,将".[3]"改为.[2];并且--raw-output 没有任何用途(当输出不是字符串时会被忽略)。
  • 另外,.[1:](跳过第一行)仅适用于输入有标题的情况;博客文章中确实如此,但我不确定这里是否属实。
  • @CharlesDuffy,您将如何解析标题然后自动制作地图?想象一下,您有不同的 CSV 文件,其中包含不同的列,并希望从标头派生 JSON 对象键。 jq 是否有某种变量?或者也许额外调用 `jq ... .[:1] 以某种方式填充 Bash 数组?
【解决方案5】:

John Kerl 的 Miller 工具内置了这个:

mlr --c2j --jlistwrap cat INPUT.csv > OUTPUT.json

【讨论】:

  • 我喜欢 'jq' 但这真的很好,至少对于将带有 CSV 的列标题转换为 JSON。 @richardkmiller
【解决方案6】:

一般来说,如果你的 jq 有 inputs 内置过滤器(自 jq 1.5 起可用),那么最好使用它而不是 -s 命令行选项。

无论如何,这里是使用inputs 的解决方案。此解决方案也是无变量的。

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

SSV、CSV 等等

上面当然假设文件在每一行都有分号分隔的字段,并且没有与 CSV 文件相关的复杂性。

如果输入具有由单个字符严格分隔的字段,则 jq 处理它应该没有问题。否则,最好使用能够可靠地转换为 jq 可以直接处理的 TSV(制表符分隔值)格式的工具。

【讨论】:

    【解决方案7】:

    由于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 数组。请参阅文档。

    它处于测试版质量,因此欢迎所有反馈或拉取请求。

    【讨论】:

      【解决方案8】:

      为了完整起见,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"
            }
          }
        ]
      }
      

      【讨论】:

        【解决方案9】:

        如果你想发疯,你可以用 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"
        ]
        

        这适用于大多数输入一次解析一行,但如果您的数据有文字换行符,您将需要将整个文件作为字符串读取。

        $ 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"
        ]
        

        【讨论】:

        • 如果最后一个字段为空,则会失败。
        • 在最后一个字符是分隔符的情况下,错误出现在_do_finalize。在这种情况下,它不会保存.[2],而是将其丢弃。用 _do_next_value 上的分隔符替换它可以修复它。
        【解决方案10】:

        Jstaabs 答案的一个版本,它避免在打印之前将整个字典结构存储在内存中,以防 CSV 文件很大。

        import csv, json, sys
        
        for r in csv.DictReader(sys.stdin):
            print(dict(r))
        

        请注意,这会输出 JSON 行格式,而不是完全 JSON。

        这是一个输出正确 JSON 数组的版本,但代价是更长。

        import csv, json, sys
        
        sys.stdout.write('[')
        
        first = True
        for r in csv.DictReader(sys.stdin):
            if not first:
                sys.stdout.write(',')
            first = False
            json.dump(dict(r), sys.stdout)
        
        sys.stdout.write(']')
        

        【讨论】:

          猜你喜欢
          • 2014-08-09
          • 2019-09-24
          • 1970-01-01
          • 2019-09-20
          • 2014-10-04
          • 1970-01-01
          • 2014-06-04
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多