当前位置: 首页 > 知识库问答 >
问题:

使用JSONPath过滤JSON文档中的属性

郗学
2023-03-14

我有一个任意定义的JSON文档,我希望能够应用一个JSONPath表达式,就像属性的白名单过滤器:所有选定的节点及其祖先返回到根节点,所有其他节点都被删除。如果节点不存在,我应该得到一个空文档。

JSON. Net中似乎没有类似的东西,我在任何地方都找不到类似的例子,所以我建立了自己的例子。我选择将选定的节点复制到新构建的文档中,而不是尝试删除所有不匹配的节点。鉴于可能存在多个匹配项,并且文档可能很大,它需要能够有效地处理将多个选择结果合并到单个树/JSON文档中。

我的尝试有点效果,但我得到了奇怪的结果。该过程涉及一个MergedAnceery方法,该方法迭代SelectTokens结果,调用GetFullAnceery递归地将树构建到该节点),然后合并结果。不过,JArray的合并似乎发生在错误的级别,正如您在下面的“实际结果”中看到的那样。

我的问题是:

  1. 有没有更好/更快/内置的方法来实现这一点?
  2. 如果没有,我做错了什么?

代码:

public static void Main()
{
    string json = @"..."; // snipped for brevity - see DotNetFiddle: https://dotnetfiddle.net/wKN1Hj
    var root = (JContainer)JToken.Parse(json);
    var t3 = root.SelectTokens("$.Array3B.[*].Array3B1.[*].*");

    // See DotNetFiddle for simpler examples that work
    Console.WriteLine($"{MergedAncestry(t3).ToString()}");  // Wrong output!

    Console.ReadKey();
}

// Returns a single document merged using the full ancestry of each of the input tokens
static JToken MergedAncestry(IEnumerable<JToken> tokens)
{
    JObject merged = null;
    foreach(var token in tokens)
    {
        if (merged == null)
        {
            // First object
            merged = (JObject)GetFullAncestry(token);
        }
        else
        {
            // Subsequent objects merged
            merged.Merge((JObject)GetFullAncestry(token), new JsonMergeSettings
            {
                // union array values together to avoid duplicates
                MergeArrayHandling = MergeArrayHandling.Union
            });
        }
    }
    return merged ?? new JObject();
}

// Recursively builds a new tree to the node matching the ancestry of the original node
static JToken GetFullAncestry(JToken node, JToken tree = null)
{
    if (tree == null)
    {
        // First level: start by cloning the current node
        tree = node?.DeepClone();
    }

    if (node?.Parent == null)
    {
        // No parents left, return the tree we've built
        return tree;
    }

    // Rebuild the parent node in our tree based on the type of node
    JToken a;
    switch (node.Parent)
    {
        case JArray _:
            return GetFullAncestry(node.Parent, new JArray(tree));
        case JProperty _:
            return GetFullAncestry(node.Parent, new JProperty(((JProperty)node.Parent).Name, tree));
        case JObject _:
            return GetFullAncestry(node.Parent, new JObject(tree));
        default:
            return tree;
    }
}

JSON示例:

{
  "Array3A": [
    { "Item_3A1": "Desc_3A1" }
  ],
  "Array3B": [
    { "Item_3B1": "Desc_3B1" },
    {
      "Array3B1": [
        { "Item_1": "Desc_3B11" },
        { "Item_2": "Desc_3B12" },
        { "Item_3": "Desc_3B13" }
      ]
    },
    {
      "Array3B2": [
        { "Item_1": "Desc_3B21" },
        { "Item_2": "Desc_3B22" },
        { "Item_3": "Desc_3B23" }
      ]
    }
  ]
}

有关完整代码和测试,请参阅DotNetFiddle

"过滤器"JSONPath:

$.Array3B.[*].Array3B1.[*].*

预期成果:

{
    "Array3B": [
    {
        "Array3B1": [
        { "Item_1": "Desc_3B11" },
        { "Item_2": "Desc_3B12" },
        { "Item_3": "Desc_3B13" }
        ]
    }
    ]
}

实际结果:

{
    "Array3B": [
    {
        "Array3B1": [ { "Item_1": "Desc_3B11" } ]
    },
    {
        "Array3B1": [ { "Item_2": "Desc_3B12" } ]
    },
    {
        "Array3B1": [ { "Item_3": "Desc_3B13" } ]
    }
    ]
}

共有1个答案

湛光华
2023-03-14

好的,我找到了一个方法。感谢@dbc的建议、改进和指出问题。

递归最终不会很好地工作,因为我需要确保树中具有公共父级的同一级别上的所有节点都匹配,而任何级别上都可能有输入节点。

我添加了一个方法来对多个JSONPaths进行过滤,以输出单个结果文档,因为这是最初的目标。

static JToken FilterByJSONPath(JToken document, IEnumerable<string> jPaths)
{
    var matches = jPaths.SelectMany(path => document.SelectTokens(path, false));
    return MergeAncestry(matches);
}

static JToken MergeAncestry(IEnumerable<JToken> tokens)
{
    if (tokens == null || !tokens.Any())
    {
        return new JObject();
    }

    // Get a dictionary of tokens indexed by their depth
    var tokensByDepth = tokens
        .Distinct(ObjectReferenceEqualityComparer<JToken>.Default)
        .GroupBy(t => t.Ancestors().Count())
        .ToDictionary(
            g => g.Key, 
            g => g.Select(node => new CarbonCopyToken { Original = node, CarbonCopy = node.DeepClone() })
                    .ToList());

    // start at the deepest level working up
    int depth = tokensByDepth.Keys.Max();
    for (int i = depth; i > 0; i--)
    {
        // If there's nothing at the next level up, create a list to hold parents of children at this level
        if (!tokensByDepth.ContainsKey(i - 1))
        {
            tokensByDepth.Add(i - 1, new List<CarbonCopyToken>());
        }

        // Merge all tokens at this level into families by common parent
        foreach (var parent in MergeCommonParents(tokensByDepth[i]))
        {
            tokensByDepth[i - 1].Add(parent);
        }
    }

    // we should be left with a list containing a single CarbonCopyToken - contining the root of our copied document and the root of the source
    var cc = tokensByDepth[0].FirstOrDefault();
    return cc?.CarbonCopy ?? new JObject();
}

static IEnumerable<CarbonCopyToken> MergeCommonParents(IEnumerable<CarbonCopyToken> tokens)
{
    var newParents = tokens.GroupBy(t => t.Original.Parent).Select(g => new CarbonCopyToken {
        Original = g.First().Original.Parent,
        CarbonCopy = CopyCommonParent(g.First().Original.Parent, g.AsEnumerable())
        });
    return newParents;
}

static JToken CopyCommonParent(JToken parent, IEnumerable<CarbonCopyToken> children)
{
    switch (parent)
    {
        case JProperty _:
            return new JProperty(((JProperty)parent).Name, children.First().CarbonCopy);
        case JArray _:
            var newParentArray = new JArray();
            foreach (var child in children)
            {
                newParentArray.Add(child.CarbonCopy);
            }
            return newParentArray;
        default: // JObject, or any other type we don't recognise
            var newParentObject = new JObject();
            foreach (var child in children)
            {
                newParentObject.Add(child.CarbonCopy);
            }
            return newParentObject;
    }

}

请注意,它使用了几个新类:CarbonCopyToken允许我们在逐级处理树时跟踪节点及其副本,以及ObjectReassiceEqualityCompler

public class CarbonCopyToken
{
    public JToken Original { get; set; }
    public JToken CarbonCopy { get; set; }
}

/// <summary>
/// A generic object comparerer that would only use object's reference, 
/// ignoring any <see cref="IEquatable{T}"/> or <see cref="object.Equals(object)"/>  overrides.
/// </summary>
public class ObjectReferenceEqualityComparer<T> : IEqualityComparer<T> where T : class
{
    // Adapted from this answer https://stackoverflow.com/a/1890230
    // to https://stackoverflow.com/questions/1890058/iequalitycomparert-that-uses-referenceequals
    // By https://stackoverflow.com/users/177275/yurik
    private static readonly IEqualityComparer<T> _defaultComparer;

    static ObjectReferenceEqualityComparer() { _defaultComparer = new ObjectReferenceEqualityComparer<T>(); }

    public static IEqualityComparer<T> Default { get { return _defaultComparer; } }

    #region IEqualityComparer<T> Members

    public bool Equals(T x, T y)
    {
        return ReferenceEquals(x, y);
    }

    public int GetHashCode(T obj)
    {
        return System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj);
    }

    #endregion
}

用法示例:

List<string> filters = new {
    "$..Test1",
    "$.Path.To.[*].Some.Nodes",
    "$.Other.*.Nodes"
}
var result = FilterByJSONPath(inputDocument, filters);

DotNetFiddle显示以前的测试和一个额外的测试:https://dotnetfiddle.net/ekABRI

 类似资料:
  • 问题内容: 嘿,我想知道是否有人知道使用正则表达式或通配符(或SQL中的pehaps )的方式,以便可以使用JSONPath在大量JSON数据内进行搜索。 例如(是的,我正在解析,而不是在应用程序中读取数据): 我希望能够浏览这样的数据: 其中参数的内容是数据对中部分或全部值的一部分。 目前,我只找到文件上,,,和关系运算符,它不给我那么多的灵活性。 有谁知道一个方法可以让我只是 刚刚 JSONP

  • 我正在尝试使用 Jolt 转换来转换 Json,在这里寻找一些输入。我正在尝试过滤一个键,该键是另一个属性的值。这是我的输入和预期输出 我看到的输出是 我试过的规格是 但是我没有得到预期的输出。我也尝试了一些其他组合,但未能获得正确的输出。有人能帮忙吗?

  • 我有一个,我希望用户输入一个人的姓名。我认为名称应该包含、和示例。我正在使用来验证用户输入。但是,我不知道如何在我的中设置它。 问题:我应该如何修改我的过滤器来实现上述行为? 任何关于如何验证一个人的名字的建议都被接受。 这是我的DocumentFilter: 这是我的测试类:

  • 考虑以下Firestore结构: 收藏 现在,我想查询宠物满足某些条件的人。例如,拥有两只以上宠物的人,或者拥有一只名为“ABC”的宠物的人。 这可能吗?

  • 问题内容: 我在子文档中有这样的数组 我可以过滤> 3的子文档吗 我的预期结果如下 我尝试使用,$elemMatch但返回数组中的第一个匹配元素 我的查询: 结果返回数组中的一个元素 我尝试使用聚合与$match但不起作用 返回数组中的所有元素 我可以过滤数组中的元素以获得预期结果吗? 问题答案: 使用是正确的方法,但在应用数组之前需要先对数组进行过滤,以便可以过滤单个元素,然后用于将其放回原处:

  • 我想知道是否有办法在过滤掉elasticsearch文档后更新它们。 假设我有一个包含以下文档的用户集合: 现在我需要做的是更新所有30岁以上用户的名字。查看大量文档并在谷歌上搜索数小时,包括以下文档http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/_updating_documents.html 我找不到办