大部分时候我在处理文件内容时,都是一次性将其读取到内存中,然后进行处理:
const { readFile } = require("fs").promises;
(async () => {
// 读取文件全部内容
const data = await readFile(__dirname + "/t.js", "utf-8");
// 处理数据
console.log(data);
})();
对于小文件来说这样做没什么关系。当文件比较大时,内存消耗会相应变大,而且在调试代码时,IDE也容易卡死(你可以尝试读取一个5M的文本文件,然后鼠标移到上面的data
变量上,会发现IDE(Visual Studio Code)直接死掉了)。这既不利于程序在生产环境的高效运行,也阻碍了开发调试效率。
怎么办?
可以借助stream
,将内容分块处理(一次处理一小块)。
const { createReadStream } = require("fs");
(async () => {
// 创建一个流
const stream = createReadStream(__dirname + "/t.js", "utf-8");
for await (const chunk of stream) {
// 分块处理数据
console.log(chunk);
}
})();
这样,一个大的文件会被分成多块,避免了一次读取所有内容的内存消耗。块的大写可以用参数highWaterMark
来自定义:
// 创建一个流
const stream = createReadStream(__dirname + "/t.js", {
encoding: "utf8",
highWaterMark: 8, // 一次读取多少bytes
});
以上介绍基本能处理大部分需求了,但还有一个更典型的场景:如果我们需要一行一行的处理文本数据怎么办?
依靠面上的stream
,我们每次得到的是一块数据,但并不能保证这一块数据刚好是某一行,它包含的可能是多行,或某一行的一部分。
所幸的是,Node.js有个内置模块readline
,可以用来解决上面的问题。
const { createReadStream } = require("fs");
const readline = require("readline");
(async () => {
const stream = createReadStream(__dirname + "/t.js", "utf-8");
// 创建一个 readline 接口
// 注意 crlfDelay 参数,让它支持多种换行符(CR LF ('\r\n'))
const rl = readline.createInterface({
input: stream,
crlfDelay: Infinity,
});
for await (const line of rl) {
// 文件中的每一行都会依次被 line 变更接收
console.log(`Line from file: ${line}`);
}
})();
另外,for await
相对比较慢,如果你期望得到更好的性能,Node.js官方也提供了另一种改进的方式(这里):
const { once } = require("events");
const { createReadStream } = require("fs");
const { createInterface } = require("readline");
(async function processLineByLine() {
const rl = createInterface({
input: createReadStream("big-file.txt"),
crlfDelay: Infinity,
});
rl.on("line", (line) => {
// Process the line.
});
await once(rl, "close");
console.log("File processed.");
})();
关于events.once()
的用法,可以参见我的另一篇文章: 发现一个有趣的方法:events.once。