使用命令行工具将JSON数组拆分为多个文件

8
假设我们有一个长度为5的JSON数组,我们想将数组拆分成多个长度为2的数组,并使用Linux命令行工具将分组项保存到不同的文件中。
我尝试使用jq和split工具(我对任何可以从bash脚本执行的方法都很满意):
$ echo '[{"key1":"value1"},{"key2":"value2"},{"key3":"value3"},{"key4":"value4"},{"key5":"value5"}]' | jq -c -M '.[]' | split -l 2 -d -a 3 - meta_
$ tail -n +1 meta_*
==> meta_000 <==
{"key1":"value1"}
{"key2":"value2"}

==> meta_001 <==
{"key3":"value3"}
{"key4":"value4"}

==> meta_002 <==
{"key5":"value5"}

前面的命令已经将项目正确地保存到文件中,但我们需要将它们转换为有效的JSON数组格式。我尝试使用--filter选项:

$ echo '[{"key1":"value1"},{"key2":"value2"},{"key3":"value3"},{"key4":"value4"},{"key5":"value5"}]' | jq -c -M '.[]' | split -l 2 -d -a 3 - meta2_ --filter='jq --slurp -c -M'
[{"key1":"value1"},{"key2":"value2"}]
[{"key3":"value3"},{"key4":"value4"}]
[{"key5":"value5"}]
$ tail -n +1 meta2_*
tail: cannot open 'meta2_*' for reading: No such file or directory

然而,它会在屏幕上显示输出,但结果不会被持久化。我尝试将输出转发,但是出现了错误:

echo '[{"key1":"value1"},{"key2":"value2"},{"key3":"value3"},{"key4":"value4"},{"key5":"value5"}]' | jq -c -M '.[]' | split -l 2 -d -a 3 - meta2_ --filter='jq --slurp -c -M > $FILE'
...
split: with FILE=meta2_000, exit 2 from command: jq --slurp -c -M > $FILE

有什么提示或更好的方法吗?

编辑:我尝试了@andlrc建议的双引号:

$ echo '[{"key1":"value1"},{"key2":"value2"},{"key3":"value3"},{"key4":"value4"},{"key5":"value5"}]' | jq -c -M '.[]' | split -l 2 -d -a 3 - meta2_ --filter="jq --slurp -c -M > $FILE"
bash: -c: line 0: syntax error near unexpected token `newline'
bash: -c: line 0: `jq --slurp -c -M > '
split: with FILE=meta2_000, exit 1 from command: jq --slurp -c -M >
$ cat meta_000 | jq --slurp -c -M
[{"key1":"value1"},{"key2":"value2"}]

你不会接受任何不使用 split 的答案吗?(也就是说:请避免在问题本身中对哪些工具是回答问题的最佳方式抱有偏见)。 - Charles Duffy
@CharlesDuffy,谢谢你的建议,我可以接受并回答,而不使用split函数。 - Emer
5个回答

9

在jq过滤器中构建数组,然后将其拆分为每行一个文件会更容易。不需要进行额外的过滤。

range(0; length; 2) as $i | .[$i:$i+2]

输出:

[{"key1":"value1"},{"key2":"value2"}]
[{"key3":"value3"},{"key4":"value4"}]
[{"key5":"value5"}]

所以,将所有内容放在一起。
$ jq -cM --argjson sublen '2' 'range(0; length; $sublen) as $i | .[$i:$i+$sublen]' \
    input.json | split -l 1 -da 3 - meta2_

2
我发现使用 jqsplit 工具可以解决问题。我错过了双引号,jq 中的 '.' 模式以及用反斜杠转义 $
$ echo '[{"key1":"value1"},{"key2":"value2"},{"key3":"value3"},{"key4":"value4"},{"key5":"value5"}]' |
  jq -c -M '.[]' |
  split -l 2 -d -a 3 - meta2_ --filter="jq --slurp -c -M '.' >\$FILE"
$ tail -n +1 meta2_*
==> meta2_000 <==
[{"key1":"value1"},{"key2":"value2"}]

==> meta2_001 <==
[{"key3":"value3"},{"key4":"value4"}]

==> meta2_002 <==
[{"key5":"value5"}]

喜欢这个,因为它不需要额外的循环。 - Matthias

2
假设我们有一个长度为5的JSON数组,我们想将该数组拆分为多个长度为2的数组,并使用Linux命令行工具将分组项保存到不同的文件中。您可以使用JSON解析器来实现您的需求,基本思路是使用XQuery 3.1 FLWOR表达式和(tumbling) Window子句
$ xidel -se '
  for tumbling window $w in 1 to 5
  start $s when $s mod 2 eq 1
  return
  join($w)
'
1 2
3 4
5

$ echo '[{"key1":"value1"},{"key2":"value2"},{"key3":"value3"},{"key4":"value4"},{"key5":"value5"}]' | \
  xidel -se '
  for tumbling window $w in 1 to count($json())
  start $s when $s mod 2 eq 1
  return
  array{$w ! $json(.)}
' --output-json-indent=compact
[{"key1": "value1"}, {"key2": "value2"}]
[{"key3": "value3"}, {"key4": "value4"}]
[{"key5": "value5"}]

要将每个数组保存为JSON文件,可以使用Xidel集成的EXPath文件模块

$ xidel -se '
  for tumbling window $w in 1 to 5
  start $s at $i when $s mod 2 eq 1
  count $i
  return
  x"output_{$i}.json - {join($w)}"
'
output_1.json - 1 2
output_2.json - 3 4
output_3.json - 5

$ echo '[{"key1":"value1"},{"key2":"value2"},{"key3":"value3"},{"key4":"value4"},{"key5":"value5"}]' | \
  xidel -se '
  for tumbling window $w in 1 to count($json())
  start $s at $i when $s mod 2 eq 1
  count $i
  return
  file:write(
    x"output_{$i}.json",
    array{$w ! $json(.)},
    {"method":"json"}
  )
'

$ xidel -s output_1.json output_2.json output_3.json -e '$raw'
$ xidel -s output_1.json output_2.json output_3.json -e '$json' --output-json-indent=compact
[{"key1":"value1"},{"key2":"value2"}]
[{"key3":"value3"},{"key4":"value4"}]
[{"key5":"value5"}]

1

如其他回答中所提到的那样,jq可能是解决问题的方法。因为我不熟悉jq,所以我编写了一个使用非常常见命令( echo、cat、wc、head、tail、sed、expr)的bash脚本(splitjson.sh)。该脚本将json文件拆分成不超过指定字节数的块。如果在指定的字节数内无法拆分(一个json项很长或每个块的最大字节数太小),脚本会停止向json文件写入,并写入错误信息。

以下是使用问题中的数据(example.json)的示例:

[{"key1":"value1"},{"key2":"value2"},{"key3":"value3"},{"key4":"value4"},{"key5":"value5"}]

执行脚本并设置每个块的最大字节数的命令是:
$ ./splitjson.sh example.json 40

结果如下:
$ head example.json.*
==> example.json.0 <==
[{"key1":"value1"},{"key2":"value2"}]
==> example.json.1 <==
[{"key3":"value3"},{"key4":"value4"}]
==> example.json.2 <==
[{"key5":"value5"}]

脚本处理大括号 '}',逗号 ',' 和起始大括号 '{' 之间的空格、制表符和换行符的情况。
我成功地在82 MB的json文件上使用了这个脚本。我期望它可以处理更大的文件。
这是脚本(splitjson.sh):
#!/bin/bash
if [ $# -ne 2 ]
then
    echo "usage: $0 file_to_split.json nb_bytes_max_per_split"
    exit 1
fi
if [[ -r $1 ]]
then
    input=$1
    echo "reading from file '$input'"
else
    echo "cannot read from specified input file '$1'"
    exit 2
fi
if [[ $2 = *[[:digit:]]* ]]; then
    maxbytes=$2
    echo "taking maximum bytes '$maxbytes'"
else
    echo "provided maximum number of bytes '$2' is not numeric"
    exit 3
fi

start=0
over=0
iteration=0
inputsize=`cat $input|wc -c`
tailwindow="$input.tail"
echo "input file size: $inputsize"
tmp="$input.tmp"
cp $input $tmp
sed -e ':a' -e 'N' -e '$!ba' -e 's/}[[:space:]]*,[[:space:]]*{/},{/g' -i'.back' $tmp
rm "$tmp.back"
inputsize=`cat $tmp|wc -c`
if [ $inputsize -eq 0 ]; then
    cp $input $tmp
    sed -e 's/}[[:space:]]*,[[:space:]]*{/},{/g' -i'.back' $tmp
    rm "$tmp.back"
fi
inputsize=`cat $tmp|wc -c`
while [ $over -eq 0 ]; do
    output="$input.$iteration"
    if [ $iteration -ne 0 ]; then
                echo -n "[{">$output
    else
                echo -n "">$output
    fi
    tailwindowsize=`expr $inputsize - $start`
    cat $tmp|tail -c $tailwindowsize>$tailwindow
    tailwindowresultsize=`cat $tailwindow|wc -c`
    if [ $tailwindowresultsize -le $maxbytes ]; then
        cat $tailwindow>>$output
        over=1
    else
        cat $tailwindow|head -c $maxbytes|sed -E 's/(.*)\},\{(.*)/\1}]/'>>$output
    fi
    jsize=`cat $output|wc -c`
    start=`expr $start + $jsize`
    if [ $iteration -eq 0 ]; then
        start=`expr $start + 1`
    else
        start=`expr $start - 1`
    fi
    endofj=`cat $output|tail -c 3`
    if [ $over -ne 1 ]; then
        if [ ${endofj:1:2} != "}]" ]; then
            if [ ${endofj:0:2} != "}]" ]; then
                echo -e "ERROR: at least one split pattern wasn't found. Aborting. This could be due to wrongly formatted json or due to a json entry too long compared to the provided maximum bytes. Maybe you should try increasing this parameter?\a"
                exit 4
            fi
        fi
    fi
    jsizefinal=`cat $output|wc -c`
    echo "wrote $jsizefinal bytes of json for iteration $iteration to $output"
    iteration=`expr $iteration + 1`
done
rm $tailwindow
rm $tmp

0
将代码分成两个独立的jq调用,允许第二个调用使用input助手一次只处理一个输入。在第二个调用中使用try助手可以优雅地处理不完整的行,如果你没有剩余的两个输入项。
s='[{"key1":"value1"},{"key2":"value2"},{"key3":"value3"},{"key4":"value4"},{"key5":"value5"}]'

jq '.[]' <<<"$s" | \
  jq -c -n 'repeat(input as $i1 | try (input as $i2 | [$i1, $i2]) catch [$i1])?' | \
  split -l 2 -d -a 3 - meta_

...在第一个文件中发出信号:

[{"key1":"value1"},{"key2":"value2"}]
[{"key3":"value3"},{"key4":"value4"}]

...而在第二个:

[{"key5":"value5"}]

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