使用C#读写结构化的二进制文件
最近工作上遇到一個問題,就是有將近200萬個CSV文件,每個CSV文件包含了成千上萬條實驗數據,CSV以一個不連續的整數值作為文件名,比如:1.CSV、2.CSV、3.CSV、5.CSV等等。另外又有200萬個XML文件,每個XML文件的文件名與CSV的文件名一一對應,在這些XML文件中,定義了所對應的CSV實驗數據文件的實驗描述信息(比如實驗名稱、實驗類型等等),也就是說,每個XML包含的是它所對應的CSV文件的元數據。現在的一個需求是,當軟件中列出其中一部分(比如幾千個或者幾萬個)CSV文件時,需要在每個文件名邊上顯示對應的實驗名稱。
咋一看這樣的需求,感覺比較簡單,當顯示某個CSV文件時,直接找到對應的XML文件,解析XML得到名稱就結束了。然而,問題是:
這就需要軟件本身自帶這200萬個XML文件,文件數量太大,如果壓縮成ZIP,ZIP的尺寸也相對較大,在程序請求實驗名稱時還需要解壓,性能極差
解析XML本身需要損耗一定的性能,如果要顯示成百上千個CSV對應的實驗名稱,那么需要對每個XML進行解析,性能也很不理想
在此,我介紹一種方法,通過預處理的方式,將所需信息提取成結構化的數據結構(Structured Data Structure),然后通過索引進行快速定位。
問題分析
雖然XML文件數量比較大,每個XML文件提供的信息也比較多,但是我們所需要的信息僅僅就是XML文件中的實驗名稱,因此,一個思路就是,首先對所有XML文件進行預處理,然后提取實驗名稱,并將其保存到另一個文件中。當需要根據CSV文件名獲取實驗名稱時,就查詢這個實驗名稱數據文件,然后顯示對應的實驗名稱。這里的問題是,使用哪種格式來產生實驗名稱數據文件呢?我們又有幾個選擇:
使用JSON,存儲“CSV文件名<—>實驗名稱”的鍵值對,這樣性能也不會很好,因為這樣的鍵值對有200萬個,解析JSON文件本身的CPU和IO負載會很高
使用桌面數據庫,比如SQLite,這樣做需要應用程序內建一個SQLite的引擎,它本身存在CPU架構的問題(x86,x64),而且中間封了一層數據庫訪問操作,性能也不見得特別高
自定義存儲結構,這種做法比較靈活,但是需要自己實現,有一定的難度,出問題的幾率也相對較大
綜合分析,我們還是打算選擇第三個方案,自己定義數據的存儲結構。
假設CSV文件名是連續的,比如是從1.CSV、2.CSV一直到2000000.CSV,那么我們可以將CSV的文件名數值作為索引值,通過查表法找到對應的實驗名稱字符串即可。比如,在內存中有以下字符串數組:
假設CSV文件名為1535.CSV,那么我們只需要assayNames[1534]即可獲得第1535個CSV(也就是1535.CSV)所對應的實驗名稱。這樣做的效率是非常高的,它直接利用了數組的索引。然而,現實并不是那么美好:
我們不可能把200萬條數據全部放在一個數組內存中,這樣做消耗內存會非常高
原始CSV文件的文件名標號并不是連續的
解決問題一的方式比較直白:我們需要將數據放在磁盤中,然后按需訪問;對于問題二,我們需要引入數據庫實現中的一個概念:索引。
解決問題
假設每條實驗名稱數據被當成一條長度固定的記錄存放在二進制文件中,但由于文件名中數值標識并不連續,因此,無法簡單地通過文件名來推斷數據記錄的位置(也就是數組的下標值),比如:
對于1.csv、2.csv尚有規律可尋,實驗名稱數據記錄在二進制文件中的位置,就是文件名數值減1,從4.csv開始,后面的位置值就與文件名沒什么關系了。此時,我們需要有一個映射,來定義文件名中的數值與數據記錄位置之間的關系。為此,我引入了另一個二進制文件,其中定義了200萬條記錄,每條記錄僅占4個字節,每條記錄(每4個字節)保存的是以該記錄的偏移值作為文件名數值的CSV文件,所對應的實驗名稱數據記錄在上述二進制文件中的記錄位置。比如:
那么,假設CSV文件的文件名為4.csv,于是,可以首先找到索引文件中偏移值為4(也就是index=3)的記錄位置值(也就是2),然后,在二進制文件中定位到索引值為2的記錄,就是4.csv所對應的實驗名稱數據。
代碼實現
我使用System.Runtime.InteropServices命名空間下的Marshal類和GCHandle類,配合System.IO命名空間下的BinaryReader、BinaryWriter類來實現結構化二進制文件的讀取和寫入。封裝代碼如下:
| public static class BinaryFileHelper{????public static T ReadStruct<T>(BinaryReader binaryReader, int idx = 0)????????where T : struct????{????????var buff = new byte[Marshal.SizeOf<T>()];????????if (binaryReader.BaseStream.CanSeek)????????{????????????binaryReader.BaseStream.Seek(idx * buff.Length, SeekOrigin.Begin);????????????binaryReader.BaseStream.Read(buff, 0, buff.Length);????????}????????var gcHandle = GCHandle.Alloc(buff, GCHandleType.Pinned);????????try????????{????????????var result = Marshal.PtrToStructure<T>(gcHandle.AddrOfPinnedObject());????????????return result;????????}????????finally????????{????????????gcHandle.Free();????????}????}????public static void WriteStruct<T>(BinaryWriter binaryWriter, T item)????????where T : struct????{????????var buff = new byte[Marshal.SizeOf<T>()];????????var gcHandle = GCHandle.Alloc(buff, GCHandleType.Pinned);????????try????????{????????????Marshal.StructureToPtr<T>(item, gcHandle.AddrOfPinnedObject(), false);????????????binaryWriter.Write(buff, 0, buff.Length);????????}????????finally????????{????????????gcHandle.Free();????????}????}} |
接下來,再寫一個測試程序來測試結構化二進制文件的讀取性能:
| [StructLayout(LayoutKind.Explicit)]public struct AssayNameStructuredIndex{????[FieldOffset(0)]????[MarshalAs(UnmanagedType.U4, SizeConst = 4)]????public int Index;}[StructLayout(LayoutKind.Explicit)]public struct AssayNameStructuredRecord{????[FieldOffset(0)]????[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]????public string Name;}static void Main(string[] args){????var stopwatch = new Stopwatch();????using (var recordFileStream = new FileStream("assayNames.bin", FileMode.Open, FileAccess.Read))????using (var indexFileStream = new FileStream("assayNames.idx", FileMode.Open, FileAccess.Read))????using (var recordReader = new BinaryReader(recordFileStream))????using (var indexReader = new BinaryReader(indexFileStream))????{????????while (true)????????{????????????Console.Write("請輸入CSV文件名(直接回車退出程序):");????????????var line = Console.ReadLine();????????????if (string.IsNullOrEmpty(line)) break;????????????if (!int.TryParse(Path.GetFileNameWithoutExtension(line), out var identifier)) continue;????????????stopwatch.Restart();????????????var indexValue = BinaryFileHelper.ReadStruct<AssayNameStructuredIndex>(indexReader, identifier);????????????if (indexValue.Index == -1)????????????{????????????????Console.WriteLine($"數據文件中未包含{line}的記錄。");????????????????Console.WriteLine();????????????????continue;????????????}????????????var assayNameValue = BinaryFileHelper.ReadStruct<AssayNameStructuredRecord>(recordReader, indexValue.Index);????????????stopwatch.Stop();????????????Console.WriteLine($"耗時:{stopwatch.ElapsedMilliseconds}毫秒,實驗名稱:{assayNameValue.Name}。");????????????Console.WriteLine();????????}????}} |
執行結果如下:
可以看到,無論CSV文件名中的數值是大還是小,從近200萬條數據中讀取實驗名稱信息的速度都是非常快的,基本上也就是零點幾個毫秒,達到了預期的目標。
總結
所謂之結構化的數據,就是表示每條數據所占用的存儲空間都是一致的,也就是每條記錄所占用的字節數是相等的,這樣才能非常容易地通過記錄的索引值以及每條記錄的大小來計算位置偏移量,從而快速讀取數據。這是一種空間換時間的方案,一個明顯的問題是,需要根據實際數據來合理選擇每條記錄所占用的存儲空間:如果太大,那么200多萬條記錄累積起來,會占用大量存儲空間,造成空間浪費;如果太小,又會導致某些數據無法正確存儲,造成信息丟失。因此,本文介紹的方案還是需要根據實際情況進行斟酌,選擇合理的記錄存儲結構。
原文地址:?http://sunnycoding.cn/2018/07/04/accessing-structural-binary-file-using-csharp/
.NET社區新聞,深度好文,歡迎訪問公眾號文章匯總 http://www.csharpkit.com
總結
以上是生活随笔為你收集整理的使用C#读写结构化的二进制文件的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Pipelines - .NET中的新I
- 下一篇: Pipelines - .NET中的新I