用例
我需要以内存高效的方式将JSON数据的大文件(~5G)拆分为具有新行分隔的JSON的较小文件(即不必将整个JSON读入内存)。每个源文件中的JSON数据都是一个对象数组。
不幸的是,源数据是而不是 新行分隔的JSON,而且在某些情况下文件中根本没有换行符。这意味着我不能简单地使用split命令将大文件按换行符分割成较小的块。下面是如何在每个文件中存储源数据的示例:
带有换行符的源文件示例。
[{"id": 1, "name": "foo"}
,{"id": 2, "name": "bar"}
,{"id": 3, "name": "baz"}
...
,{"id": 9, "name": "qux"}]没有换行符的源文件示例。
[{"id": 1, "name": "foo"}, {"id": 2, "name": "bar"}, ...{"id": 9, "name": "qux"}]下面是一个单一输出文件所需格式的示例:
{"id": 1, "name": "foo"}
{"id": 2, "name": "bar"}
{"id": 3, "name": "baz"}电流溶液
我能够通过使用jq和split实现所需的结果,如本所以波斯特中所述。由于有了流解析器,这种方法是内存高效的。下面是达到预期结果的命令:
cat large_source_file.json \
| jq -cn --stream 'fromstream(1|truncate_stream(inputs))' \
| split --line-bytes=1m --numeric-suffixes - split_output_file问题
上面的命令使用~47 mins来处理整个源文件。这似乎很慢,特别是与sed相比,后者可以更快地产生相同的输出。
下面是一些性能基准,用于显示jq和sed的处理时间。
export SOURCE_FILE=medium_source_file.json # smaller 250MB
# using jq
time cat ${SOURCE_FILE} \
| jq -cn --stream 'fromstream(1|truncate_stream(inputs))' \
| split --line-bytes=1m - split_output_file
real 2m0.656s
user 1m58.265s
sys 0m6.126s
# using sed
time cat ${SOURCE_FILE} \
| sed -E 's#^\[##g' \
| sed -E 's#^,\{#\{#g' \
| sed -E 's#\]$##g' \
| sed 's#},{#}\n{#g' \
| split --line-bytes=1m - sed_split_output_file
real 0m25.545s
user 0m5.372s
sys 0m9.072s问题
jq比sed慢的处理速度吗?考虑到jq正在做大量的验证工作,它的速度会慢一些,但4X慢一点似乎不对。jq处理这个文件的速度吗?我更喜欢使用jq来处理文件,因为我确信它可以无缝地处理其他行输出格式,但考虑到我每天要处理数千个文件,很难证明我观察到的速度差异是正确的。发布于 2020-10-19 08:47:34
限制
在一般情况下,JSON需要使用能够理解JSON的工具进行解析。您可以例外并遵循这些建议,但前提是您确信:
{id:1, name:"foo{bar}"}。使用外壳
如果满足上述条件,则可以使用shell将其转换为JSONL并拆分为较小的文件,并且它将比JSON解析或全文处理快很多倍。另外,它几乎是无记忆的,特别是当您使用带或不带sed或awk的核心-utils时。
甚至更简单的方法:
grep -o '{[^}]*}' file.json将更快,但将需要一些内存(小于jq)。
您尝试过的sed命令速度很快,但需要内存,因为流编辑器sed是逐行读取的,如果文件中根本没有换行符,它将全部加载到内存中,sed需要的大小是流最大行的2-3倍。但是,如果首先使用新行拆分流,使用tr、cut等核心实用程序,那么内存使用率非常低,性能也很好。
解决方案
经过一些测试,我发现这个更快,没有记忆。除此之外,它不依赖于对象之外的额外字符,比如逗号和几个空格,或者仅用逗号等,它只会匹配对象{...},并将它们打印到新行。
#!/bin/sh -
LC_ALL=C < "$1" cut -d '}' -f1- --output-delimiter="}"$'\n' |\
cut -sd '{' -f2 | sed 's/^/{/' > "$2"若要拆分JSONL,请使用-l而不是-c,以确保不拆分任何对象,请使用如下所示:
split -l 1000 -d --additional-suffix='.json' - path/to/file/prefix或者全部在一起
#!/bin/sh -
n=1000
LC_ALL=C < "$1" cut -d '}' -f1- --output-delimiter="}"$'\n' |\
cut -sd '{' -f2 | sed 's/^/{/' |\
split -l "$n" -d --additional-suffix='.json' - "$2"用法:
sh script.sh input.json path/to/new/files/output将在所选路径中创建输出为1.json、output2.json等的文件。
注意:如果您的流包含非UTF-8多个字符,删除LC_ALL=C,这只是一个小的速度优化,这是不必要的。
注意:我假设输入时完全没有换行符,或者像第一个用例中的换行符。为了概括并包含文件中任何地方的任何换行符,我添加了一个小的修改。在这个版本中,tr最初将截断所有换行符,而对性能几乎没有影响:
#!/bin/sh -
n=1000
LC_ALL=C < "$1" tr -d $'\n' |\
cut -d '}' -f1- --output-delimiter="}"$'\n' |\
cut -sd '{' -f2 | sed 's/^/{/' > "$2"测试
以下是一些测试结果。他们是有代表性的,所有处决的时间都是相似的。
下面是我使用的脚本,它输入了n的各种值
#!/bin/bash
make_json() {
awk -v n=2000000 'BEGIN{
x = "{\"id\": 1, \"name\": \"foo\"}"
printf "["
for (i=1;i<n;i++) { printf x ", " }
printf x"]"
}' > big.json
return 0
}
tf="Real: %E System: %S User: %U CPU%%: %P Maximum Memory: %M KB\n"
make_json
for i in {1..7}; do
printf "\n==> "
cat "${i}.sh"
command time -f "$tf" sh "${i}.sh" big.json "output${i}.json"
done在与jq一起测试时,我使用了小文件,因为它很早就进入了交换区。然后用更大的文件只使用有效的解决方案。
==> LC_ALL=C jq -c '.[]' "$1" > "$2"
Real: 0:16.26 System: 1.46 User: 14.74 CPU%: 99% Maximum Memory: 1004200 KB
==> LC_ALL=C jq length "$1" > /dev/null
Real: 0:09.19 System: 1.30 User: 7.85 CPU%: 99% Maximum Memory: 1002912 KB
==> LC_ALL=C < "$1" sed 's/^\[//; s/}[^}]*{/}\n{/g; s/]$//' > "$2"
Real: 0:02.21 System: 0.33 User: 1.86 CPU%: 99% Maximum Memory: 153180 KB
==> LC_ALL=C < "$1" grep -o '{[^}]*}' > "$2"
Real: 0:02.08 System: 0.34 User: 1.71 CPU%: 99% Maximum Memory: 103064 KB
==> LC_ALL=C < "$1" awk -v RS="}, {" -v ORS="}\n{" '1' |\
head -n -1 | sed '1 s/^\[//; $ s/]}$//' > "$2"
Real: 0:01.38 System: 0.32 User: 1.52 CPU%: 134% Maximum Memory: 3468 KB
==> LC_ALL=C < "$1" cut -d "}" -f1- --output-delimiter="}"$'\n' |\
sed '1 s/\[//; s/^, //; $d;' > "$2"
Real: 0:00.94 System: 0.24 User: 0.99 CPU%: 131% Maximum Memory: 3488 KB
==> LC_ALL=C < "$1" cut -d '}' -f1- --output-delimiter="}"$'\n' |\
cut -sd '{' -f2 | sed 's/^/{/' > "$2"
Real: 0:00.63 System: 0.28 User: 0.86 CPU%: 181% Maximum Memory: 3448 KB
# Larger files testing
==> LC_ALL=C < "$1" grep -o '{[^}]*}' > "$2"
Real: 0:20.99 System: 2.98 User: 17.80 CPU%: 99% Maximum Memory: 1017304 KB
==> LC_ALL=C < "$1" awk -v RS="}, {" -v ORS="}\n{" '1' |\
head -n -1 | sed '1 s/^\[//; $ s/]}$//' > "$2"
Real: 0:16.44 System: 2.96 User: 15.88 CPU%: 114% Maximum Memory: 3496 KB
==> LC_ALL=C < "$1" cut -d "}" -f1- --output-delimiter="}"$'\n' |\
sed '1 s/\[//; s/^, //; $d;' > "$2"
Real: 0:09.34 System: 1.93 User: 10.27 CPU%: 130% Maximum Memory: 3416 KB
==> LC_ALL=C < "$1" cut -d '}' -f1- --output-delimiter="}"$'\n' |\
cut -sd '{' -f2 | sed 's/^/{/' > "$2"
Real: 0:07.22 System: 2.79 User: 8.74 CPU%: 159% Maximum Memory: 3380 KBhttps://stackoverflow.com/questions/62825963
复制相似问题