将文件加载到内存中(Java)?

3

我有一个60 MB的文本文件,我的程序需要通过它来搜索特定的ID并提取一些相关文本。我需要重复这个过程200多次。最初,我使用循环遍历文件的每一行,查找ID并提取相关文本,但这太慢了(约2分钟)。因此,现在我正在寻找一种方法将整个文件加载到内存中,然后从那里搜索我的ID和相关文本;我想这应该比访问硬盘200多次要快。所以我编写了以下代码来将文件加载到内存中:

public String createLocalFile(String path)
{   
    String text = "";
    try
    {
        FileReader fileReader = new FileReader( path );
        BufferedReader reader = new BufferedReader( fileReader );
        String currentLine = "";
        while( (currentLine = reader.readLine() ) != null )
        {
            text += currentLine;
            System.out.println( currentLine );
        }

    }
    catch(IOException ex)
    {
        System.out.println(ex.getMessage());
    }
    return text;
}

很遗憾,将文件的文本保存到字符串变量中需要非常长的时间。有没有更快的加载文件的方法?或者有没有更好的方法来完成相同的任务?感谢任何帮助。

编辑:这是文件链接https://github.com/MVZSEQ/denovoTranscriptomeMarkerDevelopment/blob/master/Homo_sapiens.GRCh38.pep.all.fa

典型的行如下:

>ENSP00000471873 pep:putative chromosome:GRCh38:19:49496434:49499689:1 gene:ENSG00000142534 transcript:ENST00000594493 gene_biotype:protein_coding transcript_biotype:protein_coding\
MKMQRTIVIRRDYLHYIRKYNRFEKRHKNMSVHLSPCFRDVQIGDIVTVGECRPLSKTVR\
FNVLKVTKAAGTKKQFQKF\

在这里,ENSP00000471873是ID,我要提取的文本是

MKMQRTIVIRRDYLHYIRKYNRFEKRHKNMSVHLSPCFRDVQIGDIVTVGECRPLSKTVR\
    FNVLKVTKAAGTKKQFQKF\

1
你不会访问硬盘200次。没有一个理智的操作系统会这样工作。将文件放入某种理智的结构中,比如字符串数组。 - David Schwartz
如果你想在文本文件中维护某种“数据库”,也许你应该使用一个真正的数据库。 - Ya Wang
4
你可以使用 StringBuilder 替代字符串拼接(也许编译器已经自动将你的代码转换为使用它)。 - Mick Mnemonic
我认为你应该包含你的旧程序。在内存中加载可能不是一个好主意,尤其是在那么大的情况下。 - RealSkeptic
@JennaMaiz 请查看我以下的示例代码,我认为它可能会让你朝着正确的方向前进。 - SnakeDoc
显示剩余4条评论
6个回答

2

如果您考虑将数据读入内存并通过某种映射方式进行访问,那么您肯定是在正确的轨道上。这将消除很多瓶颈,即磁盘I/O和访问时间(内存速度更快)。

我建议使用HashMap将数据读入,其中ID为键,文本为值。

可以尝试以下代码:

public Map<Integer, String> getIdMap(final String pathToFile) throws IOException {
    // we'll use this later to store our mappings
    final Map<Integer, String> map = new HashMap<Integer, String>();
    // read the file into a String
    final String rawFileContents = new String(Files.readAllBytes(Paths.get(pathToFile)));
    // assumes each line is an ID + value
    final String[] fileLines = rawFileContents.split(System.getProperty("line.separator"));
    // iterate over every line, and create a mapping for the ID to Value
    for (final String line : fileLines) {
        Integer id = null;
        try {
            // assumes the id is part 1 of a 2 part line in CSV "," format
            id = Integer.parseInt(line.split(",")[0]);
        } catch (NumberFormatException e) {
            e.printStackTrace();
        }
        // assumes the value is part 2 of a 2 part line in CSV "," format
        final String value = line.split(",")[1];
        // put the pair into our map
        map.put(id, value);
    }
    return map;
}

这将把文件读入内存(作为字符串),然后将其分割成一个Map,以便轻松检索值,例如:
Map<Integer, String> map = getIdMap("/path/to/file");
final String theText = map.get(theId);
System.out.println(theText);

这段示例代码未经测试,假设文件格式为每行一个ID和值,并且ID和值用逗号分隔(CSV)。如果您的数据结构不同,只需自行修改。请注意保留HTML标签。

更新以匹配您的文件描述:

public Map<String, String> getIdMap(final String pathToFile) throws IOException {
    // we'll use this later to store our mappings
    final Map<String, String> map = new HashMap<String, String>();
    // read the file into a String
    final String rawFileContents = new String(Files.readAllBytes(Paths.get(pathToFile)));
    // assumes each line is an ID + value
    final String[] fileLines = rawFileContents.split(System.getProperty("line.separator"));
    // iterate over every line, and create a mapping for the ID to Value
    for (final String line : fileLines) {
        // get the id and remove the leading '>' symbol
        final String id = line.split(" ")[0].replace(">", "").trim();
        // use the key 'transcript_biotype:' to get the 'IG_D_gene' value
        final String value = line.split("transcript_biotype:")[1].trim();
        // put the pair into our map
        map.put(id, value);
    }
    return map;
}

1
如果文件包含一系列记录,则可以: 1. 创建一个具有ID和文本内容属性的类。 2. 从文件中读取每个记录并创建一个对象,然后将其添加到HashMap中。 3. 使用HashMap按ID检索对象。

很遗憾,它不是这样组织的。 - Jenna Maiz

1
同意大多数其他评论。对于今天的内存来说,60 MB并不算太大。但是时间被浪费的地方几乎肯定在于每行都要将其附加到一个越来越庞大的单个字符串中的"+="操作符上。可以创建一个行数组。
更好的方法是,在读取时分离ID文本和“相关文本”,以加快后期ID搜索的速度。哈希表是理想的选择。

1
这是准确的,+= 是一个不好的想法。话虽如此,应该更改方法,使数据获得一些结构,而不仅仅将整个文件作为原始字节保存在内存中。因此,我认为这个答案并没有以最好的方式提供帮助。 - TM.

0
你正在处理的是FASTA文件。试试BioPerl吧……有很多库可以解析和处理这些类型的文件。无论你在做什么,很可能已经有人做过了……

0

假设您的虚拟机已分配足够的堆内存,您可以像这样将原始文件加载到内存中:

public byte[] loadFile(File f) throws IOException {
    long size = f.length();
    InputStream source;
    byte[] bytes;
    int nread;
    int next;

    if (size > Integer.MAX_VALUE) {
        throw new IllegalArgumentException("file to long");
    }
    bytes = new byte[(int)size];

    source = new FileInputStream(f);

    for (next = 0; next < bytes.length; next += nread) {
        nread = source.read(bytes, next, bytes.length - next);
        if (nread < 0) {
            throw new FileTruncatedWhileReadingItException();
            // or whatever ...
        }
    }
    if (source.read() != -1) {
        throw new FileExtendedWhileReadingItException(); 
        // or whatever ...
    }

    return bytes;
}

你可以通过创建一个ByteArrayInputStream将这个内存中的副本处理,而不是从磁盘读取 -- 你应该能够相对容易地将其插入到你现有的代码中。

可能还有其他优化方式。例如,如果处理数据必须解码为字符,则可以使用Reader将其读入char[]而不是InputStream读入byte[],然后以类似的方式进行缓存解码结果。但请注意,将ASCII数据以char形式存储所需的空间是以byte形式存储的两倍。

如果数据适合,那么将其完全解析成更复杂的数据结构(例如Map)可能非常有用,这可以使后续查找非常快速。当然,代价是更多的内存使用。


在@bayou.io上,MappedByteBuffer肯定是一个替代方案。它有不同的优缺点。建立速度可能会快得多,但这部分是因为从文件加载数据到内存中可以分摊到后续访问中。不清楚是否可以改善整体数据访问时间。此外,内存映射文件会使结果对底层文件的修改敏感,这可能是想要或不想要的。如果您不需要能够修改数据,则我倾向于直接加载它。 - John Bollinger

0

我认为你的问题来自于对文本进行字符串添加。你应该使用StringBuffer代替。我还建议你使用Scanner类而不是FileReader

public String createLocalFile(String path)
{   
    StringBuffer text = new StringBuffer();
    try
    {
        Scanner sc = new Scanner( new File(path) );
        while( sc.hasNext() )
        {
            String currentLine = sc.nextLine();
            text.append(currentLine);
            System.out.println( currentLine );
        }

    }
    catch(IOException ex)
    {
        System.out.println(ex.getMessage());
    }
    return text.toString();
}

这应该会快得多。


这里没有必要使用 StringBuffer,除非 OP 需要线程安全(以及与之相关的开销和 StringBuffer)。相反,StringBuilder 在这里可能会做得很好。 - SnakeDoc

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接