【问题标题】:Robust way check if the input is JSON or YAML检查输入是 JSON 还是 YAML 的稳健方法
【发布时间】:2021-06-28 02:03:30
【问题描述】:

在下面的代码中,应该接受多行作为 JSON 或 YAML 格式的字符串输入。它首先尝试将输入读取为 JSON,如果 JSON 失败,它会再次尝试将其读取为 YAML,如果两者都失败则返回错误。

现在问题出在 yaml.Unmarshal() 上。我检查了一下,如果输入是 JSON 字符串,它永远不会返回错误。 (正确或不正确)。主要问题 yaml.Unmarshal 永远不会返回错误。

最初,我认为它在 yaml.Unmarshal 实现上出错,但在我看来,它在解析输入时尽了最大努力,并且结构不违反 yaml;它永远不会返回错误。

func SpecsFromString(str string) (*Something, error) {
    r := strings.NewReader(str)
    return ReadTenantsSpec(r)
}

func ReadSpec(b io.Reader) (*Spec, error) {

    var spec Spec

    buffer, err := ioutil.ReadAll(b)
    if err != nil {
        return nil, err
    }

    err = json.Unmarshal(buffer, &spec)
    if err == nil {
        return &spec, nil
    }

    err = yaml.Unmarshal(buffer, &spec)
    if err == nil {
        return &spec, nil
    }

    return nil, &InvalidTenantsSpec{"unknown format"}
}

所以我的问题是,如果输入是 JSON 或 YAML,如何正确执行此测试? 在我看来,在 JSON unmashler 上做到这一点的唯一方法是区分 两个错误案例。原因是 JSON 通常在结构上更严格。 当输入根本不是 JSON,当输入是 JSON 但 JSON 的结构有错误时。第二种情况将允许我永远不会调用 yaml 解析器 首先。

也许有人想出了一个更巧妙的解决方案?

谢谢。

【问题讨论】:

  • 正则表达式是否可以首先检测输入是否为 JSON? stackoverflow.com/a/3710506/13906951
  • 并非如此。因为 parse 必须足够智能以报告两种不同的错误情况。我检查了 go JSON 只报告一个错误,格式不正确。想象一下,正则表达式检查通过,JSON 解析失败,我尝试调用 yaml 并且它永远不会失败......我认为唯一的方法是强制 yaml 规范中必须始终存在的值。
  • YAML is a superset of JSON。如果您可以选择,我会更改函数参数以要求指定输入格式,而不是编写与格式无关的实现。
  • @blackgreen 我同意我认为这是唯一的出路。在 yaml 中强制执行特定的强制值。
  • @JohnnyBonelli,我在答案中添加了一个基本示例。在我的示例中,我没有任何特定的评分标准,但我演示了包装阅读器并评估已处理输入的一部分。我使用换行符作为分隔符,但它实际上可以是任何你认为会给出准确框架的东西,直到任何源文件的全部内容。

标签: go yaml


【解决方案1】:

json.Unmarshal 确实会在无效的 JSON 语法上返回 SyntaxError,并且当语法正确但解组失败时会出现其他不同的错误,因此您可以使用它来区分。

关于 YAML,如果你使用 yaml.v3,你可以编写一个自定义的 unmarshaler 来访问你输入的 Node 表示,并检查根节点是否设置了 Style Flow,这意味着类似于 JSON 的语法.然而,即使使用这种语法,YAML 也更加宽松(例如,字符串不需要被引用,序列中的尾随逗号和映射是允许的),虽然您可以检查包含的标量的引用样式,但可用的信息不足以确保输入是 JSON 可解析的(无法通过此接口检测到尾随逗号)。

所以检查输入是否是语法上有效的 JSON 的正确方法是检查返回的错误 json.Unmarshal

【讨论】:

  • 那是我最初尝试修复它,但如果你传递 yaml 文件它同样的错误 SyntaxError,如果你检查我在两种情况下提供的代码,如果它是 Json 或 None JSON(即 yaml 文件) err != nil,所以代码开始解析 Yaml,它接受 JSON 输入并且什么都不返回,最后一个错误从未返回,但我会再次交叉检查。
  • @JohnnyBonelli 好吧,是的,您需要修改错误检查,例如通过添加if _, ok := err.(*json.SyntaxError); !ok { return nil, &InvalidTenantsSpec{"wrong JSON structure"} }
【解决方案2】:

这就是我在对该问题的评论中引用的内容。这是一个中间件阅读器的简单示例。

  1. 此模式可让您避免必须完全解析文本主体,以防文本过大
  2. 理想情况下,它对下游操作没有影响,提供透明的 API。

从你的例子中,你会这样称呼:

b = WrapReader(b)
buffer, err := ioutil.ReadAll(b)
if err != nil {return nil, err}
if b.Writable.A > b.Writable.B {
    err = json.Unmarshal(buffer, &spec)
}
if err != nil || b.Writable.A <= b.Writable.B {
    err = yaml.Unmarshal(buffer, &spec)
}

实际上,它不会改变您正在处理的界面,同时获得对进程如何停止的一些控制。有很大的改进空间,但上面的 API 是由下面的代码提供的:

type Line []byte
type Writable struct {
    Line
    A int
    B int
}
type Decision struct{
    io.Reader
    Writable
}
func (d *Decision) Read(b_rx []byte) (int, error) {
    n, err := d.Reader.Read(b_rx)
    if err != nil && err != io.EOF {return n, err}
    for _, b_tx := range b_rx {
        d.Writable.WriteByte(b_tx)
    }
    return n, nil       
}
func (w *Writable) WriteByte (b byte) error {
    if b == '\n' {
        pJSON, pYAML, err := w.Score()
        if err != nil {return err}
        w.A += pJSON
        w.B += pYAML
        w.Line = make(Line, 0)
    } else {
        w.Line = append(w.Line, b)
    }
    return nil
}
func (w *Writable) Score () (int, int, error) {
    //whatever scoring heuristics you can think of.
    return 0,0,nil
}
func WrapReader(b io.Reader) io.Reader {
    return Decision{b,*new(Writable)}
}

【讨论】:

  • 我现在正在检查我可以环绕 Yaml 和 Json 本身并报告遇到的一些错误,但我需要检查你的想法。非常感谢您抽出宝贵时间解释您的答案。
  • 但是对一般方法的评论:虽然这可能适用于一些常见的输入类型,其中 YAML 和 JSON 有很大的不同,但从技术意义上讲,这可能是一种糟糕的方法,因为你可以有一个看起来像 100% 有效 JSON 的文件,除了在 JSON 中无效但在 YAML 中有效的单个字符。要知道某个东西是有效的 YAML 还是无效的 JSON,唯一万无一失的方法是尝试将其同时解析。
  • 举个简单的例子,一个 3 GB 的对象数组可能是有效的 JSON,直到右括号前的最后一个尾随逗号:,]。这将使其 JSON 无效,但 YAML 有效,并且除了严格的 JSON 语法检查之外,这不太可能触发任何“启发式”。
  • @Flimzy 哈哈哈!是的。你是对的。这是临睡前匆忙写的,所以没有做很多校对。同时,方括号或花括号前的逗号绝对是对“JSON”决定的惩罚。我同意你的观点,这种方法可能会失败,但如果选择错误的解析器需要付出一些有意义的代价,那么这样的方法是可行的。你正确地指出我的方法很幼稚,我同意。一个假设可靠、具有生产价值的分析器会更加健壮。
  • 使用启发式作为一种廉价的“可能有效”的方法很可能是一个好方法。如果启发式方法不确定,我可能会退回到实际的解析器。
【解决方案3】:

几天前我在 bash 脚本中遇到了同样的问题:如何检测文件是否包含 json、yaml 或纯文本?

我的解决方案是:

处理为json

  • 可以解析为json而不会出错

处理为text

  • 可以解析为yaml,但type只是yaml字符串

处理为yaml

  • 可以解析为yaml,但不仅仅是yaml字符串
  • 无法解析为 json

Bash 脚本 sn-p

parse_as_json() {
  jq -e '.' > /dev/null 2>&1 < "$1"
}

parse_as_yaml() {
  local FILE=$1
  parse_as_json $FILE && return 1
  parse_as_text $FILE && return 1
  yq -e > /dev/null 2>&1 < $FILE || return 1
}

parse_as_text() {
  [[ $(yq 'type == "string"' 2>&1 < $1) == true ]]
}

【讨论】:

    猜你喜欢
    • 2022-11-02
    • 2012-11-23
    • 1970-01-01
    • 2016-09-15
    • 1970-01-01
    • 2011-06-03
    • 2013-10-11
    • 2013-08-05
    • 2013-10-13
    相关资源
    最近更新 更多