使用 jq 将 CSV 转换 为 JSON

ujv3wf0j  于 2022-11-19  发布在  其他
关注(0)|答案(8)|浏览(212)

如果您有一个如下所示的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?
我找到了this文章,它显示了我正在尝试做的事情,但它使用了一个“手动”Map的标题字段的值。我不需要/想重命名标题字段,并有相当多的他们。我也不想改变脚本/命令,每次布局的变化。
有没有可能动态地提取头,然后用jq一行程序将它们与值组合在一起?

2vuwiymt

2vuwiymt1#

简而言之--是的,除了那句俏皮话。
jq通常非常适合文本的处理,对于支持regex的版本尤其如此。例如,有了regex支持,给定问题语句所需的修剪就变得微不足道了。
由于jq 1.5rc1包含regex支持,并且从2015年1月1日起就已经可用,因此以下程序假定使用jq 1.5版本;如果你想让它在jq1.4下工作,那么请看两条“For jq1.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"
  }
]
wfveoks0

wfveoks02#

非常简单。使用这个输入.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/

vddsk6oq

vddsk6oq3#

截至2018年,一个现代的无代码解决方案是使用Python工具csvkit
请参阅其文档https://csvkit.readthedocs.io/en/1.0.2/
如果您的脚本必须同时调试csvjson,该工具包也非常方便,是对jq的补充。
你可能还想看看一个叫做visidata的强大工具。这里有一个screencast case study,它与原始发布者的类似。你也可以从visidata生成脚本

wdebmtf2

wdebmtf24#

我玩了一点小游戏,想出了这个办法。但 * 这可能不是最好的办法 *,我很想看看 * 你的尝试是什么样的 *,因为毕竟,如果我们都找到了一个解决方案,我相信它会是两倍的好!
但我会从以下内容开始:

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[.]})

Working Example
变量$doHeaders控制是否将第一行作为标题行读取。在您的例子中,您希望它为true,但我为未来的SO用户添加了它,因为,嗯,我今天吃了一顿很棒的早餐,天气也很好,所以为什么不呢?
小小解释:
1). / "\n"按行拆分...
2)map(. / ", ")...和逗号(**大问题:**在您的版本中,您可能希望使用基于正则表达式的拆分,因为这样您也会在引号内的逗号上进行拆分。我使用这个是因为它简洁,这使我的解决方案看起来很酷,对吗?)
3)if $doHeaders then...这里我们创建一个字符串数组,根据第一行中元素的数量以及第一行是否为标题行来决定是键还是数字
4).[if $doHeaders then 1 else 0 end:]好的,如果是标题,请将顶行修剪掉
5)map({($headers[.]): $values[.]})上面我们检查了前一个csv中的每一行,并将$values放入一个变量中,将键放入一个管道中。
当然,您可能会希望使用一些正则表达式来填充这些陷阱,但我希望这能帮助您开始学习。

stszievb

stszievb5#

下面是一个解决方案,假定您使用-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"
  }
]
6jjcrrmo

6jjcrrmo6#

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"
  }
]
vcudknz3

vcudknz37#

这里有一个相当简单的jq“一行程序”版本,它可以处理“合理”大小的文件,对于非常大的文件,你需要一个不使用slurp的版本。(可能只是增加索引值,而不是储存在数据中)。如果您想让它更短、更难阅读,您可以用./"\n”和./"”取代“split”。注意:如果确实需要逗号后的空格,可以在““上拆分,或者加上**| 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):{键:#,值:字符串}
然后它使用map/reduce获取每个元素,并将其替换为键/值对的对象,使用编号键索引回标头以获取标签。(像我一样)分号之前的第一个元素是初始化'accumulator'(每次遍历元素时修改的内容)所以.[...]修改的是累加器,$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])])'
huus2vyu

huus2vyu8#

它也可以在不减少语法的情况下完成:

#! /bin/jq -fRs

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

相关问题