如何读取大文件中的第二列

6

我有一个巨大的文件,拥有数百万列,由空格分隔,但只有有限的行数:

examples.txt:

1 2 3 4 5 ........
3 1 2 3 5 .........
l 6 3 2 2 ........

现在,我只想读取第二列:

2
1
6

我该如何在Java中实现高性能?

谢谢。

更新:文件通常包含数百行,大小为1.4G。


每一行都包含完全相同数量的字符吗? - cheeken
我迷失了。格式是一个数字后跟一个空格,等等,每行的字符数完全相同吗? - Gene
长度不同,但在非常有限的范围内,比如说2048B。 - Frank
数字总是一位数吗?谢谢。 - Gene
另一个选择是在Java程序读取数据之前对其进行规范化。您可以使用Unix的_cut_命令轻松提取第二列。 - DJ.
3个回答

2
如果您的文件没有静态结构,那么您唯一的选择是幼稚的方法:逐字节地读取文件序列,寻找换行符,并在每个换行符后抓取第二列。使用FileReader
如果您的文件是静态结构的,则可以计算给定行的第二列在文件中的位置,并直接seek()到该位置。

2
不要逐行读取...只需读取大量字节并迭代它。如果该行很长,则在读取时会阻塞很长时间,并且RAM会被填满! - headgrowe
我不确定你的意思。他很清楚地说了,要按字节读取,寻找换行符,而不是按行读取。 - Gene

0

这里有一个小状态机,它使用FileInputStream作为输入,处理自己的缓冲。没有区域转换。

在我的7年前的1.4 GHz笔记本电脑上,内存为1/2 Gb,在浏览了12.8亿字节的数据后需要48秒。缓冲区大于4KB似乎运行较慢。

在一台新的1年前的4Gb MacBook上,它运行时间为14秒。文件在缓存中后,运行时间为2.7秒。同样,并不是所有大于4KB的缓冲区都有效。这是相同的12亿字节数据文件。

我希望内存映射IO会更好,但这可能更具可移植性。

它将获取您告诉它的任何列。

import java.io.*;
import java.util.Random;

public class Test {

public static class ColumnReader {

    private final InputStream is;
    private final int colIndex;
    private final byte [] buf;
    private int nBytes = 0;
    private int colVal = -1;
    private int bufPos = 0;

    public ColumnReader(InputStream is, int colIndex, int bufSize) {
        this.is = is;
        this.colIndex = colIndex;
        this.buf = new byte [bufSize];
    }

    /**
     * States for a tiny DFA to recognize columns.
     */
    private static final int START = 0;
    private static final int IN_ANY_COL = 1;
    private static final int IN_THE_COL = 2;
    private static final int WASTE_REST = 3;

    /**
     * Return value of colIndex'th column or -1 if none is found. 
     * 
     * @return value of column or -1 if none found.
     */
    public int getNext() {
        colVal = -1;
        bufPos = parseLine(bufPos);
        return colVal;
    }

    /**
     * If getNext() returns -1, this can be used to check if
     * we're at the end of file.
     * 
     * Otherwise the column did not exist.
     * 
     * @return end of file indication 
     */
    public boolean atEoF() {
        return nBytes == -1;
    }

    /**
     * Parse a line.  
     * The buffer is automatically refilled if p reaches the end.
     * This uses a standard DFA pattern.
     *
     * @param p position of line start in buffer
     * @return position of next unread character in buffer
     */
    private int parseLine(int p) {
        colVal = -1;
        int iCol = -1;
        int state = START;
        for (;;) {
            if (p == nBytes) {
                try {
                    nBytes = is.read(buf);
                } catch (IOException ex) {
                    nBytes = -1;
                }
                if (nBytes == -1) {
                    return -1;
                } 
                p = 0;
            }
            byte ch = buf[p++];
            if (ch == '\n') {
                return p;
            }
            switch (state) {
                case START:
                    if ('0' <= ch && ch <= '9') {
                        if (++iCol == colIndex) {
                            state = IN_THE_COL;
                            colVal = ch - '0';
                        } 
                        else { 
                            state = IN_ANY_COL;
                        }
                    }
                    break;

                case IN_THE_COL:
                    if ('0' <= ch && ch <= '9') {
                        colVal = 10 * colVal + (ch - '0');
                    }
                    else {
                        state = WASTE_REST;
                    }
                    break;

                case IN_ANY_COL:
                    if (ch < '0' || ch > '9') {
                        state = START;
                    }
                    break;

                case WASTE_REST:
                    break;
            }
        }
    }
}

public static void main(String[] args) {
    final String fn = "data.txt";
    if (args.length > 0 && args[0].equals("--create-data")) {
        PrintWriter pw;
        try {
            pw = new PrintWriter(fn);
        } catch (FileNotFoundException ex) {
            System.err.println(ex.getMessage());
            return;
        }
        Random gen = new Random();
        for (int row = 0; row < 100; row++) {
            int rowLen = 4 * 1024 * 1024 + gen.nextInt(10000);
            for (int col = 0; col < rowLen; col++) {
                pw.print(gen.nextInt(32));
                pw.print((col < rowLen - 1) ? ' ' : '\n');
            }
        }
        pw.close();
    }

    FileInputStream fis;
    try {
        fis = new FileInputStream(fn);
    } catch (FileNotFoundException ex) {
        System.err.println(ex.getMessage());
        return;
    }
    ColumnReader cr = new ColumnReader(fis, 1, 4 * 1024);
    int val;
    long start = System.currentTimeMillis();
    while ((val = cr.getNext()) != -1) {
        System.out.print('.');
    }
    long stop = System.currentTimeMillis();
    System.out.println("\nelapsed = " + (stop - start) / 1000.0);
}
}

正如我所说,“不要逐行读取...只需读取大量字节并迭代它...如果行很长,您在读取时会阻塞很长时间,而且内存也会被填满!” 顺便说一下,整数长度为4个字节...因此,您可以将行保存为没有空格的形式,而不是字符串...不进行字符串转换的读取速度真的更快...使用FileInputStream... - headgrowe
我们完全一致。在他发布文件的真实大小之前,我写了尝试使用BufferedReader和getLine。在确定必要性之前进行棘手的代码优化通常不是一个好主意。 - Gene

0

我必须同意 @gene 的观点,首先尝试使用 BufferedReader 和 getLine,这很简单且易于编码。只需小心不要在使用任何子字符串操作时将支持数组别名与 getLine 的结果重叠。String.substring() 是一个特别常见的罪魁祸首,我曾经因为一个 3 字符子字符串引用了它而锁定了多 MB 的字节数组。

假设是 ASCII 编码,我在执行此操作时更喜欢降到字节级别。使用 mmap 将文件视为 ByteBuffer,然后进行线性扫描以查找 0x20 和 0x0A(假设是 Unix 风格的行分隔符)。然后将相关字节转换为 String。如果您使用的是 8 位字符集,则极难比这更快。

如果您使用 Unicode,则问题会更加复杂,我强烈建议您除非性能真的无法接受,否则请使用 BufferedReader。如果 getLine() 不起作用,则考虑只循环调用 read()。

无论如何,初始化来自外部字节流的 String 时,您应始终指定 Charset。这明确记录了您的字符集假设。因此,我建议对 gene 的建议进行轻微修改,其中之一:

int i = Integer.parseInt(new String(buffer, start, length, "US-ASCII"));

int i = Integer.parseInt(new String(buffer, start, length, "ISO-8859-1"));

int i = Integer.parseInt(new String(buffer, start, length, "UTF-8"));

根据情况而定。


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