用例
我需要以内存高效的方式将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-16 07:33:19
jq的流解析器(使用-stream命令行选项调用的解析器)故意牺牲速度,以减少内存需求,如下面的度量部分所示。jstream (它的主页是https://github.com/bcicen/jstream )是一种不同的平衡(似乎更接近你想要的)的工具
在bash或bash类shell中运行命令序列:
cd
go get github.com/bcicen/jstream
cd go/src/github.com/bcicen/jstream/cmd/jstream/
go build将产生一个可执行文件,您可以这样调用该可执行文件:
jstream -d 1 < INPUTFILE > STREAM假设INPUTFILE包含一个(可能是巨大的) JSON数组,上面的操作将类似于jq的.[],并带有jq的-c (紧凑)命令行选项。事实上,如果INPUTFILE包含JSON数组流或JSON非标量流,情况也是如此.
说明性时空度量
摘要
对于手头的任务(流数组的顶级项):
mrss u+s
jq --stream: 2 MB 447
jstream : 8 MB 114
jm : 13 MB 109
jq : 5,582 MB 39用词:
space:jstream内存经济,但不如jq的流解析器经济。time:jstream运行速度略低于jq的常规解析器,但比jq的流解析器快4倍。有趣的是,空间*时间对于jstream和jq的流解析器来说几乎是一样的。
测试文件的特性
测试文件由10,000,000个简单对象组成:
[
{"key_one": 0.13888342355537053, "key_two": 0.4258700286271502, "key_three": 0.8010012924267487}
,{"key_one": 0.13888342355537053, "key_two": 0.4258700286271502, "key_three": 0.8010012924267487}
...
]$ ls -l input.json
-rw-r--r-- 1 xyzzy staff 980000002 May 2 2019 input.json
$ wc -l input.json
10000001 input.jsonjq时代与mrss
$ /usr/bin/time -l jq empty input.json
43.91 real 37.36 user 4.74 sys
4981452800 maximum resident set size
$ /usr/bin/time -l jq length input.json
10000000
48.78 real 41.78 user 4.41 sys
4730941440 maximum resident set size
/usr/bin/time -l jq type input.json
"array"
37.69 real 34.26 user 3.05 sys
5582196736 maximum resident set size
/usr/bin/time -l jq 'def count(s): reduce s as $i (0;.+1); count(.[])' input.json
10000000
39.40 real 35.95 user 3.01 sys
5582176256 maximum resident set size
/usr/bin/time -l jq -cn --stream 'fromstream(1|truncate_stream(inputs))' input.json | wc -l
449.88 real 444.43 user 2.12 sys
2023424 maximum resident set size
10000000jstream时间和mrss
$ /usr/bin/time -l jstream -d 1 < input.json > /dev/null
61.63 real 79.52 user 16.43 sys
7999488 maximum resident set size
$ /usr/bin/time -l jstream -d 1 < input.json | wc -l
77.65 real 93.69 user 20.85 sys
7847936 maximum resident set size
10000000jm时代与mrss
jm是另一个用于“流”JSON数组和对象的命令行实用工具,使用最少的内存。
$ /usr/bin/time -lp jm --count input.json
real 110.75 user 107.67 sys 1.22
13 295 616 maximum resident set size
7 303 168 peak memory footprint
10000000发布于 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 KB发布于 2022-01-06 22:52:05
我认为thanasisp的回答很好,涵盖了很多问题。使用cut非常聪明地解决了sed内存问题。
您尝试过的sed命令速度很快,但需要内存,因为sed (流编辑器)是逐行读取的,如果文件中根本没有换行符,它会将所有命令加载到内存中,sed需要2-3倍于流最大行的大小。
但是,正如在答案中所指出的,sed脚本只适用于非常简单的JSON对象(除了表示第一级对象的结束外,没有嵌套对象,也没有} )。
更高级的sed脚本
这可以通过一个更复杂的sed脚本来改进,除了发布句柄的模式之外。
[{"id": 1, "name": "foo"}
,{"id": 2, "name": "bar"}
,{"id": 3, "name": "baz"}
,{"id":4, "name": 10}
,{"id":5, "name":"\\\" },{"}
,{"id": {"a":6}, "name": 10}]我们主要通过利用{/}和"成对出现的事实来实现这一点。
json-newline-json.sed
#!/bin/sed -nf
# Skip empty lines
/^$/d
# From first line to first line starting with [
0 , /\[/ {
# Replace opening [ if exists
# + stripping leading whitespace
/^[[:space:]]*\[/ s@^[[:space:]]*\[@,@
}
# Line starts with comma
/^\,/ {
# Strip it
}
# Start of loop
: x
# Save to hold
h
# delete all chars except " and \
s@[^"\\]@@g
# Delete all reverse solidi and non-escaped " chars
s@\(\\"\|\\\)@@g
# Even match
/^\(""\)\+$/ {
# Fetch hold
g
# Delete everything between ".."
s@"[^"]*"@@g
# Delete all chars except {}
s@[^{}]@@g
# Match even {} pairs
/^\([{}][{}]\)\+$/ {
# The hold space contains our assembled ,{..} object
g
# Strip the leading comma
s@^\,@@
# Print
p
# Skip to next line
d
}
# The hold space contains our partial ,{.. object
g
N
s@\n@@
t x
}
# Odd match
/^\(""\)*"$/ {
# The hold space contains our partial ,{.." object
g
# Fetch the next line to append
N
# Delete the newline added by N (append without newline)
s@\n@@
# Restart loop x
t x
}与cut相结合
< huge.json cut -d '}' -f1- --output-delimeter="}"$'\n' |\
json-newline-json.sed |\
split ...注意:
如果您的JSON输入相对清晰(且不奇怪),那么用sed解决问题是强大的、可移植的和快速的。然而,这是不稳定的。缺点是输入或输出都没有被验证。解析序列化格式最好小心一点。因此,对于大多数意图和目的,我可能会坚持使用jq。
https://stackoverflow.com/questions/62825963
复制相似问题