在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个回答

9
在大多数情况下,这应该足够了:
const fs = require("fs")

fs.readFile('./file', 'utf-8', (err, file) => {
  const lines = file.split('\n')

  for (let line of lines)
    console.log(line)
});

8

编辑:

使用变换流


通过BufferedReader,你可以逐行读取。

new BufferedReader ("lorem ipsum", { encoding: "utf8" })
    .on ("error", function (error){
        console.log ("error: " + error);
    })
    .on ("line", function (line){
        console.log ("line: " + line);
    })
    .on ("end", function (){
        console.log ("EOF");
    })
    .read ();

1
与此同时,有一种更简单的方法可以使用readline核心模块从文件中读取行。 - Dan Dascalescu

8

在 Node.js v18.11.0 中添加了一项新功能,可以逐行读取文件

  • filehandle.readLines([options])

以下是使用该功能读取文本文件的方法

import { open } from 'node:fs/promises';
myFileReader();
async function myFileReader() {
    const file = await open('./TextFileName.txt');
    for await (const line of file.readLines()) {
        console.log(line)
    }
}

想要了解更多内容,请阅读Node.js文档,这里是文件系统readlines()的链接: https://nodejs.org/api/fs.html#filehandlereadlinesoptions


2
太好了,这个发现让我免受其他答案的陷阱困扰(例如CRLF行结尾,读取最后一行,及早正确关闭读取流)。谢谢! - undefined

6

我对缺乏全面解决方案感到沮丧,因此我自己尝试了一下 (git / npm)。以下是特性列表:

  • 交互式行处理(基于回调,无需将整个文件加载到内存中)
  • 可选地,以数组形式返回所有行(详细或原始模式)
  • 交互式中断流,或执行 map/filter 处理
  • 检测任何换行约定(PC/Mac/Linux)
  • 正确的 eof / 最后一行处理
  • 正确处理多字节 UTF-8 字符
  • 在每行基础上检索字节偏移和字节长度信息
  • 随机访问,使用基于行或基于字节的偏移量
  • 自动映射行偏移信息,以加快随机访问速度
  • 零依赖
  • 测试

NIH?你决定 :-)


6

在发布我的原始答案后,我发现split是一个非常易于使用的Node模块,可用于读取文件中的行;它还接受可选参数。

var split = require('split');
fs.createReadStream(file)
    .pipe(split())
    .on('data', function (line) {
      //each chunk now is a seperate line! 
    });

尚未在非常大的文件上进行测试。如果您有测试,请告诉我们。


5
function createLineReader(fileName){
    var EM = require("events").EventEmitter
    var ev = new EM()
    var stream = require("fs").createReadStream(fileName)
    var remainder = null;
    stream.on("data",function(data){
        if(remainder != null){//append newly received data chunk
            var tmp = new Buffer(remainder.length+data.length)
            remainder.copy(tmp)
            data.copy(tmp,remainder.length)
            data = tmp;
        }
        var start = 0;
        for(var i=0; i<data.length; i++){
            if(data[i] == 10){ //\n new line
                var line = data.slice(start,i)
                ev.emit("line", line)
                start = i+1;
            }
        }
        if(start<data.length){
            remainder = data.slice(start);
        }else{
            remainder = null;
        }
    })

    stream.on("end",function(){
        if(null!=remainder) ev.emit("line",remainder)
    })

    return ev
}


//---------main---------------
fileName = process.argv[2]

lineReader = createLineReader(fileName)
lineReader.on("line",function(line){
    console.log(line.toString())
    //console.log("++++++++++++++++++++")
})

我会测试这个,但您能告诉我,它是否保证不会破坏多字节字符(UTF-8 / UTF-16)? - hippietrail
2
@hippietrail: 对于UTF-8来说答案是否定的,尽管它是在字节流而不是字符流上工作。它会在换行符(0x0a)处中断。在UTF-8中,多字节字符的所有字节都设置了其高位比特。因此,没有多字节字符可以包含嵌入的换行符或其他常见的ASCII字符。然而,UTF-16和UTF-32另当别论。 - George
@George:我认为我们之间有些误解。由于CR和LF都在ASCII范围内,而UTF-8保留了128个ASCII字符不变,因此CR和LF都不能成为多字节UTF-8字符的一部分。我的问题是,在调用stream.on("data")时,是否可能出现仅包含多字节UTF-8字符的一部分(例如,即U+10D0,由三个字节e1 83 90组成)的开头或结尾。 - hippietrail
1
这仍然会将整个文件内容加载到内存中,然后才将其变为“新行”。它不是一次读取一行,而是获取所有行,然后根据“新行”缓冲区长度将它们分解。这种方法违背了创建流的目的。 - Justin
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Dan Dascalescu
@hippietrail 因为.slice()仅围绕ASCII的换行符,所以在UTF-8中没有将字符拆分的危险。但是在其他编码(例如UTF-16)中则有可能。在您的示例中,有效的UTF-8输入永远不会是0xe1830a90,因为0x0a 从不 可能是多字节字符的一部分。实际上,George回答了您的问题,尽管您声称他没有。 - binki

5

我希望你能帮我解决这个问题,基本上就是在Perl中的:

while (<>) {
    process_line($_);
}

我的用例只是一个独立的脚本,不是服务器,因此同步方式很好。这些是我的标准:
  • 最小同步代码,可在多个项目中重复使用。
  • 没有文件大小或行数的限制。
  • 没有行长度限制。
  • 能够处理UTF-8中的完整Unicode,包括超出BMP的字符。
  • 能够处理*nix和Windows行结尾(对我来说,旧式Mac不需要)。
  • 包含行结尾字符。
  • 能够处理带有或不带有行结尾字符的最后一行。
  • 不使用任何未包含在node.js分发中的外部库。
这是一个让我感受node.js低级脚本类型代码并决定它是否可以替代其他脚本语言(如Perl)的项目。
经过惊人的努力和几次失败的尝试后,这是我想出的代码。它相当快,但比我预期的要复杂得多: (在GitHub上派生它)
var fs            = require('fs'),
    StringDecoder = require('string_decoder').StringDecoder,
    util          = require('util');

function lineByLine(fd) {
  var blob = '';
  var blobStart = 0;
  var blobEnd = 0;

  var decoder = new StringDecoder('utf8');

  var CHUNK_SIZE = 16384;
  var chunk = new Buffer(CHUNK_SIZE);

  var eolPos = -1;
  var lastChunk = false;

  var moreLines = true;
  var readMore = true;

  // each line
  while (moreLines) {

    readMore = true;
    // append more chunks from the file onto the end of our blob of text until we have an EOL or EOF
    while (readMore) {

      // do we have a whole line? (with LF)
      eolPos = blob.indexOf('\n', blobStart);

      if (eolPos !== -1) {
        blobEnd = eolPos;
        readMore = false;

      // do we have the last line? (no LF)
      } else if (lastChunk) {
        blobEnd = blob.length;
        readMore = false;

      // otherwise read more
      } else {
        var bytesRead = fs.readSync(fd, chunk, 0, CHUNK_SIZE, null);

        lastChunk = bytesRead !== CHUNK_SIZE;

        blob += decoder.write(chunk.slice(0, bytesRead));
      }
    }

    if (blobStart < blob.length) {
      processLine(blob.substring(blobStart, blobEnd + 1));

      blobStart = blobEnd + 1;

      if (blobStart >= CHUNK_SIZE) {
        // blobStart is in characters, CHUNK_SIZE is in octets
        var freeable = blobStart / CHUNK_SIZE;

        // keep blob from growing indefinitely, not as deterministic as I'd like
        blob = blob.substring(CHUNK_SIZE);
        blobStart -= CHUNK_SIZE;
        blobEnd -= CHUNK_SIZE;
      }
    } else {
      moreLines = false;
    }
  }
}

这可能需要进一步清理,这是试错的结果。


3

基于生成器的行读取器:https://github.com/neurosnap/gen-readlines

var fs = require('fs');
var readlines = require('gen-readlines');

fs.open('./file.txt', 'r', function(err, fd) {
  if (err) throw err;
  fs.fstat(fd, function(err, stats) {
    if (err) throw err;

    for (var line of readlines(fd, stats.size)) {
      console.log(line.toString());
    }

  });
});

2
var fs = require('fs');

function readfile(name,online,onend,encoding) {
    var bufsize = 1024;
    var buffer = new Buffer(bufsize);
    var bufread = 0;
    var fd = fs.openSync(name,'r');
    var position = 0;
    var eof = false;
    var data = "";
    var lines = 0;

    encoding = encoding || "utf8";

    function readbuf() {
        bufread = fs.readSync(fd,buffer,0,bufsize,position);
        position += bufread;
        eof = bufread ? false : true;
        data += buffer.toString(encoding,0,bufread);
    }

    function getLine() {
        var nl = data.indexOf("\r"), hasnl = nl !== -1;
        if (!hasnl && eof) return fs.closeSync(fd), online(data,++lines), onend(lines); 
        if (!hasnl && !eof) readbuf(), nl = data.indexOf("\r"), hasnl = nl !== -1;
        if (!hasnl) return process.nextTick(getLine);
        var line = data.substr(0,nl);
        data = data.substr(nl+1);
        if (data[0] === "\n") data = data.substr(1);
        online(line,++lines);
        process.nextTick(getLine);
    }
    getLine();
}

我曾经遇到过同样的问题,并想出了上述解决方案,看起来与其他解决方案相似,但是它是异步的,并且可以非常快速地读取大型文件。

希望这可以帮到你。


2
如果您想逐行读取文件并将其写入另一个文件:
var fs = require('fs');
var readline = require('readline');
var Stream = require('stream');

function readFileLineByLine(inputFile, outputFile) {

   var instream = fs.createReadStream(inputFile);
   var outstream = new Stream();
   outstream.readable = true;
   outstream.writable = true;

   var rl = readline.createInterface({
      input: instream,
      output: outstream,
      terminal: false
   });

   rl.on('line', function (line) {
        fs.appendFileSync(outputFile, line + '\n');
   });
};

你和kofrasa的答案有什么不同? - Buffalo

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