首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >在使用jq处理大型文件时提高性能

在使用jq处理大型文件时提高性能
EN

Stack Overflow用户
提问于 2020-07-10 01:17:51
回答 3查看 5.3K关注 0票数 20

用例

我需要以内存高效的方式将JSON数据的大文件(~5G)拆分为具有新行分隔的JSON的较小文件(即不必将整个JSON读入内存)。每个源文件中的JSON数据都是一个对象数组。

不幸的是,源数据是而不是 新行分隔的JSON,而且在某些情况下文件中根本没有换行符。这意味着我不能简单地使用split命令将大文件按换行符分割成较小的块。下面是如何在每个文件中存储源数据的示例:

带有换行符的源文件示例。

代码语言:javascript
运行
复制
[{"id": 1, "name": "foo"}
,{"id": 2, "name": "bar"}
,{"id": 3, "name": "baz"}
...
,{"id": 9, "name": "qux"}]

没有换行符的源文件示例。

代码语言:javascript
运行
复制
[{"id": 1, "name": "foo"}, {"id": 2, "name": "bar"}, ...{"id": 9, "name": "qux"}]

下面是一个单一输出文件所需格式的示例:

代码语言:javascript
运行
复制
{"id": 1, "name": "foo"}
{"id": 2, "name": "bar"}
{"id": 3, "name": "baz"}

电流溶液

我能够通过使用jqsplit实现所需的结果,如本所以波斯特中所述。由于有了流解析器,这种方法是内存高效的。下面是达到预期结果的命令:

代码语言:javascript
运行
复制
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相比,后者可以更快地产生相同的输出。

下面是一些性能基准,用于显示jqsed的处理时间。

代码语言:javascript
运行
复制
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

问题

  1. 这是jqsed慢的处理速度吗?考虑到jq正在做大量的验证工作,它的速度会慢一些,但4X慢一点似乎不对。
  2. 我能做些什么来提高jq处理这个文件的速度吗?我更喜欢使用jq来处理文件,因为我确信它可以无缝地处理其他行输出格式,但考虑到我每天要处理数千个文件,很难证明我观察到的速度差异是正确的。
EN

回答 3

Stack Overflow用户

回答已采纳

发布于 2020-10-16 07:33:19

jq的流解析器(使用-stream命令行选项调用的解析器)故意牺牲速度,以减少内存需求,如下面的度量部分所示。jstream (它的主页是https://github.com/bcicen/jstream )是一种不同的平衡(似乎更接近你想要的)的工具

在bash或bash类shell中运行命令序列:

代码语言:javascript
运行
复制
cd
go get github.com/bcicen/jstream
cd go/src/github.com/bcicen/jstream/cmd/jstream/
go build

将产生一个可执行文件,您可以这样调用该可执行文件:

代码语言:javascript
运行
复制
jstream -d 1 < INPUTFILE > STREAM

假设INPUTFILE包含一个(可能是巨大的) JSON数组,上面的操作将类似于jq的.[],并带有jq的-c (紧凑)命令行选项。事实上,如果INPUTFILE包含JSON数组流或JSON非标量流,情况也是如此.

说明性时空度量

摘要

对于手头的任务(流数组的顶级项):

代码语言:javascript
运行
复制
                  mrss   u+s
jq --stream:      2 MB   447
jstream    :      8 MB   114
jm         :     13 MB   109
jq         :  5,582 MB    39

用词:

  1. space:jstream内存经济,但不如jq的流解析器经济。
  2. time:jstream运行速度略低于jq的常规解析器,但比jq的流解析器快4倍。

有趣的是,空间*时间对于jstream和jq的流解析器来说几乎是一样的。

测试文件的特性

测试文件由10,000,000个简单对象组成:

代码语言:javascript
运行
复制
[
{"key_one": 0.13888342355537053, "key_two": 0.4258700286271502, "key_three": 0.8010012924267487}
,{"key_one": 0.13888342355537053, "key_two": 0.4258700286271502, "key_three": 0.8010012924267487}
...
]
代码语言:javascript
运行
复制
$ ls -l input.json
-rw-r--r--  1 xyzzy  staff  980000002 May  2  2019 input.json

$ wc -l input.json
 10000001 input.json

jq时代与mrss

代码语言:javascript
运行
复制
$ /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
 10000000

jstream时间和mrss

代码语言:javascript
运行
复制
$ /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
 10000000

jm时代与mrss

jm是另一个用于“流”JSON数组和对象的命令行实用工具,使用最少的内存。

代码语言:javascript
运行
复制
$ /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
票数 8
EN

Stack Overflow用户

发布于 2020-10-19 08:47:34

限制

在一般情况下,JSON需要使用能够理解JSON的工具进行解析。您可以例外并遵循这些建议,但前提是您确信:

  • 您有一个没有嵌套对象的带有平面 JSON对象的数组(如用例中的)。
  • 大括号在对象中的任何地方都不存在,这意味着您没有类似于以下内容的内容:{id:1, name:"foo{bar}"}

使用外壳

如果满足上述条件,则可以使用shell将其转换为JSONL并拆分为较小的文件,并且它将比JSON解析或全文处理快很多倍。另外,它几乎是无记忆的,特别是当您使用带或不带sedawk的核心-utils时。

甚至更简单的方法:

代码语言:javascript
运行
复制
grep -o '{[^}]*}' file.json

将更快,但将需要一些内存(小于jq)。

您尝试过的sed命令速度很快,但需要内存,因为流编辑器sed是逐行读取的,如果文件中根本没有换行符,它将全部加载到内存中,sed需要的大小是流最大行的2-3倍。但是,如果首先使用新行拆分流,使用trcut等核心实用程序,那么内存使用率非常低,性能也很好。

解决方案

经过一些测试,我发现这个更快,没有记忆。除此之外,它不依赖于对象之外的额外字符,比如逗号和几个空格,或者仅用逗号等,它只会匹配对象{...},并将它们打印到新行。

代码语言:javascript
运行
复制
#!/bin/sh -
LC_ALL=C < "$1" cut -d '}' -f1- --output-delimiter="}"$'\n' |\
    cut -sd '{' -f2 | sed 's/^/{/' > "$2"

若要拆分JSONL,请使用-l而不是-c,以确保不拆分任何对象,请使用如下所示:

代码语言:javascript
运行
复制
split -l 1000 -d --additional-suffix='.json' - path/to/file/prefix

或者全部在一起

代码语言:javascript
运行
复制
#!/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"

用法:

代码语言:javascript
运行
复制
sh script.sh input.json path/to/new/files/output

将在所选路径中创建输出为1.json、output2.json等的文件。

注意:如果您的流包含非UTF-8多个字符,删除LC_ALL=C,这只是一个小的速度优化,这是不必要的。

注意:我假设输入时完全没有换行符,或者像第一个用例中的换行符。为了概括并包含文件中任何地方的任何换行符,我添加了一个小的修改。在这个版本中,tr最初将截断所有换行符,而对性能几乎没有影响:

代码语言:javascript
运行
复制
#!/bin/sh -
n=1000
LC_ALL=C < "$1" tr -d $'\n' |\
    cut -d '}' -f1- --output-delimiter="}"$'\n' |\
    cut -sd '{' -f2 | sed 's/^/{/' > "$2"

测试

以下是一些测试结果。他们是有代表性的,所有处决的时间都是相似的。

下面是我使用的脚本,它输入了n的各种值

代码语言:javascript
运行
复制
#!/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一起测试时,我使用了小文件,因为它很早就进入了交换区。然后用更大的文件只使用有效的解决方案。

代码语言:javascript
运行
复制
==> 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
票数 4
EN

Stack Overflow用户

发布于 2022-01-06 22:52:05

我认为thanasisp的回答很好,涵盖了很多问题。使用cut非常聪明地解决了sed内存问题。

您尝试过的sed命令速度很快,但需要内存,因为sed (流编辑器)是逐行读取的,如果文件中根本没有换行符,它会将所有命令加载到内存中,sed需要2-3倍于流最大行的大小。

但是,正如在答案中所指出的,sed脚本只适用于非常简单的JSON对象(除了表示第一级对象的结束外,没有嵌套对象,也没有} )。

更高级的sed脚本

这可以通过一个更复杂的sed脚本来改进,除了发布句柄的模式之外。

代码语言:javascript
运行
复制
[{"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

代码语言:javascript
运行
复制
#!/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相结合

代码语言:javascript
运行
复制
< huge.json cut -d '}' -f1- --output-delimeter="}"$'\n' |\
 json-newline-json.sed |\
 split ...

注意:

如果您的JSON输入相对清晰(且不奇怪),那么用sed解决问题是强大的、可移植的和快速的。然而,这是不稳定的。缺点是输入或输出都没有被验证。解析序列化格式最好小心一点。因此,对于大多数意图和目的,我可能会坚持使用jq

票数 1
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/62825963

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档