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

2

这是我最喜欢的浏览文件的方式,它是一种简单的本地解决方案,用现代的async/await进行渐进式(不是“ slurp”或全内存方式)文件读取。当处理大型文本文件时,我发现这是一种“自然”的解决方案,而无需使用readline软件包或任何非核心依赖项。

let buf = '';
for await ( const chunk of fs.createReadStream('myfile') ) {
    const lines = buf.concat(chunk).split(/\r?\n/);
    buf = lines.pop() ?? '';
    for( const line of lines ) {
        console.log(line);
    }
}
if(buf.length) console.log(buf);  // last line, if file does not end with newline

您可以在fs.createReadStream中调整编码或使用chunk.toString(<arg>)。这样可以更好地微调行拆分以适应您的需求,例如使用.split(/\n+/)跳过空行,并使用fs.createReadStream('myfile', { highWaterMark: <chunkSize> })控制块大小。
不要忘记创建一个像processLine(line)这样的函数,以避免由于剩余的buf而重复处理代码两次。不幸的是,在此设置中,ReadStream实例不会更新其文件结束标志,因此没有办法(据我所知)在循环中检测到我们处于最后一次迭代,除非使用一些更冗长的技巧,如使用fs.Stats()将文件大小与.bytesRead进行比较。因此,最终的buf处理解决方案是必要的,除非您绝对确定您的文件以换行符\n结尾,在这种情况下,for await循环就足够了。
性能考虑
块大小对性能至关重要,默认值为64k,对于多MB文件,较大的块可以将速度提高一个数量级
上述片段的运行速度至少与基于NodeJS v18的fs.readLine()或基于readline模块的代码(接受的答案)相同,一旦您将highWaterMark调整为您的机器可以处理的内容,即如果您的可用内存允许,将其设置为与文件相同的大小是最快的
在任何情况下,这里的任何NodeJS逐行阅读答案都比Perl或本地*Nix解决方案慢一个数量级。
类似的替代方案
★ 如果您喜欢事件驱动的异步版本,则应该是:
let buf = '';
fs.createReadStream('myfile')
.on('data', chunk => {
    const lines = buf.concat(chunk).split(/\r?\n/);
    buf = lines.pop();
    for( const line of lines ) {
        console.log(line);
    }
})
.on('end', () => buf.length && console.log(buf) );

★ 如果您不介意导入stream核心包,那么这就是等效的管道流版本,它允许链接转换,如gzip解压缩:

const { Writable } = require('stream');
let buf = '';
fs.createReadStream('myfile').pipe(
    new Writable({
        write: (chunk, enc, next) => {
            const lines = buf.concat(chunk).split(/\r?\n/);
            buf = lines.pop();
            for (const line of lines) {
                console.log(line);
            }
            next();
        }
    })
).on('finish', () => buf.length && console.log(buf) );

我会添加 buf = lines.pop() ?? '' - 因为 Array.pop() 可能返回 undefined,强制我追溯并查找是否 lines 可能为空,这也让 TypeScript 愉快。 - Victor Rybynok

2

在执行这些操作时,我们必须问自己两个问题:

  1. 执行此操作所需的内存量是多少?
  2. 随着文件大小的增加,内存消耗是否急剧增加?

require('fs').readFileSync() 这样的解决方案会将整个文件加载到内存中。这意味着执行操作所需的内存量几乎等于文件大小。对于大于 50mbs 的任何内容,我们应该避免使用这些方法。

我们可以通过在函数调用后放置以下代码行来轻松跟踪函数使用的内存量

    const used = process.memoryUsage().heapUsed / 1024 / 1024;
    console.log(
      `The script uses approximately ${Math.round(used * 100) / 100} MB`
    );

目前从大文件中读取特定行的最佳方法是使用Node的readline。文档中有很棒的示例


这个 npm 模块和 Github 仓库已经消失了。 - James Moore
@JamesMoore 链接已更新,现在引用了readline库。 - oligofren

1
我有一个小模块,可以很好地完成这个任务,并被许多其他项目使用npm readline。请注意,在Node v10中有一个本地的readline模块,因此我将我的模块重新发布为linebylinehttps://www.npmjs.com/package/linebyline
如果你不想使用这个模块,那么这个函数非常简单:
var fs = require('fs'),
EventEmitter = require('events').EventEmitter,
util = require('util'),
newlines = [
  13, // \r
  10  // \n
];
var readLine = module.exports = function(file, opts) {
if (!(this instanceof readLine)) return new readLine(file);

EventEmitter.call(this);
opts = opts || {};
var self = this,
  line = [],
  lineCount = 0,
  emit = function(line, count) {
    self.emit('line', new Buffer(line).toString(), count);
  };
  this.input = fs.createReadStream(file);
  this.input.on('open', function(fd) {
    self.emit('open', fd);
  })
  .on('data', function(data) {
   for (var i = 0; i < data.length; i++) {
    if (0 <= newlines.indexOf(data[i])) { // Newline char was found.
      lineCount++;
      if (line.length) emit(line, lineCount);
      line = []; // Empty buffer.
     } else {
      line.push(data[i]); // Buffer new line data.
     }
   }
 }).on('error', function(err) {
   self.emit('error', err);
 }).on('end', function() {
  // Emit last line if anything left over since EOF won't trigger it.
  if (line.length){
     lineCount++;
     emit(line, lineCount);
  }
  self.emit('end');
 }).on('close', function() {
   self.emit('close');
 });
};
util.inherits(readLine, EventEmitter);

1
另一个解决方案是通过顺序执行器nsynjs运行逻辑。它使用节点readline模块逐行读取文件,并且不使用承诺或递归,因此不会在大文件上失败。以下是代码的样子:
var nsynjs = require('nsynjs');
var textFile = require('./wrappers/nodeReadline').textFile; // this file is part of nsynjs

function process(textFile) {

    var fh = new textFile();
    fh.open('path/to/file');
    var s;
    while (typeof(s = fh.readLine(nsynjsCtx).data) != 'undefined')
        console.log(s);
    fh.close();
}

var ctx = nsynjs.run(process,{},textFile,function () {
    console.log('done');
});

上面的代码基于这个例子:https://github.com/amaksr/nsynjs/blob/master/examples/node-readline/index.js


0
我使用这个:
function emitLines(stream, re){
    re = re && /\n/;
    var buffer = '';

    stream.on('data', stream_data);
    stream.on('end', stream_end);

    function stream_data(data){
        buffer += data;
        flush();
    }//stream_data

    function stream_end(){
        if(buffer) stream.emmit('line', buffer);
    }//stream_end


    function flush(){
        var re = /\n/;
        var match;
        while(match = re.exec(buffer)){
            var index = match.index + match[0].length;
            stream.emit('line', buffer.substring(0, index));
            buffer = buffer.substring(index);
            re.lastIndex = 0;
        }
    }//flush

}//emitLines

在流上使用此函数,并监听它将发出的行事件。

gr-


0
我将每日线路处理的整个逻辑封装为一个npm模块:line-kithttps://www.npmjs.com/package/line-kit

// example
var count = 0
require('line-kit')(require('fs').createReadStream('/etc/issue'),
                    (line) => { count++; },
                    () => {console.log(`seen ${count} lines`)})


0
const fs = require("fs")

fs.readFile('./file', 'utf-8', (err, data) => {
var innerContent;
    console.log("Asynchronous read: " + data.toString());
    const lines = data.toString().split('\n')
    for (let line of lines)
        innerContent += line + '<br>';


});

0

虽然顶部答案建议您使用readline模块,但是readline似乎更适用于命令行界面而不是行读取。它在缓冲方面也有点更加不透明。(任何需要流式行定向读取器的人可能都需要调整缓冲区大小)。 readline模块大约有1000行,而这个模块只有34行,包括统计和测试。

const EventEmitter = require('events').EventEmitter;
class LineReader extends EventEmitter{
    constructor(f, delim='\n'){
        super();
        this.totalChars = 0;
        this.totalLines = 0;
        this.leftover = '';

        f.on('data', (chunk)=>{
            this.totalChars += chunk.length;
            let lines = chunk.split(delim);
            if (lines.length === 1){
                this.leftover += chunk;
                return;
            }
            lines[0] = this.leftover + lines[0];
            this.leftover = lines[lines.length-1];
            if (this.leftover) lines.pop();
            this.totalLines += lines.length;
            for (let l of lines) this.onLine(l);
        });
        // f.on('error', ()=>{});
        f.on('end', ()=>{console.log('chars', this.totalChars, 'lines', this.totalLines)});
    }
    onLine(l){
        this.emit('line', l);
    }
}
//Command line test
const f = require('fs').createReadStream(process.argv[2], 'utf8');
const delim = process.argv[3];
const lineReader = new LineReader(f, delim);
lineReader.on('line', (line)=> console.log(line));

这是一个更短的版本,没有统计数据,只有19行:

class LineReader extends require('events').EventEmitter{
    constructor(f, delim='\n'){
        super();
        this.leftover = '';
        f.on('data', (chunk)=>{
            let lines = chunk.split(delim);
            if (lines.length === 1){
                this.leftover += chunk;
                return;
            }
            lines[0] = this.leftover + lines[0];
            this.leftover = lines[lines.length-1];
            if (this.leftover) 
                lines.pop();
            for (let l of lines)
                this.emit('line', l);
        });
    }
}

-1

我已经查看了所有上面的答案,它们都使用第三方库来解决这个问题。在Node的API中有一个简单的解决方案。例如:

const fs= require('fs')

let stream = fs.createReadStream('<filename>', { autoClose: true })

stream.on('data', chunk => {
    let row = chunk.toString('ascii')
}))

我猜测这个回答被踩是因为它不能一次性读取整个文件,但是你怎么确定每个块都以换行符(\n)结尾呢?验证和存储不完整的行的逻辑也没有在这里。 - YoniXw

-1
我使用以下代码,读取经过验证不是目录且未包含在无需检查的文件列表中的行。
(function () {
  var fs = require('fs');
  var glob = require('glob-fs')();
  var path = require('path');
  var result = 0;
  var exclude = ['LICENSE',
    path.join('e2e', 'util', 'db-ca', 'someother-file'),
    path.join('src', 'favicon.ico')];
  var files = [];
  files = glob.readdirSync('**');

  var allFiles = [];

  var patternString = [
    'trade',
    'order',
    'market',
    'securities'
  ];

  files.map((file) => {
    try {
      if (!fs.lstatSync(file).isDirectory() && exclude.indexOf(file) === -1) {
        fs.readFileSync(file).toString().split(/\r?\n/).forEach(function(line){
          patternString.map((pattern) => {
            if (line.indexOf(pattern) !== -1) {
              console.log(file + ' contain `' + pattern + '` in in line "' + line +'";');
              result = 1;
            }
          });
        });
      }
    } catch (e) {
      console.log('Error:', e.stack);
    }
  });
  process.exit(result);

})();

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