使用jq将CSV转换为JSON

39

如果您有这样的一个csv数据集:

name, age, gender
john, 20, male
jane, 30, female
bob, 25, male

你能够到达这个页面吗:

[ {"name": "john", "age": 20, "gender": "male"},
  {"name": "jane", "age": 30, "gender": "female"},
  {"name": "bob", "age": 25, "gender": "male"} ]

只使用 jq 能实现吗?

我找到了这篇文章,它展示了我想做的事情,但它使用了一个“手动”映射标题字段到值。我不需要/想重命名标题字段,并且有很多标题字段。我也不想每次布局更改时都必须更改脚本/命令。

能否使用 jq 一行代码动态提取标题,然后将其与值组合起来?


2
你所要求的操作毫无意义。jq是一种以json作为输入并生成输出的工具。csv不是json。你不能期望这个工具处理它,因为它不是为此而设计的。你需要使用能够处理csv的工具或其他脚本语言。 - Jeff Mercado
1
不要试图强制使用 jq,这可以通过一个几乎微不足道的 shell/sed 脚本来完成,例如基于 https://dev59.com/EW855IYBdhLWcg3wiU71。 - Hans Z.
5
我是被OP引用文章的作者。虽然我提供的示例对于快速处理jq项目可能有用,但并不太健壮。有一些很好的工具可用于处理CSV,我建议使用其中之一,例如:http://johnkerl.org/miller/doc/(类似于CSV的jq),或者这个NPM软件包https://www.npmjs.com/package/csv2json或这个gem https://rubygems.org/gems/csv2json/versions/0.3.0。 - Noah Sussman
9个回答

41

简而言之,除了一行代码可能需要特殊处理外,是可以的。

jq通常非常适合文本处理,特别是支持正则表达式的版本。例如,有了正则表达式的支持,所给定问题陈述中所需的修剪操作就是微不足道的。

由于jq 1.5rc1包括正则表达式支持并可用自2015年1月1日起,因此以下程序假定使用jq 1.5版本;如果您希望它与jq 1.4版本一起工作,则请参阅两个“对于jq 1.4”的注释。

还请注意,该程序不能处理CSV的所有普遍性和复杂性。(有关更一般地处理CSV的类似方法,请参见https://github.com/stedolan/jq/wiki/Cookbook#convert-a-csv-file-with-headers-to-json

# objectify/1 takes an array of string values as inputs, converts
# numeric values to numbers, and packages the results into an object
# with keys specified by the "headers" array
def objectify(headers):
  # For jq 1.4, replace the following line by: def tonumberq: .;
  def tonumberq: tonumber? // .;
  . as $in
  | reduce range(0; headers|length) as $i ({}; .[headers[$i]] = ($in[$i] | tonumberq) );

def csv2table:
  # For jq 1.4, replace the following line by:  def trim: .;
  def trim: sub("^ +";"") |  sub(" +$";"");
  split("\n") | map( split(",") | map(trim) );

def csv2json:
  csv2table
  | .[0] as $headers
  | reduce (.[1:][] | select(length > 0) ) as $row
      ( []; . + [ $row|objectify($headers) ]);

csv2json

示例(假设csv.csv是给定的CSV文本文件):

$ jq -R -s -f csv2json.jq csv.csv
[
  {
    "name": "john",
    "age": 20,
    "gender": "male"
  },
  {
    "name": "jane",
    "age": 30,
    "gender": "female"
  },
  {
    "name": "bob",
    "age": 25,
    "gender": "male"
  }
]

我确认这确实有效(已测试使用jq版本1.5)。模块化解决方案的演示很好。无论如何,我需要一些时间来理解所有的结构。不错。 - Jan Vlcinsky
1
@peak 如何扩展它以避免将 "" 包装成 "\"\"",并将 FALSE 转换为 false 和 TRUE 转换为 true - philk
@peak 我已经成功地转换了布尔值,但是字符串中的双引号仍然困扰着我。此外,我需要在哪里扩展 csv2json 函数以仅转换其 Brand 值为 "MYBRAND" 的行? - philk
@philk - 也许你可以调整trim函数来去除最外层的双引号。我不是很理解你问题的其他部分。也许这个问题值得在SO上单独提问? - peak
1
这个解决方案相当不错,但我发现它在处理包含逗号的带引号数值的 CSV 时表现不佳。也就是说,如果标题行包含 name,age,而数据行包含 "smith, bill",42,那么它将出现在 JSON 中为 {"name": "\"smith", "age": "bill\""} - Mr. Lance E Sloan

35

使用Miller(http://johnkerl.org/miller/doc/)非常简单。使用此input.csv文件即可。

name,age,gender
john,20,male
jane,30,female
bob,25,male

并且运行中

mlr --c2j --jlistwrap cat input.csv

您将拥有

[
{ "name": "john", "age": 20, "gender": "male" }
,{ "name": "jane", "age": 30, "gender": "female" }
,{ "name": "bob", "age": 25, "gender": "male" }
]

编辑

这是一个旧问题:新的文档页面在https://miller.readthedocs.io/en/latest/


@user3041539 我并不完全同意。没错,这是一个jq问题,但jq并不是读取csv的工具,所以必须给一个“x”问题一个“y”回答。 所以我同意你的观点,但这是有一些强制性的。 - aborruso
4
也许如果不够清楚的话——我支持这个答案,并认为它应该排名更高。 - user3041539
https://miller.readthedocs.io/en/latest/ - viktorkho
2
我很高兴在这个答案中偶然发现了_miller_。这个答案确实值得它的位置。这些珍贵的信息真正增强了Stack。 - bobbogo

17
截至2018年,一个现代的无代码解决方案是使用Python工具csvkit进行操作:csvjson data.csv > data.json
请参阅他们的文档:https://csvkit.readthedocs.io/en/1.0.2/ 如果您的脚本需要调试csvjson格式,则这个工具包也非常方便且与jq互补。
您可能还想检查一个强大的工具叫做visidata。这里有一个类似于原始海报的屏幕录像案例研究。您还可以从visidata生成脚本。

1
这个问题是一个经典的X/Y问题 - X的解决方案比实现Y所需的要容易得多 - xyproblem.info。感谢您回答X! - user3041539
2
感谢csvkit的提示。非常棒的工具! - while

13

yq(免责声明:由我编写)支持开箱即用此功能:

yq file.csv -p=csv -o=json

产量:

[
  {
    "name": "john",
    " age": 20,
    " gender": "male"
  },
  {
    "name": "jane",
    " age": 30,
    " gender": "female"
  },
  {
    "name": "bob",
    " age": 25,
    " gender": "male"
  }
]

原始的CSV文件中,第2列和第3列开头有空格 - 不确定是否是一个错误。您可以通过添加表达式来去掉它们:

yq '(... | select(tag == "!!str")) |= trim'  file.csv -p=csv -o=json

这将匹配所有字符串并去除前导空格,生成:

[
  {
    "name": "john",
    "age": 20,
    "gender": "male"
  },
  {
    "name": "jane",
    "age": 30,
    "gender": "female"
  },
  {
    "name": "bob",
    "age": 25,
    "gender": "male"
  }
]

这甚至解析了我的包含 JSON 内容的列的 CSV 文件! - Abhishek Gayakwad

10

我随便做了一个小玩意,但可能并不是最好的方法。我很想看看你的尝试结果,因为如果我们都能想出一个解决方案,那肯定会比一个人好!

但我会从类似以下的东西开始:

true as $doHeaders
| . / "\n"
| map(. / ", ")
| (if $doHeaders then .[0] else [range(0; (.[0] | length)) | tostring] end) as $headers
| .[if $doHeaders then 1 else 0 end:][]
| . as $values
| keys
| map({($headers[.]): $values[.]})

工作示例

变量$doHeaders控制是否将首行作为标题行读取。在您的情况下,您希望它为true,但我添加了它是为了未来的Stack Overflow用户以及因为今天我有一个美味的早餐和天气很好,为什么不呢?

稍微解释一下:

1). / "\n"按行分割...

2)map(. / ", ") ...和逗号(注意:在您的版本中,您需要使用基于正则表达式的拆分,因为这样您将在引号内部的逗号上进行拆分。我只是使用它,因为它很简洁,这使得我的解决方案看起来很酷,对吧?)

3)if $doHeaders then...在这里,我们根据第一行元素的数量以及第一行是否为标题行创建了一个字符串键或数字数组

4).[if $doHeaders then 1 else 0 end:]如果头是标题,则删除顶部行。

5)map({($headers[.]): $values[.]})在上面,我们遍历了前面csv的每一行,并将$values放到一个变量中,将键放入管道中。然后我们构造您想要的对象。

当然,您需要使用一些正则表达式来填充这些陷阱,但是我希望这可以让您得到启示。


1
我只想眨眨眼,但现在必须在评论中写更多的内容。 - Tom
感谢您对此的努力!诚然,这更多是一个理论问题而非实际问题。我最终在bash中完成了这个任务,但一直在思考是否可以仅使用jq来完成,所以我提出了这个问题。上面的代码已经接近答案了。它输出了[ {"name": "john"}, {"age": 20}, {"gender": "male"}... - jpl1079
如果我有足够的声望,我会给你点赞的 :) 再次感谢。 - jpl1079
抱歉,发生了一些小错误。我在查询的最后漏掉了 | add。但没关系,祝好。 - Tom

4
这里有一个解决方案,假设你使用-s-R选项运行jq。
[
  [                                               
    split("\n")[]                  # transform csv input into array
  | split(", ")                    # where first element has key names
  | select(length==3)              # and other elements have values
  ]                                
  | {h:.[0], v:.[1:][]}            # {h:[keys], v:[values]}
  | [.h, (.v|map(tonumber?//.))]   # [ [keys], [values] ]
  | [ transpose[]                  # [ [key,value], [key,value], ... ]
      | {key:.[0], value:.[1]}     # [ {"key":key, "value":value}, ... ]
    ]
  | from_entries                   # { key:value, key:value, ... }
]

示例运行:

jq -s -R -f filter.jq data.csv

示例输出

[
  {
    "name": "john",
    "age": 20,
    "gender": "male"
  },
  {
    "name": "jane",
    "age": 30,
    "gender": "female"
  },
  {
    "name": "bob",
    "age": 25,
    "gender": "male"
  }
]

2
当解析过滤器文件时,jq出现错误“jq: error: syntax error, unexpected ?//, expecting ';' or ')' (Unix shell quoting issues?) at <top-level>, line 8: | [.h, (.v|map(tonumber?//.))] # [ [keys], [values] ] jq: 1 compile error” - QkiZ

4

以下是一个相对简单的jq版本的“一行式”代码,适用于“相当”大小的文件,对于非常大的文件,您需要使用不使用slurp的版本。我对jq比较新,我相信有更好的方法来完成这个任务(也许只需增加索引值而不是存储在数据中)。如果您想使它更短,更难读,请将“split”替换为./"\n"和./","。注意:如果您真的需要逗号后面的空格,可以在逗号分割后拆分“,”或添加|map(gsub("^\s+|\s+$";""))以去除前导和尾随空格。

jq -Rs 'split("\n")|map(split(",")|to_entries)|.[0] as $header|.[1:]|map(reduce .[] as $item ({};.[$header[$item.key].value]=$item.value))'

以下是有注释的版本:

# jq -Rs
split("\n") | map( split(",") | to_entries ) # split lines, split comma & number
  | .[0] as $header # save [0]
  | .[1:] # and then drop it
  | map( reduce .[] as $item ( {}; .[$header[$item.key].value] = $item.value ) )

顶部部分很简单:按换行符拆分数据,然后对于每个元素按逗号拆分,然后 to_entries 将这些转换为键/值条目,并使用键的编号 (0..N):{key:#, value:string}。然后它使用 map/reduce 来采取每个元素并将其替换为键/值对对象,使用编号键来索引回标题以获取标签。对于那些新手降低(像我一样),第一个元素到分号是初始化“累加器”(每次通过元素修改的东西),所以.[...] 修改累加器,$item 是我们操作的对象。更新:现在我有一个更好的版本可以工作,不使用 slurp,并且我们不使用 -n 选项,因为它会特别处理第一行。
jq -R 'split(",") as $h|reduce inputs as $in ([]; . += [$in|split(",")|. as $a|reduce range(0,length) as $i ({};.[$h[$i]]=$a[$i])])'

你的答案接近正确,但是键和值有额外的空格,并且数字类型是字符串。请查看此 jqplay.org 以查看您的答案。 - rickhg12hs
我正在尝试将一个tsv文件转换为JSON,我已经成功地根据您上面的命令调整了我的目的。然而,我想知道是否可能输出一个数组的对象而不是对象的数组。例如,如何调整您的脚本以输出 {"name": ["john","jane","bob"], "age": [20,30,40], "gender": ["male","female","male"]}?谢谢。 - undefined

3

最近做了类似的事情,这里有另一个jq单行命令将CSV转换为JSON数组。

jq --null-input --raw-input '[input|scan("\\w+")] as $header |[inputs as $data |[$header,[$data|scan("\\w+")|tonumber? // .]] |transpose |map({(.[0]):.[1]}) |add]' input.csv

输出,给定示例输入:

[
  {
    "name": "john",
    "age": 20,
    "gender": "male"
  },
  {
    "name": "jane",
    "age": 30,
    "gender": "female"
  },
  {
    "name": "bob",
    "age": 25,
    "gender": "male"
  }
]

jqplay.org 上试一试。


1
它也可以不使用reduce语法来完成:
#! /bin/jq -fRs

split("\n")|map(select(.!="")|split(","))
|.[0] as $headers
|.[1:][]
|with_entries(.key=$headers[.key])

2
你的回答可以通过提供更多支持信息来改进。请编辑以添加进一步的细节,例如引用或文档,以便他人可以确认你的答案是正确的。您可以在帮助中心中找到有关如何编写良好答案的更多信息。 - Community
1
此回答已在 低质量队列 中接受审核。以下是关于如何写出好的答案的一些指南:如何撰写一个好的答案?。仅提供代码的答案不被视为好的答案,并且很可能会因此对学习者社区的帮助较少而被踩或删除。这个对你来说很明显,但请解释它的作用,以及与现有答案的不同/更好之处。 - Trenton McKinney
你的答案接近正确,但是键和值有额外的空格,并且数字类型是字符串。请查看此 jqplay.org 以查看您的答案。 - rickhg12hs

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