Node.js에서 거대한 로그 파일 구문 분석-한 줄씩 읽기


126

Javascript / Node.js에서 큰 (5-10Gb) 로그 파일을 파싱해야합니다 (Cube를 사용하고 있습니다).

로그 라인은 다음과 같습니다.

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

우리는 (예 : 빼내야 몇 가지 분석을, 각 라인을 읽을 필요가 5, 7그리고 SUCCESS다음 큐브로이 데이터 (펌프) https://github.com/square/cube를 자신의 JS 클라이언트를 사용하여).

첫째, 노드에서 파일을 한 줄씩 읽는 표준 방법은 무엇입니까?

온라인에서 매우 일반적인 질문 인 것 같습니다.

많은 답변이 여러 타사 모듈을 가리키는 것 같습니다.

그러나 이것은 매우 기본적인 작업처럼 보입니다. 확실히 stdlib 내에 텍스트 파일을 한 줄씩 읽는 간단한 방법이 있습니까?

둘째, 그런 다음 각 줄을 처리해야합니다 (예 : 타임 스탬프를 Date 객체로 변환하고 유용한 필드 추출).

처리량을 최대화하는 가장 좋은 방법은 무엇입니까? 각 줄을 읽거나 큐브로 보내는 것을 차단하지 않는 방법이 있습니까?

셋째-문자열 분할을 사용하여 추측하고 있으며 JS에 해당하는 contains (IndexOf! = -1?)가 정규식보다 훨씬 빠를 것입니까? Node.js에서 방대한 양의 텍스트 데이터를 파싱 한 경험이있는 사람이 있습니까?

건배, 빅터


'캡처'가 내장 된 정규식 문자열을 가져와 JSON으로 출력하는 노드에 로그 파서를 빌드했습니다. 계산을 수행하려면 각 캡처에서 함수를 호출 할 수도 있습니다. 그것은 당신이 원하는 것을 할 수 있습니다 : npmjs.org/package/logax
Jess

답변:


209

스트림을 사용하여 한 줄씩 매우 큰 파일 (gbs)을 구문 분석하는 솔루션을 찾았습니다. 모든 타사 라이브러리와 예제는 파일을 한 줄씩 처리하지 않았거나 (예 : 1, 2, 3, 4 ..) 전체 파일을 메모리로 읽었 기 때문에 내 요구에 맞지 않았습니다.

다음 솔루션은 스트림 및 파이프를 사용하여 한 줄씩 매우 큰 파일을 구문 분석 할 수 있습니다. 테스트를 위해 17.000.000 레코드가있는 2.1GB 파일을 사용했습니다. RAM 사용량은 60MB를 초과하지 않았습니다.

먼저 이벤트 스트림 패키지를 설치합니다 .

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 2015-06-16

4
고마워요, 이것이 제가 생각할 때 실제로 일시 중지했다가 다시 시작할 수있는 유일한 해결책이었습니다. Readline은 그렇지 않았습니다.
Brent

3
멋진 예입니다. 실제로 일시 중지됩니다. 또한 파일 읽기를 일찍 중지하기로 결정한 경우 다음을 사용할 수 있습니다.s.end();
zipzit

2
매력처럼 작동했습니다. 1 억 5 천만 개의 문서를 elasticsearch 색인에 색인화하는 데 사용했습니다. readline모듈은 고통입니다. 그것은 멈추지 않고 4 천만 ~ 5 천만 명 이후 매번 고장을 일으켰습니다. 하루를 낭비했습니다. 답변 해 주셔서 감사합니다. 이 사람은 완벽하게 작동
디프 싱에게

3
이벤트 스트림이 손상되었습니다 : medium.com/intrinsic/… 그러나 4+는 분명히 안전합니다. blog.npmjs.org/post/180565383195/…
John Vandivier

72

내장 readline패키지를 사용할 수 있습니다 . 여기에서 문서를 참조 하십시오 . 내가 사용하는 스트리밍 새로운 출력 스트림을 만들 수 있습니다.

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

1
@jchook readline은 일시 중지 / 재개를 시도 할 때 많은 문제를 일으켰습니다. 다운 스트림 프로세스가 느린 경우 제대로 문제를 많이 만드는 스트림을 일시 정지하지 않습니다
디프 싱

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 19.11.02

또한 이것은 콜백 함수를 호출하지 않습니다. 따라서 batchSize가 100이고 파일 크기가 150이면 100 개의 항목 만 처리됩니다. 내가 잘못?
Vanuan

16

텍스트 파일에서 1000000 줄 이상을 읽기 위해 https://www.npmjs.com/package/line-by-line 을 사용했습니다 . 이 경우 RAM의 점유 용량은 약 50-60MB입니다.

    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의 백만 줄에 대해 선택한 답변은 800MB의 낮은 노드 프로세스를 가졌습니다. 'line-by-line'을 사용하면 지속적으로 낮은 700 초에있었습니다. 이 모듈은 또한 코드를 깨끗하고 읽기 쉽게 유지합니다. 총 1,800 만 개를 읽어야하므로 모든 MB가 중요합니다!
네오

표준 '청크'대신 자체 이벤트 '라인'을 사용하는 것은 부끄러운 일입니다. 즉, '파이프'를 사용할 수 없다는 의미입니다.
Rene Wooller

몇 시간 동안 테스트하고 검색 한 후에는 lr.cancel()메서드에서 실제로 중단되는 유일한 솔루션입니다 . 5Gig 파일의 처음 1000 줄을 1ms 안에 읽습니다. 대박!!!!
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를 사용 합니다 .

처리량을 최대화하려면 소프트웨어 클러스터 사용을 고려할 수 있습니다. 노드 네이티브 클러스터 모듈을 아주 잘 감싸는 멋진 모듈이 있습니다. 나는 클러스터 마스터를 좋아한다 isaacs의 를 . 예를 들어 모두 파일을 계산하는 x 작업자 클러스터를 만들 수 있습니다.

벤치마킹 분할 대 정규식의 경우 benchmark.js를 사용하십시오 . 나는 지금까지 그것을 테스트하지 않았습니다. Benchmark.js는 노드 모듈로 제공됩니다.


2

질문에 대한 답변을 바탕으로 .NET을 사용하여 파일을 한 줄씩 동기식으로 읽는 데 사용할 수있는 클래스를 구현했습니다 fs.readSync(). Q약속 을 사용하여이 "일시 중지"및 "재개"를 수행 할 수 있습니다 ( 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
Gerard의 답변에서 복사 한 것 같습니다. 복사 한 부분에 대해 Gerard에게 크레딧을 제공해야합니다.
Paul Lynch
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.