【问题标题】:Efficiently replacing properties of a large JSON using System.Text.Json使用 System.Text.Json 有效地替换大型 JSON 的属性
【发布时间】:2020-01-21 10:49:15
【问题描述】:

我正在处理大型 JSON,其中一些元素包含以 Base 64 编码的大型(最大 100MB)文件。例如:

{ "name": "One Name", "fileContent": "...base64..." }

我想将 fileContent 属性值存储在磁盘中(以字节为单位)并将其替换为文件的路由,如下所示:

{ "name": "One Name", "fileRoute": "/route/to/file" }

是否可以通过 System.Text.Json 使用流或任何其他方式来实现这一点,以避免在内存中处理非常大的 JSON?

【问题讨论】:

  • 您的基本要求不是一次性将"fileContent" 的值作为一个完整的字符串(或字节数组)在内存中实现,对吧?如果是这样,尚不清楚System.Text.Json 是否可以轻松做到这一点。有一个方法Utf8JsonReader.ValueSequence,但它似乎并不容易使用,因为例如它不会对任何 JSON 转义序列进行转义,也不会检查字符串是否为格式良好的 JSON 原语。
  • 而Newtonsoft绝对做不到,它总是将每个字符串原语具体化。
  • 但是,奇怪的是,JsonReaderWriterFactory.CreateJsonReader() 返回的读者可以为所欲为。您对使用它的答案感兴趣吗?
  • 感谢@dbc 抽出宝贵时间回答这个问题。是的,请。我很想看看你是如何与那个读者一起做的,因为我看文档时无法理解它是如何工作的。

标签: json asp.net-core .net-core system.text.json


【解决方案1】:

您的基本要求是将包含属性"fileContent": "...base64..." 的JSON 转换为"fileRoute": "/route/to/file",同时还将fileContent 的值写入单独的二进制文件而不将fileContent 的值作为完整的具体化字符串

目前尚不清楚这是否可以通过 System.Text.Json 的 .NET Core 3.1 实现来完成。即使可以,也不容易。简单地从Stream 生成Utf8JsonReader 需要工作,请参阅Parsing a JSON file with .NET core 3.0/System.text.Json。完成此操作后,有一个方法 Utf8JsonReader.ValueSequence 将最后处理的令牌的原始值作为输入有效负载的 ReadOnlySequence<byte> 切片返回。但是,该方法似乎并不容易使用,因为它仅在令牌包含在多个段中时才有效,不能保证值的格式正确,并且不会转义 JSON 转义序列。

Newtonsoft 在这里根本不起作用,因为 JsonTextReader 总是完全实现每个原始字符串值。

作为替代方案,您可以考虑JsonReaderWriterFactory 返回的读者和作者。这些读取器和写入器由DataContractJsonSerializer 使用,并将JSON 即时转换为XML,因为它是readwritten。由于这些读取器和写入器的基类是XmlReaderXmlWriter,因此它们支持通过XmlReader.ReadValueChunk(Char[], Int32, Int32) 读取块中的字符串值。更好的是,它们支持通过 XmlReader.ReadContentAsBase64(Byte[], Int32, Int32) 读取块中的 Base64 二进制值。

鉴于这些读取器和写入器,我们可以使用流式转换算法将 fileContent 节点转换为 fileRoute 节点,同时将 Base64 二进制文件提取到单独的二进制文件中。

首先介绍以下XML流转换方法,由Mark Fussellthis answer松散地基于Combining the XmlReader and XmlWriter classes for simple streaming transformationsAutomating replacing tables from external files

public static class XmlWriterExtensions
{
    // Adapted from this answer https://stackoverflow.com/a/28903486
    // to https://stackoverflow.com/questions/28891440/automating-replacing-tables-from-external-files/
    // By https://stackoverflow.com/users/3744182/dbc

    /// <summary>
    /// Make a DEEP copy of the current xmlreader node to xmlwriter, allowing the caller to transform selected elements.
    /// </summary>
    /// <param name="writer"></param>
    /// <param name="reader"></param>
    /// <param name="shouldTransform"></param>
    /// <param name="transform"></param>
    public static void WriteTransformedNode(this XmlWriter writer, XmlReader reader, Predicate<XmlReader> shouldTransform, Action<XmlReader, XmlWriter> transform)
    {
        if (reader == null || writer == null || shouldTransform == null || transform == null)
            throw new ArgumentNullException();

        int d = reader.NodeType == XmlNodeType.None ? -1 : reader.Depth;
        do
        {
            if (reader.NodeType == XmlNodeType.Element && shouldTransform(reader))
            {
                using (var subReader = reader.ReadSubtree())
                {
                    transform(subReader, writer);
                }
                // ReadSubtree() places us at the end of the current element, so we need to move to the next node.
                reader.Read();
            }
            else
            {
                writer.WriteShallowNode(reader);
            }
        }
        while (!reader.EOF && (d < reader.Depth || (d == reader.Depth && reader.NodeType == XmlNodeType.EndElement)));
    }

    /// <summary>
    /// Make a SHALLOW copy of the current xmlreader node to xmlwriter, and advance the XML reader past the current node.
    /// </summary>
    /// <param name="writer"></param>
    /// <param name="reader"></param>
    public static void WriteShallowNode(this XmlWriter writer, XmlReader reader)
    {
        // Adapted from https://docs.microsoft.com/en-us/archive/blogs/mfussell/combining-the-xmlreader-and-xmlwriter-classes-for-simple-streaming-transformations
        // By Mark Fussell https://docs.microsoft.com/en-us/archive/blogs/mfussell/
        // and rewritten to avoid using reader.Value, which fully materializes the text value of a node.
        if (reader == null)
            throw new ArgumentNullException("reader");
        if (writer == null)
            throw new ArgumentNullException("writer");

        switch (reader.NodeType)
        {   
            case XmlNodeType.None:
                // This is returned by the System.Xml.XmlReader if a Read method has not been called.
                reader.Read();
                break;

            case XmlNodeType.Element:
                writer.WriteStartElement(reader.Prefix, reader.LocalName, reader.NamespaceURI);
                writer.WriteAttributes(reader, true);
                if (reader.IsEmptyElement)
                {
                    writer.WriteEndElement();
                }
                reader.Read();
                break;

            case XmlNodeType.Text:
            case XmlNodeType.Whitespace:
            case XmlNodeType.SignificantWhitespace:
            case XmlNodeType.CDATA:
            case XmlNodeType.XmlDeclaration:
            case XmlNodeType.ProcessingInstruction:
            case XmlNodeType.EntityReference:
            case XmlNodeType.DocumentType:
            case XmlNodeType.Comment:
                //Avoid using reader.Value as this will fully materialize the string value of the node.  Use WriteNode instead,
                // it copies text values in chunks.  See: https://referencesource.microsoft.com/#system.xml/System/Xml/Core/XmlWriter.cs,368
                writer.WriteNode(reader, true);
                break;

            case XmlNodeType.EndElement:
                writer.WriteFullEndElement();
                reader.Read();
                break;

            default:
                throw new XmlException(string.Format("Unknown NodeType {0}", reader.NodeType));
        }
    }
}

public static partial class XmlReaderExtensions
{
    // Taken from this answer https://stackoverflow.com/a/54136179/3744182
    // To https://stackoverflow.com/questions/54126687/xmlreader-how-to-read-very-long-string-in-element-without-system-outofmemoryex
    // By https://stackoverflow.com/users/3744182/dbc
    public static bool CopyBase64ElementContentsToFile(this XmlReader reader, string path)
    {
        using (var stream = File.Create(path))
        {
            byte[] buffer = new byte[8192];
            int readBytes = 0;

            while ((readBytes = reader.ReadElementContentAsBase64(buffer, 0, buffer.Length)) > 0)
            {
                stream.Write(buffer, 0, readBytes);
            }
        }
        return true;
    }
}

接下来,使用JsonReaderWriterFactory,引入以下方法从一个JSON文件流式传输到另一个JSON文件,根据需要重写fileContent节点:

public static class JsonPatchExtensions
{
    public static string[] PatchFileContentToFileRoute(string oldJsonFileName, string newJsonFileName, FilenameGenerator generator)
    {
        var newNames = new List<string>();

        using (var inStream = File.OpenRead(oldJsonFileName))
        using (var outStream = File.Open(newJsonFileName, FileMode.Create))
        using (var xmlReader = JsonReaderWriterFactory.CreateJsonReader(inStream, XmlDictionaryReaderQuotas.Max))
        using (var xmlWriter = JsonReaderWriterFactory.CreateJsonWriter(outStream))
        {
            xmlWriter.WriteTransformedNode(xmlReader, 
                r => r.LocalName == "fileContent" && r.NamespaceURI == "",
                (r, w) =>
                {
                    r.MoveToContent();
                    var name = generator.GenerateNewName();
                    r.CopyBase64ElementContentsToFile(name);
                    w.WriteStartElement("fileRoute", "");
                    w.WriteAttributeString("type", "string");
                    w.WriteString(name);
                    w.WriteEndElement();
                    newNames.Add(name);
                });
        }

        return newNames.ToArray();
    }
}

public abstract class FilenameGenerator
{
    public abstract string GenerateNewName();
}

// Replace the following with whatever algorithm you need to generate unique binary file names.

public class IncrementalFilenameGenerator : FilenameGenerator
{
    readonly string prefix;
    readonly string extension;
    int count = 0;

    public IncrementalFilenameGenerator(string prefix, string extension)
    {
        this.prefix = prefix;
        this.extension = extension;
    }

    public override string GenerateNewName()
    {
        var newName = Path.ChangeExtension(prefix + (++count).ToString(), extension);
        return newName;
    }
}

然后调用如下:

var binaryFileNames = JsonPatchExtensions.PatchFileContentToFileRoute(
    oldJsonFileName, 
    newJsonFileName,
    // Replace the following with your actual binary file name generation algorithm
    new IncrementalFilenameGenerator("Question59839437_fileContent_", ".bin"));

来源:

演示小提琴here.

【讨论】:

  • 此解决方案适用于 500MB 文件,内存占用约为 15MB。感谢@dbc 的精彩回答,当然是十年来我在 SO 中收到的最完整、格式最完整的回答!
猜你喜欢
  • 2022-01-08
  • 2020-02-15
  • 2013-09-06
  • 1970-01-01
  • 1970-01-01
  • 2011-04-02
  • 1970-01-01
  • 2017-06-20
  • 1970-01-01
相关资源
最近更新 更多