在node.js中逐行读取文件?

757

我正在尝试一次读取一个大文件的一行。我在Quora上找到了一个问题,其中涉及这个主题,但我缺少一些联系来使整个过程完美地契合在一起。

 var Lazy=require("lazy");
 new Lazy(process.stdin)
     .lines
     .forEach(
          function(line) { 
              console.log(line.toString()); 
          }
 );
 process.stdin.resume();

我想要弄清楚的是如何从文件中一次读取一行,而不是像这个示例中从标准输入读取。

我尝试过:

 fs.open('./VeryBigFile.csv', 'r', '0666', Process);

 function Process(err, fd) {
    if (err) throw err;
    // DO lazy read 
 }

但它没有起作用。 我知道在紧急情况下我可以退回使用类似PHP的东西,但我想搞清楚这个问题。

我认为其他答案行不通,因为文件比我正在运行的服务器具有更多的内存。


3
仅使用低级别的 fs.readSync() 会发现处理起来非常困难。你可以将二进制八位字节读入缓冲区,但是在将其转化为 JavaScript 字符串并扫描 EOLs(行结束符)之前,没有简单的方法来处理部分 UTF-8 或 UTF-16 字符。Buffer() 类型没有如本地字符串一样丰富的函数集来操作其实例,但是本地字符串无法包含二进制数据。在我看来,缺少从任意文件句柄读取文本行的内置方式是 node.js 中的一个真正差距。 - hippietrail
5
使用这种方法读入的空行将被转换为一行只包含一个0(0的实际字符代码)的行。我不得不在这里添加一行代码:if (line.length==1 && line[0] == 48) special(line);,以处理这种情况。 - Thabo
2
一个人也可以使用“逐行”包,它可以完美地完成工作。 - Patrice
1
请更新问题,说明解决方案是使用转换流 - Gabriel Llamas
2
@DanDascalescu 如果你愿意,可以将此添加到列表中:你的示例在 node 的 API 文档中略有修改,详情请见 https://github.com/nodejs/node/pull/4609。 - eljefedelrodeodeljefe
显示剩余4条评论
30个回答

1108
自从Node.js v0.12版本以及Node.js v4.0.0版本,已经有一个稳定的readline核心模块。以下是从文件中读取行的最简单方法,无需使用任何外部模块:
const fs = require('fs');
const readline = require('readline');

async function processLineByLine() {
  const fileStream = fs.createReadStream('input.txt');

  const rl = readline.createInterface({
    input: fileStream,
    crlfDelay: Infinity
  });
  // Note: we use the crlfDelay option to recognize all instances of CR LF
  // ('\r\n') in input.txt as a single line break.

  for await (const line of rl) {
    // Each line in input.txt will be successively available here as `line`.
    console.log(`Line from file: ${line}`);
  }
}

processLineByLine();

或者,另一种选择是:

var lineReader = require('readline').createInterface({
  input: require('fs').createReadStream('file.in')
});

lineReader.on('line', function (line) {
  console.log('Line from file:', line);
});

lineReader.on('close', function () {
    console.log('all done, son');
});

即使没有最后的\n,最后一行也会被正确读取(截至Node v0.12或更高版本)。

更新:此示例已被添加到Node的API官方文档中。


8
在createInterface定义中需要加入"terminal:false"。 - glasspill
75
如何确定最后一行?通过捕获“close”事件:rl.on('close', cb) - Green
40
Readline的作用类似于GNU Readline并非逐行读取文件。使用它来逐行读取文件存在几个注意事项,这不是最佳实践。 - Nakedible
11
@Nakedible:有趣。您能否发布一种更好的方法的答案? - Dan Dascalescu
10
我认为 https://github.com/jahewson/node-byline 是最好的逐行读取实现,但不同人意见可能会有所不同。 - Nakedible
显示剩余17条评论

177

对于这样一个简单的操作,不应该依赖第三方模块。要轻松自如。

var fs = require('fs'),
    readline = require('readline');

var rd = readline.createInterface({
    input: fs.createReadStream('/path/to/file'),
    output: process.stdout,
    console: false
});

rd.on('line', function(line) {
    console.log(line);
});

rd.on('close', function() {
    console.log('all done, son');
});

40
遗憾的是,这个吸引人的解决方案并不能正常工作——line事件仅在命中\n后发生,也就是说,所有的替代方案都被忽略了(请参见http://www.unicode.org/reports/tr18/#Line_Boundaries)。其次,最后一个`\n`之后的数据会被默默地忽略掉(请参见https://dev59.com/P3bZa4cB1Zd3GeqPCCDG)。我会称这种解决方案为*危险的*,因为它可以适用于99%的文件和数据,但对于剩下的1%则**默默失败**。每当您执行`fs.writeFileSync(path, lines.join('\n'))`操作时,您已经写入了一个只能被上述解决方案部分读取的文件。 - flow
4
这个解决方案存在问题。如果你使用"your.js <lines.txt"命令,最后一行将无法显示,当然,前提是这最后一行没有以'\n'结尾。 - zag2art
1
“readline”包对于有经验的Unix/Linux程序员来说,其行为方式确实非常奇怪。 - Pointy
13
rd.on("close", ..); 可以用作回调函数(当所有行都被读取时触发)。 - Luca Steeb
7
“最后一个 \n 后面的数据”问题似乎在我的 Node 版本(0.12.7)中得到了解决。因此,我更喜欢这个答案,它看起来最简单和最优雅。 - Myk Melez
显示剩余2条评论

70

2019年更新

官方Nodejs文档已经发布了一个很棒的示例。在这里

这需要您的计算机安装最新的Nodejs,版本号>11.4。

const fs = require('fs');
const readline = require('readline');

async function processLineByLine() {
  const fileStream = fs.createReadStream('input.txt');

  const rl = readline.createInterface({
    input: fileStream,
    crlfDelay: Infinity
  });
  // Note: we use the crlfDelay option to recognize all instances of CR LF
  // ('\r\n') in input.txt as a single line break.

  for await (const line of rl) {
    // Each line in input.txt will be successively available here as `line`.
    console.log(`Line from file: ${line}`);
  }
}

processLineByLine();

3
这个答案比上面任何一个都好,因为它采用了基于 Promise 的行为方式,明显地指示了 EOF。 - phil294
谢谢,太好了。 - Goran Stoyanov
13
也许这对别人来说很明显,但我调试了一段时间才发现:如果在 createInterface() 调用和 for await 循环开始之间有任何 await,你会神秘地失去文件开头的一些行。 createInterface() 立即在后台开始发出行,并且使用 const line of rl 隐式创建的异步迭代器无法在创建之前开始监听那些行。 - andrewdotn

61

您不需要打开文件,而是需要创建一个ReadStream

fs.createReadStream

然后将该流传递给Lazy


2
Lazy 是否有类似于结束事件的东西?当所有行都被读取完毕时? - Max
1
@Max,尝试使用以下代码:new lazy(fs.createReadStream('...')).lines.forEach(function(l) { /* ... */ }).join(function() { /* Done */ }) - Cecchi
6
@Cecchi和@Max,不要使用join,因为它会将整个文件缓存在内存中。相反,只需监听“end”事件即可:new lazy(...).lines.forEach(...).on('end', function() {...}) - Corin
3
就您说的内容而言,我理解为您在编程时将.on('end'...)事件绑定放在.forEach(...)方法之后,导致程序出现问题。但是当您将事件绑定放在方法之前时,程序则表现正常。 - crowjonah
59
这个搜索结果排名很高,值得注意的是Lazy看起来已经被放弃了。它已经有7个月没有更新,而且存在一些可怕的错误(忽略最后一行、大量内存泄漏等)。 - blu
显示剩余3条评论

56
require('fs').readFileSync('file.txt', 'utf-8').split(/\r?\n/).forEach(function(line){
  console.log(line);
})

70
这将在内存中读取整个文件,然后将其分成行。这不是问题所要求的。关键是能够按需顺序读取大型文件。 - Dan Dascalescu
8
这符合我的使用情况,我正在寻找一种简单的方法将输入从一个脚本转换成另一种格式。谢谢! - Callat
3
如果符合您的记忆限制,这可能并不回答原来的问题,但仍然有用。 - Kenny Worden

43

有一个非常棒的模块可以逐行读取文件,它被称为line-reader

你只需要简单地写下这句话:

var lineReader = require('line-reader');

lineReader.eachLine('file.txt', function(line, last) {
  console.log(line);
  // do whatever you want with line...
  if(last){
    // or check if it's the last one
  }
});

如果需要更多控制,您甚至可以使用“Java风格”的界面迭代文件:

lineReader.open('file.txt', function(reader) {
  if (reader.hasNextLine()) {
    reader.nextLine(function(line) {
      console.log(line);
    });
  }
});

4
这个很好用,它甚至能读取最后一行!值得一提的是它可以保留\r(回车符)如果文件是Windows格式的文本文件。使用line.trim()就可以轻松地去掉多余的\r。 - Pierre-Luc Bertrand
它的子优化在于输入只能来自命名文件,而不能(对于一个明显且极其重要的例子,process/stdin)。至少,如果可以的话,从代码和尝试中肯定不是显而易见的。 - Pointy
2
与此同时,有一种内置的方法可以使用readline核心模块从文件中读取行。 - Dan Dascalescu
这是老旧的内容,但如果有人偶然遇到它:function(reader)function(line)应该改为:function(err,reader)function(err,line) - jallmer
3
仅供参考,line-reader 是异步读取文件的工具。它的同步替代方案是 line-reader-sync - Prajwal

19

你总是可以自己编写行读取器。我还没有对这个片段进行基准测试,但它可以正确地将输入的数据流分割成一行而不包含结尾的 '\n'。

var last = "";

process.stdin.on('data', function(chunk) {
    var lines, i;

    lines = (last+chunk).split("\n");
    for(i = 0; i < lines.length - 1; i++) {
        console.log("line: " + lines[i]);
    }
    last = lines[i];
});

process.stdin.on('end', function() {
    console.log("line: " + last);
});

process.stdin.resume();

当我在编写一个快速的日志解析脚本时,需要在日志解析期间累积数据,我想尝试使用JS和Node而不是使用perl或bash。无论如何,我认为小型的Node.js脚本应该是自包含的,不应依赖第三方模块,因此在阅读了所有回答此问题的答案后,每个答案都使用各种模块来处理行解析,使用13 SLOC本机Node.js解决方案可能会引起兴趣。


除了使用“stdin”之外,似乎没有简单的方法来扩展它以处理任意文件...除非我漏掉了什么。 - hippietrail
3
@hippietrail,你可以使用fs.createReadStream('./myBigFile.csv')创建一个ReadStream,并将其用于替代stdin - nolith
2
每个块是否保证只包含完整的行?多字节UTF-8字符是否保证不会在块边界处被分割? - hippietrail
1
@hippietrail 我认为这个实现没有正确处理多字节字符。为了正确处理,必须首先将缓冲区正确转换为字符串,并跟踪分割在两个缓冲区之间的字符。为了正确地执行此操作,可以使用内置的StringDecoder - Ernelli
与此同时,有一种内置的方法可以使用readline核心模块从文件中读取行。 - Dan Dascalescu
让它支持UTF-8并不难。只需将last设为Buffer,使用last.indexOf('\n)last.slice()代替split()即可。UTF-8的好处在于,只有可以呈现为ASCII字符的字节才会将第8位设置为0。因此,在缓冲区中搜索10只会匹配换行符,而不是多字节字符的一部分。但如果需要更多的编码方式,则通用解码方案会更好。 - binki

19

旧话题,但这有效:

var rl = readline.createInterface({
      input : fs.createReadStream('/path/file.txt'),
      output: process.stdout,
      terminal: false
})
rl.on('line',function(line){
     console.log(line) //or parse line
})

简单。不需要外部模块。


2
如果你遇到了 readline is not defined 或者 fs is not defined 的错误,那么请添加 var readline = require('readline');var fs = require('fs'); 以使其正常工作。否则,代码将无法运行。谢谢。 - bergie3000
12
这个答案与早期的回答完全相同,但没有警告readline包标记不稳定(截至2015年4月仍未稳定),在2013年中期,存在无法读取结尾没有换行符的文件的最后一行的问题。这个问题在我第一次在v0.10.35中使用它时出现了,然后就消失了。/啊 - ruffin
如果你只是从文件流中读取数据,就不需要指定输出。 - Dan Dascalescu

12

使用carrier模块

var carrier = require('carrier');

process.stdin.resume();
carrier.carry(process.stdin, function(line) {
    console.log('got one line: ' + line);
});

很好。这也适用于任何输入文件: var inStream = fs.createReadStream('input.txt', {flags:'r'}); 但是您的语法比使用.on()的文档方法更清晰:carrier.carry(inStream).on('line', function(line) { ... - Brent Faust
carrier 似乎只处理 \r\n\n 的行尾。如果您需要处理早期 OS X 之前的 MacOS 样式的文本文件,它们使用 \r 而 carrier 不支持此格式。令人惊讶的是,在野外仍有这样的文件存在。您可能还需要显式地处理 Unicode BOM(字节顺序标记),在 MS Windows 环境中,它被用于文本文件的开头。 - hippietrail
与此同时,有一种内置的方法可以使用readline核心模块从文件中读取行。 - Dan Dascalescu

11
我使用 Lazy 按行读取文本时出现了一个非常严重的内存泄漏问题,因为在尝试处理这些行并写入另一个流时,Node 中 drain/pause/resume 的机制会导致这种情况发生(请参考:http://elegantcode.com/2011/04/06/taking-baby-steps-with-node-js-pumping-data-between-streams/ (我喜欢这个家伙))。我没有仔细研究 Lazy,以理解其中的原因,但我无法暂停我的读取流以允许 drain,否则 Lazy 就会退出。
我编写了用于将大型 CSV 文件处理成 XML 文档的代码,您可以在此处查看代码:https://github.com/j03m/node-csv2xml 如果您使用带有 Lazy 行的之前的版本运行它,则会发生内存泄漏。最新的修订版没有任何泄漏,您可以将其用作读取器/处理器的基础。不过,其中还包含一些自定义内容。
编辑:我想我还应该注意到,我的 Lazy 代码在处理较小的块时工作正常。直到我发现自己需要处理足够大的 XML 片段时,才需要 drain/pause/resume。

与此同时,有一种更简单的方法可以使用readline核心模块从文件中读取行。 - Dan Dascalescu
是的,那是现在正确的方式。但这是从2011年的内容。 :) - j03m

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接