在bash中将CSV转换为JSON

62
尝试将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

我的代码哪里有问题?


2
你为什么不使用jq或其他JSON感知工具呢?使用字符串拼接生成JSON(或其他结构化数据序列化)最多是容易出错的。 - Charles Duffy
1
除此之外,这里有很多不好的做法。你忽略了很多必要的引用,如果你的值包含一个被空格包围的 *,它将被替换为文件名列表。你不必要地使用了 -e,因此如果你的 JSON 数据包含 \n 序列(在 JSON 中应该表示为这些字符),它将被替换为字面上的换行符;而且由于 -e 的存在,你的代码在具有 POSIX:标准 echo 的 shell 上无法正常工作。 - Charles Duffy
1
顺便提一下,关于缺少引号的问题,请考虑养成通过http://shellcheck.net/运行代码的习惯。 - Charles Duffy
13个回答

85

这是一个 Python 一行代码/脚本,可以帮你解决问题:

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

9
这应该是被接受的解决方案 - 它可以用于任何有效载荷。jq版本只能使用一次,并需要费力匹配模式。 - btk
1
我喜欢这个想法。我稍微修改了一下,让它可以写入文件... 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()' - Abraham Labkovsky
2
要将其写入文件,只需在末尾添加 | > filename.json。像这样:cat my.csv | python -c 'import csv, json, sys; print(json.dumps([dict(r) for r in csv.DictReader(sys.stdin)]))' | > my.json - K14
3
@tink 我认为将标题添加到 .csv 文件比折腾复杂的 jq 查询要容易得多。 - btk
1
我认为不需要使用 dict,你可以直接使用 list(csv.DictReader(sys.stdin)) - Lauren Yim
显示剩余7条评论

40

这项工作的正确工具是 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管道到循环中毫无意义(并且有很多理由不这样做);因此,我们使用重定向(<)直接将文件作为循环的标准输入打开。
  • read可以传递一个目标变量列表;因此,没有必要读入一个数组(或者首先读入一个字符串,然后生成一个here文档,再从中读取到一个数组中)。末尾的_确保多余的列被丢弃(通过将它们放入名为_的虚拟变量中)而不是附加到pos中。
  • "${array[*]}"通过用IFS中的字符连接array的元素来生成一个字符串;因此,我们可以使用它来确保只在需要时才在输出中出现逗号。
  • 根据echo本身的规范的应用使用方法,优先使用printf而不是echo
  • 这仍然存在固有的漏洞,因为它通过字符串连接生成JSON。不要使用它。

谢谢,我不知道jq。但是我无法弄清楚如何输入我的CSV文件。你行末的$s是什么? - HydrUra
哦,那是从字符串读取,而不是从文件读取。抱歉,测试时留下了这个代码。 - Charles Duffy
实际上,我在一段时间前编辑了那个部分 - 你能否刷新一下页面以确保你看到的是答案的最新版本? - Charles Duffy
那是一些漂亮的东西,谢谢...我喜欢那里使用输入的方式。 - Greg

23

接受的答案使用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键分配为“a”,“b”和“c”。
对于写入CSV文件也是同样的道理——csvkit可以读取JSON数组或基于新行的JSON,并通过in2csv智能输出CSV。

1
太棒了,谢谢!我知道一定会有比用 jq 解析CSV更好的方法的! - Tobias J
在我看来,csvjson 是最好的方法,因为它可以推断数据类型,而不是将数字视为字符串,避免在其中添加双引号。 - Marcos Roberto Silva

12

John Kerl的Miller工具已经内置了此功能:

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

2
我非常喜欢“jq”,但是至少对于将携带CSV的列标题转换为JSON而言,这真的很好。@richardkmiller - Nirmalya

6
这是一篇关于将CSV转换为JSON的文章: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的数据结构?(或者第三方编辑是否受欢迎?) - Charles Duffy
@CharlesDuffy,我试了一下,但没有测试 - 如有需要请随意修复/改进。 - Ondra Žižka
1
需要进行一些小的调整--将分隔符从','更改为';',将".[3]"更改为.[2];而且当输出不是字符串时,--raw-output没有任何作用(它会被忽略)。 - Charles Duffy
此外,如果输入没有标题行,则仅使用 .[1:](跳过第一行)是不合适的;这在博客文章中是正确的,但我不确定在这里是否正确。 - Charles Duffy
@CharlesDuffy, 你怎么解析头文件然后自动生成映射呢?假设你有不同列的 CSV 文件,并且希望从头文件派生 JSON 对象键。jq 是否有某种变量?或者可能需要额外调用 jq ... .[:1] 以某种方式填充 Bash 数组吗? - Ondra Žižka
是的,jq确实有变量。 - Charles Duffy

5

以下是Ruby中的一行代码解决方案:

ruby -r json -r csv -e 'puts CSV.parse(STDIN, headers:true).map(&:to_h).to_json' < INPUT.csv 

2

这里有另一种使用Miller(mlr)的方法

mlr --implicit-csv-header --icsv --fs ';' --ojson label position,taxo,espece example.csv

这将会产生以下结果。
[
{
  "position": -21.3214077,
  "taxo": 55.4851413,
  "espece": "Ruizia cordata"
},
{
  "position": -21.3213078,
  "taxo": 55.4849803,
  "espece": "Cossinia pinnata"
}
]

如果您使用--ojsonl(JSON行)而不是纯JSON,将生成以下内容

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

2
一般来说,如果你的 jq 已经安装了内置过滤器 inputs(从 jq 1.5 开始可用),那么最好使用它,而不是使用 -s 命令行选项。以下是一种使用 inputs 的解决方案。此解决方案也没有变量。
{"occurrences":
  [inputs
   | select(length > 0)
   | . / ";"
   | {"position": [.[0], .[1]], 
      "taxo": {"espece": .[2]}} ]}

SSV、CSV等等

当然,以上假设文件中每行都有用分号分隔的字段,并且没有与CSV文件相关的复杂问题。

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


1
由于jq解决方案无法处理CSV转义、第一行的列名、注释行和其他常见的CSV“特性”,因此我扩展了CSV Cruncher工具,允许将CSV读取并写入JSON。它不完全是“Bash”,但jq也不是:)。
它主要是一个将CSV作为SQL处理的应用程序,因此它并不完全简单,但这是诀窍:
./crunch -in myfile.csv -out output.csv --json -sql 'SELECT * FROM myfile'

它还允许以每行一个JSON对象或正确的JSON数组输出。请参阅文档。 由于它处于beta质量,因此欢迎所有反馈或拉取请求。

1
避免在打印之前将整个字典结构存储在内存中的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(']')

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接