天天看點

《Python 3程式開發指南(第2版•修訂版)》——7.4 随機存取二進制檔案

本節書摘來自異步社群《python 3程式開發指南(第2版•修訂版)》一書中的第7章,第7.4節,作者[英]mark summerfield,王弘博,孫傳慶 譯,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視。

前面幾節中,工作的基礎是程式的所有資料都是作為一個整體讀入記憶體、進行适當處理,最後再作為整體寫出。現代計算機具有很大的ram容量,使得這種方法可以有效運作,即便對很大的資料集也是如此。然而,有些情況下,将資料存放在磁盤上,并隻讀入需要的部分,處理之後再将變化的部分寫回磁盤,這是一種更好的解決方案。基于磁盤的随機存取方法最易于使用鍵-值資料庫(“dbm”)或完整的sql資料庫來實作——兩者都将在第12章進行介紹——但在這一節中,我們将展示如何手動處理随機存取檔案。

我們首先給出的是binaryrecordfile.binaryrecordfile類,該類的執行個體用于表示通用的可讀/可寫二進制檔案,在結構上則是固定長度的記錄組成的序列。之後給出的是bikestock.bikestock類,該類用于存放一組bikestock.bike對象(以記錄的形式存放在binaryrecordfile.binaryrecordfile中),通過該類可以了解二進制随機存取檔案的使用。

binaryrecordfile.binaryrecordfile類的api類似于清單,因為我們可以擷取/設定/删除給定的索引位置處的記錄。記錄被删除後,隻是簡單地标記為“已删除”,這使得我們不必移動該記錄後面的所有記錄來保證連續性,也意味着删除操作之後,所有原始的索引位置仍然是有效的。另一個好處是隻要取消“已删除”标記,就可以反删除一條記錄。當然,這種方法即便删除了記錄,也仍然不能節省任何磁盤空間。為解決這一問題,我們将提供适當的方法來“壓縮”檔案,移除已删除的記錄(并使得該索引位置無效)。

在講述其具體實作之前,我們先看一些基本的使用方法。

這裡,我們建立了一個結構(little-endian位元組順序,一個15位元組的位元組字元串,一個4位元組的有符号整數),用于表示每條記錄。之後建立了一個binaryrecordfile. binaryrecordfile執行個體,并使用一個檔案名和一個記錄大小做參數,以便比對目前正在使用的結構。如果該檔案存在,就将打開該檔案(并保證其内容不被改變),否則建立一個檔案——無論哪種情況,都将以二進制讀/寫模式打開檔案。

我們可以将檔案當作一個清單,并使用項存取操作符[]對其進行操作,這裡,我們對該檔案的兩個索引位置處進行了指派操作,指派為位元組字元串(bytes對象,每個包含一個編碼的字元串與一個整數),這兩個指派操作将重寫任何現存的内容,如果檔案尚未包含6條記錄,那麼前面索引位置處的記錄将被建立,并且其中每個位元組設定為0x00。

由于字元串“cindy dove”在長度上小于結構中15個utf-8字元的限制,是以,在對其打包時,會在後面填充一些0x00位元組。是以,取回該記錄時,contact_data中存放的是一個二進制組(b'cindy dovex00x00x00x00x00', 987)。為擷取名稱,我們必須對utf-8字元進行解碼,以便産生一個unicode字元串,并剝離其中的填充位元組0x00。

在大概了解了該類的一些使用之後,現在來檢視該類的實作代碼。binaryrecordfile.binaryrecordfile類實作于檔案binaryrecordfile.py中,在通常的一些預備内容之後,該檔案從一對私有位元組值的定義開始:

每條記錄都以一個“state”位元組引導,該位元組或者是_deleted,或者是_okay(如果是空記錄,就是b"x00")。

下面給出其class行及初始化程式:

有兩個不同的記錄大小,binaryrecordfile.record_size是由使用者設定的,是從使用者角度看到的記錄大小;私有的binaryrecordfile.__record_size是内部實際的記錄大小,包含狀态位元組。

打開檔案時,要注意不要截取該檔案。是以,如果檔案已存在,就應該使用“r+b”模式;如果檔案不存在,就應該使用“w+b”模式建立——這裡,模式字元串的“+”部分表示的是讀與寫。如果布爾型值binaryrecordfile.auto_flush為true,就在每次讀之前與寫之後都将其清空。

我們将記錄大小與檔案名設定為隻讀的特性。我們向使用者報告的記錄大小是使用者請求的,并可以與其記錄比對。flush方法與close方法則是簡單地對檔案對象進行相應處理。

這一方法支援文法格式brf[i] = data,其中,brf是一個二進制記錄檔案,i是一個記錄索引位置,data是一個位元組字元串。注意,記錄必須與建立二進制記錄檔案時指定的大小相同。如果參數正确,就将檔案指針移動到記錄的第一個位元組處——這裡使用的是實際記錄大小,也就是包含了狀态位元組。預設情況下,seek()方法可以将檔案指針移動到位元組的絕對位置,也可以給定另一個參數,使得檔案指針移動到相對于目前位置或結果位置有多遠的索引位置處(檔案對象提供的屬性與方法在表7-4與表7-5中列出。)

《Python 3程式開發指南(第2版•修訂版)》——7.4 随機存取二進制檔案
《Python 3程式開發指南(第2版•修訂版)》——7.4 随機存取二進制檔案
《Python 3程式開發指南(第2版•修訂版)》——7.4 随機存取二進制檔案
《Python 3程式開發指南(第2版•修訂版)》——7.4 随機存取二進制檔案

由于項正在被設定,顯然沒有被删除,是以,我們寫入狀态位元組_okay,之後寫入使用者的二進制記錄資料,二進制記錄檔案不知道也不關心正在使用的記錄結構,而隻要求記錄大小是正确的。

我們沒有檢測索引值是否在有效取值範圍之内。如果索引值超出了檔案末尾,那麼記錄将被寫入到正确的索引位置,并且在檔案末尾與新記錄之間的每個位元組被設定為b"x00",這樣的空白記錄既不是_okay,也不是_deleted,是以,在需要的時候可以區分出來。

取回記錄時,需要考慮4種情況:記錄不存在,也就是說,給定的索引位置超出了範圍;記錄是空的;記錄已删除;記錄狀态為okay。記錄不存在,私有的__seek_to_ index()方法将産生indexerror異常,否則,該方法将尋找該記錄的引導位元組,之後我們讀入狀态位元組。如果狀态不是_okay,那麼記錄必須為空或已删除,這兩種情況将傳回none;否則,我們将讀入并傳回該記錄。(另一種政策是,對空記錄或已删除記錄,産生自定義異常,比如blankrecorderror或deletedrecorderror,而不是傳回none。)

這是一個私有的支援方法,其他一些方法會使用本方法将檔案位置指針移動到記錄的首位元組(從給定的索引位置)。我們從檢測給定的索引位置是否在取值範圍之内開始,為此,我們定位到檔案結尾處(到檔案結尾的位元組偏移量為0),并使用tell()方法取回我們已定位到的位元組位置。如果記錄的偏移量在結尾處或超過結尾處,就說明索引位置已超出範圍,此時應該産生适當的異常。否則,我們就定位到索引偏移位置,并做好下一次讀寫的準備。

首先,我們将檔案位置指針移動到合适的位置,如果索引位置在取值範圍之内(也就是說,沒有産生indexerror異常),并且假定記錄不是空白的或已删除的,我們就删除該記錄,這是通過将其狀态重寫為_deleted來實作的。

該方法首先找到記錄并讀取其狀态位元組,如果記錄已删除,就使用_okay重寫其狀态位元組,并向調用者傳回true,以表明操作成功,否則(對空白記錄或未删除的記錄),傳回false。

這一方法将報告二進制記錄檔案中包含了多少條記錄,這是通過用結尾位元組位置(也即檔案中包含多少個位元組)與記錄大小相除得到的。

至此,我們講述了binaryrecordfile.binaryrecordfile類提供的所有基本功能,但還有一個需要考慮的功能:壓縮檔案,以便删除其中空白記錄與已删除記錄。為此,有兩種方法。一種方法是使用索引位置更大的記錄重寫空白記錄或已删除記錄,以便記錄之間沒有縫隙,并對檔案進行截取(如果結尾處有任意的空白行或删除的記錄),inplace_compact()方法用于完成這一功能。另一種方法是将非空白且未删除的記錄複制到一個臨時檔案中,之後将臨時檔案重命名為原始的檔案名。如果正好需要進行備份,那麼使用臨時檔案是一種非常便利的方法,compact()方法用于實作這一功能。

我們分兩個部分來檢視inplace_compact()方法:

我們對每條記錄進行疊代,依次讀入每條記錄的狀态。如果發現了空白記錄或已删除記錄,則繼續尋找檔案中下一條非空白且未删除的記錄,找到後,就使用該條非空白且未删除的記錄替換空白記錄或已删除記錄,并删除原始的非空白且未删除的記錄;如果一直未找到,就跳出整個while循環,因為我們已經處理完了非空白且未删除的記錄。

如果第一條記錄就是空白記錄或已删除記錄,那麼所有記錄必然都是空白記錄或已删除記錄,因為前面的代碼已經将所有非空白且未删除的記錄移動到檔案起始處,并将空白記錄或已删除記錄移動到檔案末尾處。對這種情況,我們可以簡單地将檔案截取為0位元組。

如果至少有一條非空白且未删除的記錄,那麼我們就沿着從最後一條記錄到第一條記錄的方向進行疊代,因為我們知道,空白記錄或已删除記錄已經被移動到檔案結尾處。變量limit被設定為最靠前的空白記錄或已删除記錄(如果沒有這樣的記錄,就将其設定為none),并對檔案進行相應的截取。

另一種實作壓縮的替代方案是将其複制到另外的檔案中——如果我們正好需要進行備份,那麼這種方法是有用的,接下來我們要檢視的compact()方法展示了這種做法。

這一方法建立兩個檔案,一個壓縮後檔案,一個原始檔案的備份檔案。壓縮後檔案與原始檔案名稱相同,但名稱最後附加了.

$$

$,類似地,備份檔案與原始檔案名稱相同,但名稱最後附加了.bak。我們逐個記錄讀入現有的檔案,對那些非空白且未删除的記錄,就将其寫入到壓縮後檔案中。(注意,我們寫入的是真實的記錄,也即每次都寫入狀态位元組與使用者記錄。)

if data[:1] == _okay:這行代碼是相當微妙的。data對象與_okay對象都是bytes類型,該行代碼中,我們需要将data對象的首位元組與(1位元組的)_okay對象進行比較,如果我們提取bytes對象的分片,就擷取了一個bytes對象;如果我們提取一個單獨的位元組,比如data[0],擷取的則是一個整數——位元組的值。

是以,這裡我們将data的一個位元組的分片(其首位元組,也即狀态位元組)與一個位元組的對象_okay進行比較。(另一種實作方式是使用代碼if data[0] == _okay[0],該代碼将對兩個int值進行比較。)

最後,我們将原始檔案重命名為備份檔案,将壓縮後檔案重命名為原始檔案。之後,如果keep_backup為false(預設情況),就移除備份檔案。最後,我們打開壓縮後檔案(現在該檔案與原始檔案名稱一緻),以備進一步的讀寫。

binaryrecordfile.binaryrecordfile類是底層的,但可以作為高層類的基礎,這些高層類需要對由固定大小記錄組成的檔案進行随機存取,下一小節将對其進行展示。

###7.4.2 執行個體:bikestock子產品的類

bikestock子產品使用binaryrecordfile.binaryrecordfile來提供一個簡單的倉庫控制類,倉庫項為自行車,每個由一個bikestock.bike執行個體表示,整個倉庫的自行車則存放在一個bikestock.bikestock執行個體中。bikestock.bikestock類将字典(其鍵為自行車id,值為記錄索引位置)整合到binaryrecordfile.binaryrecordfile中,下面給出一個簡短的執行個體,有助于了解這些類的工作方式:

上面的代碼段打開一個自行車倉庫檔案,并對其中所有自行車記錄進行疊代,以便計算其中存放的自行車的總體價值(價格乘以數量)。之後遞增倉庫中“gekko”自行車的數量(以2為遞增值)與存放所有自行車id以“b4u”開始的自行車的倉庫(以1為遞增值)。所有這些操作都在磁盤上進行,是以,讀取字形成倉庫檔案的任意其他程序總是可以擷取最新資料。

binaryrecordfile.binaryrecordfile根據索引進行工作,bikestock.bikestock類根據自行車id進行工作,這是由bikestock.bikestock執行個體(其中存放一個字典,該字典将自行車id與索引進行關聯)進行管理的。

我們首先檢視bikestock.bike類的class行與初始化程式,之後檢視其中標明的幾個bikestock.bikestock方法,最後将檢視用于在bikestock.bike對象與二進制記錄(用于在binaryrecordfile.binaryrecordfile中對其進行表示)提供橋梁的代碼。(所有代碼都在bikestock.py檔案中。)

自行車的所有屬性都是以特性形式存在的——自行車id(self.__identity)是一個隻讀的bike.identity特性,其他屬性則是讀/寫特性,并使用斷言進行有效性驗證。此外,隻讀特性bike.value傳回的是數量與價格的乘積。(我們沒有展示該特性的實作,因為前面看到過類似的代碼。)

bikestock.bikestock類提供了自己的用于操縱自行車對象的方法,并依次使用可寫的自行車特性。

bikestock.bikestock類是一個自定義組合類,其中聚集了一個二進制記錄檔案(elf.__file)與一個字典(self.__index_from_identity),該字典的鍵是自行車id,值為記錄索引位置。

檔案打開(如果不存在就建立)後,我們對其内容(如果存在)進行疊代。每個自行車都被取回,并使用私有的_bike_from_record()函數将其從bytes對象轉換為bikestock.bike,自行車的identity與索引位置則添加到self.__index_from_identity字典中。

如果需要向其中添加一台自行車,實際上所做的工作就是找到适當的索引位置,并将該索引位置處的記錄設定為自行車的二進制表象形式。此外,我們還需要更新self.__index_from_identity字典。

删除一條自行車記錄是容易的,我們隻需要找到該記錄的索引位置,并删除該索引位置處的記錄。在bike-stock.bikestock類中,我們沒有使用binaryrecordfile.binary-recordfile的反删除功能。

自行車記錄可以通過自行車id取回,如果沒有要尋找的id,那麼在self.__index_from_identity字典中的搜尋将産生keyerror異常。如果記錄為空白記錄或已删除記錄,那麼binaryrecordfile.binaryrecordfile将傳回none;如果可以成功取回記錄,就将其傳回為一個bikestock.bike對象。

私有方法__change_stock()提供了increase_stock()方法與decrease_stock() 方法的實作。首先找到自行車的索引位置,并取回該記錄,之後将資料轉換為一個bikestock.bike對象。相應的變化作用于自行車,之後,使用更新後自行車對象的二進制表示形式重寫檔案中的原記錄(還有一個__change_bike()方法,提供了對change_name()方法與change_price()方法的實作,但這裡沒有展示,因為與我們這裡展示的非常類似)。

這一方法確定可以對bikestock.bikestock對象進行疊代,就像對清單一樣,每次疊代傳回一個bikestock.bike對象,并跳過空白記錄與已删除記錄。

私有函數_bike_from_record()與record_from_bike()将bikestock.bike類的二進制表示從bikestock.bikestock類(存放一組自行車)中隔離出來。圖7-6展示了自行車記錄檔案的邏輯結構,實體結構稍有差别,因為每條記錄都是由一個狀态位元組引導的。

《Python 3程式開發指南(第2版•修訂版)》——7.4 随機存取二進制檔案

在将二進制記錄轉換為bikestock.bike時,我們首先将unpack()傳回的元組轉換為清單。這允許我們對元素進行修改,這裡是将utf-8編碼的位元組轉換為字元串,并剝離其中的填充位元組0x00。之後,我們使用序列拆分操作符(*)将相應部分提供給bikestock.bike 初始化程式。打包資料更簡單,我們隻是必須確定将字元串編碼為utf-8位元組。

對現代的桌面系統而言,随着ram大小與磁盤速度的增長,應用程式對随機存取二進制資料的需求降低了。需要這樣的功能時,通常最簡單的方法是使用dbm檔案或sql資料庫。盡管如此,這裡展示的技術對有些系統仍然是有用的,比如,嵌入式系統或其他資源受限型的系統。