Эффективная замена свойств большого JSON с помощью System.Text. Json - PullRequest
3 голосов
/ 21 января 2020

Я работаю с большими JSON с некоторыми элементами, содержащими большие (до 100 МБ) файлы, закодированные в Base 64. Например:

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

Я хочу сохранить значение свойства fileContent на диске (как байт) и замените его на маршрут к файлу, например:

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

Возможно ли добиться этого с помощью System.Text. Json с использованием потоков или любым другим способом, чтобы избежать необходимости работать с очень большой JSON в памяти?

1 Ответ

3 голосов
/ 22 января 2020

Основным требованием c является преобразование JSON, содержащего свойство "fileContent": "...base64...", в "fileRoute": "/route/to/file", а также запись значения fileContent в отдельный двоичный файл без материализации значения fileContent как полная строка .

Неясно, можно ли это сделать с помощью. NET Реализация Core 3.1 System.Text.Json. Даже если бы это было возможно, это было бы нелегко. Простое создание Utf8JsonReader из Stream требует работы, см. Анализ файла JSON с. NET core 3.0 / System.text. Json. После этого существует метод Utf8JsonReader.ValueSequence, который возвращает необработанное значение последнего обработанного токена в виде среза ReadOnlySequence<byte> входной полезной нагрузки. Однако этот метод не кажется простым в использовании, поскольку он работает только тогда, когда токен содержится в нескольких сегментах, не гарантирует правильности формирования значения и не экранирует JSON escape-последовательности.

И Newtonsoft здесь вообще не будет работать, потому что JsonTextReader всегда полностью материализует каждое примитивное строковое значение.

В качестве альтернативы вы можете рассмотреть читателей и писателей, возвращаемых JsonReaderWriterFactory. Эти читатели и писатели используются DataContractJsonSerializer и переводят JSON в XML на лету в том виде, как они читаются и пишутся . Поскольку базовыми классами для этих читателей и писателей являются XmlReader и XmlWriter, они поддерживают чтение строковых значений в чанках с помощью XmlReader.ReadValueChunk(Char[], Int32, Int32). Более того, они поддерживают чтение двоичных значений Base64 порциями через XmlReader.ReadContentAsBase64(Byte[], Int32, Int32).

Учитывая этих читателей и авторов, мы можем использовать алгоритм потокового преобразования для преобразования узлов fileContent в узлы fileRoute, одновременно извлекая двоичный файл Base64 в отдельные двоичные файлы.

Сначала представьте следующие XML методы потокового преобразования, основанные на Объединение классов XmlReader и XmlWriter для простых потоковых преобразований с помощью Марк Фусселл и этот ответ до Автоматизация замены таблиц из внешних файлов :

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 файла в другой, переписывая 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"));

Источники:

Демонстрационная скрипка здесь .

...