使用JQ将多字值的JSON字符串解析为关联数组

使用JQ将多字值的JSON字符串解析为关联数组

我正在使用bashwithjq来解析从返回的数据https://ipinfo.io/json到关联数组中。我找到了一个很好的例子,几乎可以完成工作@https://gist.github.com/awesome/b3f65084c70264e87be3e72ee8abd0e5

虽然该代码能够解析大部分数据,但当值包含多字字符串时,它会失败。我怀疑这个问题与将引号放在正确的位置有关,但我不知道在哪里。我查看了jq文档,并有了一个大概的想法,但细节让我难住了。我在理解jq管道、模板和缩减 之间的相互作用时遇到了一些困难。(这是我第一次使用jq,尽管我对 很扎实regex。)

我的代码版本是:

locationResult=$(curl -s 'https://ipinfo.io/json')
arrayAsString=$(echo "$locationResult" | jq --raw-output '. | to_entries | map("[\(.key)]=\(.value)") | reduce .[] as $item ("associativeArray=("; . + $item + " ") + ")"')
declare -A "$arrayAsString"
echo ${associativeArray[org]}

对于我的位置,org返回一个多字的公司名称,这会导致declare -A "$arrayAsString"生成警告/错误,并且echo ${associativeArray[org]}仅生成该字段的第一个单词org

我尝试jq根据以下内容引用结果当 json 值包含空格时将 jq 输出分配给 bash 数组问题但这没有用。

任何帮助将不胜感激。

答案1

你总是可以这样做:

typeset -A ipinfo
while IFS= read -rd '' key && IFS= read -rd '' value; do
  ipinfo[$key]=$value
done < <(
  set -o pipefail
  curl -s https://ipinfo.io/json |
    jq -j '
      to_entries[] |
      [.key, .value | tostring] |
      map(gsub("\u0000"; "") + "\u0000") |
      add'
)

wait "$!" || exit # if curl or jq failed. Needs bash 4.4 or newer.

也就是说,获取jq输出以 NUL 分隔的键和值(转换为字符串并删除 NUL 字符(bash 无法存储在其变量中)),因此可以使用IFS= read -rd ''.

这将允许任意键和值,但包含 NUL 字符和空键的键和值除外(作为 bash 关联数组的一个不幸的限制)。当有一天ipinfo.io添加一个带有空键的元素时,该脚本将会中断,因此您可能需要显式排除带有空键的成员。另请注意,我们将值转换为字符串,因为 bash(与 ksh93 相反)不支持复杂/递归数据结构。

zsh而不是bash

typeset -A ipinfo
ipinfo=(
  ${(0)"$(
    set -o pipefail
    curl -s https://ipinfo.io/json |
      jq -j '
        to_entries |
          map([.key, .value | tostring]) |
          flatten |
          map(gsub("\u0000"; "")) |
          join("\u0000")'
  )"}
) || exit

typeset -p ipinfo

zsh确实支持空键,并且有一种正确的方法可以从键和值列表中安全地分配关联数组作为一个整体。它确实支持在变量中存储 NUL,但由于这里我们使用 NUL 作为分隔符,所以我们仍然需要从值中删除它们。

Ksh93(shell bash 从中复制了大部分 API)确实支持复杂的数据结构,ksh93v-beta 版本甚至对将 json 解析为实验性支持,但这仍然有很多错误。基于它的 ksh2020 中删除了 json 支持(现已废弃),而仍在维护的 ksh93 版本则基于 ksh93u+,因此也没有它,因此您需要手动实现解析。 ksh93 也不支持在其变量中存储 NUL,尽管有一些帮助程序可以在 Base64 编码之间进行转换,这些可以在此处利用。


请注意,您的方法引入了任意命令执行漏洞。您不希望 shell 解析器暴露于来自互联网的一些随机数据。即使使用jq's @sh(旨在以适合作为sh代码输入的格式对值进行编码),当数据不是预期类型时,也很容易忽视问题。像eval "$untrusteddata"or typeset/ 这样的事情declare ... "$untrusteddata"应该敲响非常响亮的警钟。

在这里,我将使用适当的编程语言而不是 shell,尤其是bash.

答案2

我可能会这样做:

declare -A arr
eval "$(
    curl -s https://ipinfo.io/json |
    jq -r 'to_entries[] | @sh "arr[\(.key|tostring)]=\(.value|tostring)"'
)"

这使用 引用 shell 的键和值@sh,并且使用 显式地将键和值转换为字符串tostring(处理(字符串化)意外类型的值)。表达式jq吐出 shell 代码,shell 对其求值,创建arr关联数组。

史蒂芬·查泽拉斯 (Stéphane Chazelas) 在书中提到了警钟他的回答,但假设输入是有效的 JSON,我确实找不到这种情况。

答案3

map中的函数中jq,您需要将键和值括在单引号 ( ') 中。在您的情况下,空格仅出现在值中,但将键嵌入引号中也将处理包含空格的键。

您需要'"'"'在键和值之前和之后添加:

arrayAsString=$(echo "$locationResult" | jq --raw-output '. | to_entries | map("['"'"'\(.key)'"'"']='"'"'\(.value)'"'"'") | reduce .[] as $item ("associativeArray=("; . + $item + " ") + ")"')

$或者在命令的第一个单引号之前添加美元符号jq,然后将键和值嵌入到\'.

arrayAsString=$(echo "$locationResult" | jq --raw-output $'. | to_entries | map("[\'\(.key)\']=\'\(.value)\'") | reduce .[] as $item ("associativeArray=("; . + $item + " ") + ")"')

请注意,如果值的键本身包含单引号,则此解决方法将不起作用。但除此之外应该没问题。

答案4

首先,我想感谢 @StéphaneChazelas,他用一个运行良好的解决方案回答了我的问题,并激励我在box.
对键的关注可以通过在之前插入解析管道来null解决。del(."") |to_entries[]

虽然@StéphaneChazelas 的解决方案有效,但我想要一个更干净的解决方案,不需要循环和线程,最重要的是,只查看输入一次。经过一系列实验后,我已经能够生成以下代码,它以我认为更有效的方式实现了相同的结果。 (此代码受到 @StéphaneChazelas 和 github 的思想的影响awesome,后者是我在 OP 中链接的示例的作者。)

parsed=$(echo "$theInput" | jq --raw-output ' . | del(."") | to_entries | map("[\(.key)]=\u0022\(.value)\u0022") | reduce .[] as $item ("locationData=("; . + $item + " ") + ")"')
echo -e "parsed=\n$parsed"
declare -A "$parsed"
echo -e "locationData[org]=\n${locationData[org]}"
echo -e "locationData[city]=\n${locationData[city]}"
echo -e "locationData[\"loc coord\"]=\n${locationData['loc coord']}\n\n"

其中echo "$theInput"可以用curl内联调用代替。curl在使用上述代码中的变量之前,我采用了将调用提供给 bash 变量的方法。上面的代码使用以下示例数据工作,这些数据已被“屠宰”以使其尽可能“糟糕”。

  1. 有一个带有null钥匙的条目。
  2. 有一个键没有对应的值。
  3. 有一个多字字符串包含[]'转义引号\"
  4. 包含key由两个以空格分隔的单词组成的 a (根据 @aviro 建议)。
  5. 到目前为止,我发现的唯一问题是值中不能包含未转义的引号,这会导致运行时错误。
theInput=$(cat <<EOF
{
  "ip": "W.X.Y.Z",
  "": "123.456.789.ABC",
  "city": "Some City",
  "town": "",
  "region": "My State",
  "country": "My Country",
  "loc coord": "0.000,0.0000",
  "org": "Company's [Short] \"Corporation\" Name",
  "postal": "12345",
  "timezone": "Some/Timezone",
  "readme": "https://ipinfo.io/missingauth"
}
EOF
)

请随意指出我的主张和方法论中的漏洞。

相关内容