- Python自動化運維快速入門
- 鄭征
- 6417字
- 2019-12-09 14:48:17
2.1 文本處理
在日常的運維工作中一般都離不開與文本,如日志分析、編碼轉換、ETL加工等。本節從編碼原理、文件操作、讀寫配置文件、解析XML等實用編程知識出發,希望能拋磚引玉,為讀者在處理文本問題時提供可實踐的方法。
2.1.1 Python編碼解碼
我們編寫程序處理文本的時候,不可避免地遇到各種各樣的編碼問題,如果對編碼解碼過程一知半解,遇到這類問題就會很棘手。本小節從編碼解碼的原理出發,結合Python 3代碼實例一步步揭開文本編碼的面紗,編碼解碼的原理是相通的,學會編碼解碼,對學習其他編程語言也非常有幫助。
首先我們需要明白,計算機只處理二進制數據,如果要處理文本,就需要將文本轉換為二進制數據,再由計算機進行處理。
將文本轉換為二進制數據就是編碼,將二進制數據轉換為文本就是解碼。編碼和解碼要按照一定的規則進行,這個規則就是字符集。
以常見的ASCII編碼為例,字符'a'在ASCII碼表中對應的數據是97,二進制是1100001。下面在Python中驗證一下:

由于ASCII編碼只占用一個字節,也就是二進制8位,共有28 256種可能,完全可以覆蓋英文大小寫字母及特殊符號。而我們中文漢字遠超過256個,使用ASCII編碼的一個字節來處理中文顯然是不夠用的,于是我國就制訂了支持中文的GB2312編碼,使用兩個字節,可以支持216共65536種漢字,可以覆蓋常用的中文漢字60370個(當代《漢語大字典》(2010年版)收字60370個)。
例如:漢字的“漢”。

在這里介紹幾種常見的中文編碼。
GB2312或GB2312-80是中國國家標準簡體中文字符集,共收錄6763個漢字,同時收錄了包括拉丁字母、希臘字母、日文平假名及片假名字母、俄語西里爾字母在內的682個字符。
GBK即漢字內碼擴展規范,共收入21886個漢字和圖形符號。
GB 8030與GB2312-1980和GBK兼容,共收錄漢字70244個,是一二四字節變長編碼。
由上可以看出支持的漢字范圍:GB18030 > GBK > GB2312。
對于一些生僻字,可能需要GBK或GB18030進行編碼,如“祎”。

這僅僅是適用中文文本的一個編碼,全世界有上百種語言,每種語言都設計自己獨特的編碼,這樣計算機在跨語言進行信息傳輸時還是無法溝通(出現亂碼)的,于是Unicode編碼應運而生,Unicode使用2~4個字節編碼,已經收錄136690個字符,并且還在一直不斷擴張中。把所有語言統一到一套編碼中,這套編碼就是Unicode編碼。使用Unicode編碼,無論處理什么文本都不會出現亂碼問題。Unicode編碼使用兩個字節(16位bit)表示一個字符,比較偏僻的字符需要使用4個字節。
Unicode起到以下作用。
直接支持全球所有語言,每個國家都可以不用再使用自己之前的舊編碼了,用Unicode就可以。
Unicode包含了與全球所有國家編碼的映射關系。
幾乎所有的系統、編程語言都默認支持Unicode。但是新的問題又來了,如果一段純英文文本,用Unicode編碼存儲就會比用ASCII編碼多占用一倍空間!存儲和網絡傳輸時一般數據都會非常多。為了解決上述問題,UTF編碼應運而生,UTF編碼將一個Unicode字符編碼成1~6個字節,常用的英文字母被編碼成1個字節,漢字通常是3個字節,只有很生僻的字符才會被編碼成4~6個字節。注意,從Unicode到UTF并不是直接對應的,而是通過一些算法和規則來轉換的。UTF編碼有以下三種。
UTF-8:使用1、2、3、4個字節表示所有字符,優先使用1個字節,若無法滿足,則增加一個字節,最多4個字節。英文占1個字節、歐洲語系占2個字節、東亞占3個字節,其他及特殊字符占4個字節。
UTF-16:使用2、4個字節表示所有字符,優先使用2個字節,否則使用4個字節表示。
UTF-32:使用4個字節表示所有字符。
例如:漢字的“漢”,在UTF-8字符集中3個字節。
>>> list("漢".encode("utf-8")) [230, 177, 137]
而英文無論采集哪種編碼,都是一致的。如果使用純英文編寫代碼,就基本不會遇到編碼問題。如"a"在ASCII、GBK、UTF-8中的編碼結果都是一致的。
>>> list("a".encode("ascii")) [97] >>> list("a".encode("gbk")) [97] >>> list("a".encode("utf-8")) [97]
下面結合Python代碼實例來理解編碼,如圖2.1所示。

圖2.1 代碼實例
我們使用vim編輯器編寫str_encode_decode.py,在第一行指定Python解釋器以UTF-8編碼解碼源文件,并保存為UTF-8編碼的文本文件,然后運行程序。這一編碼解碼過的程如圖2.2所示。

圖2.2 Python源代碼的編碼解碼過程
上圖中的Unicode字符串就是我們在編輯器中看到的字符串,如“我是中國人”這個字符串,在Python 3中所定義的字符串就是Unicode字符串。Unicode字符串可以編碼為任意編碼格式的字節碼,解碼時使用同一編碼解碼即可得到原來的Unicode字符串。
上述1.py的第5行將Unicode字符串內容以UTF-8的編碼方式寫入到a.txt,第9行從a.txt讀取內容并以UTF-8的編碼解碼輸出Unicode字符串,確保寫入編碼和讀取編碼一致就不會出現編碼問題。
上述代碼的運行結果如圖2.3所示。

圖2.3 運行結果
在這里順帶介紹一下Python語言的with … as …的用法。有一些任務,可能事先需要設置,事后做清理工作。對于這種場景,Python的with語句提供了一種非常方便的處理方式。一個很好的例子是文件處理,你需要獲取一個文件句柄,并從文件中讀取數據,然后關閉文件句柄。如果不用with語句,代碼如下:
1 file = open("a.txt") 2 data = file.read() 3 file.close()
這里有兩個問題:一是可能忘記關閉文件句柄;二是文件讀取數據發生異常,沒有進行任何處理。下面是處理異常的加強版本:

雖然這段代碼運行良好,但是太冗長了,這時候就是with一展身手的時候了。除了有更優雅的語法,with還可以很好地處理上下文環境產生的異常。下面是with版本的代碼:

with語句里是怎么執行的呢?Python對with的處理是非常聰明的,with所求值的對象必須有__enter__()方法和__exit__()方法,緊跟with后面的語句被求值后,調用對象的__enter__()方法,該方法的返回值將被賦值給as后面的變量。當with后面的代碼塊全部被執行之后,將調用前面返回對象的__exit__()方法來收尾。
讀者可能會有疑問,如果編寫Python程序時未指定Python解釋器以何種編碼解碼呢?答案是使用系統的默認編碼。默認編碼可以通過sys.getdefaultencoding()來查看Python解釋器會用的默認編碼,以Windows系統為例:
>>> import sys >>> sys.getdefaultencoding() 'utf-8' >>>
說明在此電腦上Python解釋器默認使用的是UTF-8編碼,如果不指定Python解釋器以何種編碼解碼,則默認以UTF-8方式解碼源文件,因此在保存源代碼文件時請確保以UTF-8編碼保存。
2.1.2 文件操作
用Python或其他語言編寫應用程序時,若想把數據永久保存下來,必須保存于硬盤中,這就涉及我們編寫應用程序來操作硬件,而應用程序是無法直接操作硬件的,需要通知操作系統,由操作系統完成復雜的硬件操作。操作系統把復雜的硬件操作封裝成簡單的接口給用戶/應用程序使用,其中文件就是操作系統提供給應用程序來操作硬盤虛擬接口,用戶或應用程序通過操作文件,可以將自己的數據永久保存下來。有了文件的概念,我們無須再考慮操作硬盤的細節,只需要關注操作文件的流程即可。
(1)打開文件,得到一個文件句柄,并賦值給一個變量。
(2)通過句柄對文件進行操作。
(3)關閉文件。
1. 普通文件操作
Python文件操作也是非常簡單,只需要一個open函數返回一個文件句柄,無須導入任何模塊。

open函數原型如下:
open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
其中:
(1)參數file是一個表示文件名稱的字符串,如果文件不在程序當前的路徑下,就需要在前面加上相對路徑或絕對路徑。
(2)參數mode是一個可選字參數,指示打開文件的方式,若不指定,則默認為以讀文本的方式打開文件。字符串及含義可參見表2-1。
表2-1 字符串及含義

默認的打開方式是'rt' (mode='rt')。Python是區分二進制方式和文本方式的,當以二進制方式打開一個文件時(mode參數后面跟'b'),返回一個未經解碼的字節對象;當以文本方式打開文件時(默認是以文本方式打開,也可以mode參數后面跟't'),返回一個按系統默認編碼或參數encoding傳入的編碼來解碼的字符串對象。
(3)buffering是一個可選的參數,buffering=0表示關閉緩沖區(僅在二進制方式打開時可用);buffering=1表示選擇行緩沖區(僅在文本方式打開時可用);buffering大于1時,其值代表固定大小的塊緩沖區的大小。當不指定該參數時,默認的緩沖策略是這樣的:二進制文件使用固定大小的塊緩沖區,文本文件使用行緩沖區。
【示例2-1】先來看一個例子。

將上述代碼保存為read_write_file.py,運行結果如圖2.4所示。

圖2.4 運行結果
從上面的例子可以看出,以二進制讀取文件時,讀取的是文件字符串的編碼(以encoding指定的編碼格式進行的編碼),將讀取的字節對象解碼,可得出原字符串。
請注意以下幾點:
(1)記得使用完畢后及時關閉文件,釋放資源。打開一個文件包含兩部分資源:操作系統級打開的文件+應用程序的變量。在操作完畢一個文件時,必須把與該文件的這兩部分資源一個不落地回收,回收操作系統級打開的文件,如f.close(),回收應用程序級的變量,如del f。其中del f一定要發生在f.close()之后,否則就會導致操作系統打開的文件還沒有關閉,白白占用資源,而Python自動的垃圾回收機制決定了我們無須考慮del f,這就要求我們,在操作完畢文件后,一定要記住f.close()。剛開始的時候很容易忘記使用f.close()方法去關閉,推薦傻瓜式操作方式:使用with關鍵字來幫我們管理上下文,系統會自動為我們關閉文件和處理異常,如下面兩行代碼即可完成安全的寫操作。
with open('a.txt','w') as f: f.write(“hello word”)
(2)open()函數是由操作系統打開文件,如果我們沒有為open指定編碼,那么打開文件的默認編碼很明顯是操作系統默認的編碼:在Windows下是gbk,在Linux下是utf-8。若要保證不亂碼,就必須讓讀取文件和寫入文件使用的編碼一致。
常見的文件操作方法可參見表2-2。
表2-2 常見的文件操作方法

讀取文件內位置的定位方法:
(1)通過read方法傳輸參數,如read(3),當文件打開方式為文本模式時,代表讀取3個字符,當文件打開方式為二進制模式時,代表讀取3個字節。
(2)以字節為單位定位,如seek、tell等方法。其中seek有3種移動方式:0、1、2,其中1和2必須在二進制模式下進行,但無論哪種模式,都是以bytes為單位移動的。f.tell()返回文件對象當前所處的位置,它是從文件開頭開始算起的字節數。如果要改變文件當前的位置,可以使用f.seek(offset, from_what)函數。from_what如果是0,則表示開頭;如果是1,則表示當前位置;如果是2,則表示文件的結尾。例如:
seek(x,0)表示從起始位置即文件首行首字符開始移動x個字符;
seek(x,1)表示從當前位置向后移動x個字符;
seek(-x,2)表示從文件的結尾向前移動x個字符。
【示例2-2】在文件中定位。
>>> f = open("tmp.txt", "rb+") >>> f.write(b"abcdefghi") 9 >>> f.seek(5) # 移動到文件的第六個字節 5 >>> print(f.read(1)) b'f' >>> f.seek(-3, 2) # 移動到文件的倒數第三個字節 6 >>> print(f.read(1)) b'g'
【示例2-3】基于seek實現類似Linux命令tail -f的功能(文件名為lx_tailf.py)。

當tmp.txt追加新的內容時,新內容會被程序立即打印出來。
2. 大文件的讀取
當文件較小時,我們可以一次性全部讀入內存,對文件的內容做出任意修改,再保存至磁盤,這一過程會非常快。
【示例2-4】如下代碼將文件a.txt中的字符串str1替換為str2。

當文件很大時,如GB級的文本文件,上面的代碼運行將會非常緩慢,此時我們需要使用文件的可迭代方式將文件的內容逐行讀入內存,再逐行寫入新文件,最后用新文件覆蓋源文件。
【示例2-5】對大文件進行讀寫。

本示例中的大文件為a.txt,當我們打開文件時,會得到一個可迭代對象read_f,對可迭代對象進行逐行讀取,可防止內存溢出,也會加快處理速度。
處理大數據還有多種方法,如下:
(1)通過read(size)增加參數,指定讀取的字節數。

(2)通過readline,每次只讀一行。

file對象常用的函數參見表2-3。
表2-3 file對象常用的函數

3. 序列化和反序列化
什么是序列化和反序列化呢?我們可以這樣簡單地理解:
序列化:將數據結構或對象轉換成二進制串的過程。
反序列化:將在序列化過程中所生成的二進制串轉換成數據結構或對象的過程。
Python的pickle模塊實現了基本的數據序列和反序列化。通過pickle模塊的序列化操作,我們能夠將程序中運行的對象信息保存到文件中并永久存儲。通過pickle模塊的反序列化操作,我們能夠從文件中創建上一次程序保存的對象。
基本方法如下:
pickle.dump(obj, file, [,protocol])
該方法實現序列化,將對象obj保存至文件中。
x=pickle.load(file)
該方法實現反序列化,從文件中恢復對象,并將其重構為原來的Python對象。
注解:從file中讀取一個字符串,并將其重構為原來的Python對象。
【示例2-6】序列化實例(example_serialize.py)。

上述代碼將不同的Python對象依次寫入文件,并打印對象的相關信息,運行結果如圖2.5所示。

圖2.5 運行結果
【示例2-7】反序列化演示(example_deserialization.py)。

上述代碼從文件中依次恢復序列化對象,并打印對象的相關信息,運行結果如圖2.6所示。

圖2.6 運行結果
可以看出運行結果與序列化實例運行的結果完全一致。
2.1.3 讀寫配置文件
配置文件是供程序運行時讀取配置信息的文件,用于將配置信息與程序分離,這樣做的好處是顯而易見的:例如在開源社區貢獻自己源代碼時,將一些敏感信息通過配置文件讀取;提交源代碼時不提交配置文件可以避免自己的用戶名、密碼等敏感信息泄露;我們可以通過配置文件保存程序運行時的中間結果;將環境信息(如操作系統類型)寫入配置文件會增加程序的兼容性,使程序變得更加通用。
Python內置的配置文件解析器模塊configparser提供ConfigParser類來解析基本的配置文件,我們可以使用它來編寫Python程序,讓用戶最終通過配置文件輕松定制自己需要的Python應用程序。
常見的pip配置文件如下。
[global] index-url = https://pypi.doubanio.com/simple trusted-host = pypi.doubanio.com
【示例2-8】現在我們編寫一個程序來讀取配置文件的信息(read_conf.py)。

上述代碼通過實例化ConfigParser類讀取配置文件,遍歷配置文件中的section信息及其鍵值信息,通過索引獲取值信息。在命令窗口執行python read_conf.py得到如圖2.7所示的運行結果。

圖2.7 運行結果圖
【示例2-9】將相關信息寫入配置文件(write_conf.py)。

上述write_conf.py通過實例化ConfigParser類增加相關配置信息,最后寫入配置文件。執行python write_conf.py,運行結果如圖2.8所示。

圖2.8 運行結果
從上面讀寫配置文件的例子可以看出,configparser模塊的接口非常直接、明確。請注意以下幾點:
section名稱是區分大小寫的。
section下的鍵值對中鍵是不區分大小寫的,config["bitbucket.org"]["User"]在寫入時會統一變成小寫user保存在文件中。
section下的鍵值對中的值是不區分類型的,都是字符串,具體使用時需要轉換成需要的數據類型,如int(config['topsecret.server.com'][ 'port']),值為整數50022。對于一些不方便轉換的,解析器提供了一些常用的方法,如getboolean()、getint()、getfloat()等,如config["DEFAULT"].getboolean('Compression'))的類型為bool,值為True。用戶可以注冊自己的轉換器或定制提供的轉換方法。
section的名稱是[DEFAULT]時,其他section的鍵值會繼承[DEFAULT]的鍵值信息。如本例中config["bitbucket.org"]['ServerAliveInterval'])的值是45。
2.1.4 解析XML文件
XML的全稱是eXtensible Markup Language,意為可擴展的標記語言,是一種用于標記電子文件使其具有結構性的標記語言。以XML結構存儲數據的文件就是XML文件,它被設計用來傳輸和存儲數據。例如有以下內容的xml文件:
<note> <to>George</to> <from>John</from> <heading>Reminder</heading> <body>Don't forget the meeting!</body> </note>
其內容表示一份便簽,來自John,發送給George,標題是Reminder,正文是Don't forget the meeting!。XML本身并沒有定義note、to、from等標簽,是生成xml文件時自定義的,但我們仍能理解其含義。XML文檔仍然沒有做任何事情,它僅僅是包裝在XML標簽中的純粹信息。我們編寫程序來獲取文檔結構信息就是解析XML文件。
Python有三種方法解析XML:SAX、DOM、ElementTre。
1. SAX(simple API for XML )
SAX是一種基于事件驅動的API,使用時涉及兩個部分,即解析器和事件處理器。解析器負責讀取XML文件,并向事件處理器發送相應的事件(如元素開始事件、元素結束事件)。事件處理器對相應的事件做出響應,對數據做出處理。使用方法是先創建一個新的XMLReader對象,然后設置XMLReader的事件處理器ContentHandler,最后執行XMLReader的parse()方法。
創建一個新的XMLReader對象,parser_list是可選參數,是解析器列表xml.sax.make_parser( [parser_list] )。
自定義事件處理器,繼承ContentHandler類,該類的方法可參見表2-4。
表2-4 ContentHandler類的方法

執行XMLReader的parse()方法:
xml.sax.parse( xmlfile, contenthandler[, errorhandler])
參數說明:
xmlstring:xml字符串。
contenthandler:必須是一個ContentHandler的對象。
errorhandler:如果指定該參數,errorhandler必須是一個SAX ErrorHandler對象。
【示例2-10】下面來看一個解析XML的例子。example.xml內容如下:

read_xml.py內容如下:



代碼說明:read_xml.py自定義一個MenuHandler,繼承自xml.sax.ContentHandler,使用ContentHandler的方法來處理相應的標簽。在主程序入口先獲取一個XMLReader對象,并設置其事件處理器為自定義的MenuHandler,最后調用parse方法來解析example.xml。運行結果如圖2.9所示。

圖2.9 運行結果
SAX用事件驅動模型,通過在解析XML的過程中觸發一個個的事件并調用用戶定義的回調函數來處理XML文件,一次處理一個標簽,無須事先全部讀取整個XML文檔,處理效率較高。其適用場景如下:
對大型文件進行處理。
只需要文件的部分內容,或者只須從文件中得到特定信息。
想建立自己的對象模型時。
2. DOM(Document Object Model)
文件對象模型(Document Object Model,DOM)是W3C組織推薦的處理可擴展置標語言的標準編程接口。一個DOM的解析器在解析一個XML文檔時,一次性讀取整個文檔,把文檔中的所有元素保存在內存中一個樹結構里,之后可以利用DOM提供的不同函數來讀取或修改文檔的內容和結構,也可以把修改過的內容寫入xml文件。
【示例2-11】使用xml.dom.minidom解析xml文件。
dom_xml.py內容如下:

代碼說明:代碼使用minidom解析器打開XML文檔,使用getElementsByTagName方法獲取所有標簽并遍歷子標簽,邏輯上比SAX要直觀,運行結果如圖2.10所示,與SAX運行結果一致。

圖2.10 運行結果
3. ElementTre
ElementTre將XML數據在內存中解析成樹,通過樹來操作XML。
【示例2-12】ElementTre解析XML。
ElementTre_xml.py內容如下:

代碼相當簡潔,運行結果如圖2.11所示。

圖2.11 運行結果
- Manga Studio Ex 5 Cookbook
- 程序員面試筆試寶典
- Visual Basic編程:從基礎到實踐(第2版)
- Linux命令行與shell腳本編程大全(第4版)
- Windows Server 2016 Automation with PowerShell Cookbook(Second Edition)
- 小學生C++創意編程(視頻教學版)
- R Data Analysis Cookbook(Second Edition)
- Angular開發入門與實戰
- Cocos2d-x by Example:Beginner's Guide(Second Edition)
- PyQt編程快速上手
- C語言程序設計
- 現代CPU性能分析與優化
- Scrapy網絡爬蟲實戰
- Python應用與實戰
- Java從入門到精通(視頻實戰版)