使用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。
public interface Decoder<T> { public T decode(ByteBuffer buffer); }
然後讓FileReader 實現Iterable介面。每一個迭代器將會處理下一個4096位元組的資料,並使用Decoder把它們解碼成一個物件的List集合。注意,FileReader 接收檔案(files)的list物件,這樣是很好的,因為它可以遍歷資料,並且不需要考慮聚合的問題。順便說一下,4096個位元組塊對於大檔案來說是非常小的。
public class FileReader implements Iterable<List<T>> { private static final long CHUNK_SIZE = 4096; private final Decoder<T> decoder; private Iterator<File> files; private FileReader(Decoder<T> decoder, File... files) { this(decoder, Arrays.asList(files)); } private FileReader(Decoder<T> decoder, List<File> files) { this.files = files.iterator(); this.decoder = decoder; } public static <T> FileReader<T> create(Decoder<T> decoder, List<File> files) { return new FileReader<T>(decoder, files); } public static <T> FileReader<T> create(Decoder<T> decoder, File... files) { return new FileReader<T>(decoder, files); } @Override public Iterator<List<T>> iterator() { return new Iterator<List<T>>() { private List<T> entries; private long chunkPos = 0; private MappedByteBuffer buffer; private FileChannel channel; @Override public boolean hasNext() { if (buffer == null || !buffer.hasRemaining()) { buffer = nextBuffer(chunkPos); if (buffer == null) { return false; } } T result = null; while ((result = decoder.decode(buffer)) != null) { if (entries == null) { entries = new ArrayList<T>(); } entries.add(result); } // set next MappedByteBuffer chunk chunkPos += buffer.position(); buffer = null; if (entries != null) { return true; } else { Closeables.closeQuietly(channel); return false; } } private MappedByteBuffer nextBuffer(long position) { try { if (channel == null || channel.size() == position) { if (channel != null) { Closeables.closeQuietly(channel); channel = null; } if (files.hasNext()) { File file = files.next(); channel = new RandomAccessFile(file, "r").getChannel(); chunkPos = 0; position = 0; } else { return null; } } long chunkSize = CHUNK_SIZE; if (channel.size() - position < chunkSize) { chunkSize = channel.size() - position; } return channel.map(FileChannel.MapMode.READ_ONLY, chunkPos, chunkSize); } catch (IOException e) { Closeables.closeQuietly(channel); throw new RuntimeException(e); } } @Override public List<T> next() { List<T> res = entries; entries = null; return res; } @Override public void remove() { throw new UnsupportedOperationException(); } }; } }
下一個任務就是寫一個Decoder 。針對逗號分隔的任何文字格式,編寫一個TextRowDecoder 類。接收的引數是每行欄位的數量和一個欄位分隔符,返回byte的二維陣列。TextRowDecoder 可以被操作不同字符集的特定格式解碼器重複利用。
public class TextRowDecoder implements Decoder<byte[][]> { private static final byte LF = 10; private final int numFields; private final byte delimiter; public TextRowDecoder(int numFields, byte delimiter) { this.numFields = numFields; this.delimiter = delimiter; } @Override public byte[][] decode(ByteBuffer buffer) { int lineStartPos = buffer.position(); int limit = buffer.limit(); while (buffer.hasRemaining()) { byte b = buffer.get(); if (b == LF) { // reached line feed so parse line int lineEndPos = 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; } return entry; } } buffer.position(lineStartPos); return null; } public byte[][] parseRow(ByteBuffer buffer) { int fieldStartPos = buffer.position(); int fieldEndPos = 0; int fieldNumber = 0; byte[][] fields = new byte[numFields][]; while (buffer.hasRemaining()) { byte b = buffer.get(); if (b == delimiter || b == LF) { fieldEndPos = buffer.position(); // save limit int limit = 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) { return fields; } } return null; } private byte[] parseField(ByteBuffer buffer, int pos, int length) { byte[] field = new byte[length]; for (int i = 0; i < field.length; i++) { field[i] = buffer.get(); } return field; } }
這是檔案被處理的過程。每一個List包含的元素都從一個單獨的buffer中解碼,每一個元素都是被TextRowDecoder定義的byte二維陣列。
TextRowDecoder decoder = new TextRowDecoder(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。
public class PeriodEntries<T extends Timestamped> implements Iterable<List<T>> { private final Iterator<List<T extends Timestamped>> entriesIt; private final long interval; private PeriodEntries(Iterable<List<T>> entriesIt, long interval) { this.entriesIt = entriesIt.iterator(); this.interval = interval; } public static <T extends Timestamped> PeriodEntries<T> create(Iterable<List<T>> entriesIt, long interval) { return new PeriodEntries<T>(entriesIt, interval); } @Override public Iterator<List<T extends Timestamped>> iterator() { return new Iterator<List<T>>() { private Queue<List<T>> queue = new LinkedList<List<T>>(); private long previous; private Iterator<T> entryIt; @Override public boolean hasNext() { if (!advanceEntries()) { return false; } T entry = entryIt.next(); long time = normalizeInterval(entry); if (previous == 0) { previous = time; } if (queue.peek() == null) { List<T> group = new ArrayList<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()) { return false; } return true; } private boolean advanceEntries() { // if there are no rows left if (entryIt == null || !entryIt.hasNext()) { // try get more rows if possible if (entriesIt.hasNext()) { entryIt = entriesIt.next().iterator(); return true; } else { // no more rows return false; } } return true; } private long normalizeInterval(Timestamped entry) { long time = entry.getTime(); int utcOffset = TimeZone.getDefault().getOffset(time); long utcTime = time + utcOffset; long elapsed = utcTime % interval; return time - elapsed; } @Override public List<T> next() { return queue.poll(); } @Override public void remove() { throw new UnsupportedOperationException(); } }; } }
最後的處理程式碼通過引入這個函式並無太大變動,只有一個乾淨的且緊密的迴圈,不必關心檔案、緩衝區、時間週期的分組元素。PeriodEntries也是足夠的靈活管理任何時長的時間。
TrueFxDecoder decoder = new TrueFxDecoder(); FileReader<TrueFxData> reader = FileReader.create(decoder, file.listFiles()); long periodLength = 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 } }
你也許意識到了,使用集合不可能解決這樣的問題;選擇迭代器是一個關鍵的設計決策,能夠解析兆位元組的陣列,且不會消耗過多的空間。
原文連結: javacodegeeks 翻譯: ImportNew - 踏雁尋花
相關文章
- java讀取大檔案並處理Java
- JAVA ZIP 處理檔案Java
- Python處理大檔案Python
- java 檔案處理 工具類Java
- 如何使用vi處理GB級別的大檔案
- Python 如何處理大檔案Python
- Java使用javacv處理影片檔案過程記錄Java
- java中 檔案壓縮處理Java
- java自己封裝檔案處理Java封裝
- 使用 Python 處理 CSV 檔案Python
- Java記憶體對映,上G大檔案輕鬆處理Java記憶體
- php上傳大檔案失敗處理PHP
- java大資料處理:如何使用Java技術實現高效的大資料處理Java大資料
- .NET Core 如何上傳檔案及處理大檔案上傳
- [R]檔案處理
- bat處理檔案BAT
- bat檔案處理BAT
- 使用 Java API 處理 WebSphere MQ 大訊息JavaAPIWebMQ
- 如何處理大體積 XLSX/CSV/TXT 檔案?
- 大體積XML檔案處理效能問題XML
- laravel 使用PHP-FFMpeg處理影片檔案LaravelPHP
- 使用資料流的思想處理檔案
- Linux學習之檔案處理命令(二)目錄處理命令 && 檔案處理命令Linux
- Golang 快速讀取處理大日誌檔案工具Golang
- Serverless Streaming:毫秒級流式大檔案處理探秘Server
- tempfile檔案過大問題處理 for logical standby
- oracle bdump 下.trc檔案過大問題處理Oracle
- window 批處理檔案
- python處理檔案Python
- Go xml檔案處理GoXML
- python檔案處理Python
- python 檔案處理Python
- Python 檔案處理Python
- 批處理檔案命令
- 檔案處理函式函式
- Windows批處理檔案Windows
- bat批處理檔案BAT
- 用批處理檔案編譯並執行java編譯Java