一个叫木头,一个叫马尾

Node.js一行一行地读取文件

大部分时候我在处理文件内容时,都是一次性将其读取到内存中,然后进行处理:

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",
  highWaterMark8// 一次读取多少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,
    crlfDelayInfinity,
  });

  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"),
    crlfDelayInfinity,
  });

  rl.on("line", (line) => {
    // Process the line.
  });

  await once(rl, "close");

  console.log("File processed.");
})();

关于events.once()的用法,可以参见我的另一篇文章: 发现一个有趣的方法:events.once