檔案I/O是寫程式時常使用的功能,例如,寫個文字編輯器一定要會讀寫文字檔案, 寫影像處理程式就要會讀寫影像檔,甚至是一般的小程式,也時常使用檔案來儲存程式的設定值。這一篇文章會簡單的說明Tcl裡操作檔案的方法。
11.1 讀取檔案
一般來說操作檔案需要經過3個步驟:- 開啟檔案。
- 讀取或寫入檔案。
- 關閉檔案。
在Tcl裡你可以使用open命令來開啟指定路徑的檔案,它的語法如下:
open fileName access
open命令開啟fileName指定的檔案路徑,如果成功的開啟檔案open命令會回傳一個channelId(通道代碼),往後的檔案讀寫操作都要以這個channelId為依據。channelId的觀念就像是開啟一個通往檔案的通道,你可以透過通道去讀取或寫入檔案。第2個參數access可以用來指定開啟檔案時通道的模式,以下是access可以指定的值:
r | 以「讀取」模式開啟檔案。在這種模式下檔案一定要預先存在,而且只能對檔案作讀取的操作,而不能做寫入的操作。 |
r+ | 以「讀取」+「寫入」模式開啟檔案。在這種模式下檔案一定要預先存在,但可以允許對檔案做讀取及寫入的操作。 |
w | 以「寫入」模式開啟檔案。在這種模式下如果檔案已存在,它的內容自動被清空,如果檔案不存在則會自動建立新檔案,而對於被開啟的檔案只能對檔案作寫入的操作,而不能做讀取的操作。 |
w+ | 以「寫入」+「讀取」模式開啟檔案。在這種模式下如果檔案已存在,它內容自動被清空,如果檔案不存在會自動建立新檔案,這種模式可以允許對檔案做讀取及寫入的操作。 |
a | 以「附加」模式開啟檔案。在這種模式下如果檔案已存在,所有輸出到檔案的內容會被串接在尾巴,如果檔案不存在則會自動建立新檔案,而對於被開啟的檔案只能對檔案作寫入的操作,而不能做讀取的操作。 |
a+ | 以「附加」+「讀取」模式開啟檔案。在這種模式下如果檔案已存在,所有輸出到檔案的內容都會被串接在尾巴,如果檔案不存在則會自動建立新檔案,這種模式可以允許對檔案做讀取及寫入的操作。 |
以下是一個讀取檔案的範例:
這個例子是一個非常典型的讀檔程式,程式的第1行使用讀取模式開啟/Users/dai/a.txt這個檔案,當然這個檔案要預先存在不然程式會發生錯誤,如果開啟成功,檔案的channelId會被儲存在fd變數裡。第2行使用read命令從指定的channelId讀取所有的檔案內容,讀取到的內容最後會被儲存在data變數裡頭。第3行很單純的把data變數的內容輸出到螢幕上,然後最後一行使用close命令把不再使用的channelId關起來,以免佔用系統的資源。
§ 關於開檔案
在開啟檔案時你可以指定絕對路徑或相對路徑,並不一定只能用絕對路徑。另外,一個程式同時間內可以開啟的檔案數量是有限制的,所以檔案使用完畢後請立刻把它關上。
如果檔案的內容不是很大,上面的例子真的是一個不錯的方法,因為整個程式真的很簡單,但如果檔案的內容非常的大,上面的例子就不是一個好方法了,試想如果a.txt有2G元位組這麼大,那麼data變數將會佔用非常大的記憶體空間。下面是一個改進的方法,它可以讓我們一次讀4K個字元的資料進來處理,而不是一次讀整個檔案的內容進來。
對於檔案操作大部份的程式語言都有一個特性,就是如果你這次由檔案中讀取或寫入了N個位元組的資料,除非你有特別指定,否則下一次的讀寫的位置將從第N+1個位元組開始,以此類推如果再讀了N個位元組,那再下次就是由2N+1的位置開始。寫程式的人只要了解這一點再配合迴圈,就可以很方便的讀取整個檔案的內容。
程式第2行中的eof命令可以用來判斷指定的channelId是否已經讀到檔案的結尾,如果是的話eof命令回傳1,否則會回傳0,所以while條件式的意義是「如果檔案還沒讀到結尾就執行後面的程式區塊」。
第3行我們在read命令的最後面加上了4096用來指定一次讀取4096個字元,當然你也可以指定別的數值來設定一次讀取的字元數。第4行很單純的把讀取的資料輸出。最後一行把channelId關起來,以免佔用系統資源。
除了一次讀取N個字的方法外,Tcl還提供了一次讀一行的功能,它也可以與迴圈配合來讀取整個檔案。如下是程式範例:
這個程式和前一個程式只有第3行不同,gets可以由指定的channelId一次讀入一行,然後把讀到的資料儲存在後面的data變數裡。在使用上gets命令會自動判別檔案的斷行字元,所以不管在Windows、Linux或Mac OSX下都可以正常運作。注意!! gets會把斷行字元自動去除,所以讀到的資料將不會包含換行字元。
§ 關於gets命令
一般的情況下gets命令只適用在讀取純文字檔案,對於包含binary資料的檔案並不適用,原因是binary檔案裡的換行字元其目的可能並不是真的要換行,而可能是有意義的資料內容。
11.2 寫入檔案
寫入檔案不像讀取檔案有好多種方式,Tcl裡寫入檔案的方法只有一種,而且它使用的命令就是我們之前使用過的puts命令,以下是寫入檔案的範例:程式的第1行以寫入模式開啟/Users/dai/b.txt這個檔案,如果檔案存在的話,檔案裡的內容會被清空,如果檔案不存在的話會自動建立。第2行用puts命令把指定的字串輸出到指定的channelId裡(即$fd的值),如同之前puts的用法,輸出到檔案的內容會自動加上換行,若不想要在輸出時自動加上換行,可以像第3行一樣指定-nonewline參數。第5行把channelId關起來,以免佔用系統資源。
程式執行完b.txt的檔案內容如下:
這是第1行,而且會自動斷行 這是第2行,但不會自動斷行,這會接在尾巴。
如果你不想要把已存在的檔案清空,又想要把新的資料寫入檔案,這時可以使用「附加」的模式開啟檔案。在附加模式裡所有新寫入的資料預設都會串接在檔案的尾巴。以下是一個簡單的例子。
11.3 檔案指標
上面的章節裡有提到,大部份的程式語言在檔案操作上都有一個特性,就是如果你這次由檔案中讀取或寫入了N個位元組的資料,除非你有特別指定否則下一次的讀取或寫入將從檔案中的N+1個位元組開始操作。事實上大部份的情況下我們都是使用這樣的特性在讀寫檔案,但如果你真的想要自己指定檔案讀寫的位置,那你可以使用下面的命令:seek channelId offset ?origin?
seek 命令用來設定檔案的讀寫位置。offset是您要移動的偏移量,偏移量必需為整數,origin是您要移動的參考點,您可以指定下列的參考點:
start | 從檔案的開頭,offset必需是正整數。 |
current | 從目前的讀寫位置,若offset的值是負整數表示由目前位置向前移動。 |
end | 從檔案的結尾,offset必需是負整數,用來表示由檔案的尾巴向前移動。 |
若不指定origin參數,那它的預設值是start。注意!!seek預設是以字元為單位移動哦。
讀取檔案兩次的範例:
讀取檔案最後10個位元組的範例:
上面程式的第3行,把檔案的讀寫位置移到「檔尾的前10個位元組」,然後在第4行用read命令讀取10個位元組,如此檔案最後的10個位元組就可以讀進data變數了。注意哦!! 這個程式的重點在示範如何移動讀寫位置,所以data裡的內容不太有意義。
這個程式還有一個重點是在第2行,因為jpg是一種binary格式的檔案內容,它沒辦法當成一般的純文字檔案來處理。所以我們需要把$fd的通道設定為binary模式,為什麼要這麼做呢? 讓我們用一個例子來說明,現在假設a.txt是一個純文字檔案,然後你用下面的程式去讀取a.txt:
請問read命令讀進了多少位元組? 其實這個問題是無解的,因為read是以字元為單位來讀取檔案,也就是說在UTF-8的環境下,讀入一個字可能會有3個位元組,在其它的環境可能又是另一種狀況,也就是說read讀了多少位元組的資料是要看情況而定的。
看了上面的問題後,現在讓我再問一個問題「請問對於jpg這種圖檔read命令要怎麼正確的一次讀入1個字元?」。可想而知的圖檔資料並不是純文字資料,以字元為單位讀取肯定會完蛋對吧!! 為了解決這個問題所以我們要在讀取資料之前,先把通道設定為binary模式,這樣可以讓通道的操作變成以位元組為單位了,如此一來資料的讀取就不會發生錯誤了。
最後Tcl也有提供讓你取得目前讀寫位置的命令,語法如下:
tell channelId
tell命令用來取得指定channelId目前的讀寫位置。
11.4 binary檔案操作
前面的幾個小節介紹了檔案讀寫的操作,其實上面介紹的方法只適用在純文字檔案,換句話說如果你想要操作binary檔案的話,上面的程式就不適用了。對於binary檔案的讀寫,Tcl希望你手動去設定channelId讓它可以去操作binary的檔案內容,例如下面是一個複製jpg檔案的程式,請注意到第2及第6行,如果你要讀寫的檔案包含了binary資料,請在開始讀取及寫入資料前使用fconfigure命令的-translation參數及-encoding參數指定channelId為binary模式。這個程式假設01.jpg是很小的檔案,如果你要複製較大的檔案,可以參考下面的程式:
這個程式配合迴圈由來源的檔案一次讀入4k位元組的資料,然後逐一寫入目的檔案。
§ 關於binary檔案
如果你不知道怎麼判別檔案內容是不是屬於binary的資料。一個簡單方法是,用記事本打開檔案,如果檔案內容看起來是一堆亂碼那就應該是binary的資料了。
11.5 字元編碼
Tcl對字元編碼有很好的支援,而且Tcl的內部預設是以UTF-8編碼系統來處理資料。Tcl為了讓內部的資料有統一的編碼方法,所以當你在讀取或是寫入檔案之前,Tcl會自動幫你做字元編碼轉換工作,而轉換的規則是依據目前作業系統預設的編碼方式然後轉入或轉出為UTF-8編碼。舉例來說:假設你目前的作業系統是採用BIG5的編碼方式,那麼你在讀取檔案內容時,Tcl會假設檔案的內容是BIG5的編碼,然後把檔案的內容通通做BIG5轉UTF-8的的動作。另外在寫入檔案時Tcl則會幫你把要寫入的資料由UTF-8轉回BIG5再儲存到檔案裡。一般的情況下Tcl的自動編碼轉換都不會有問題,除非你是在目前的編碼系統下使用另一種編碼系統。例如:目前的作業系統預設是用UTF-8編碼,但你要操作的檔案內容是BIG5編碼。在這種情況下編碼的轉換就會出錯,因為Tcl會把BIG5的檔案內容當作是UTF-8的檔案內容,然後執行轉換的動作。若要處理好這個問題,我們就需要手動指定編碼轉換方法。以下是一個在UTF-8環境下讀取BIG5檔案的範例:
這個程式的重點在第2行,我們使用fconfigure的-encoding參數來指定channelId的編碼將採用BIG5。在上面的例子裡data變數儲存的內容將是BIG5轉換為UTF-8後的結果,簡單的說data的內容是UTF-8編碼的資料。
反相思考,如果你想要寫一個轉換檔案編碼的程式,例如:BIG5轉UTF-8則可以這麼寫:
程式先以BIG5編碼的方式讀取index.html檔案,並把資料儲存在data變數裡,然後再以UTF-8的編碼方式把data變數的內容寫入檔案,所以檔案的內容最後就變為UTF-8的資料了。
按右上方的「#」號切換側邊欄