一般在UNIX下面處理文本文件的方法是sed、awk等shell命令,對於處理大文件受CPU,IO等因素影響,對服務器也有必定的壓力。關於sed的說明能夠看瞭解sed的工做原理,本文將介紹經過python的mmap模塊來實現對大文件的處理,來對比看他們的差別。html
mmap是一種虛擬內存映射文件的方法,即將一個文件或者其它對象映射到進程的地址空間,實現文件磁盤地址和進程虛擬地址空間中一段虛擬地址的一一對映關係。關於系統中mmap的理論說明能夠看百度百科和維基百科說明以及mmap函數介紹,這裏的說明是針對在Python下mmap模塊的使用說明。python
使用:
1,建立:建立並返回一個 mmap 對象msql
m=mmap.mmap(fileno, length[, flags[, prot[, access[, offset]]]])
fileno: 文件描述符,能夠是file對象的fileno()方法,或者來自os.open(),在調用mmap()以前打開文件,再也不須要文件時要關閉。shell
os.O_RDONLY 以只讀的方式打開 Read only os.O_WRONLY 以只寫的方式打開 Write only os.O_RDWR 以讀寫的方式打開 Read and write os.O_APPEND 以追加的方式打開 os.O_CREAT 建立並打開一個新文件 os.O_EXCL os.O_CREAT| os.O_EXCL 若是指定的文件存在,返回錯誤 os.O_TRUNC 打開一個文件並截斷它的長度爲零(必須有寫權限) os.O_BINARY 以二進制模式打開文件(不轉換) os.O_NOINHERIT 阻止建立一個共享的文件描述符 os.O_SHORT_LIVED os.O_TEMPORARY 與O_CREAT一塊兒建立臨時文件 os.O_RANDOM 緩存優化,但不限制從磁盤中隨機存取 os.O_SEQUENTIAL 緩存優化,但不限制從磁盤中序列存取 os.O_TEXT 以文本的模式打開文件(轉換)
length:要映射文件部分的大小(以字節爲單位),這個值爲0,則映射整個文件,若是大小大於文件當前大小,則擴展這個文件。緩存
flags:MAP_PRIVATE:這段內存映射只有本進程可用;mmap.MAP_SHARED:將內存映射和其餘進程共享,全部映射了同一文件的進程,都可以看到其中一個所作的更改;
prot:mmap.PROT_READ, mmap.PROT_WRITE 和 mmap.PROT_WRITE | mmap.PROT_READ。最後一者的含義是同時可讀可寫。服務器
access:在mmap中有可選參數access的值有app
ACCESS_READ:讀訪問。ide
ACCESS_WRITE:寫訪問,默認。函數
ACCESS_COPY:拷貝訪問,不會把更改寫入到文件,使用flush把更改寫到文件。post
2,方法:mmap 對象的方法,對象m
m.close() 關閉 m 對應的文件; m.find(str, start=0) 從 start 下標開始,在 m 中從左往右尋找子串 str 最先出現的下標;
m.flush([offset, n]) 把 m 中從offset開始的n個字節刷到對應的文件中; m.move(dstoff, srcoff, n) 等於 m[dstoff:dstoff+n] = m[srcoff:srcoff+n],把從 srcoff 開始的 n 個字節複製到從 dstoff 開始的n個字節,可能會覆蓋重疊的部分。 m.read(n) 返回一個字符串,從 m 對應的文件中最多讀取 n 個字節,將會把 m 對應文件的位置指針向後移動; m.read_byte() 返回一個1字節長的字符串,從 m 對應的文件中讀1個字節,要是已經到了EOF還調用 read_byte(),則拋出異常 ValueError; m.readline() 返回一個字符串,從 m 對應文件的當前位置到下一個'\n',當調用 readline() 時文件位於 EOF,則返回空字符串; m.resize(n) ***有問題,執行不了*** 把 m 的長度改成 n,m 的長度和 m 對應文件的長度是獨立的; m.seek(pos, how=0) 同 file 對象的 seek 操做,改變 m 對應的文件的當前位置; m.size() 返回 m 對應文件的長度(不是 m 對象的長度len(m)); m.tell() 返回 m 對應文件的當前位置; m.write(str) 把 str 寫到 m 對應文件的當前位置,若是從 m 對應文件的當前位置到 m 結尾剩餘的空間不足len(str),則拋出 ValueError; m.write_byte(byte) 把1個字節(對應一個字符)寫到 m 對應文件的當前位置,實際上 m.write_byte(ch) 等於 m.write(ch)。若是 m 對應文件的當前位置在 m 的結尾,也就是 m 對應文件的當前位置到 m 結尾剩餘的空間不足1個字節,write() 拋出異常ValueError,而 write_byte() 什麼都不作。
方法的使用說明:介紹上面經常使用的方法
測試文本:test.txt,mmap對象m
-- MySQL dump 10.13 Distrib 5.6.19, for osx10.7 (x86_64) -- -- Host: localhost Database: test -- ------------------------------------------------------ -- Server version 5.6.19 /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; /*!40101 SET NAMES utf8 */; /*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; /*!40103 SET TIME_ZONE='+00:00' */; /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
①: m.close(),關閉對象
>>> import os,mmap >>> m=mmap.mmap(os.open('test.txt',os.O_RDWR),0) #創業內存映射對象, >>> m.read(10) #可使用方法 '-- MySQL d' >>> m.close() #關閉對象 >>> m.read(10) #方法不可用 Traceback (most recent call last): File "<stdin>", line 1, in <module> ValueError: mmap closed or invalid
②:m.find(str, start=0),從start的位置開始尋找第一次出現的str。
>>> m.find('SET',0) #從頭開始查找第一次出現SET的字符串 197
③:m.read(n),返回一個從 m對象文件中讀取的n個字節的字符串,將會把 m 對象的位置指針向後移動,後續讀取會繼續往下讀。
>>> m.read(10) #讀取10字節的字符串 '-- MySQL d' >>> m.read(10) #讀取上面10字節後,再日後的10字節數據 'ump 10.13 '
④:m.read_byte(),返回一個1字節長的字符串,從 m 對應的文件中讀1個字節
>>> m.read_byte() #讀取第一個字節 '-' >>> m.read_byte() #讀取第二個字節 '-' >>> m.read_byte() #讀取第三個字節 ' '
⑤:m.readline():返回一個字符串,從 m 對應文件的當前位置到下一個'\n',當調用 readline() 時文件位於 EOF,則返回空字符串
>>> m.readline() #讀取一正行 '-- MySQL dump 10.13 Distrib 5.6.19, for osx10.7 (x86_64)\n' >>> m.readline() #讀取下一正行 '--\n'
⑥:m.size():返回 m 對應文件的長度(不是 m 對象的長度len(m))
>>> m.size() #整個文件的大小 782
⑦:m.tell():返回 m 對應文件的當前光標位置
>>> m.tell() #當前光標的位置0 0 >>> m.read(10) #讀取10個字節 '-- MySQL d' >>> m.tell() #當前光標位置10 10
⑧:m.seek(pos, how=0),改變 m 對應的文件的當前位置
>>> m.seek(10) #當前光標定位到10 >>> m.tell() #讀取當前光標的位置 10 >>> m.read(10) #讀取當前光標以後的10字節內容 'ump 10.13 '
⑨:m.move(dstoff, srcoff, n):等於 m[dstoff:dstoff+n] = m[srcoff:srcoff+n],把從 srcoff 開始的 n 個字節複製到從 dstoff 開始的n個字節
>>> m[101:108] #切片101到108的值 '-------' >>> m[1:8] #切片1到8的值 '- MySQL' >>> m.move(1,101,8) #從101開始到後面的8字節(108),替換從1開始到後面的8字節(8)效果:m[1:8]=m[101:108] >>> m[1:8] #被替換後 '-------'
⑩:m.write(str):把 str 寫到 m 對應文件的當前光標位置(覆蓋對應長度),若是從 m 對應文件的當前光標位置到 m 結尾剩餘的空間不足len(str),則拋出 ValueError
>>> m.tell() #當前光標位置 0 >>> m.write('zhoujy') #寫入str,要是寫入的大小大於本來的文件,會報錯。m.write_byte(byte)不會報錯。
>>> m.tell() #寫入後光標位置
6
>>> m.seek(0) #重置,光標從頭開始
>>> m.read(10) #查看10個字節,肯定是否被修改爲功
'zhoujy---d'
⑪:m.flush():把 m 中從offset開始的n個字節刷到對應的文件中
注意:對於m的修改操做,能夠當成一個列表進行切片操做,可是對於切片操做的修改須要改爲一樣長度的字符串,不然都會報錯。如m中的10個字符串進行修改,必須改爲10個字符的長度。
3,應用說明:
1):讀文件,ACCESS_READ
①:讀取整個文件
#!/usr/bin/python # -*- encoding: utf-8 -*- import mmap import contextlib f = open('test.txt', 'r') with contextlib.closing(mmap.mmap(f.fileno(), 0,access=mmap.ACCESS_READ)) as m:
#readline須要循環才能讀取整個文件 while True: line = m.readline().strip() print line
#光標到最後位置(讀完),就退出 if m.tell()==m.size(): break
效果:
~$ python untitled.py 1 ↵ -- ZHOUJY ---dump 10.13 Distrib 5.6.19, for osx10.7 (x86_64) -- -- Host: localhost Database: test -- ------------------------------------------------------ -- Server version 5.6.19 /*!40101 ZHOUJY SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */ZHOUJY; /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; /*!40101 SET NAMES utf8 */; /*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; /*!40103 SET TIME_ZONE='+00:00' */ ZHOUJY; /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
②:逐步讀取指定字節數文件
#!/usr/bin/python # -*- encoding: utf-8 -*- import mmap import contextlib with open('test.txt', 'r') as f: with contextlib.closing(mmap.mmap(f.fileno(), 0,access=mmap.ACCESS_READ)) as m: print '讀取10個字節的字符串 :', m.read(10) print '支持切片,對讀取到的字符串進行切片操做:', m[2:10] print '讀取以前光標後的10個字符串', m.read(10)
效果:
~$ python untitled.py 讀取10個字節的字符串 : -- ZHOUJY 支持切片,對讀取到的字符串進行切片操做: ZHOUJY 讀取以前光標後的10個字符串 ---dump 1
2):查找文件,ACCESS_READ
①:從整個文件查找全部匹配的
#!/usr/bin/python # -*- encoding: utf-8 -*- import mmap import contextlib word = 'ZHOUJY' print '查找:', word f = open('test.txt', 'r') with contextlib.closing(mmap.mmap(f.fileno(), 0,access=mmap.ACCESS_READ)) as m:
#也能夠經過find(str,pos)來處理 while True: line = m.readline().strip() if line.find(word)>=0: print "結果:" print line elif m.tell()==m.size(): break else: pass
效果:
~$ python untitled.py 查找: ZHOUJY 結果: -- ZHOUJY ---dump 10.13 Distrib 5.6.19, for osx10.7 (x86_64) 結果: /*!40101 ZHOUJY SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */ZHOUJY; 結果: /*!40103 SET TIME_ZONE='+00:00' */ ZHOUJY;
②:從整個文件裏查找,找到就退出(確認究竟是否存在)
#!/usr/bin/python # -*- encoding: utf-8 -*- import mmap import contextlib word = 'ZHOUJY' print '查找:', word f = open('test.txt', 'r') with contextlib.closing(mmap.mmap(f.fileno(), 0,access=mmap.ACCESS_READ)) as m:
#不須要循環,只要找到一個就能夠了 loc = m.find(word) if loc >= 0: print loc print m[loc:loc+len(word)]
效果:
~$ python untitled.py 查找: ZHOUJY 194 ZHOUJY
③:經過正則查找,(找出40開頭的數字)
#!/usr/bin/python # -*- encoding: utf-8 -*- import mmap import re import contextlib pattern = re.compile(r'(40\d*)') with open('test.txt', 'r') as f: with contextlib.closing(mmap.mmap(f.fileno(), 0,access=mmap.ACCESS_READ)) as m: print pattern.findall(m)
效果:
~$ python untitled.py ['40101', '40101', '40101', '40101', '40103', '40103', '40014', '40014', '40101', '40111']
3):處理文本,只能等長處理(經過上面的查找方法,來替換查找出的內容),模式:ACCESS_WRITE、ACCESS_COPY
通過上面對mmap方法的介紹和使用說明,大體瞭解了mmap的特色。這裏經過對比sed的方法,來看看到底處理大文件使用哪一種方法更高效。
①:替換文本中出現一次的內容。好比想把A庫的備份文件(9G)還原到B庫,須要把裏面的USE `A`改爲USE `B`。
1> sed處理:時間消耗近105s;磁盤IO幾乎跑滿;內存幾乎沒消耗、CPU消耗10~20%之間。
1:替換文本中第一次出現的內容 ~$ date && sed -i '0,/USE `edcba`;/s//USE `ABCDE`;/' test.sql && date 2016年 11月 16日 星期三 12:04:17 CST 2016年 11月 16日 星期三 12:06:02 CST 2:替換文本中指定行的內容 ~$ date && sed -i '24s/USE `ABCDE`;/USE `edcba`;/' test.sql && date 2016年 11月 16日 星期三 12:09:05 CST 2016年 11月 16日 星期三 12:10:50 CST
IO消耗:
Device: rrqm/s wrqm/s r/s w/s rMB/s wMB/s avgrq-sz avgqu-sz await r_await w_await svctm %util sda 1.00 7.00 772.00 105.00 87.22 92.06 418.65 27.90 31.35 2.21 245.56 1.14 100.00 Device: rrqm/s wrqm/s r/s w/s rMB/s wMB/s avgrq-sz avgqu-sz await r_await w_await svctm %util sda 1.00 4.00 778.00 102.00 87.59 90.03 413.36 25.08 30.30 2.59 241.65 1.13 99.60 Device: rrqm/s wrqm/s r/s w/s rMB/s wMB/s avgrq-sz avgqu-sz await r_await w_await svctm %util sda 2.00 5.00 771.00 101.00 87.48 88.04 412.22 29.80 30.24 2.34 243.21 1.14 99.60 Device: rrqm/s wrqm/s r/s w/s rMB/s wMB/s avgrq-sz avgqu-sz await r_await w_await svctm %util sda 1.00 18.00 431.00 137.00 49.08 122.04 616.99 66.20 70.25 3.02 281.75 1.75 99.60 Device: rrqm/s wrqm/s r/s w/s rMB/s wMB/s avgrq-sz avgqu-sz await r_await w_await svctm %util sda 0.00 1.00 1.00 248.00 0.00 177.04 1456.16 105.24 416.53 24.00 418.11 4.02 100.00
2> python處理:時間消耗是毫秒級別的,幾乎是秒級別完成,該狀況比較特別:搜索的關鍵詞在大文本里比較靠前的位置,這樣處理上T的大文件也是很是快的,要是搜索的關鍵詞靠後怎會怎麼樣呢?後面會說明。
#!/usr/bin/python # -*- encoding: utf-8 -*- import mmap import contextlib import re word = 'USE `EDCBA`;' replace = 'USE `ABCDE`;' print '查找:', word print'替換:', replace f = open('test.sql', 'r+') with contextlib.closing(mmap.mmap(f.fileno(), 0,access=mmap.ACCESS_WRITE)) as m: loc = m.find(word) if loc >=0: print loc m[loc:loc + len(word)] = replace
執行:
~$ date && python mmap_python.py && date 2016年 11月 16日 星期三 12:14:19 CST 查找: USE `EDCBA`; 替換: USE `ABCDE`; 929 2016年 11月 16日 星期三 12:14:19 CST
②:替換文本中全部匹配的關鍵詞。好比想把備份文件裏的ENGINE=MYISAM改爲ENGINE=InnoDB,看看性能如何。
1> sed處理:時間消耗110s;磁盤IO幾乎跑滿(讀寫IO高);內存幾乎沒消耗、CPU消耗10~30%之間。
~$ date && sed -i 's/ENGINE=InnoDB/ENGINE=MyISAM/g' test.sql && date 2016年 11月 16日 星期三 12:19:30 CST 2016年 11月 16日 星期三 12:21:20 CST
和①中sed的執行效果差很少,其實對於處理一條仍是多條記錄,sed都是作一樣工做量的事情,至於緣由能夠看瞭解sed的工做原理說明,我的理解大體意思就是:sed是1行1行讀取(因此內存消耗很小),放入到本身設置的緩衝區裏,替換完以後再寫入(因此IO很高),處理速度受限於CPU和IO。
2> python處理:時間消耗20多秒,比sed少。由於不用重寫全部內容,只須要替換指定的內容便可,而且是在內存中處理的,因此寫IO的壓力幾乎沒有。當關鍵詞比較靠後,其讀入的數據就比較大,文件須要從磁盤讀入到內存,這時磁盤的讀IO也很高,寫IO仍是沒有。由於是虛擬內存映射文件,因此佔用的物理內存很少,雖然經過TOP看到的內存使用率%mem很高,這裏能夠不用管,由於大部分都是在SHR列裏的消耗,真正使用掉的內存能夠經過RES-SHR來計算。關於top中SHR的意思,能夠去看相關文章說明。
#!/usr/bin/python # -*- encoding: utf-8 -*- import mmap import contextlib word = 'ENGINE=MyISAM' replace = 'ENGINE=InnoDB' print '查找:', word print'替換:', replace
loc = 0 f = open('test.sql', 'r+') with contextlib.closing(mmap.mmap(f.fileno(), 0,access=mmap.ACCESS_WRITE)) as m: while True: loc = m.find(word,loc) if loc >=0: print loc m[loc:loc + len(word)] = replace #要是access=mmap.ACCESS_COPY須要執行flush #m.flush() elif loc == -1: break else: pass
效果:
~$ date && python mmap_python.py && date 2016年 11月 16日 星期三 13:19:30 CST 查找: ENGINE=MyISAM 替換: ENGINE=InnoDB 1663 5884938 11941259 12630481 12904261 64852169 64859312 65018692 65179617 65181544 65709930 149571849 3592900115 5874952354 7998151839 2016年 11月 16日 星期三 13:19:55 CST
③:正則匹配修改,這個能夠經過上面介紹的查找方法,作下修改便可,就再也不作說明。
小結:
對比sed和python處理文件的方法,這裏來小結下:對於sed無論修改的關鍵字在文本中的任意位置、次數,修改的工做量都同樣(全文的讀寫IO),差距不大;對於python mmap的修改,要是關鍵字出如今比較靠前的地方,修改起來速度很是快,不然修改也會有大量的讀IO,寫IO沒有。經過上面的對比分析來看,mmap的修改要比sed修改性能高。
Python還有另外一個讀取操做的方法:open中的read、readline、readlines,這個方法是把文件所有載入內存,再進行操做。若內存不足直接用swap或則報錯退出,內存消耗和文本大小成正比,而經過mmap模塊的方法能夠很好的避免了這個問題。
經過上面的介紹,大體知道如何使用mmap模塊了,其大體特色以下:
最後,能夠把mmap封裝起來進行使用了,腳本信息:
#!/usr/bin/python # -*- encoding: utf-8 -*- import mmap import contextlib import time from optparse import OptionParser def calc_time(func): def _deco(*args, **kwargs): begin_time = time.time() func(*args, **kwargs) cost_time = time.time() - begin_time print 'cost time: %s' % (cost_time) return _deco @calc_time def replace_keyword_all(filename,old_word,new_word): if len(old_word) == len(new_word): loc = 0 print "%s 替換成 %s " %(new_word,old_word) with open(filename,'r+') as f: with contextlib.closing(mmap.mmap(f.fileno(), 0,access=mmap.ACCESS_WRITE)) as m: while True: loc = m.find(old_word,loc) if loc >= 0: m[loc:loc+len(old_word)] = new_word elif loc == -1: break else: pass f.close() else: print "替換的詞要和被替換的詞長度一致!" exit() @calc_time def replace_keyword_once(filename,old_word,new_word): if len(old_word) == len(new_word): print "%s 替換成 %s " %(new_word,old_word) with open(filename,'r+') as f: with contextlib.closing(mmap.mmap(f.fileno(), 0,access=mmap.ACCESS_WRITE)) as m: loc = m.find(old_word) if loc >= 0: m[loc:loc+len(old_word)] = new_word f.close() else: print "替換的詞要和被替換的詞長度一致!" exit() if __name__ == "__main__": parser = OptionParser() parser.add_option("-f", "--filename", help="Filename for search", dest="filename") parser.add_option("-o", "--oldword", help="the ip to use", dest="old_word") parser.add_option("-n", "--newword", help="the ip to use", dest="new_word") (options, args) = parser.parse_args() if not options.filename: print 'params filename need to apply' exit() if not options.old_word: print 'params oldword need to apply' exit() if not options.new_word: print 'params newword need to apply' exit() # 替換文本中第一次出現的內容(查到一個就處理退出,越靠前越快) # replace_keyword_once(options.filename,options.old_word,options.new_word) # 替換文本中出現的內容(查找處理整個文本) replace_keyword_all(options.filename,options.old_word,options.new_word)
方法:
~$ python mmap_search.py -h Usage: mmap_search.py [options] Options: -h, --help show this help message and exit -f FILENAME, --filename=FILENAME Filename for search -o OLD_WORD, --oldword=OLD_WORD the ip to use -n NEW_WORD, --newword=NEW_WORD the ip to use
腳本處理效果:(40G的文本)
1)sed:替換文本中第一次出現的內容 ~$ date && sed -i '0,/USE `EDCBA`;/s//USE `ABCDE`;/' test.sql && date 2016年 11月 17日 星期四 11:15:33 CST 2016年 11月 17日 星期四 11:21:47 CST 2)mmap:替換文本中第一次出現的內容(使用replace_keyword_once方法,查到一個就處理退出,越靠前越快) ~$ python mmap_search.py --filename='test.sql' --oldword="USE \`EDCBA\`;" --newword="USE \`ABCDE\`;" USE `ABCDE`; 替換成 USE `EDCBA`; cost time: 0.000128984451294 3)sed:替換文本中出現的內容(查找處理整個文本) ~$ date && sed -i 's/ENGINE=InnoDB/ENGINE=MyISAM/g' test.sql && date 2016年 11月 17日 星期四 10:04:49 CST 2016年 11月 17日 星期四 10:11:34 CST 4)mmap:替換文本中出現的內容(使用replace_keyword_all方法,查找處理整個文本) ~$ python mmap_search.py --filename="test.sql" --oldword="ENGINE=MyISAM" --newword="ENGINE=InnoDB" ENGINE=InnoDB 替換成 ENGINE=MyISAM cost time: 198.471223116
結論:修改大文本文件,經過sed處理,無論被修改的詞在哪一個位置都須要重寫整個文件;而mmap修改文本,被修改的詞越靠前性能越好,不須要重寫整個文本,只要替換被修改詞語的長度便可。