Python中mmap模塊處理大文本的操作方法
如果現(xiàn)在有一個(gè)需求,我們需要處理一個(gè)20G的大文件,我們會(huì)怎么處理呢?思考下,我們需要怎么實(shí)現(xiàn)這個(gè)功能。
我們可能會(huì)這么實(shí)現(xiàn):
def get_datas(): source_text_path = "路徑" with open(source_text_path, 'rb') as f: data = f.readlines() yield data if __name__ == '__main__': for e in get_datas(): deal_data(e) # 處理數(shù)據(jù)
這樣雖然能實(shí)現(xiàn),但是我們處理的時(shí)候需要消耗的資源和性能不是很友好,所以我們要優(yōu)化,也就是使用mmap模塊。
mmap是一種虛擬內(nèi)存映射文件的方法,即將一個(gè)文件或者其它對(duì)象映射到進(jìn)程的地址空間,實(shí)現(xiàn)文件磁盤地址和進(jìn)程虛擬地址空間中一段虛擬地址的一一映射關(guān)系。它省掉了內(nèi)核態(tài)和用戶態(tài)頁copy這個(gè)動(dòng)作(兩態(tài)間copy),直接將用戶態(tài)的虛擬地址與內(nèi)核態(tài)空間進(jìn)行映射,進(jìn)程直接讀取內(nèi)核空間,速度提高了,內(nèi)存占用也少了。
簡單點(diǎn)來說,mmap函數(shù)實(shí)現(xiàn)的是內(nèi)存共享。內(nèi)存共享是兩個(gè)不同的進(jìn)程共享內(nèi)存的意思:同一塊物理內(nèi)存被映射到兩個(gè)進(jìn)程的各自的進(jìn)程地址空間。這個(gè)物理內(nèi)存已經(jīng)被規(guī)定了大?。ù笮∫欢ㄒ葘?shí)際寫入的東東大)以及名稱。當(dāng)需要寫入時(shí),找到內(nèi)存名稱,然后寫入內(nèi)存,等需要讀取時(shí)候, 首先要知道你要讀取多大(因?yàn)槲锢韮?nèi)存比你要讀取的東西大,全部讀取的話會(huì)讀到一些“空”的東西),然后尋找對(duì)應(yīng)名稱的物理塊,然后讀取。
mmap 介紹
Windows
mmap.mmap(fileno, length, tagname=None, access=ACCESS_DEFAULT[, offset])
參數(shù)說明:
fileno:文件描述符,可以是file對(duì)象的fileno()方法,或者來自os.open(),在調(diào)用mmap()之前打開文件,不再需要文件時(shí)要關(guān)閉。
length:要映射文件部分的大小(以字節(jié)為單位),這個(gè)值為0,則映射整個(gè)文件,如果大小大于文件當(dāng)前大小,則擴(kuò)展這個(gè)文件。
tagname:為映射提供標(biāo)簽名稱的字符串,Windows 允許你對(duì)同一文件擁有許多不同的映射。如果指定現(xiàn)有標(biāo)簽的名稱,則會(huì)打開該標(biāo)簽,否則將創(chuàng)建該名稱的新標(biāo)簽。如果省略此參數(shù)或設(shè)置為None ,則創(chuàng)建的映射不帶名稱。避免使用tag參數(shù)將有助于使代碼在Unix和Windows之間可移植。
access:文件權(quán)限
ACCESS_READ:讀訪問;
ACCESS_WRITE:寫訪問,默認(rèn);
ACCESS_COPY:拷貝訪問,不會(huì)把更改寫入到文件,使用flush把更改寫到文件。
offset:非負(fù)整數(shù)偏移量,默認(rèn)從0開始。
Unix
mmap.mmap(fileno, length, flags=MAP_SHARED, prot=PROT_WRITE|PROT_READ, access=ACCESS_DEFAULT[, offset])
參數(shù)說明:
- flags:映射的性質(zhì),默認(rèn)MAP_SHARED。
- MAP_PRIVATE 會(huì)創(chuàng)建私有的寫入時(shí)拷貝映射,因此對(duì)mmap對(duì)象內(nèi)容的修改將為該進(jìn)程所私有;
- MAP_SHARED 會(huì)創(chuàng)建與其他映射同一文件區(qū)域的進(jìn)程所共享的映射。
- prot:它將給出所需的內(nèi)存保護(hù)方式;最有用的兩個(gè)值是 PROT_READ和 PROT_WRITE,分別指明頁面為可讀或可寫。 prot 默認(rèn)為PROT_READ | PROT_WRITE。
- access:注意的是可以指定 access 作為替代 flags 和 prot 的可選關(guān)鍵字形參。 同時(shí)指定 flags,prot 和 access 將導(dǎo)致錯(cuò)誤。
fileno文件描述符有如下
- os.O_RDONLY:以只讀的方式打開Readonly
- os.O_WRONLY:以只寫的方式打開Write only
- os.O_RDWR:以讀寫的方式打開 Read and write
- os.O_APPEND:以追加的方式打開
- os.O_CREAT:創(chuàng)建并打開一個(gè)新文件
- os.O_EXCL:os.O_CREAT| os.O_EXCL 如果指定的文件存在,返回錯(cuò)誤
- os.O_TRUNC:打開一個(gè)文件并截?cái)嗨拈L度為零(必須有寫權(quán)限)
- os.O_BINARY:以二進(jìn)制模式打開文件(不轉(zhuǎn)換)
- os.O_NOINHERIT:阻止創(chuàng)建一個(gè)共享的文件描述符
- os.O_SHORT_LIVED
- os.O_TEMPORARY:與O_CREAT一起創(chuàng)建臨時(shí)文件
- os.O_RANDOM:緩存優(yōu)化,但不限制從磁盤中隨機(jī)存取
- os.O_SEQUENTIAL :緩存優(yōu)化,但不限制從磁盤中序列存取
- os.O_TEXT:以文本的模式打開文件(轉(zhuǎn)換)
支持的方法
- close(): 關(guān)閉 mmap。 后續(xù)調(diào)用該對(duì)象的其他方法將導(dǎo)致引發(fā) ValueError 異常。 此方法將不會(huì)關(guān)閉打開的文件。
- closed: 如果文件已關(guān)閉則返回 True。
- find(str, start, end): 從 start 下標(biāo)開始,在 m中從左往右尋找子串 str最早出現(xiàn)的下標(biāo);沒有找到則返回-1。
- flush([offset, n]):將對(duì)文件的內(nèi)存副本的修改刷新至磁盤。 如果不使用此調(diào)用則無法保證在對(duì)象被銷毀前將修改寫回存儲(chǔ)。 如果指定了 offset和 size,則只將對(duì)指定范圍內(nèi)字節(jié)的修改刷新至磁盤;在其他情況下,映射的全部范圍都會(huì)被刷新。
- windows: 返回的非零值表示成功;否則返回0。 零表示失敗。
- unix: 返回零值以表示成功。 當(dāng)調(diào)用失敗時(shí)將引發(fā)異常。
- move(dest, src, count): 將從偏移量 src開始的 count個(gè)字節(jié)拷貝到目標(biāo)索引號(hào) dest。 如果 mmap 創(chuàng)建時(shí)設(shè)置了 ACCESS_READ,則調(diào)用 move將引發(fā)異常。
- read([n]): 返回一個(gè)字節(jié),其中包含從當(dāng)前文件位置開始的至多 n 個(gè)字節(jié)。 如果參數(shù)省略,為 None 或負(fù)數(shù),則返回從當(dāng)前文件位置開始直至映射結(jié)尾的所有字節(jié)。 文件位置會(huì)被更新為返回字節(jié)數(shù)據(jù)之后的位置
- read_byte():返回一個(gè)1字節(jié)長的字符串,從 m 對(duì)應(yīng)的文件中讀1個(gè)字節(jié),要是已經(jīng)到了EOF還調(diào)用 read_byte(),則拋出異常 ValueError。
- readline():返回一個(gè)字符串,從 m 對(duì)應(yīng)文件的當(dāng)前位置到下一個(gè)’\n’,當(dāng)調(diào)用 readline() 時(shí)文件位于 EOF,則返回空字符串。
- resize(newsize):如果存在的話, 改變映射以及下層文件的大小。 如果 mmap 創(chuàng)建時(shí)設(shè)置了 ACCESS_READ或 ACCESS_COPY,則改變映射大小將引發(fā)異常。
- rfind(sub[, start[, end]]):返回子序列 sub在對(duì)象內(nèi)被找到的最大索引號(hào),使得 sub 被包含在 [start, end] 范圍中。 可選參數(shù) start和 end 會(huì)被解讀為切片表示法。 如果未找到則返回 -1。
- seek(pos[, whence]):設(shè)置文件的當(dāng)前位置。 whence 參數(shù)為可選項(xiàng)并且默認(rèn)為 os.SEEK_SET 或 0 (絕對(duì)文件定位);其他值還有 os.SEEK_CUR 或 1 (相對(duì)當(dāng)前位置查找) 和 os.SEEK_END 或 2 (相對(duì)文件末尾查找)。
- size():返回文件的長度,該數(shù)值可以大于內(nèi)存映射區(qū)域的大小。
- tell():返回文件指針的當(dāng)前位置。
- write(str):將str寫入文件指針當(dāng)前位置的內(nèi)存并返回寫入的字節(jié)總數(shù) (一定不小于 len(str),因?yàn)槿绻麑懭胧。瑢?huì)引發(fā)錯(cuò)誤)。 在字節(jié)數(shù)據(jù)被寫入后文件位置將會(huì)更新。 如果 mmap 創(chuàng)建時(shí)設(shè)置了 ACCESS_READ,則向其寫入將引發(fā) 異常
- write_byte(byte):將整數(shù)值 byte 寫入文件指針當(dāng)前位置的內(nèi)存;文件位置前進(jìn) 1。 如果 mmap 創(chuàng)建時(shí)設(shè)置了 ACCESS_READ,則向其寫入將引發(fā)異常。
對(duì)于EOF的處理,write() 和 read_byte() 拋出異常 ValueError,而 write_byte() 和 read() 什么都不做。
使用mmap讀取大文件
from mmap import mmap def read_data(file_path): with open(file_path, "r+") as f: m = mmap(f.fileno(), 0) g_index = 0 for index, char in enumerate(m): if char == b"\n": yield m[g_index:index + 1].decode() g_index = index + 1 if __name__ == "__main__": file_path = "" for content in read_data(file_path): print(content)
什么時(shí)候用mmap?
用mmap來讀取超大文件,不是mmap的主要應(yīng)用場景,Python官方文件也沒有提到這一點(diǎn)。如果僅僅是讀取超大文件,使用文件對(duì)象的read(N),來得更快更好更簡單。
關(guān)于標(biāo)準(zhǔn)庫中的mmap模塊。現(xiàn)有一個(gè)需求,要對(duì)超大文件(接近40G)進(jìn)行讀寫,notepad++等工具直接拒絕打開此文件。用 r+ 模式打開文件,可以隨意讀寫,但是要特別小心。readline()是否能夠使用,要看這個(gè)文件每行都多長,如果沒有換行,就不能用,就算知道每行的大小,也要帶個(gè)參數(shù)N來控制最大讀取數(shù)量。readlines()是肯定不能用的,就算帶參數(shù),也可能直接卡死!read(N)沒問題,主要控制是N的大小。
總之,傳統(tǒng)讀寫文件的方式可以用,但是不夠方便。速度也是個(gè)問題,傳統(tǒng)的緩存IO方式,涉及到OS內(nèi)核態(tài)的內(nèi)存和進(jìn)程虛擬空間內(nèi)存的內(nèi)容交換,對(duì)于超大文件而言,這種交換會(huì)浪費(fèi)大量的CPU時(shí)間和內(nèi)存。mmap是另一個(gè)方式!它省掉了內(nèi)核態(tài)和用戶態(tài)頁拷貝這個(gè)動(dòng)作(兩態(tài)間copy),直接將用戶態(tài)的虛擬地址與內(nèi)核態(tài)空間進(jìn)行映射,進(jìn)程直接讀取內(nèi)核空間,速度提高了,內(nèi)存占用也少了。
總結(jié)來說,常規(guī)文件操作為了提高讀寫效率和保護(hù)磁盤,使用了頁緩存機(jī)制。這樣造成讀文件時(shí)需要先將文件頁從磁盤拷貝到頁緩存中,由于頁緩存處在內(nèi)核空間,不能被用戶進(jìn)程直接尋址,所以還需要將頁緩存中數(shù)據(jù)頁再次拷貝到內(nèi)存對(duì)應(yīng)的用戶空間中。這樣,通過了兩次數(shù)據(jù)拷貝過程,才能完成進(jìn)程對(duì)文件內(nèi)容的獲取任務(wù)。寫操作也是一樣,待寫入的buffer在內(nèi)核空間不能直接訪問,必須要先拷貝至內(nèi)核空間對(duì)應(yīng)的主存,再寫回磁盤中(延遲寫回),也是需要兩次數(shù)據(jù)拷貝。
而使用mmap操作文件中,創(chuàng)建新的虛擬內(nèi)存區(qū)域和建立文件磁盤地址和虛擬內(nèi)存區(qū)域映射這兩步,沒有任何文件拷貝操作。而之后訪問數(shù)據(jù)時(shí)發(fā)現(xiàn)內(nèi)存中并無數(shù)據(jù)而發(fā)起的缺頁異常過程,可以通過已經(jīng)建立好的映射關(guān)系,只使用一次數(shù)據(jù)拷貝,就從磁盤中將數(shù)據(jù)傳入內(nèi)存的用戶空間中,供進(jìn)程使用。
總而言之,常規(guī)文件操作需要從磁盤到頁緩存再到用戶主存的兩次數(shù)據(jù)拷貝。而mmap操控文件,只需要從磁盤到用戶主存的一次數(shù)據(jù)拷貝過程。說白了,mmap的關(guān)鍵點(diǎn)是實(shí)現(xiàn)了用戶空間和內(nèi)核空間的數(shù)據(jù)直接交互而省去了空間不同數(shù)據(jù)不通的繁瑣過程。因此在某些場景下,mmap效率更高。
從python官網(wǎng)上看mmap的介紹,生成的mmap對(duì)象,就像一個(gè)bytearray對(duì)象,可以直接用index的方式讀寫,可以切片。同時(shí),mmap對(duì)象還有一組類似文件操作的接口,read,readline,flush等等。即mmap對(duì)象兼具bytearray和file對(duì)象的功能。不過還是要注意,對(duì)于超大文件的讀(先不考慮寫的問題吧),從磁盤到內(nèi)核,依然會(huì)占用內(nèi)存,因此絕對(duì)不能一口氣全部讀出來。read(N)是必須的,mmap的使用只是可能會(huì)提高效率。(如果頻繁的創(chuàng)建和關(guān)閉mmap映射,這種創(chuàng)建是為了指向超大文件的不同位置,反而效率更低。一般情況下的read(N)實(shí)現(xiàn),不需要使用mmap。)
mmap的另一個(gè)應(yīng)用場景,是進(jìn)程間的內(nèi)存共享。多個(gè)進(jìn)程將同一個(gè)文件map到同一段內(nèi)核地址上,即實(shí)現(xiàn)了相互之間的共同訪問。
總結(jié):使用mmap的時(shí)機(jī)
- 將一個(gè)普通文件映射到內(nèi)存中,通常在需要對(duì)文件進(jìn)行頻繁讀寫時(shí)使用,這樣用內(nèi)存映射讀寫取代I/O緩存讀寫,以獲得較高的性能;
- 將特殊文件進(jìn)行匿名內(nèi)存映射,可以為關(guān)聯(lián)進(jìn)程提供共享內(nèi)存空間;
- 為無關(guān)聯(lián)的進(jìn)程提供共享內(nèi)存空間,一般也是將一個(gè)普通文件映射到內(nèi)存中。
到此這篇關(guān)于Python中mmap模塊(處理大文本)的文章就介紹到這了,更多相關(guān)Python中mmap模塊內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Python利用多進(jìn)程將大量數(shù)據(jù)放入有限內(nèi)存的教程
這篇文章主要介紹了Python利用多進(jìn)程將大量數(shù)據(jù)放入有限內(nèi)存的教程,使用了multiprocessing和pandas來加速內(nèi)存中的操作,需要的朋友可以參考下2015-04-04python如何統(tǒng)計(jì)代碼運(yùn)行的時(shí)長
這篇文章主要介紹了python如何統(tǒng)計(jì)代碼運(yùn)行的時(shí)長,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-07-07解決python繪圖使用subplots出現(xiàn)標(biāo)題重疊的問題
這篇文章主要介紹了python繪圖使用subplots出現(xiàn)標(biāo)題重疊的問題及解決方法,本文通過實(shí)例圖文相結(jié)合給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-04-04