首页 > 解决方案 > 使用 jq 处理大文件时提高性能

问题描述

用例

我需要以一种内存有效的方式(即,无需将整个 JSON blob 读入内存)~5GJSON 数据的大文件(每个源文件中的 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"}

当前解决方案

我可以通过使用jqsplit如this SO Post中所述来实现所需的结果。由于jq流解析器,这种方法具有内存效率。这是实现所需结果的命令:

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以下是一些性能基准,用于显示vs.的处理时间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

问题

  1. jq与 相比,这种较慢的处理速度是预期的sed吗?考虑到它jq在后台进行了大量验证,所以速度会慢一些,但慢 4 倍似乎并不正确。
  2. 我能做些什么来提高jq处理这个文件的速度吗?我更喜欢用它jq来处理文件,因为我相信它可以无缝处理其他行输出格式,但鉴于我每天要处理数千个文件,很难证明我观察到的速度差异是合理的。

标签: jsonsedsplitjq

解决方案


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's .[],带有 jq 的 -c (紧凑)命令行选项。事实上,如果 INPUTFILE 包含 JSON 数组流,或者 JSON 非标量流,情况也是如此……

说明性时空度量

概括

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

                  mrss   u+s
jq --stream:      2 MB   447
jstream    :      8 MB   114
jq         :  5,582 MB    39

用一句话来说:

  1. space: jstream 节省内存,但不如 jq 的流解析器。

  2. time:jstream 的运行速度比 jq 的常规解析器稍慢,但比 jq 的流解析器快 4 倍。

有趣的是,两个流解析器的空间*时间大致相同。

测试文件的表征

测试文件包含 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.json

jq时代和夫人

$ /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时代和夫人

$ /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


推荐阅读