Node.jsでの巨大なログファイルの解析-1行ずつ読み込む


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クライアントを使用して)。

まず、ノードでファイルを1行ずつ読み取るための標準的な方法は何ですか?

オンラインでよくある質問のようです:

答えの多くはサードパーティのモジュールの束を指しているようです:

しかし、これはかなり基本的なタスクのようです-確かに、stdlib内でテキストファイルを行ごとに読み取る簡単な方法はありますか?

次に、各行を処理する必要があります(たとえば、タイムスタンプをDateオブジェクトに変換し、有用なフィールドを抽出します)。

これを実行してスループットを最大化する最良の方法は何ですか?各行の読み取り、またはキューブへの送信のいずれかをブロックしない方法はありますか?

3番目に、文字列分割を使用していると思いますが、JSのcontains(IndexOf!= -1?)に相当するものは、正規表現よりもはるかに高速です。Node.jsで大量のテキストデータを解析した経験はありますか?

乾杯、ビクター


ノードにログパーサーを構築しました。このパーサーは、「キャプチャ」が組み込まれた一連の正規表現文字列を受け取り、JSONに出力します。計算を行いたい場合は、キャプチャごとに関数を呼び出すこともできます。それはあなたが望むことをするかもしれません: npmjs.org/package/logax
Jess

回答:


208

ストリームを使用して行ごとに非常に大きなファイル(GBS)を解析するソリューションを検索しました。サードパーティのライブラリとサンプルはすべて、1行ずつではなく(1、2、3、4 ..など)ファイルを処理したり、ファイル全体をメモリに読み込んだりするため、私のニーズに合いませんでした。

次のソリューションでは、ストリームとパイプを使用して、非常に大きなファイルを1行ずつ解析できます。テストでは、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億5000万のドキュメントをelasticsearchインデックスにインデックス付けしました。readlineモジュールは苦痛です。一時停止せず、毎回4千万から5千万回失敗しました。1日を無駄にしました。答えてくれてありがとう。これは完璧に機能します
Mandeep Singh

3
イベント・ストリームが侵害された:medium.com/intrinsic/...しかし4+は明らかに安全であるblog.npmjs.org/post/180565383195/...
ジョンVandivier

72

組み込みreadlineパッケージを使用できます。こちらのドキュメントをご覧くださいstreamを使用して、新しい出力ストリームを作成します。

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行目が失敗します。
グレッグ

2
を使用readlineして、「ストリーム」領域で非同期操作を実行するために読み取りストリームを一時停止/再開できますか?
jchook 2016年

1
@jchook readlineで一時停止/再開しようとすると、多くの問題が発生しました。ダウンストリームプロセスが遅い場合、ストリームを適切に一時停止しないため、多くの問題が発生します
Mandeep Singh

31

私は@gerardの回答が本当に気に入りました。私はいくつかの改善を行いました:

  • コードはクラスにあります(モジュラー)
  • 解析が含まれています
  • 非同期ジョブがDBへの挿入やHTTPリクエストのようなCSVの読み取りにチェーンされている場合、再開する機能が外部に与えられます。
  • ユーザーが宣言できるチャンク/バッチサイズで読み取る。異なるエンコーディングのファイルがある場合に備えて、ストリーム内のエンコーディングも担当しました。

これがコードです:

'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

テキストファイルから1 000 000行以上を読み取るために、https://www.npmjs.com/package/line-by-lineを使用しました。この場合、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.
    });

'line-by-line'は、選択した回答よりもメモリ効率が高くなります。csvで100万行の場合、選択された回答のノードプロセスは800 MB未満でした。'line-by-line'を使用すると、一貫して700年代後半になりました。このモジュールは、コードをクリーンで読みやすい状態に保ちます。合計で約1800万を読む必要があるので、すべてのmbがカウントされます!
ネオ

これは、標準の「チャンク」の代わりに独自のイベント「ライン」を使用するのは残念です。つまり、「パイプ」を使用することはできません。
Rene Wooller、2017

何時間ものテストと検索の後、これは実際にlr.cancel()メソッドで停止する唯一のソリューションです。5ギガファイルの最初の1000行を1ミリ秒で読み取ります。驚くばかり!!!!
Perez Lamed van Niekerk

6

大きなファイルを1行ずつ読み取るだけでなく、チャンクごとに読み取ることもできます。詳細については、この記事を参照してください

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

私はまだ同じ問題を抱えていました。この機能を備えていると思われるいくつかのモジュールを比較した後、私は自分で行うことにしました。思ったよりも簡単です。

gist: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を使用します

スループットを最大化するには、ソフトウェアクラスタの使用を検討してください。ノードネイティブのクラスターモジュールを非常にうまくラップするniceモジュールがいくつかあります。以下のような私クラスタ・マスター・アイザックスから。たとえば、すべてファイルを計算するxワーカーのクラスターを作成できます。

分割と正規表現のベンチマークには、benchmark.jsを使用します。今までテストしていません。benchmark.jsはノードモジュールとして利用可能です


2

この質問の回答に基づいて、で行ごとにファイルを同期的に読み取るために使用できるクラスを実装しましたfs.readSync()Qpromise を使用して、この「一時停止」と「再開」を行うことができます(jQueryDOMが必要なため、で実行できないようです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
私はあなたがジェラールの答えからコピーしたようです。コピーした部分にジェラールのクレジットを与える必要があります。
ポールリンチ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.