【问题标题】:Strategy for splitting a large JSON file拆分大型 JSON 文件的策略
【发布时间】:2015-10-03 07:12:42
【问题描述】:

我正在尝试将非常大的 JSON 文件拆分为给定数组的较小文件。例如:

{
    "headerName1": "headerVal1",
    "headerName2": "headerVal2",
    "headerName3": [{
        "element1Name1": "element1Value1"
    },
    {
        "element2Name1": "element2Value1"
    },
    {
        "element3Name1": "element3Value1"
    },
    {
        "element4Name1": "element4Value1"
    },
    {
        "element5Name1": "element5Value1"
    },
    {
        "element6Name1": "element6Value1"
    }]
}

...向下到 { "elementNName1": "elementNValue1" } 其中 N 是一个大数

用户提供代表要拆分的数组的名称(在本例中为“headerName3”)和每个文件的数组对象数,例如1,000,000

这将产生 N 个文件,每个文件都包含顶部名称:值对(headerName1、headerName3)和每个文件中最多 1,000,000 个 headerName3 对象。

我正在使用出色的 Newtonsof JSON.net,并且了解我需要使用流来执行此操作。

到目前为止,我已经查看了 JToken 对象的读数,以确定在读取令牌时 PropertyName == "headerName3" 发生的位置,但我想要做的是读取数组中每个对象的整个 JSON 对象并且不必继续将 JSON 解析为 JToken;

这是我目前正在构建的代码的 sn-p:

        using (StreamReader oSR = File.OpenText(strInput))
        {
            using (var reader = new JsonTextReader(oSR))
            {
                while (reader.Read())
                {
                    if (reader.TokenType == JsonToken.StartObject)
                    {
                        intObjectCount++;
                    }
                    else if (reader.TokenType == JsonToken.EndObject)
                    {
                        intObjectCount--;

                        if (intObjectCount == 1)
                        {
                            intArrayRecordCount++;
                            // Here I want to read the entire object for this record into an untyped JSON object

                            if( intArrayRecordCount % 1000000 == 0)
                            {
                                //write these to the split file
                            }
                        }
                    }
                }
            }
        }

我不知道——事实上,也不关心——JSON 本身的结构,对象在数组中可以是不同的结构。因此,我没有对类进行序列化。

这是正确的方法吗? JSON.net 库中是否有一组方法可以轻松用于执行此类操作?

任何帮助表示赞赏。

【问题讨论】:

  • 您确定要拆分的数组值属性将是根 JSON 对象中的 last 项吗? JSON standard 声明,“object 是一组无序的名称/值对”,因此原则上要拆分的属性不必排在最后。
  • 到目前为止,供应商已经给出了将数组作为最后一个对象的文件。由于我正在阅读流,我相信它将是顶层的最后一个对象。但是,是的,用更一般的术语来说,我不会依赖这个。我已经为较小的文件编写了另一个版本,它克隆了原始 JSON,然后用原始 JSON 的子集替换指定的数组。

标签: c# json stream json.net


【解决方案1】:

您可以使用JsonWriter.WriteToken(JsonReader reader, true) 将单个数组条目及其后代从JsonReader 流式传输到JsonWriter。您还可以使用JProperty.Load(JsonReader reader)JProperty.WriteTo(JsonWriter writer) 来读取和写入整个属性及其后代。

使用这些方法,您可以创建一个状态机,它解析 JSON 文件,遍历根对象,加载“前缀”和“后缀”属性,拆分数组属性,并写入前缀、数组切片和后缀属性输出到新文件。

这是一个原型实现,它采用 TextReader 和一个回调函数来为拆分文件创建顺序输出 TextWriter 对象:

    enum SplitState
    {
        InPrefix,
        InSplitProperty,
        InSplitArray,
        InPostfix,
    }

    public static void SplitJson(TextReader textReader, string tokenName, long maxItems, Func<int, TextWriter> createStream, Formatting formatting)
    {
        List<JProperty> prefixProperties = new List<JProperty>();
        List<JProperty> postFixProperties = new List<JProperty>();
        List<JsonWriter> writers = new List<JsonWriter>();

        SplitState state = SplitState.InPrefix;
        long count = 0;

        try
        {
            using (var reader = new JsonTextReader(textReader))
            {
                bool doRead = true;
                while (doRead ? reader.Read() : true)
                {
                    doRead = true;
                    if (reader.TokenType == JsonToken.Comment || reader.TokenType == JsonToken.None)
                        continue;
                    if (reader.Depth == 0)
                    {
                        if (reader.TokenType != JsonToken.StartObject && reader.TokenType != JsonToken.EndObject)
                            throw new JsonException("JSON root container is not an Object");
                    }
                    else if (reader.Depth == 1 && reader.TokenType == JsonToken.PropertyName)
                    {
                        if ((string)reader.Value == tokenName)
                        {
                            state = SplitState.InSplitProperty;
                        }
                        else
                        {
                            if (state == SplitState.InSplitProperty)
                                state = SplitState.InPostfix;
                            var property = JProperty.Load(reader);
                            doRead = false; // JProperty.Load() will have already advanced the reader.
                            if (state == SplitState.InPrefix)
                            {
                                prefixProperties.Add(property);
                            }
                            else
                            {
                                postFixProperties.Add(property);
                            }
                        }
                    }
                    else if (reader.Depth == 1 && reader.TokenType == JsonToken.StartArray && state == SplitState.InSplitProperty)
                    {
                        state = SplitState.InSplitArray;
                    }
                    else if (reader.Depth == 1 && reader.TokenType == JsonToken.EndArray && state == SplitState.InSplitArray)
                    {
                        state = SplitState.InSplitProperty;
                    }
                    else if (state == SplitState.InSplitArray && reader.Depth == 2)
                    {
                        if (count % maxItems == 0)
                        {
                            var writer = new JsonTextWriter(createStream(writers.Count)) { Formatting = formatting };
                            writers.Add(writer);
                            writer.WriteStartObject();
                            foreach (var property in prefixProperties)
                                property.WriteTo(writer);
                            writer.WritePropertyName(tokenName);
                            writer.WriteStartArray();
                        }
                        count++;
                        writers.Last().WriteToken(reader, true);
                    }
                    else
                    {
                        throw new JsonException("Internal error");
                    }
                }
            }
            foreach (var writer in writers)
                using (writer)
                {
                    writer.WriteEndArray();
                    foreach (var property in postFixProperties)
                        property.WriteTo(writer);
                    writer.WriteEndObject();
                }
        }
        finally
        {
            // Make sure files are closed in the event of an exception.
            foreach (var writer in writers)
                using (writer)
                {
                }

        }
    }

如果需要附加出现在数组属性之后的“后缀”属性,此方法会使所有文件保持打开状态。请注意,有一个limit of 16384 open files at one time,因此如果您需要创建更多拆分文件,这将不起作用。如果在实践中从未遇到过后缀属性,您可以在打开下一个文件之前关闭每个文件,并在找到任何后缀属性的情况下抛出异常。否则,您可能需要分两遍解析大文件或关闭并重新打开拆分文件以附加它们。

这是一个如何使用内存中 JSON 字符串的方法的示例:

    private static void TestSplitJson(string json, string tokenName)
    {
        var builders = new List<StringBuilder>();
        using (var reader = new StringReader(json))
        {
            SplitJson(reader, tokenName, 2, i => { builders.Add(new StringBuilder()); return new StringWriter(builders.Last()); }, Formatting.Indented);
        }
        foreach (var s in builders.Select(b => b.ToString()))
        {
            Console.WriteLine(s);
        }
    }

原型fiddle

【讨论】:

  • 谢谢 - 今天早上我会去看看。这适用于 >25GB 的输入文件吗?
  • @surreydude - 我没有要测试的文件,但 JsonTextReader provides fast, non-cached, forward-only access to JSON text data,所以它应该没问题; JsonTextWriter 也是如此。原型代码只将前缀和后缀属性保留在内存中,所以只要它们很小,内存使用应该是合理的。我最担心的是一次打开太多文件。
  • 好的。大文件实际上在数组中具有相对较小的结构,它只是一个非常大的数组。谢谢你的帮助 - 我会看看我能得到什么。
  • 我现在已经设法采用您的原型并使用 StreamWriter 对象而不是字符串构建器,它工作得很好。使用 >25GB 的输入文件,我在不到 1 分钟的时间内得到了 1,000,000 个拆分文件。谢谢!
猜你喜欢
  • 2020-04-15
  • 1970-01-01
  • 2022-11-24
  • 1970-01-01
  • 2017-05-10
  • 2017-03-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多