【问题标题】:Extract key-value pairs from a JSON file with custom output formatting使用自定义输出格式从 JSON 文件中提取键值对
【发布时间】:2017-07-12 09:19:29
【问题描述】:

我想从一个巨大的日志文件中 grep 两个单词的组合,这些单词是分散的并且没有任何特定的顺序。

示例日志:

    {"1a":"2017-01-28 00:00:00","2a":"sample","a":"12345","b":"2017-02-06","c":"2017-02-06T17:51:02.454-08:00","d":"Mozilla/5.0
    ; en-US; rv:1.9.0.1) Gecko/2008070208 Firefox/3.0.1","e":"2017-02-06 
    ","f":"03","g":"example","h":"logA","i":"IFX","j":"a85","k":"12345678"},
{"1a":"2017-01-28 00:00:11","2a":"sample","a":"12345","b":"2017-02-06","c":"2017-02-06T17:51:02.454-08:00","d":"Mozilla/5.0
    ; en-US; rv:1.9.0.1) Gecko/2008070208 Firefox/3.0.1","e":"2017-02-06 
    ","f":"03","g":"example","h":"logB","i":"IFX","j":"a85","k":"12345678"}

在这个文件中,我想 grep "1a":"<value>""h":"<value of logA or logB>" 不应该有任何重复。

预期输出:

"1a":"2017-01-28 00:00:00" "h":"logA"
"1a":"2017-01-28 00:00:11" "h":"logB"

我尝试以这种方式使用 egrep,但它给出了整行:

egrep -oE '1a\|"h"' but this does not give the required output.

awk /pattern1/ && /pattern2/ filename #no use

感谢您的帮助

【问题讨论】:

  • DO NOT 使用文本/流处理器/编辑器来解析JSON,使用像jq这样的适当解析器
  • 以正确的JSON格式格式化您的文本输入并安装jq
  • 另外,正则表达式不是设计模式。标记已删除。

标签: json awk sed grep jq


【解决方案1】:

考虑使用非常灵活的jq JSON CLI,而不是标准实用程序,它:

  • 简化解决方案
  • 使其健壮
  • 允许泛化。
echo '
[
  { "1a":"2017-01-28 00:00:00", "2a":"sample", "h":"logA", "i":"IFX" }, 
  { "1a":"2017-01-28 00:00:11", "2a":"sample", "h":"logB", "i":"IFX" }
]' |
  jq -r --argjson keys '[ "1a", "h" ]' '
    .[] | "\"\($keys[0])\": \"\(.[$keys[0]])\" \"\($keys[1])\": \"\(.[$keys[1]])\""
  ' 

为了独立,该命令通过管道提供文字输入,并为可读性而格式化。
要改为将 文件 传递给 jq 命令,只需在脚本结束后指定其路径 '
(jq -r ... '...' file.json)

产量:

"1a": "2017-01-28 00:00:00" "h": "logA"
"1a": "2017-01-28 00:00:11" "h": "logB"
  • --argjson keys '[ "1a", "h" ]' 将变量 $keys 定义为要提取的键(属性)名称的 JSON 格式数组。

  • .[] 枚举输入数组的所有元素 - 单个对象 - 并且 $keys[<n>].[$keys[<n>]] 扩展为具有索引 <n>value 的属性名称分别是该属性名称(注意 .[...] 访问器)。

  • 大部分精力都花在了输出格式上:嵌入的" 字符。必须转义为 \",并且嵌入的变量引用必须包含在 \(...) 中 - 尽管使用带有单独标记的 + 来构建字符串也是一种选择。

推广解决方案

上述命令不容易推广到每行输出任意数量的键值对,因为数组索引(01)是明确指定的.

peak's helpful answer 启发,它展示了在jq 中定义帮助器函数 的简单示例,以下变体使用内置函数和自定义函数的组合来接受任意个要提取的密钥

echo '
[
  { "1a":"2017-01-28 00:00:00", "2a":"sample", "h":"logA", "i":"IFX" },
  { "1a":"2017-01-28 00:00:11", "2a":"sample", "h":"logB", "i":"IFX" }
]
' |
  jq -r --argjson keys '[ "1a", "h", "i"  ]' '
    def printKv($k; $v): "\"\($k)\": \"\($v)\"";
    .[] | . as $o | 
      reduce $keys[] as $k (""; . + if .=="" then "" else " " end + printKv($k; $o[$k]))
  '

yields(每行 3 个键值对,因为传递了 3 个键):

"1a": "2017-01-28 00:00:00" "h": "logA" "i": "IFX"
"1a": "2017-01-28 00:00:11" "h": "logB" "i": "IFX"

内置的reduce 函数用于通过迭代键值对并在自定义函数printKv 的帮助下为每个键值对创建字符串表示来构建目标字符串。


根据peak 的另一个建议,这里是一个更简单、更像jq 的替代方案,它产生相同的输出

echo '
[
  { "1a":"2017-01-28 00:00:00", "2a":"sample", "h":"logA", "i":"IFX" },
  { "1a":"2017-01-28 00:00:11", "2a":"sample", "h":"logB", "i":"IFX" }
]
' |
  jq -r --argjson keys '[ "1a", "h", "i"  ]' '
    def printKv($k): "\"\($k)\": \"\(.[$k])\"";
    .[] | [ $keys[] as $k | printKv($k) ] | join(" ")
  '
  • printKv() 现在只接受一个参数 - key $k - 并依赖管道输入 - 仍然包含输入对象 - 来提取关联的 - .[$k]

  • [ ... ] 中包含$keys[] as $k | printKv($k)多个 printKv 调用的输出作为单个数组 通过管道传递。

  • 这允许内置的join 函数将数组元素与空格连接起来以形成单个输出行。

【讨论】:

  • 感谢@mklement0,感谢您对答案的冗长解释:)
【解决方案2】:

这是对@mklement0 出色答案的调整。通过定义“print-me”函数,该调整最大限度地减少了必须转义双引号的烦恼:

def q: "\"\(tostring)\"";

.[] | "\($keys[0]|q): \(.[$keys[0]]|q) \($keys[1]|q): \(.[$keys[1]]|q)"

或者,如果您愿意:

def printKV($k): "\"\($k)\": \"\(.[$k])\""; 

.[] | printKV($keys[0]) + " " + printKV($keys[1])

广义解

使用上面刚刚定义的printKV/1,并假设在命令行(或通过其他方式)将 $keys 定义为字符串数组:

def printKeyValues(keys):
  [keys[] as $key | printKV($key)] | join(" ");

.[] | printKeyValues($keys)

【讨论】:

  • 我们怎样才能通过文件传递上述jq命令,只为这些键生成唯一值?
【解决方案3】:

awk 来救援!

$ awk -F, -v RS={ 'NR>1 {for(i=1;i<=NF;i++)
                         {if($i~/"1a":/) printf "%s", $i OFS
                          if($i~/"h":"log(A|B)"/) printf "%s\n", $i}}' file


"1a":"2017-01-28 00:00:00" "h":"logA"
"1a":"2017-01-28 00:00:11" "h":"logB"

当然最好使用 json 感知工具。

【讨论】:

  • 感谢您的回答,我怎样才能在指定的键(此处为 1a 和 h)之后打印整个 json 字符串,示例输出如 - "1a":"2017-01-28 00:00:00" "h":"logA" {"1a":"2017-01-28 00:00:00","2a":"sample","a":"12345","b":"2017-02-06","c":"20‌​17-02-06T17:51:02.45‌​4-08:00","d":"Mozill‌​a/5.0 ; en-US; rv:1.9.0.1) Gecko/2008070208 Firefox/3.0.1","e":"2017-02-06 ","f":"03","g":"example","h":"logA","i":"IFX","j":"a85","k":‌​"12345678"}
【解决方案4】:

输入

$ cat log
    {"1a":"2017-01-28 00:00:00","2a":"sample","a":"12345","b":"2017-02-06","c":"2017-02-06T17:51:02.454-08:00","d":"Mozilla/5.0
    ; en-US; rv:1.9.0.1) Gecko/2008070208 Firefox/3.0.1","e":"2017-02-06 
    ","f":"03","g":"example","h":"logA","i":"IFX","j":"a85","k":"12345678"},
{"1a":"2017-01-28 00:00:11","2a":"sample","a":"12345","b":"2017-02-06","c":"2017-02-06T17:51:02.454-08:00","d":"Mozilla/5.0
    ; en-US; rv:1.9.0.1) Gecko/2008070208 Firefox/3.0.1","e":"2017-02-06 
    ","f":"03","g":"example","h":"logB","i":"IFX","j":"a85","k":"12345678"}

输出

$ awk -F, -v RS='[{}]' '{s=""; for(i=1;i<=NF;i++)if($i~/^"(1a|h)":/)s=(s?s OFS:"") $i; if(s)print s}'  log 
"1a":"2017-01-28 00:00:00" "h":"logA"
"1a":"2017-01-28 00:00:11" "h":"logB"

【讨论】:

  • 感谢您的回答,我怎样才能在指定的键(此处为 1a 和 h)之后打印整个 json 字符串,示例输出如 - "1a":"2017-01-28 00:00:00" "h":"logA" {"1a":"2017-01-28 00:00:00","2a":"sample","a":"12345","b":"2017-02-06","c":"2017-02-06T17:51:02.454-08:00","d":"Mozilla/5.0 ; en-US; rv:1.9.0.1) Gecko/2008070208 Firefox/3.0.1","e":"2017-02-06 ","f":"03","g":"example","h":"logA","i":"IFX","j":"a85","k":"12345678"}
  • @user2340345: 你只需要print s,$0 这样awk -F, -v RS='[{}]' '{s=""; for(i=1;i&lt;=NF;i++)if($i~/^"(1a|h)":/)s=(s?s OFS:"") $i; if(s)print s,$0}' log
  • 感谢 0 美元的提示,这帮助我充分利用了您的答案和 @karafka 的答案。
猜你喜欢
  • 2018-11-21
  • 2019-07-05
  • 2018-02-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-02-09
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多