使用jq将CSV转换为JSON

时间:2015-04-16 00:14:58

标签: json csv jq

如果您有这样的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文章,其中显示了我尝试做的事情,但它使用的是“手册”#39;标头字段到值的映射。我不需要/想要重命名标题字段并且有很多字段。我也不想每次布局更改时都要更改脚本/命令。

是否可以动态提取标题,然后使用jq one-liner将它们与值组合在一起?

6 个答案:

答案 0 :(得分:18)

总之 - 是的!

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

答案 1 :(得分:8)

我玩了一下,然后想出了这个。但是它可能不是最好的方式,我有兴趣看到你的尝试是什么样的,因为毕竟如果我们两个都找到了解决方案我就是确定它好两倍!

但我会从以下内容开始:

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控制是否将顶行读作标题行。在你的情况下你想要它是真的,但我把它添加到未来的SO用户,因为,今天我吃了一顿美味的早餐,天气很好,为什么不呢?

小解释:

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

2)map(. / ", ") ...和逗号( Big gotcha:在你的版本中,你会想要使用基于正则表达式的分割,因为这样你就会在逗号内部分开引号也是。我只是用它,因为它很简洁,这使我的解决方案看起来很酷吗?)

3)if $doHeaders then...这里我们创建一个字符串键或数字的数组,具体取决于第一行中元素的数量以及第一行是否为标题行

4).[if $doHeaders then 1 else 0 end:]好的,如果它是标题

,那么修剪顶行

5)map({($headers[.]): $values[.]})上面我们遍历前csv中的每一行,并将$values放入变量,将密钥放入管道。然后我们构建你想要的对象。

当然你会想用几个正则表达式来填补陷阱,但我希望你能在路上开始。

答案 2 :(得分:2)

这是一个假设您使用-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"
  }
]

答案 3 :(得分:2)

从2018年开始,现代的无代码解决方案是使用csvkit具有csvjson data.csv > data.json的Python工具。

请参阅他们的文档https://csvkit.readthedocs.io/en/1.0.2/

如果您的脚本必须同时调试jqcsv格式,则该工具包也非常方便并且是json的补充。

您可能还想检查一个名为visidata的强大工具。这是与原始海报相似的screencast case study。您也可以从visidata

生成脚本

答案 4 :(得分:2)

使用Miller(http://johnkerl.org/miller/doc/)进行

非常简单。使用此input.csv文件

router.put('/:user_id', async (req, res) => {
  const { user_id } = req.params;
  const gradeFields = {
    classCode: req.body.classCode,
    gradeLevel: req.body.gradeLevel,
    credit: req.body.credit,
    mark: req.body.mark
  };
  try {
    // Authenticate with Passport
    await passport.authenticate('jwt', { session: false });
    // Grab user with this user_id
    const existingUser = await UserGrades.findOne({ user: user_id });
    if(!existingUser) {
      // If user does not exist, throw 404
      res.status(404).send("User with this ID does not exist");
    }
    // Check if user has classCode already on an existing StudentGrade
    if(existingUser.StudentGrades.some(sg => sg.classCode === req.body.classCode)) {
      res.status(409).send("Student already has grade with this class code.");
    }
    // Update user record with new StudentGrade and return updates document
    const updatedUser = await UserGrades.findOneAndUpdate(
      { user: user_id },
      { $push: { StudentGrades: gradeFields } },
      { new: true }
    );
    res.status(200).send(updatedUser);
  } catch (e) {
    console.log('Failed to update user grades', e);
    // Unknown server error, send 500
    res.status(500).send(e)
  }
});

并运行

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

您将拥有

mlr --c2j --jlistwrap cat input.csv

答案 5 :(得分:0)

这是 jq 的一个相当简单的“单行”版本,适用于“合理”大小的文件,对于非常大的文件,您需要一个不使用 slurp 的版本。我对 jq 还很陌生,我相信有更好的方法来做到这一点(也许只是增加一个索引值而不是存储在数据中)。如果你想让它更短更难读,你可以用 ./"\n" 和 ./"," 替换 "split"。注意:如果你真的需要逗号后的空格可以在“,”上拆分或在逗号分隔后添加 |map(gsub("^\s+|\s+$";""))修剪前导和尾随空白。

#include <stdio.h>
#include <stdlib.h>

int main() {
    char* input=calloc(100,sizeof(char));
    gets(input);
    char *p = strtok(input, " ");
    while (p != NULL) {
        puts(p);
        p = strtok(NULL, " ");
    }
    return(EXIT_SUCCESS);
}

这是一个评论版本:

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

顶部非常简单:在换行符上拆分数据,然后将每个元素拆分为逗号,然后 to_entries 会将每个元素转换为键/值条目,并带有键编号(0.. N): {key:#, value:string}

然后它使用 map/reduce 来获取每个元素并使用键/值对的对象替换它,使用编号的键索引回标头以获取标签。对于那些新来减少(像我一样)分号之前的第一个元素是初始化“累加器”(你修改每个元素的东西)所以 .[...] 正在修改累加器和 $item是我们正在操作的对象。

更新:我现在有一个更好的版本,它不使用 slurp,我们不使用 -n 选项,因为它会特别对待第一行:

# 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 ) )