Эффективная замена свойств большого JSON с помощью System.Text.Json
Я работаю с большими JSON с некоторыми элементами, содержащими большие (до 100 МБ) файлы, закодированные в Base 64. Например:
{ "name": "One Name", "fileContent": "...base64..." }
Я хочу сохранить значение свойства fileContent на диске (в байтах) и заменить его на путь к файлу, например:
{ "name": "One Name", "fileRoute": "/route/to/file" }
Можно ли добиться этого с помощью System.Text.Json, используя потоки или любой другой способ избежать работы с очень большими JSON в памяти?
1 ответ
Ваше основное требование - преобразовать 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.
И 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://stackru.com/a/28903486
// to https://stackru.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://stackru.com/a/54136179/3744182
// To https://stackru.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"));
Источники:
Комбинация классов XmlReader и XmlWriter для простых потоковых преобразований. Автор Марк Фасселл.
XmlReader - как читать очень длинную строку в элементе без System.OutOfMemoryException.
Демо скрипка здесь.