使用Java处理大文件

来源:互联网

我最近要处理一套存储历史实时数据的大文件fx market data,我很快便意识到,使用传统的InputStream不能够将它们读取到内存,因为每一个文件都超过了4G。甚至编辑器都不能够打开这些文件。

在这种特殊情况下,我可以写一个简单的bash脚本将这些文件分成更小的文件块,然后再读取它。但是我不想这样做,因为二进制格式会使这个方法失效。

处理这个问题的方式通常就是使用内存映射文件递增地处理区域的数据。关于内存映射文件的一个好处就是它们不会使用虚拟内存和换页空间,因为它们是从磁盘上的文件返回来的数据。

很好,让我们来看一看这些文件和额外的一些数据。似乎它们使用逗号分隔的字段包含ASCII文本行。

格式:[currency-pair],[timestamp],[bid-price],[ask-price]

例子:EUR/USD,20120102 00:01:30.420,1.29451,1.2949

我可以为这种格式去写一个程序,但是,读取文件和解析文件是无关的概念。让我们退一步来想一个通用的设计,当在将来面临相似的问题时这个设计可以被重复利用。

这个问题可以归结为递增地解码一个已经在无限长的数组中被编码的记录,并且没有耗尽内存。实际上,以逗号分割的示例格式编码与通常的解决方案是不相关的。所以,很明显需要一个解码器来处理不同的格式。

再来看,知道整个文件处理完成,每一条记录都不能被解析并保存在内存中,所以我们需要一种方式来转移记录,在它们成为垃圾被回收之前可以被写到其他地方,例如磁盘或者网络。

迭代器是处理这个需求的很好的抽象,因为它们就像游标一样,可以正确的指向某个位置。每一次迭代都可以转发文件指针,并且可以让我们使用数据做其他的事情。

首先来写一个Decoder 接口,递增地把对象从MappedByteBuffer中解码,如果buffer中没有对象,则返回null。

publicinterfaceDecoder<T> {

publicT decode(ByteBuffer buffer);

}

然后让FileReader 实现Iterable接口。每一个迭代器将会处理下一个4096字节的数据,并使用Decoder把它们解码成一个对象的List集合。注 意,FileReader 接收文件(files)的list对象,这样是很好的,因为它可以遍历数据,并且不需要考虑聚合的问题。顺便说一下,4096个字节块对于大文件来说是非 常小的。

publicclassFileReaderimplementsIterable<List<T>> {

privatestaticfinallongCHUNK_SIZE =4096;

privatefinalDecoder<T> decoder;

privateIterator<File> files;

privateFileReader(Decoder<T> decoder, File... files) {

this(decoder, Arrays.asList(files));

}

privateFileReader(Decoder<T> decoder, List<File> files) {

this.files = files.iterator();

this.decoder = decoder;

}

publicstatic<T> FileReader<T> create(Decoder<T> decoder, List<File> files) {

returnnewFileReader<T>(decoder, files);

}

publicstatic<T> FileReader<T> create(Decoder<T> decoder, File... files) {

returnnewFileReader<T>(decoder, files);

}

@Override

publicIterator<List<T>> iterator() {

returnnewIterator<List<T>>() {

privateList<T> entries;

privatelongchunkPos =0;

privateMappedByteBuffer buffer;

privateFileChannel channel;

@Override

publicbooleanhasNext() {

if(buffer ==null|| !buffer.hasRemaining()) {

buffer = nextBuffer(chunkPos);

if(buffer ==null) {

returnfalse;

}

}

T result =null;

while((result = decoder.decode(buffer)) !=null) {

if(entries ==null) {

entries =newArrayList<T>();

}

entries.add(result);

}

// set next MappedByteBuffer chunk

chunkPos += buffer.position();

buffer =null;

if(entries !=null) {

returntrue;

}else{

Closeables.closeQuietly(channel);

returnfalse;

}

}

privateMappedByteBuffer nextBuffer(longposition) {

try{

if(channel ==null|| channel.size() == position) {

if(channel !=null) {

Closeables.closeQuietly(channel);

channel =null;

}

if(files.hasNext()) {

File file = files.next();

channel =newRandomAccessFile(file,"r").getChannel();

chunkPos =0;

position =0;

}else{

returnnull;

}

}

longchunkSize = CHUNK_SIZE;

if(channel.size() - position < chunkSize) {

chunkSize = channel.size() - position;

}

returnchannel.map(FileChannel.MapMode.READ_ONLY, chunkPos, chunkSize);

}catch(IOException e) {

Closeables.closeQuietly(channel);

thrownewRuntimeException(e);

}

}

@Override

publicList<T> next() {

List<T> res = entries;

entries =null;

returnres;

}

@Override

publicvoidremove() {

thrownewUnsupportedOperationException();

}

};

}

}

下一个任务就是写一个Decoder 。针对逗号分隔的任何文本格式,编写一个TextRowDecoder 类。接收的参数是每行字段的数量和一个字段分隔符,返回byte的二维数组。TextRowDecoder 可以被操作不同字符集的特定格式解码器重复利用。

publicclassTextRowDecoderimplementsDecoder<byte[][]> {

privatestaticfinalbyteLF =10;

privatefinalintnumFields;

privatefinalbytedelimiter;

publicTextRowDecoder(intnumFields,bytedelimiter) {

this.numFields = numFields;

this.delimiter = delimiter;

}

@Override

publicbyte[][] decode(ByteBuffer buffer) {

intlineStartPos = buffer.position();

intlimit = buffer.limit();

while(buffer.hasRemaining()) {

byteb = buffer.get();

if(b == LF) {// reached line feed so parse line

intlineEndPos = buffer.position();

// set positions for one row duplication

if(buffer.limit() < lineEndPos +1) {

buffer.position(lineStartPos).limit(lineEndPos);

}else{

buffer.position(lineStartPos).limit(lineEndPos +1);

}

byte[][] entry = parseRow(buffer.duplicate());

if(entry !=null) {

// reset main buffer

buffer.position(lineEndPos);

buffer.limit(limit);

// set start after LF

lineStartPos = lineEndPos;

}

returnentry;

}

}

buffer.position(lineStartPos);

returnnull;

}

publicbyte[][] parseRow(ByteBuffer buffer) {

intfieldStartPos = buffer.position();

intfieldEndPos =0;

intfieldNumber =0;

byte[][] fields =newbyte[numFields][];

while(buffer.hasRemaining()) {

byteb = buffer.get();

if(b == delimiter || b == LF) {

fieldEndPos = buffer.position();

// save limit

intlimit = buffer.limit();

// set positions for one row duplication

buffer.position(fieldStartPos).limit(fieldEndPos);

fields[fieldNumber] = parseField(buffer.duplicate(), fieldNumber, fieldEndPos - fieldStartPos -1);

fieldNumber++;

// reset main buffer

buffer.position(fieldEndPos);

buffer.limit(limit);

// set start after LF

fieldStartPos = fieldEndPos;

}

if(fieldNumber == numFields) {

returnfields;

}

}

returnnull;

}

privatebyte[] parseField(ByteBuffer buffer,intpos,intlength) {

byte[] field =newbyte[length];

for(inti =0; i < field.length; i++) {

field[i] = buffer.get();

}

returnfield;

}

}

这是文件被处理的过程。每一个List包含的元素都从一个单独的buffer中解码,每一个元素都是被TextRowDecoder定义的byte二维数组。

TextRowDecoder decoder =newTextRowDecoder(4, comma);

FileReader<byte[][]> reader = FileReader.create(decoder, file.listFiles());

for(List<byte[][]> chunk : reader) {

// do something with each chunk

}

我们可以在这里打住,不过还有额外的需求。每一行都包含一个时间戳,每一批都必须分组,使用时间段来代替buffers,如按照天分组、或者按照小 时分组。我还想要遍历每一批的数据,因此,第一反应就是,为FileReader创建一个Iterable包装器,实现它的行为。一个额外的细节,每一个 元素必须通过实现Timestamped接口(这里没有显示)提供时间戳到PeriodEntries。

publicclassPeriodEntries<TextendsTimestamped>implementsIterable<List<T>> {

privatefinalIterator<List<TextendsTimestamped>> entriesIt;

privatefinallonginterval;

privatePeriodEntries(Iterable<List<T>> entriesIt,longinterval) {

this.entriesIt = entriesIt.iterator();

this.interval = interval;

}

publicstatic<TextendsTimestamped> PeriodEntries<T> create(Iterable<List<T>> entriesIt,longinterval) {

returnnewPeriodEntries<T>(entriesIt, interval);

}

@Override

publicIterator<List<TextendsTimestamped>> iterator() {

returnnewIterator<List<T>>() {

privateQueue<List<T>> queue =newLinkedList<List<T>>();

privatelongprevious;

privateIterator<T> entryIt;

@Override

publicbooleanhasNext() {

if(!advanceEntries()) {

returnfalse;

}

T entry = entryIt.next();

longtime = normalizeInterval(entry);

if(previous ==0) {

previous = time;

}

if(queue.peek() ==null) {

List<T> group =newArrayList<T>();

queue.add(group);

}

while(previous == time) {

queue.peek().add(entry);

if(!advanceEntries()) {

break;

}

entry = entryIt.next();

time = normalizeInterval(entry);

}

previous = time;

List<T> result = queue.peek();

if(result ==null|| result.isEmpty()) {

returnfalse;

}

returntrue;

}

privatebooleanadvanceEntries() {

// if there are no rows left

if(entryIt ==null|| !entryIt.hasNext()) {

// try get more rows if possible

if(entriesIt.hasNext()) {

entryIt = entriesIt.next().iterator();

returntrue;

}else{

// no more rows

returnfalse;

}

}

returntrue;

}

privatelongnormalizeInterval(Timestamped entry) {

longtime = entry.getTime();

intutcOffset = TimeZone.getDefault().getOffset(time);

longutcTime = time + utcOffset;

longelapsed = utcTime % interval;

returntime - elapsed;

}

@Override

publicList<T> next() {

returnqueue.poll();

}

@Override

publicvoidremove() {

thrownewUnsupportedOperationException();

}

};

}

}

最后的处理代码通过引入这个函数并无太大变动,只有一个干净的且紧密的循环,不必关心文件、缓冲区、时间周期的分组元素。PeriodEntries也是足够的灵活管理任何时长的时间。

TrueFxDecoder decoder =newTrueFxDecoder();

FileReader<TrueFxData> reader = FileReader.create(decoder, file.listFiles());

longperiodLength = TimeUnit.DAYS.toMillis(1);

PeriodEntries<TrueFxData> periods = PeriodEntries.create(reader, periodLength);

for(List<TrueFxData> entries : periods) {

// data for each day

for(TrueFxData entry : entries) {

// process each entry

}

}

你也许意识到了,使用集合不可能解决这样的问题;选择迭代器是一个关键的设计决策,能够解析兆字节的数组,且不会消耗过多的空间。

发表评论