Node.js文件操作詳解
Node有一組數(shù)據(jù)流API,可以像處理網(wǎng)絡(luò)流那樣處理文件,用起來(lái)很方便,但是它只允許順序處理文件,不能隨機(jī)讀寫(xiě)文件。因此,需要使用一些更底層的文件系統(tǒng)操作。
本章覆蓋了文件處理的基礎(chǔ)知識(shí),包括如何打開(kāi)文件,讀取文件某一部分,寫(xiě)數(shù)據(jù),以及關(guān)閉文件。
Node的很多文件API幾乎是UNIX(POSIX)中對(duì)應(yīng)文件API 的翻版,比如使用文件描述符的方式,就像UNIX里一樣,文件描述符在Node里也是一個(gè)整型數(shù)字,代表一個(gè)實(shí)體在進(jìn)程文件描述符表里的索引。
有3個(gè)特殊的文件描述符——1,2和3。他們分別代表標(biāo)準(zhǔn)輸入,標(biāo)準(zhǔn)輸出和標(biāo)準(zhǔn)錯(cuò)誤文件描述符。標(biāo)準(zhǔn)輸入,顧名思義,是個(gè)只讀流,進(jìn)程用它來(lái)從控制臺(tái)或者進(jìn)程通道讀取數(shù)據(jù)。標(biāo)準(zhǔn)輸出和標(biāo)準(zhǔn)錯(cuò)誤是僅用來(lái)輸出數(shù)據(jù)的文件描述符,他們經(jīng)常被用來(lái)向控制臺(tái),其它進(jìn)程或文件輸出數(shù)據(jù)。標(biāo)準(zhǔn)錯(cuò)誤負(fù)責(zé)錯(cuò)誤信息輸出,而標(biāo)準(zhǔn)輸出負(fù)責(zé)普通的進(jìn)程輸出。
一旦進(jìn)程啟動(dòng)完畢,就能使用這幾個(gè)文件描述符了,它們其實(shí)并不存在對(duì)應(yīng)的物理文件。你不能讀寫(xiě)某個(gè)隨機(jī)位置的數(shù)據(jù),(譯者注:原文是You can write to and read from specific positions within the file.根據(jù)上下文,作者可能少寫(xiě)了個(gè)“not”),只能像操作網(wǎng)絡(luò)數(shù)據(jù)流那樣順序的讀取和輸出,已寫(xiě)入的數(shù)據(jù)就不能再修改了。
普通文件不受這種限制,比如Node里,你即可以創(chuàng)建只能向尾部追加數(shù)據(jù)的文件,還可以創(chuàng)建讀寫(xiě)隨機(jī)位置的文件。
幾乎所有跟文件相關(guān)的操作都會(huì)涉及到處理文件路徑,本章先會(huì)將介紹這些工具函數(shù),然后再深入講解文件讀寫(xiě)和數(shù)據(jù)操作
處理文件路徑
文件路徑分為相對(duì)路徑和絕對(duì)路徑兩種,用它們來(lái)表示具體的文件。你可以合并文件路徑,可以提取文件名信息,甚至可以檢測(cè)文件是否存在。
Node里,可以用字符串來(lái)操處理文件路徑,但是那樣會(huì)使問(wèn)題變復(fù)雜,比如你要連接路徑的不同部分,有些部分以 “/”結(jié)尾有些卻沒(méi)有,而且路徑分割符在不同操作系統(tǒng)里也可能會(huì)不一樣,所以,當(dāng)你連接它們時(shí),代碼就會(huì)非常羅嗦和麻煩。
幸運(yùn)的是,Node有個(gè)叫path的模塊,可以幫你標(biāo)準(zhǔn)化,連接,解析路徑,從絕對(duì)路徑轉(zhuǎn)換到相對(duì)路徑,從路徑中提取各部分信息,檢測(cè)文件是否存在??偟膩?lái)說(shuō),path模塊其實(shí)只是些字符串處理,而且也不會(huì)到文件系統(tǒng)去做驗(yàn)證(path.exists函數(shù)例外)。
路徑的標(biāo)準(zhǔn)化
在存儲(chǔ)或使用路徑之前將它們標(biāo)準(zhǔn)化通常是個(gè)好主意。比如,由用戶輸入或者配置文件獲得的文件路徑,或者由兩個(gè)或多個(gè)路徑連接起來(lái)的路徑,一般都應(yīng)該被標(biāo)準(zhǔn)化。可以用path模塊的normalize函數(shù)來(lái)標(biāo)準(zhǔn)化一個(gè)路徑,而且它還能處理“..”,“.”“//”。比如:
var path = require('path');
path.normalize('/foo/bar//baz/asdf/quux/..');
// => '/foo/bar/baz/asdf'
連接路徑
使用path.join()函數(shù),可以連接任意多個(gè)路徑字符串,只用把所有路徑字符串依次傳遞給join()函數(shù)就可以:
var path = require('path');
path.join('/foo', 'bar', 'baz/asdf', 'quux', '..');
// => '/foo/bar/baz/asdf'
如你所見(jiàn),path.join()內(nèi)部會(huì)自動(dòng)將路徑標(biāo)準(zhǔn)化。
解析路徑
用path.resolve()可以把多個(gè)路徑解析為一個(gè)絕對(duì)路徑。它的功能就像對(duì)這些路徑挨個(gè)不斷進(jìn)行“cd”操作,和cd命令的參數(shù)不同,這些路徑可以是文件,并且它們不必真實(shí)存在——path.resolve()方法不會(huì)去訪問(wèn)底層文件系統(tǒng)來(lái)確定路徑是否存在,它只是一些字符串操作。
比如:
var path = require('path');
path.resolve('/foo/bar', './baz');
// => /foo/bar/baz
path.resolve('/foo/bar', '/tmp/file/');
// => /tmp/file
如果解析結(jié)果不是絕對(duì)路徑,path.resolve()會(huì)把當(dāng)前工作目錄作為路徑附加到解析結(jié)果前面,比如:
path.resolve('wwwroot', 'static_files/png/', '../gif/image.gif');
// 如果當(dāng)前工作目錄是/home/myself/node, 將返回
// => /home/myself/node/wwwroot/static_files/gif/image.gif'
計(jì)算兩個(gè)絕對(duì)路徑的相對(duì)路徑
path.relative()可以告訴你如果從一個(gè)絕對(duì)地址跳轉(zhuǎn)到另外一個(gè)絕對(duì)地址,比如:
var path = require('path');
path.relative('/data/orandea/test/aaa', '/data/orandea/impl/bbb');
// => ../../impl/bbb
從路徑提取數(shù)據(jù)
以路徑“/foo/bar/myfile.txt”為例,如果你想獲取父目錄(/foo/bar)的所有內(nèi)容,或者讀取同級(jí)目錄的其它文件,為此,你必須用path.dirname(filePath)獲得文件路徑的目錄部分,比如:
var path = require('path');
path.dirname('/foo/bar/baz/asdf/quux.txt');
// => /foo/bar/baz/asdf
或者,你想從文件路徑里得到文件名,也就是文件路徑的最后那一部分,可以使用path.basename函數(shù):
var path = require('path');
path.basename('/foo/bar/baz/asdf/quux.html')
// => quux.html
文件路徑里可能還包含文件擴(kuò)展名,通常是文件名中最后一個(gè)“.”字符之后的那部分字符串。
path.basename還可以接受一個(gè)擴(kuò)展名字符串作為第二個(gè)參數(shù),這樣返回的文件名就會(huì)自動(dòng)去掉擴(kuò)展名,僅僅返回文件的名稱部分:
var path = require('path');
path.basename('/foo/bar/baz/asdf/quux.html', '.html');
// => quux
要想這么做你首先還得知道文件的擴(kuò)展名,可以用path.extname()來(lái)獲取擴(kuò)展名:
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.');
// => '.'
檢查路徑是否存在
目前為止,前面涉及到的路徑處理操作都跟底層文件系統(tǒng)無(wú)關(guān),只是一些字符串操作。然而,有些時(shí)候你需要判斷一個(gè)文件路徑是否存在,比如,你有時(shí)候需要判斷文件或目錄是否存在,如果不存在的話才創(chuàng)建它,可以用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版本開(kāi)始,exists從path模塊移到了fs模塊,變成了fs.exists,除了命名空間不同,其它都沒(méi)變:
var fs = require('fs');
fs.exists('/does_not_exist', function(exists) {
console.log('exists:', exists);
// => false
});
path.exists()是個(gè)I/O操作,因?yàn)樗钱惒降?,因此需要一個(gè)回調(diào)函數(shù),當(dāng)I/O操作返回后調(diào)用這個(gè)回調(diào)函數(shù),并把結(jié)果傳遞給它。你還可以使用它的同步版本path.existsSync(),功能完全一樣,只是它不會(huì)調(diào)用回調(diào)函數(shù),而是直接返回結(jié)果:
var path = require('path');
path.existsSync('/etc/passwd');
// => true
fs模塊介紹
fs模塊包含所有文件查詢和處理的相關(guān)函數(shù),用這些函數(shù),可以查詢文件信息,讀寫(xiě)和關(guān)閉文件。這樣導(dǎo)入fs模塊:
var fs = require(‘fs')
查詢文件信息
有時(shí)你可能需要知道文件的大小,創(chuàng)建日期或者權(quán)限等文件信息,可以使用fs.stath函數(shù)來(lái)查詢文件或目錄的元信息:
var fs = require('fs');
fs.stat('/etc/passwd', function(err, stats) {
if (err) { throw err;}
console.log(stats);
});
這塊代碼片斷會(huì)有類似下面的輸出
{ 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 }
1.fs.stat()調(diào)用會(huì)將一個(gè)stats類的實(shí)例作為參數(shù)傳遞給它的回調(diào)函數(shù),可以像下面這樣使用stats實(shí)例:
2.stats.isFile() —— 如果是個(gè)標(biāo)準(zhǔn)文件,而不是目錄,socket,符號(hào)鏈接或者設(shè)備,則返回true,否則false
3.stats.isDiretory() —— 如果是目錄則返回tue,否則false
4.stats.isBlockDevice() —— 如果是塊設(shè)備則返回true,在大多數(shù)UNIX系統(tǒng)中塊設(shè)備通常都在/dev目錄下
5.stats.isChracterDevice() —— 如果是字符設(shè)備返回true
6.stats.isSymbolickLink() —— 如果是文件鏈接返回true
7.stats.isFifo() —— 如果是個(gè)FIFO(UNIX命名管道的一個(gè)特殊類型)返回true
8.stats.isSocket() —— 如果是個(gè)UNIX socket(TODO:googe it)
打開(kāi)文件
在讀取或處理文件之前,必須先使用fs.open函數(shù)打開(kāi)文件,然后你提供的回調(diào)函數(shù)會(huì)被調(diào)用,并得到這個(gè)文件的描述符,稍后你可以用這個(gè)文件描述符來(lái)讀寫(xiě)這個(gè)已經(jīng)打開(kāi)的文件:
var fs = require('fs');
fs.open('/path/to/file', 'r', function(err, fd) {
// got fd file descriptor
});
fs.open的第一個(gè)參數(shù)是文件路徑,第二個(gè)參數(shù)是一些用來(lái)指示以什么模式打開(kāi)文件的標(biāo)記,這些標(biāo)記可以是r,r+,w,w+,a或者a+。下面是這些標(biāo)記的說(shuō)明(來(lái)自UNIX文檔的fopen頁(yè))
1.r —— 以只讀方式打開(kāi)文件,數(shù)據(jù)流的初始位置在文件開(kāi)始
2.r+ —— 以可讀寫(xiě)方式打開(kāi)文件,數(shù)據(jù)流的初始位置在文件開(kāi)始
3.w ——如果文件存在,則將文件長(zhǎng)度清0,即該文件內(nèi)容會(huì)丟失。如果不存在,則嘗試創(chuàng)建它。數(shù)據(jù)流的初始位置在文件開(kāi)始
4.w+ —— 以可讀寫(xiě)方式打開(kāi)文件,如果文件不存在,則嘗試創(chuàng)建它,如果文件存在,則將文件長(zhǎng)度清0,即該文件內(nèi)容會(huì)丟失。數(shù)據(jù)流的初始位置在文件開(kāi)始
5.a —— 以只寫(xiě)方式打開(kāi)文件,如果文件不存在,則嘗試創(chuàng)建它,數(shù)據(jù)流的初始位置在文件末尾,隨后的每次寫(xiě)操作都會(huì)將數(shù)據(jù)追加到文件后面。
6.a+ ——以可讀寫(xiě)方式打開(kāi)文件,如果文件不存在,則嘗試創(chuàng)建它,數(shù)據(jù)流的初始位置在文件末尾,隨后的每次寫(xiě)操作都會(huì)將數(shù)據(jù)追加到文件后面。
讀文件
一旦打開(kāi)了文件,就可以開(kāi)始讀取文件內(nèi)容,但是在開(kāi)始之前,你得先創(chuàng)建一個(gè)緩沖區(qū)(buffer)來(lái)放置這些數(shù)據(jù)。這個(gè)緩沖區(qū)對(duì)象將會(huì)以參數(shù)形式傳遞給fs.read函數(shù),并被fs.read填充上數(shù)據(jù)。
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));
}
});
});
上面代碼嘗試打開(kāi)一個(gè)文件,當(dāng)成功打開(kāi)后(調(diào)用opened函數(shù)),開(kāi)始請(qǐng)求從文件流第100個(gè)字節(jié)開(kāi)始讀取隨后1024個(gè)字節(jié)的數(shù)據(jù)(第11行)。
fs.read()的最后一個(gè)參數(shù)是個(gè)回調(diào)函數(shù)(第16行),當(dāng)下面三種情況發(fā)生時(shí),它會(huì)被調(diào)用:
1.有錯(cuò)誤發(fā)生
2.成功讀取了數(shù)據(jù)
3.沒(méi)有數(shù)據(jù)可讀
如果有錯(cuò)誤發(fā)生,第一個(gè)參數(shù)(err)會(huì)為回調(diào)函數(shù)提供一個(gè)包含錯(cuò)誤信息的對(duì)象,否則這個(gè)參數(shù)為null。如果成功讀取了數(shù)據(jù),第二個(gè)參數(shù)(readBytes)會(huì)指明被讀到緩沖區(qū)里數(shù)據(jù)的大小,如果值是0,則表示到達(dá)了文件末尾。
注意:一旦把緩沖區(qū)對(duì)象傳遞給fs.open(),緩沖對(duì)象的控制權(quán)就轉(zhuǎn)移給給了read命令,只有當(dāng)回調(diào)函數(shù)被調(diào)用,緩沖區(qū)對(duì)象的控制權(quán)才會(huì)回到你手里。因此在這之前,不要讀寫(xiě)或者讓其它函數(shù)調(diào)用使用這個(gè)緩沖區(qū)對(duì)象;否則,你可能會(huì)讀到不完整的數(shù)據(jù),更糟的情況是,你可能會(huì)并發(fā)地往這個(gè)緩沖區(qū)對(duì)象里寫(xiě)數(shù)據(jù)。
寫(xiě)文件
通過(guò)傳遞給fs.write()傳遞一個(gè)包含數(shù)據(jù)的緩沖對(duì)象,來(lái)往一個(gè)已打開(kāi)的文件里寫(xiě)數(shù)據(jù):
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');
});
});
這個(gè)例子里,第2(譯者注:原文為3)行代碼嘗試用追加模式(a)打開(kāi)一個(gè)文件,然后第7行代碼(譯者注:原文為9)向文件寫(xiě)入數(shù)據(jù)。緩沖區(qū)對(duì)象需要附帶幾個(gè)信息一起做為參數(shù):
1.緩沖區(qū)的數(shù)據(jù)
2.待寫(xiě)數(shù)據(jù)從緩沖區(qū)的什么位置開(kāi)始
3.待寫(xiě)數(shù)據(jù)的長(zhǎng)度
4.數(shù)據(jù)寫(xiě)到文件的哪個(gè)位置
5.當(dāng)操作結(jié)束后被調(diào)用的回調(diào)函數(shù)wrote
這個(gè)例子里,filePostion參數(shù)為null,也就是說(shuō)write函數(shù)將會(huì)把數(shù)據(jù)寫(xiě)到文件指針當(dāng)前所在的位置,因?yàn)槭且宰芳幽J酱蜷_(kāi)的文件,因此文件指針在文件末尾。
跟read操作一樣,千萬(wàn)不要在fs.write執(zhí)行過(guò)程中使用哪個(gè)傳入的緩沖區(qū)對(duì)象,一旦fs.write開(kāi)始執(zhí)行它就獲得了那個(gè)緩沖區(qū)對(duì)象的控制權(quán)。你只能等到回調(diào)函數(shù)被調(diào)用后才能再重新使用它。
關(guān)閉文件
你可能注意到了,到目前為止,本章的所有例子都沒(méi)有關(guān)閉文件的代碼。因?yàn)樗鼈冎皇切﹥H使用一次而且又小又簡(jiǎn)單的例子,當(dāng)Node進(jìn)程結(jié)束時(shí),操作系統(tǒng)會(huì)確保關(guān)閉所有文件。
但是,在實(shí)際的應(yīng)用程序中,一旦打開(kāi)一個(gè)文件你要確保最終關(guān)閉它。要做到這一點(diǎn),你需要追蹤所有那些已打開(kāi)的文件描述符,然后在不再使用它們的時(shí)候調(diào)用fs.close(fd[,callback])來(lái)最終關(guān)閉它們。如果你不仔細(xì)的話,很容易就會(huì)遺漏某個(gè)文件描述符。下面的例子提供了一個(gè)叫openAndWriteToSystemLog的函數(shù),展示了如何小心的關(guān)閉文件:
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');
}
);
在這兒,提供了一個(gè)叫openAndWriteToSystemLog的函數(shù),它接受一個(gè)包含待寫(xiě)數(shù)據(jù)的緩沖區(qū)對(duì)象,以及一個(gè)操作完成或者出錯(cuò)后被調(diào)用的回調(diào)函數(shù),如果有錯(cuò)誤發(fā)生,回調(diào)函數(shù)的第一個(gè)參數(shù)會(huì)包含這個(gè)錯(cuò)誤對(duì)象。
注意那個(gè)內(nèi)部函數(shù)notifyError,它會(huì)關(guān)閉文件,并報(bào)告發(fā)生的錯(cuò)誤。
注意:到此為止,你知道了如何使用底層的原子操作來(lái)打開(kāi),讀,寫(xiě)和關(guān)閉文件。然而,Node還有一組更高級(jí)的構(gòu)造函數(shù),允許你用更簡(jiǎn)單的方式來(lái)處理文件。
比如,你想用一種安全的方式,讓兩個(gè)或者多個(gè)write操作并發(fā)的往一個(gè)文件里追加數(shù)據(jù),這時(shí)你可以使用WriteStream。
還有,如果你想讀取一個(gè)文件的某個(gè)區(qū)域,可以考慮使用ReadStream。這兩種用例會(huì)在第九章“數(shù)據(jù)的讀,寫(xiě)流”里介紹。
小結(jié)
當(dāng)你使用文件時(shí),多數(shù)情況下都需要處理和提取文件路徑信息,通過(guò)使用path模塊你可以連接路徑,標(biāo)準(zhǔn)化路徑,計(jì)算路徑的差別,以及將相對(duì)路徑轉(zhuǎn)化成絕對(duì)路徑。你可以提取指定文件路徑的擴(kuò)展名,文件名,目錄等路徑組件。
Node在fs模塊里提供了一套底層API來(lái)訪問(wèn)文件系統(tǒng),底層API使用文件描述符來(lái)操作文件。你可以用fs.open打開(kāi)文件,用fs.write寫(xiě)文件,用fs.read讀文件,并用fs.close關(guān)閉文件。
當(dāng)有錯(cuò)誤發(fā)生時(shí),你應(yīng)該總是使用正確的錯(cuò)誤處理邏輯來(lái)關(guān)閉文件——以確保在調(diào)用返回前關(guān)閉那些已打開(kāi)的文件描述符。
- Node.js本地文件操作之文件拷貝與目錄遍歷的方法
- Node.js程序中的本地文件操作用法小結(jié)
- Node.js中常規(guī)的文件操作總結(jié)
- Node.js中文件操作模塊File System的詳細(xì)介紹
- 基于node.js的fs核心模塊讀寫(xiě)文件操作(實(shí)例講解)
- Node.js文件操作方法匯總
- 詳解Node.js讀寫(xiě)中文內(nèi)容文件操作
- 從零學(xué)習(xí)node.js之文件操作(三)
- Node.js Streams文件讀寫(xiě)操作詳解
- node.js中fs文件系統(tǒng)目錄操作與文件信息操作
- node.js文件操作系統(tǒng)實(shí)例詳解
相關(guān)文章
node.js連接mongoDB數(shù)據(jù)庫(kù) 快速搭建自己的web服務(wù)
這篇文章主要為大家詳細(xì)介紹了node.js連接mongoDB數(shù)據(jù)庫(kù),如何快速搭建自己的web服務(wù),感興趣的小伙伴們可以參考一下2016-04-04Node.JS發(fā)送http請(qǐng)求批量檢查文件中的網(wǎng)頁(yè)地址、服務(wù)是否有效可用
這篇文章主要介紹了Node.JS發(fā)送http請(qǐng)求批量檢查文件中的網(wǎng)頁(yè)地址、服務(wù)是否有效可用,本文通過(guò)實(shí)例代碼文字說(shuō)明給大家講解的非常詳細(xì),需要的朋友參考下2019-11-11nodejs發(fā)送http請(qǐng)求時(shí)遇到404長(zhǎng)時(shí)間未響應(yīng)的解決方法
這篇文章主要為大家詳細(xì)介紹了nodejs發(fā)送http請(qǐng)求時(shí)遇到404長(zhǎng)時(shí)間未響應(yīng)的解決方法2017-12-12node-webkit打包成exe文件被360誤報(bào)木馬的解決方法
這篇文章主要介紹了node-webkit打包成exe文件被360誤報(bào)木馬的解決方法的相關(guān)資料,需要的朋友可以參考下2015-03-03node.js中的events.EventEmitter.listenerCount方法使用說(shuō)明
這篇文章主要介紹了node.js中的events.EventEmitter.listenerCount方法使用說(shuō)明,本文介紹了events.EventEmitter.listenerCount的方法說(shuō)明、語(yǔ)法、使用實(shí)例和實(shí)現(xiàn)源碼,需要的朋友可以參考下2014-12-12