我有一个包含数百万列的巨大文件,按空间分割,但它只有有限的行数:
示例.txt:
1 2 3 4 5 ........
3 1 2 3 5 .........
l 6 3 2 2 ........
现在,我只想阅读第二列:
2
1
6
我如何在高性能的 java 中做到这一点。
谢谢
更新:该文件通常为 1.4G,包含数百行。
如果您的文件不是静态结构的,那么您唯一的选择是天真的选择:逐个字节序列地读取文件字节序列以查找换行符并在每个换行符之后获取第二列。使用FileReader
.
如果您的文件是静态结构的,您可以计算文件中第二列对于给定行的位置并seek()
直接指向它。
这是一个小状态机,它使用 aFileInputStream
作为输入并处理自己的缓冲。没有语言环境转换。
在我使用 7 年、内存为 1/2 Gb 的 1.4 GHz 笔记本电脑上,处理 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);
}
}
我必须同意@gene,首先尝试使用 BufferedReader 和 getLine,它简单且易于编码。请注意不要在 getLine 的结果和您使用的任何子字符串操作之间为支持数组起别名。String.substring() 是一个特别常见的罪魁祸首,我已经将多 MB 字节数组锁定在内存中,因为 3 字符子字符串正在引用它。
假设 ASCII,我这样做时的偏好是下降到字节级别。使用 mmap 以 a 形式查看文件ByteBuffer
,然后对 0x20 和 0x0A 进行线性扫描(假设为 unix 样式的行分隔符)。然后将相关字节转换为字符串。如果您使用的是 8 位字符集,则很难比这更快。
如果您使用的是 Unicode,那么问题会更加复杂,我强烈建议您使用BufferedReader
,除非该性能确实无法接受。如果getLine()
不起作用,则考虑仅循环调用read()
.
无论如何,在从外部字节流初始化字符串时,您应该始终指定字符集。这明确记录了您的字符集假设。所以我建议对基因的建议做一个小的修改,所以其中之一:
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"));
作为适当的。