【问题标题】:Modify content of large file修改大文件内容
【发布时间】:2020-12-08 17:51:38
【问题描述】:

我已经从我的数据库中的 json 文件中提取了我的表,现在我想读取这些文件并删除它们上的所有双引号,看起来很简单并且尝试了数百种解决方案,有些让我遇到了内存不足的问题。我正在处理大小超过 1Gb 的文件。您将在下面找到的代码有一个奇怪的行为,我不明白为什么它返回空文件

  public void replaceDoubleQuotes(String fileName){
    log.debug(" start formatting " + fileName + " ...");
    File firstFile = new File ("C:/sqlite/db/tables/" + fileName);
    String oldContent = "";
    String newContent = "";
    BufferedReader reader = null;
    BufferedWriter writer = null;
    FileWriter writerFile = null;
    String stringQuotes = "\\\\\\\\\"";
    try {
        reader = new BufferedReader(new FileReader(firstFile));
        writerFile = new FileWriter("C:/sqlite/db/tables/" + fileName);
        writer = new BufferedWriter(writerFile);
        
    while   (( oldContent = reader.readLine()) != null ){
        newContent = oldContent.replaceAll(stringQuotes, "");
        writer.write(newContent);
        }
    
    writer.flush();
    writer.close();
    } catch (Exception e) {
        log.error(e);
    }
}

当我尝试使用FileWriter(path,true)在文件末尾写入时,程序不会停止增加文件内存,直到硬盘已满,谢谢帮助

ps:我也尝试使用 subString 并附加新内容,然后我写了 subString 但也不起作用

【问题讨论】:

  • 您似乎将数据库文件视为由文本行组成。我对 SQLite 一无所知,但这似乎不太可能。
  • 我还想知道什么 db 通常以 JSON 格式转储其数据,但我认为这与问题无关。还是很高兴知道。
  • 源文件new File ("C:/sqlite/db/tables/" + fileName)与目标new FileWriter("C:/sqlite/db/tables/" + fileName)相同?

标签: java bufferedreader filewriter


【解决方案1】:

TL;博士;

不要同时读写同一个文件。

问题

您的代码开始读取,然后立即截断正在读取的文件。

 reader = new BufferedReader(new FileReader(firstFile));
 writerFile = new FileWriter("C:/sqlite/db/tables/" + fileName);
 writer = new BufferedWriter(writerFile);
    

第一行打开文件的读取句柄。 第二行打开同一个文件的写句柄。 看FileWriter构造函数的文档不是很清楚,但是当你不使用允许指定append参数的构造函数时,默认值为false,意思是,你立即截断如果文件已经存在,则文件。

此时(第 2 行)您刚刚删除了您将要读取的文件。所以你最终得到一个空文件。

如何使用 append=true

好吧,那么文件在创建时并没有被擦除,这就是“好”。所以你的程序开始读取第一行,并输出(到同一个文件)过滤后的版本。

因此,每次读取一行时,都会附加另一行。

难怪您的程序永远不会到达文件末尾:每次前进一行,它都会创建另一行来处理。一般来说,您永远不会到达文件末尾(当然,如果文件开头是单行,您可能会遇到这种情况,但这是一种极端情况)。

解决方案

写入一个临时文件,如果(并且只有如果)你成功了,然后如果你真的需要交换文件。

此解决方案的一个优点:如果您的进程因任何原因崩溃,您将保持原始文件不变,您可以稍后重试,这通常是一件好事。您的过程是“可重复的”。

缺点:在某些时候您需要两倍的空间。 (虽然你可以压缩临时文件并减少这个因素,但仍然)。

关于内存不足问题

在处理任意大的文件时,您选择的路径(使用缓冲读取器和写入器)是正确的,因为您一次只使用一行内存。

因此它通常可以避免内存使用问题(当然,除非你有一个没有换行符的文件,在这种情况下它根本没有区别)。

其他解决方案,包括一次读取整个文件,然后在内存中执行搜索/替换,然后将内容写回,并不能很好地扩展,所以最好避免这种计算。

不相关但重要

查看try with resources 语法以正确关闭您的资源(读取器/写入器)。在这里,您忘记关闭阅读器,并且无论如何您都没有适当地关闭作者(即:在 finally 子句中)。

另一件事:我敢肯定,凡人编写的 Java 程序都无法击败大多数 unix 平台(以及更多平台)上可用的工具,如 sedawk。也许您想检查一下自己在 java 中滚动是否值得使用 shell one-liner。

【讨论】:

  • 这是一个很好的答案,并且解释得很好。值得一提的是,将 1GB 读入堆并不是一个好主意,OP 应该一次读取文件的一部分。
  • OP 正在使用 BufferedReader 一次读取一行,而不是整个文件(假设文件不是一行,这是另一个问题)。由于明确提到了 OutOfMemory,我将添加一个旁注,但我觉得问题与该特定问题无关。
  • 是的,这是一个很好的观点,但如果建议是先读再写,那么我只能假设他们会先读完再写,这可能是个问题。尽管如此,这是一个很好的答案,是的,您的答案更围绕问题。
  • @Jason - 他们仍然可以(并且应该)按行读取和写入。可以为输出文件指定一个临时名称,然后在文件成功完成后重命名。
  • 感谢您的回答和解释,我现在正在磁盘上消耗更多资源,因为我在单独的文件中写入,但目前还可以。再次感谢大家
【解决方案2】:

@GPI 已经提供了一个很好的答案,说明为什么同时读写会导致您遇到的问题。还值得注意的是,如果没有分配足够的堆,一次将 1gb 的数据读入堆肯定会导致OutOfMemoryError,这很可能。要解决此问题,您可以使用InputStream 并一次读取文件的块,然后写入另一个文件直到该过程完成,最终将现有文件替换为修改后的文件并删除。使用这种方法,您甚至可以使用 ForkJoinTask 来帮助解决这个问题,因为这是一项艰巨的工作。

旁注; 可能有比create new file, write to new file, replace existing, delete new file 更好的解决方案。

【讨论】:

  • 由于转储是某种数据库的 JSON 文件,我会谨慎推荐多线程方法。首先是因为它甚至不确定replaceAll 是否是一个瓶颈(与磁盘 IO 相比),其次是因为在 DB 转储中,行顺序可能很重要,同时保持顺序和并行化看起来很头疼。所以我真的会检查一下性能提升是否真的如此重要,值得麻烦。
  • BufferedReader 有一个默认缓冲区,用于分配所需的内存以供读取,一旦构建它就可以调整此大小。 OutOfMemoryError 不应该是这里的主题。
  • @GBI 很好,这可能不值得。 Traychoivanov,好吧,这很公平。 +1
猜你喜欢
  • 2010-12-09
  • 1970-01-01
  • 1970-01-01
  • 2019-03-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-11-24
  • 1970-01-01
相关资源
最近更新 更多