【问题标题】:Append to JSON file, without serializing first and then saving the file追加到 JSON 文件,不先序列化再保存文件
【发布时间】:2021-09-08 19:46:26
【问题描述】:

我有一个方法,在方法内部,我有一个Parallel.ForEach() 来做一些事情。完成后,我希望它立即将所做的事情记录到 JSON 文件中。

目前我有一个单例类,其中包含所有当前 JSON 条目的字典。当stuff完成后,我将新完成的stuff添加到字典中,然后序列化整个字典,然后保存文件。

这很糟糕,尤其是在字典很大的情况下,因为一遍又一遍地序列化大量数据只是浪费时间。如果我可以将stuff 直接保存到 JSON 文件的末尾,我应该可以无缝地完成。

因为我知道我只是在附加数据,所以我想使用 FileStream 来寻找到最后,然后做一些 hack 会起作用,但显然这会弄乱文件的格式。

这是一个例子:

{
  "test": {
    "id": "test",
    "name": "the best test"
  },
  "test2": {
    "id": "test2",
    "name": "the second best test"
  }
}

附加到 JSON(“伪代码”实际上并不能工作,但你明白了):

FileStream fs = File.OpenWrite("file.json");
fs.Seek(-1, SeekOrigin.End);
await fs.WriteAsync(Encoding.UTF8.GetBytes("},"), 0, 2);
await fs.WriteAsync(jsonAsBytes, 0, jsonAsBytes.Length);

输出:

{
  "test": {
    "id": "test",
    "name": "the best test"
  },
  "test2": {
    "id": "test2",
    "name": "the second best test"
},
"test3": {
  "id": "test3",
  "name": "the third best test"
}
}

有没有更优雅的解决方案?我需要数千次调用这个“添加到 json 文件”方法。如果我在请求/应用程序结束时序列化字典,它不会保存已经发生的事情,这会很糟糕。

【问题讨论】:

  • 有更优雅的解决方案吗?是的....使用数据库
  • @Selvin 这是一个单一的二进制控制台应用程序。为此使用数据库,将是 1) 过度杀伤 2) 让用户感到困惑。当然我可以在本地创建一个数据库(如 sqlite),然后创建一个“dump sqlite”命令,生成一个 JSON 文件,但这也不优雅。
  • 如果您唯一关心的是格式化,您只需要在每行之前添加两个空格,您的解决方案就可以工作。另一种解决方案是您选择其他 json,例如 yaml,您可以轻松附加到它
  • 我不认为,追加似乎是更好的解决方案。只需 fs.Seek(-Encoding.UTF8.GetBytes("\n}").Length-1, SeekOrigin.End); 在最后一个元素之后追加。也许 CSV 会更容易处理。

标签: c# append .net-5


【解决方案1】:

首先,您应该保持文件打开,这样您就不必一直寻找。然后在所有内容完成并写入文件后立即关闭它。

为了确保您的并行内容不会同时写入文件,我将创建一个小型 Helper 类,例如:

public class JsonFileAppender : IDisposable
{
    readonly FileStream fs;
    public JsonFileAppender(string filename)
    {
        fs = File.OpenWrite(filename);
        // start with opening the json
        fs.WriteByte((byte)'{');
    }

    public void Append(string content)
    {
        // prepend every line with 2 spaces
        content = string.Join('\n', content.Split('\n').Select(line => $"  {line}"));
        byte[] byteContent = Encoding.UTF8.GetBytes(content);

        // no parallel writes
        lock (fs)
        {
            fs.Write(byteContent, 0, byteContent.Length);
        }
    }

    public void Dispose()
    {
        // close the json
        fs.WriteByte((byte)'}');
        // and close the stream
        fs.Dispose();
    }
}

并像这样使用它:

using var jsonFileAppender = new JsonFileAppender("file.json");
jsonFileAppender.Append(jsonPart);

您当然应该创建一次 JsonFileAppender 实例并在您的 Parallel.ForEach() 中使用它。

(代码未经测试)

【讨论】:

  • 对,这解决了线程问题(幸运的是,我已经解决了这个问题)。不幸的是,它并没有解决最大的问题,即格式化文件。
  • 啊,好的。您可以在 JsonFileAppender 的 Append-Method 中处理它。如果 Append-Method 的输入已经预先格式化,那应该没什么大不了的。我更新了答案以符合您的要求(请注意,Append-Method 现在需要一个字符串而不是 byte[])。
【解决方案2】:

如果格式不强制,我会考虑使用 CSV(可以在 Excel 中打开)的可能性。

如果真的是 JSON,我会做最少的 JSON(不格式化)并使用其他工具在阅读时格式化(Notepadd++、VS Code、在线工具……)。

但是要获得一个漂亮的格式化文件,您可以添加空格,例如:

static void Main(string[] args)
{
    var nl = Environment.NewLine;
    var endOfFile = Encoding.UTF8.GetBytes($"{nl}}}");

    using FileStream fs = File.OpenWrite("file.json");
    //Set the cusor after the last element
    fs.Seek(-endOfFile.Length, SeekOrigin.End);
            
    //Separate the last element and new element
    fs.Write(Encoding.UTF8.GetBytes($",{nl}"));
    //Add the new element
    fs.Write(Encoding.UTF8.GetBytes($"  \"test42\": {{{nl}"));
    fs.Write(Encoding.UTF8.GetBytes($"    \"id\": \"test42\",{nl}"));
    fs.Write(Encoding.UTF8.GetBytes($"    \"name\": \"A message with some informations...\"{nl}"));
    fs.Write(Encoding.UTF8.GetBytes($"  }}"));
    //Close root json element
    fs.Write(endOfFile);
}

请参阅使用Seek 将光标设置在最后一个元素之后。然后追加将覆盖文件的末尾。为什么文件末尾需要重新追加。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-10-17
    • 2021-07-31
    • 1970-01-01
    • 2019-12-02
    • 1970-01-01
    相关资源
    最近更新 更多