文章目錄
本系列文章列表和翻譯進度,請移步:Node.js進階編程:用Javascript構建可伸縮應用(〇)
本文對應原文第三部分第七章:Files, Processes, Streams, and Networking:Querying, Reading from, and Writing to Files
文章是從Word複製到過來的,版面有些不一致,可以點這裡下載本文的PDF版。
第七章:查詢和讀寫檔案本章內容:
- 處理檔案路徑
- 從檔案路徑萃取資訊
- 理解檔案描述符
- 使用fs.stat()擷取檔案資訊
- 開啟,讀寫,關閉檔案
- 避免檔案描述符泄露
Node有一組資料流API,可以像處理網路流那樣處理檔案,用起來很方便,但是它只允許順序處理檔案,不能隨機讀寫檔案。因此,需要使用一些更底層的檔案系統操作。
本章覆蓋了檔案處理的基礎知識,包括如何開啟檔案,讀取檔案某一部分,寫資料,以及關閉檔案。
Node的很多檔案API幾乎是UNIX(POSIX)中對應檔案API 的翻版,比如使用檔案描述符的方式,就像UNIX裡一樣,檔案描述符在Node裡也是一個整型數字,代表一個實體在進程檔案描述符表裡的索引。
有3個特殊的檔案描述符——1,2和3。他們分別代表標準輸入,標準輸出和標準錯誤檔案描述符。標準輸入,顧名思義,是個唯讀流,進程用它來從控制台或者進程通道讀取資料。標準輸出和標準錯誤是僅用來輸出資料的檔案描述符,他們經常被用來向控制台,其它進程或檔案輸出資料。標準錯誤負責錯誤資訊輸出,而標準輸出負責普通的進程輸出。
一旦進程啟動完畢,就能使用這幾個檔案描述符了,它們其實並不存在對應的物理檔案。你不能讀寫某個隨機位置的資料,(譯者註:原文是You can write to and read from specific positions within the file.根據上下文,作者可能少寫了個“not”),只能像操作網路資料流那樣順序的讀取和輸出,已寫入的資料就不能再修改了。
普通檔案不受這種限制,比如Node裡,你即可以建立只能向尾部追加資料的檔案,還可以建立讀寫隨機位置的檔案。
幾乎所有跟檔案相關的操作都會涉及到處理檔案路徑,本章先會將介紹這些工具函數,然後再深入講解檔案讀寫和資料操作
處理檔案路徑
檔案路徑分為相對路徑和絕對路徑兩種,用它們來表示具體的檔案。你可以合并檔案路徑,可以提取檔案名稱資訊,甚至可以檢測檔案是否存在。
Node裡,可以用字串來操處理檔案路徑,但是那樣會使問題變複雜,比如你要串連路徑的不同部分,有些部分以 “/”結尾有些卻沒有,而且路徑分割符在不同作業系統裡也可能會不一樣,所以,當你串連它們時,代碼就會非常羅嗦和麻煩。
幸運的是,Node有個叫path的模組,可以幫你標準化,串連,解析路徑,從絕對路徑轉換到相對路徑,從路徑中提取各部分資訊,檢測檔案是否存在。總的來說,path模組其實只是些字串處理,而且也不會到檔案系統去做驗證(path.exists函數例外)。
路徑的標準化
在儲存或使用路徑之前將它們標準化通常是個好主意。比如,由使用者輸入或者設定檔獲得的檔案路徑,或者由兩個或多個路徑串連起來的路徑,一般都應該被標準化。可以用path模組的normalize函數來標準化一個路徑,而且它還能處理“..”,“.”“//”。比如:
var path = require('path'); path.normalize('/foo/bar//baz/asdf/quux/..'); // => '/foo/bar/baz/asdf'
串連路徑
使用path.join()函數,可以串連任意多個路徑字串,只用把所有路徑字串依次傳遞給join()函數就可以:
var path = require('path'); path.join('/foo', 'bar', 'baz/asdf', 'quux', '..'); // => '/foo/bar/baz/asdf'
如你所見,path.join()內部會自動將路徑標準化。
解析路徑
用path.resolve()可以把多個路徑解析為一個絕對路徑。它的功能就像對這些路徑挨個不斷進行“cd”操作,和cd命令的參數不同,這些路徑可以是檔案,並且它們不必真實存在——path.resolve()方法不會去訪問底層檔案系統來確定路徑是否存在,它只是一些字串操作。
比如:
var path = require('path'); path.resolve('/foo/bar', './baz'); // => /foo/bar/baz path.resolve('/foo/bar', '/tmp/file/'); // => /tmp/file
如果解析結果不是絕對路徑,path.resolve()會把當前工作目錄作為路徑附加到解析結果前面,比如:
path.resolve('wwwroot', 'static_files/png/', '../gif/image.gif'); // 如果當前工作目錄是/home/myself/node, 將返回 // => /home/myself/node/wwwroot/static_files/gif/image.gif'
計算兩個絕對路徑的相對路徑
path.relative()可以告訴你如果從一個絕對位址跳轉到另外一個絕對位址,比如:
var path = require('path'); path.relative('/data/orandea/test/aaa', '/data/orandea/impl/bbb'); // => http://www.cnblogs.com/impl/bbb
從路徑提取資料
以路徑“/foo/bar/myfile.txt”為例,如果你想擷取父目錄(/foo/bar)的所有內容,或者讀取同級目錄的其它檔案,為此,你必須用path.dirname(filePath)獲得檔案路徑的目錄部分,比如:
var path = require('path'); path.dirname('/foo/bar/baz/asdf/quux.txt'); // => /foo/bar/baz/asdf
或者,你想從檔案路徑裡得到檔案名稱,也就是檔案路徑的最後那一部分,可以使用path.basename函數:
var path = require('path'); path.basename('/foo/bar/baz/asdf/quux.html') // => quux.html
檔案路徑裡可能還包含副檔名,通常是檔案名稱中最後一個“.”字元之後的那部分字串。
path.basename還可以接受一個副檔名字串作為第二個參數,這樣返回的檔案名稱就會自動去掉副檔名,僅僅返迴文件的名稱部分:
var path = require('path'); path.basename('/foo/bar/baz/asdf/quux.html', '.html'); // => quux
要想這麼做你首先還得知道檔案的副檔名,可以用path.extname()來擷取副檔名:
var path = require('path'); path.extname('/a/b/index.html'); // => '.html' path.extname('/a/b.c/index'); // => '' path.extname('/a/b.c/.'); // => '' path.extname('/a/b.c/d.'); // => '.'
檢查路徑是否存在
目前為止,前面涉及到的路徑處理操作都跟底層檔案系統無關,只是一些字串操作。然而,有些時候你需要判斷一個檔案路徑是否存在,比如,你有時候需要判斷檔案或目錄是否存在,如果不存在的話才建立它,可以用path.exsits():
var path = require('path'); path.exists('/etc/passwd', function(exists) { console.log('exists:', exists); // => true }); path.exists('/does_not_exist', function(exists) { console.log('exists:', exists); // => false });
注意:從Node0.8版本開始,exists從path模組移到了fs模組,變成了fs.exists,除了命名空間不同,其它都沒變:
var fs = require('fs'); fs.exists('/does_not_exist', function(exists) { console.log('exists:', exists); // => false });
path.exists()是個I/O操作,因為它是非同步,因此需要一個回呼函數,當I/O操作返回後調用這個回呼函數,並把結果傳遞給它。你還可以使用它的同步版本path.existsSync(),功能完全一樣,只是它不會調用回呼函數,而是直接返回結果:
var path = require('path'); path.existsSync('/etc/passwd'); // => true
fs模組介紹
fs模組包含所有檔案查詢和處理的相關函數,用這些函數,可以查詢檔案資訊,讀寫和關閉檔案。這樣匯入fs模組:
var fs = require(‘fs’)
查詢檔案資訊
有時你可能需要知道檔案的大小,建立日期或者許可權等檔案資訊,可以使用fs.stath函數來查詢檔案或目錄的元資訊:
var fs = require('fs'); fs.stat('/etc/passwd', function(err, stats) { if (err) { throw err;} console.log(stats); });
這塊代碼片斷會有類似下面的輸出:
{ dev: 234881026,ino: 95028917,mode: 33188,nlink: 1,uid: 0,gid: 0,rdev: 0,size: 5086,blksize: 4096,blocks: 0,atime: Fri, 18 Nov 2011 22:44:47 GMT,mtime: Thu, 08 Sep 2011 23:50:04 GMT,ctime: Thu, 08 Sep 2011 23:50:04 GMT }
fs.stat()調用會將一個stats類的執行個體作為參數傳遞給它的回呼函數,可以像下面這樣使用stats執行個體:
- stats.isFile() —— 如果是個標準檔案,而不是目錄,socket,符號連結或者裝置,則返回true,否則false
- stats.isDiretory() —— 如果是目錄則返回tue,否則false
- stats.isBlockDevice() —— 如果是塊裝置則返回true,在大多數UNIX系統中塊裝置通常都在/dev目錄下
- stats.isChracterDevice() —— 如果是字元裝置返回true
- stats.isSymbolickLink() —— 如果是檔案連結返回true
- stats.isFifo() —— 如果是個FIFO(UNIX具名管道的一個特殊類型)返回true
- stats.isSocket() —— 如果是個UNIX socket(TODO:googe it)
開啟檔案
在讀取或處理檔案之前,必須先使用fs.open函數開啟檔案,然後你提供的回呼函數會被調用,並得到這個檔案的描述符,稍後你可以用這個檔案描述符來讀寫這個已經開啟的檔案:
var fs = require('fs'); fs.open('/path/to/file', 'r', function(err, fd) { // got fd file descriptor });
fs.open的第一個參數是檔案路徑,第二個參數是一些用來指示以什麼模式開啟檔案的標記,這些標記可以是r,r+,w,w+,a或者a+。下面是這些標記的說明(來自UNIX文檔的fopen頁)
- r —— 以唯讀方式開啟檔案,資料流的初始位置在檔案開始
- r+ —— 以可讀寫方式開啟檔案,資料流的初始位置在檔案開始
- w ——如果檔案存在,則將檔案長度清0,即該檔案內容會丟失。如果不存在,則嘗試建立它。資料流的初始位置在檔案開始
- w+ —— 以可讀寫方式開啟檔案,如果檔案不存在,則嘗試建立它,如果檔案存在,則將檔案長度清0,即該檔案內容會丟失。資料流的初始位置在檔案開始
- a —— 以唯寫方式開啟檔案,如果檔案不存在,則嘗試建立它,資料流的初始位置在檔案末尾,隨後的每次寫操作都會將資料追加到檔案後面。
- a+ ——以可讀寫方式開啟檔案,如果檔案不存在,則嘗試建立它,資料流的初始位置在檔案末尾,隨後的每次寫操作都會將資料追加到檔案後面。
讀檔案
一旦開啟了檔案,就可以開始讀取檔案內容,但是在開始之前,你得先建立一個緩衝區(buffer)來放置這些資料。這個緩衝區對象將會以參數形式傳遞給fs.read函數,並被fs.read填充上資料。
var fs = require('fs');fs.open('./my_file.txt', 'r', function opened(err, fd) {if (err) { throw err }var readBuffer = new Buffer(1024),bufferOffset = 0,bufferLength = readBuffer.length,filePosition = 100;fs.read(fd, readBuffer, bufferOffset, bufferLength, filePosition, function read(err, readBytes) { if (err) { throw err; } console.log('just read ' + readBytes + ' bytes'); if (readBytes > 0) { console.log(readBuffer.slice(0, readBytes)); }});});
上面代碼嘗試開啟一個檔案,當成功開啟後(調用opened函數),開始請求從檔案流第100個位元組開始讀取隨後1024個位元組的資料(第11行)。
fs.read()的最後一個參數是個回呼函數(第16行),當下面三種情況發生時,它會被調用:
如果有錯誤發生,第一個參數(err)會為回呼函數提供一個包含錯誤資訊的對象,否則這個參數為null。如果成功讀取了資料,第二個參數(readBytes)會指明被讀到緩衝區裡資料的大小,如果值是0,則表示到達了檔案末尾。
注意:一旦把緩衝區對象傳遞給fs.open(),緩衝對象的控制權就轉移給給了read命令,只有當回呼函數被調用,緩衝區對象的控制權才會回到你手裡。因此在這之前,不要讀寫或者讓其它函數調用使用這個緩衝區對象;否則,你可能會讀到不完整的資料,更糟的情況是,你可能會並發地往這個緩衝區對象裡寫資料。
寫檔案
通過傳遞給fs.write()傳遞一個包含資料的緩衝對象,來往一個已開啟的檔案裡寫資料:
var fs = require('fs');fs.open('./my_file.txt', 'a', function opened(err, fd) { if (err) { throw err; } var writeBuffer = new Buffer('writing this string'), bufferPosition = 0, bufferLength = writeBuffer.length, filePosition = null; fs.write( fd, writeBuffer, bufferPosition, bufferLength, filePosition, function wrote(err, written) { if (err) { throw err; } console.log('wrote ' + written + ' bytes'); });});
這個例子裡,第2(譯者註:原文為3)行代碼嘗試用追加模式(a)開啟一個檔案,然後第7行代碼(譯者註:原文為9)向檔案寫入資料。緩衝區對象需要附帶幾個資訊一起做為參數:
- 緩衝區的資料
- 待寫資料從緩衝區的什麼位置開始
- 待寫資料的長度
- 資料寫到檔案的哪個位置
- 當操作結束後被調用的回呼函數wrote
這個例子裡,filePostion參數為null,也就是說write函數將會把資料寫到檔案指標當前所在的位置,因為是以追加模式開啟的檔案,因此檔案指標在檔案末尾。
跟read操作一樣,千萬不要在fs.write執行過程中使用哪個傳入的緩衝區對象,一旦fs.write開始執行它就獲得了那個緩衝區對象的控制權。你只能等到回呼函數被調用後才能再重新使用它。
關閉檔案
你可能注意到了,到目前為止,本章的所有例子都沒有關閉檔案的代碼。因為它們只是些僅使用一次而且又小又簡單的例子,當Node進程結束時,作業系統會確保關閉所有檔案。
但是,在實際的應用程式中,一旦開啟一個檔案你要確保最終關閉它。要做到這一點,你需要追蹤所有那些已開啟的檔案描述符,然後在不再使用它們的時候調用fs.close(fd[,callback])來最終關閉它們。如果你不仔細的話,很容易就會遺漏某個檔案描述符。下面的例子提供了一個叫openAndWriteToSystemLog的函數,展示了如何小心的關閉檔案:
var fs = require('fs');function openAndWriteToSystemLog(writeBuffer, callback){ fs.open('./my_file', 'a', function opened(err, fd) { if (err) { return callback(err); } function notifyError(err) { fs.close(fd, function() { callback(err); }); } var bufferOffset = 0, bufferLength = writeBuffer.length, filePosition = null; fs.write( fd, writeBuffer, bufferOffset, bufferLength, filePosition, function wrote(err, written) { if (err) { return notifyError(err); } fs.close(fd, function() { callback(err); }); } ); });}openAndWriteToSystemLog( new Buffer('writing this string'), function done(err) { if (err) { console.log("error while opening and writing:", err.message); return; } console.log('All done with no errors'); });
在這兒,提供了一個叫openAndWriteToSystemLog的函數,它接受一個包含待寫資料的緩衝區對象,以及一個操作完成或者出錯後被調用的回呼函數,如果有錯誤發生,回呼函數的第一個參數會包含這個錯誤對象。
注意那個內建函式notifyError,它會關閉檔案,並報告發生的錯誤。
注意:到此為止,你知道了如何使用底層的原子操作來開啟,讀,寫和關閉檔案。然而,Node還有一組更進階的建構函式,允許你用更簡單的方式來處理檔案。
比如,你想用一種安全的方式,讓兩個或者多個write操作並發的往一個檔案裡追加資料,這時你可以使用WriteStream。
還有,如果你想讀取一個檔案的某個地區,可以考慮使用ReadStream。這兩種用例會在第九章“資料的讀,寫流”裡介紹。
小結
當你使用檔案時,多數情況下都需要處理和提取檔案路徑資訊,通過使用path模組你可以串連路徑,標準化路徑,計算路徑的差別,以及將相對路徑轉化成絕對路徑。你可以提取指定檔案路徑的副檔名,檔案名稱,目錄等路徑組件。
Node在fs模組裡提供了一套底層API來訪問檔案系統,底層API使用檔案描述符來操作檔案。你可以用fs.open開啟檔案,用fs.write寫檔案,用fs.read讀檔案,並用fs.close關閉檔案。
當有錯誤發生時,你應該總是使用正確的錯誤處理邏輯來關閉檔案——以確保在調用返回前關閉那些已開啟的檔案描述符。