在Node.js中解析巨大的日志文件-逐行阅读


125

我需要在Javascript / Node.js中解析大型(5-10 Gb)日志文件(我正在使用Cube)。

日志行看起来像:

10:00:43.343423 I'm a friendly log message. There are 5 cats, and 7 dogs. We are in state "SUCCESS".

我们需要阅读每一行,做了一些分析(如带出来57SUCCESS),然后该泵将数据立方体(https://github.com/square/cube使用他们的JS客户端)。

首先,Node中逐行读取文件的规范方式是什么?

在线上似乎是一个相当普遍的问题:

许多答案似乎都指向一堆第三方模块:

但是,这似乎是一项相当基本的任务-当然,stdlib中有一种简单的方法可以逐行读取文本文件?

其次,然后我需要处理每一行(例如,将时间戳转换为Date对象,并提取有用的字段)。

最大化吞吐量的最佳方法是什么?是否有某种方式在读取每一行或将其发送到Cube时不会阻塞?

第三-我猜想使用字符串拆分,而包含(JS)的JS(IndexOf!= -1?)会比正则表达式快很多吗?是否有人在Node.js中解析大量文本数据方面有丰富的经验?

干杯,维克多


我在节点中构建了一个日志解析器,该解析器接受了一堆带有内置“捕获”的正则表达式字符串,并输出到JSON。如果要进行计算,甚至可以在每个捕获上调用函数。它可能会满足您的要求: npmjs.org/package/logax
Jess

Answers:


208

我搜索了一种使用流逐行解析非常大的文件(gbs)的解决方案。所有第三方库和示例都不符合我的需要,因为它们不逐行处理文件(例如1、2、3、4 ..)或将整个文件读取到内存中

以下解决方案可以使用流和管道逐行解析非常大的文件。为了进行测试,我使用了一个具有17.000.000条记录的2.1 GB文件。Ram使用量不超过60 mb。

首先,安装事件流包:

npm install event-stream

然后:

var fs = require('fs')
    , es = require('event-stream');

var lineNr = 0;

var s = fs.createReadStream('very-large-file.csv')
    .pipe(es.split())
    .pipe(es.mapSync(function(line){

        // pause the readstream
        s.pause();

        lineNr += 1;

        // process line here and call s.resume() when rdy
        // function below was for logging memory usage
        logMemoryUsage(lineNr);

        // resume the readstream, possibly from a callback
        s.resume();
    })
    .on('error', function(err){
        console.log('Error while reading file.', err);
    })
    .on('end', function(){
        console.log('Read entire file.')
    })
);

在此处输入图片说明

请让我知道情况如何!


6
仅供参考,此代码不是同步的。它是异步的。如果console.log(lineNr)在代码的最后一行之后插入,它将不会显示最后一行计数,因为该文件是异步读取的。
jfriend00

4
谢谢,这是我能找到的唯一解决方案,该解决方案实际上应在原本应该暂停和恢复的情况下进行。Readline没有。
布伦特

3
很棒的例子,它确实暂停了。此外,如果您决定停止提早读取文件,则可以使用s.end();
zipzit

2
像魅力一样工作。用它来索引1.5亿份文档到elasticsearch索引。readline模块很痛苦。它不会暂停,并且在40-50百万之后每次都会导致故障。浪费了一天。非常感谢您的回答。这是一个完美的作品
Mandeep Singh


72

您可以使用内置readline包,请参阅docs此处的。我使用来创建新的输出流。

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

var instream = fs.createReadStream('/path/to/file');
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) {
    console.log(line);
    //Do your stuff ...
    //Then write to outstream
    rl.write(cubestuff);
});

大文件将需要一些时间来处理。告诉它是否有效。


2
如前所述,倒数第二行失败,因为未定义cubestuff。
格雷格

2
使用readline,是否可以暂停/恢复读取流以在“操作”区域中执行异步操作?
jchook '16

1
readline当我尝试暂停/恢复时,@ jchook 给了我很多问题。如果下游过程较慢,它不会正确暂停流,从而会带来很多问题
Mandeep Singh 2016年

31

我真的很喜欢@gerard的答案,在这里实际上应该是正确的答案。我做了一些改进:

  • 代码在一个类中(模块化)
  • 包括解析
  • 如果有异步作业被链接到读取CSV(如插入数据库或HTTP请求),则可以恢复外部功能
  • 读取用户可以声明的块/批次大小。如果您有不同编码的文件,我也会在流中进行编码。

这是代码:

'use strict'

const fs = require('fs'),
    util = require('util'),
    stream = require('stream'),
    es = require('event-stream'),
    parse = require("csv-parse"),
    iconv = require('iconv-lite');

class CSVReader {
  constructor(filename, batchSize, columns) {
    this.reader = fs.createReadStream(filename).pipe(iconv.decodeStream('utf8'))
    this.batchSize = batchSize || 1000
    this.lineNumber = 0
    this.data = []
    this.parseOptions = {delimiter: '\t', columns: true, escape: '/', relax: true}
  }

  read(callback) {
    this.reader
      .pipe(es.split())
      .pipe(es.mapSync(line => {
        ++this.lineNumber

        parse(line, this.parseOptions, (err, d) => {
          this.data.push(d[0])
        })

        if (this.lineNumber % this.batchSize === 0) {
          callback(this.data)
        }
      })
      .on('error', function(){
          console.log('Error while reading file.')
      })
      .on('end', function(){
          console.log('Read entirefile.')
      }))
  }

  continue () {
    this.data = []
    this.reader.resume()
  }
}

module.exports = CSVReader

因此,基本上,这是您将如何使用它:

let reader = CSVReader('path_to_file.csv')
reader.read(() => reader.continue())

我用一个35GB的CSV文件进行了测试,它对我有用,这就是为什么我选择在@gerard的答案上构建它 ,因此欢迎反馈。


花了多少时间?
Z. Khullah,

显然,这没有pause()通话,不是吗?
Vanuan

另外,这不会最终调用回调函数。因此,如果batchSize为100,文件大小为150,则仅处理100个项目。我错了吗?
Vanuan

16

我使用https://www.npmjs.com/package/line-by-line从文本文件读取超过1000000行。在这种情况下,RAM的占用容量约为50-60兆字节。

    const LineByLineReader = require('line-by-line'),
    lr = new LineByLineReader('big_file.txt');

    lr.on('error', function (err) {
         // 'err' contains error object
    });

    lr.on('line', function (line) {
        // pause emitting of lines...
        lr.pause();

        // ...do your asynchronous line processing..
        setTimeout(function () {
            // ...and continue emitting lines.
            lr.resume();
        }, 100);
    });

    lr.on('end', function () {
         // All lines are read, file is closed now.
    });

“逐行”比选择的答案具有更高的存储效率。对于csv中的100万行,选择的答案使我的节点进程达到了800兆字节的低位。使用“逐行”,一直处于700s的低位。该模块还可以使代码保持整洁并易于阅读。总共我将需要阅读大约1800万个内容,因此每个mb都很重要!

遗憾的是,它使用自己的事件“ line”而不是标准的“ chunk”,这意味着您将无法使用“ pipe”。
Rene Wooller

经过数小时的测试和搜索,这是唯一在lr.cancel()方法上实际停止的解决方案。在1毫秒内读取5Gig文件的前1000行。太棒了!!!!
Perez Lamed van Niekerk

6

除了逐行读取大文件之外,您还可以逐块读取大文件。有关更多信息,请参阅本文

var offset = 0;
var chunkSize = 2048;
var chunkBuffer = new Buffer(chunkSize);
var fp = fs.openSync('filepath', 'r');
var bytesRead = 0;
while(bytesRead = fs.readSync(fp, chunkBuffer, 0, chunkSize, offset)) {
    offset += bytesRead;
    var str = chunkBuffer.slice(0, bytesRead).toString();
    var arr = str.split('\n');

    if(bytesRead = chunkSize) {
        // the last item of the arr may be not a full line, leave it to the next chunk
        offset -= arr.pop().length;
    }
    lines.push(arr);
}
console.log(lines);

莫非,这下面应该是一个比较,而不是分配:if(bytesRead = chunkSize)
Stefan Rein

4

Node.js文档使用Readline模块提供了一个非常优雅的示例。

示例:逐行读取文件流

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

const rl = readline.createInterface({
    input: fs.createReadStream('sample.txt'),
    crlfDelay: Infinity
});

rl.on('line', (line) => {
    console.log(`Line from file: ${line}`);
});

注意:我们使用crlfDelay选项将CR LF('\ r \ n')的所有实例识别为单个换行符。


3

我有同样的问题。在比较了几个似乎具有此功能的模块之后,我决定自己做,这比我想的要简单。

要点:https : //gist.github.com/deemstone/8279565

var fetchBlock = lineByline(filepath, onEnd);
fetchBlock(function(lines, start){ ... });  //lines{array} start{int} lines[0] No.

它覆盖了关闭中打开的文件,fetchBlock()返回的文件将从文件中获取一个块,最后拆分为数组(将处理最后一次获取的段)。

我已将每个读取操作的块大小设置为1024。这可能有错误,但是代码逻辑很明显,请自己尝试。


2

node-byline使用流,因此我希望为您的大文件使用该流。

对于您的日期转换,我将使用moment.js

为了最大程度地提高吞吐量,您可以考虑使用软件集群。有一些不错的模块很好地包装了本机节点集群模块。我喜欢来自ISAAC的Cluster-master。例如,您可以创建一个x工人集群,它们全部计算一个文件。

对于splits和regexes 基准测试,请使用Benchmark.js。到目前为止,我还没有对其进行测试。Benchmark.js可作为节点模块使用


2

基于问题,我实现了一个类,您可以使用该类与逐行同步读取文件fs.readSync()。您可以使用QPromise(jQuery似乎需要DOM,因此无法使用来运行它nodejs)来使“暂停”和“恢复” :

var fs = require('fs');
var Q = require('q');

var lr = new LineReader(filenameToLoad);
lr.open();

var promise;
workOnLine = function () {
    var line = lr.readNextLine();
    promise = complexLineTransformation(line).then(
        function() {console.log('ok');workOnLine();},
        function() {console.log('error');}
    );
}
workOnLine();

complexLineTransformation = function (line) {
    var deferred = Q.defer();
    // ... async call goes here, in callback: deferred.resolve('done ok'); or deferred.reject(new Error(error));
    return deferred.promise;
}

function LineReader (filename) {      
  this.moreLinesAvailable = true;
  this.fd = undefined;
  this.bufferSize = 1024*1024;
  this.buffer = new Buffer(this.bufferSize);
  this.leftOver = '';

  this.read = undefined;
  this.idxStart = undefined;
  this.idx = undefined;

  this.lineNumber = 0;

  this._bundleOfLines = [];

  this.open = function() {
    this.fd = fs.openSync(filename, 'r');
  };

  this.readNextLine = function () {
    if (this._bundleOfLines.length === 0) {
      this._readNextBundleOfLines();
    }
    this.lineNumber++;
    var lineToReturn = this._bundleOfLines[0];
    this._bundleOfLines.splice(0, 1); // remove first element (pos, howmany)
    return lineToReturn;
  };

  this.getLineNumber = function() {
    return this.lineNumber;
  };

  this._readNextBundleOfLines = function() {
    var line = "";
    while ((this.read = fs.readSync(this.fd, this.buffer, 0, this.bufferSize, null)) !== 0) { // read next bytes until end of file
      this.leftOver += this.buffer.toString('utf8', 0, this.read); // append to leftOver
      this.idxStart = 0
      while ((this.idx = this.leftOver.indexOf("\n", this.idxStart)) !== -1) { // as long as there is a newline-char in leftOver
        line = this.leftOver.substring(this.idxStart, this.idx);
        this._bundleOfLines.push(line);        
        this.idxStart = this.idx + 1;
      }
      this.leftOver = this.leftOver.substring(this.idxStart);
      if (line !== "") {
        break;
      }
    }
  }; 
}

0
import * as csv from 'fast-csv';
import * as fs from 'fs';
interface Row {
  [s: string]: string;
}
type RowCallBack = (data: Row, index: number) => object;
export class CSVReader {
  protected file: string;
  protected csvOptions = {
    delimiter: ',',
    headers: true,
    ignoreEmpty: true,
    trim: true
  };
  constructor(file: string, csvOptions = {}) {
    if (!fs.existsSync(file)) {
      throw new Error(`File ${file} not found.`);
    }
    this.file = file;
    this.csvOptions = Object.assign({}, this.csvOptions, csvOptions);
  }
  public read(callback: RowCallBack): Promise < Array < object >> {
    return new Promise < Array < object >> (resolve => {
      const readStream = fs.createReadStream(this.file);
      const results: Array < any > = [];
      let index = 0;
      const csvStream = csv.parse(this.csvOptions).on('data', async (data: Row) => {
        index++;
        results.push(await callback(data, index));
      }).on('error', (err: Error) => {
        console.error(err.message);
        throw err;
      }).on('end', () => {
        resolve(results);
      });
      readStream.pipe(csvStream);
    });
  }
}
import { CSVReader } from '../src/helpers/CSVReader';
(async () => {
  const reader = new CSVReader('./database/migrations/csv/users.csv');
  const users = await reader.read(async data => {
    return {
      username: data.username,
      name: data.name,
      email: data.email,
      cellPhone: data.cell_phone,
      homePhone: data.home_phone,
      roleId: data.role_id,
      description: data.description,
      state: data.state,
    };
  });
  console.log(users);
})();

-1

我已经制作了一个节点模块来异步读取大文件文本或JSON。经过大文件测试。

var fs = require('fs')
, util = require('util')
, stream = require('stream')
, es = require('event-stream');

module.exports = FileReader;

function FileReader(){

}

FileReader.prototype.read = function(pathToFile, callback){
    var returnTxt = '';
    var s = fs.createReadStream(pathToFile)
    .pipe(es.split())
    .pipe(es.mapSync(function(line){

        // pause the readstream
        s.pause();

        //console.log('reading line: '+line);
        returnTxt += line;        

        // resume the readstream, possibly from a callback
        s.resume();
    })
    .on('error', function(){
        console.log('Error while reading file.');
    })
    .on('end', function(){
        console.log('Read entire file.');
        callback(returnTxt);
    })
);
};

FileReader.prototype.readJSON = function(pathToFile, callback){
    try{
        this.read(pathToFile, function(txt){callback(JSON.parse(txt));});
    }
    catch(err){
        throw new Error('json file is not valid! '+err.stack);
    }
};

只需将文件另存为file-reader.js,然后像这样使用它:

var FileReader = require('./file-reader');
var fileReader = new FileReader();
fileReader.readJSON(__dirname + '/largeFile.json', function(jsonObj){/*callback logic here*/});

7
我似乎是您从杰拉德的答案中抄袭过来的。您应该将Gerard归功于您复制的部分。
保罗·林奇
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.