Phân tích cú pháp các tệp nhật ký khổng lồ trong Node.js - đọc từng dòng


126

Tôi cần thực hiện một số phân tích cú pháp các tệp nhật ký lớn (5-10 Gb) trong Javascript / Node.js (Tôi đang sử dụng Cube).

Dòng nhật ký trông giống như sau:

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

Chúng ta cần phải đọc từng dòng, làm một số phân tích cú pháp (ví dụ như loại bỏ 5, 7SUCCESS), sau đó bơm dữ liệu này vào Cube ( https://github.com/square/cube ) sử dụng client JS của họ.

Thứ nhất, cách chuẩn trong Node để đọc trong một tệp, từng dòng là gì?

Có vẻ như đây là một câu hỏi khá phổ biến trên mạng:

Rất nhiều câu trả lời dường như chỉ ra một loạt các mô-đun của bên thứ ba:

Tuy nhiên, đây có vẻ như là một nhiệm vụ khá cơ bản - chắc chắn, có một cách đơn giản trong stdlib để đọc trong một tệp văn bản, từng dòng?

Thứ hai, sau đó tôi cần xử lý từng dòng (ví dụ: chuyển đổi dấu thời gian thành đối tượng Ngày và trích xuất các trường hữu ích).

Cách tốt nhất để làm điều này, tối đa hóa thông lượng là gì? Có cách nào đó không chặn việc đọc từng dòng hoặc gửi nó đến Cube không?

Thứ ba - Tôi đang đoán sử dụng phân tách chuỗi và JS tương đương của hàm chứa (IndexOf! = -1?) Sẽ nhanh hơn rất nhiều so với regexes? Có ai có nhiều kinh nghiệm trong việc phân tích cú pháp một lượng lớn dữ liệu văn bản trong Node.js không?

Chúc mừng, Victor


Tôi đã xây dựng một trình phân tích cú pháp nhật ký trong nút lấy một loạt các chuỗi regex có tích hợp 'capture' và xuất ra JSON. Bạn thậm chí có thể gọi các chức năng trên mỗi lần chụp nếu bạn muốn thực hiện một calc. Nó có thể làm những gì bạn muốn: npmjs.org/package/logax
Jess

Câu trả lời:


209

Tôi đã tìm kiếm một giải pháp để phân tích cú pháp các tệp rất lớn (gbs) từng dòng bằng cách sử dụng một luồng. Tất cả các thư viện và ví dụ của bên thứ ba không phù hợp với nhu cầu của tôi vì họ xử lý tệp không theo dòng (như 1, 2, 3, 4 ..) hoặc đọc toàn bộ tệp vào bộ nhớ

Giải pháp sau có thể phân tích cú pháp các tệp rất lớn, từng dòng bằng cách sử dụng luồng & đường ống. Để thử nghiệm, tôi đã sử dụng tệp 2.1 gb với 17.000.000 bản ghi. Ram sử dụng không vượt quá 60 mb.

Trước tiên, hãy cài đặt gói luồng sự kiện :

npm install event-stream

Sau đó:

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.')
    })
);

nhập mô tả hình ảnh ở đây

Xin vui lòng cho tôi biết làm thế nào nó đi!


6
FYI, mã này không đồng bộ. Nó không đồng bộ. Nếu bạn chèn console.log(lineNr)sau dòng cuối cùng của mã, nó sẽ không hiển thị số dòng cuối cùng vì tệp được đọc không đồng bộ.
jfriend00

4
Cảm ơn bạn, đây là giải pháp duy nhất tôi có thể tìm thấy thực sự bị tạm dừng và tiếp tục khi nó được cho là. Readline đã không.
Brent

3
Ví dụ tuyệt vời và nó thực sự tạm dừng. Ngoài ra, nếu bạn quyết định dừng đọc tệp sớm, bạn có thể sử dụngs.end();
zipzit

2
Làm việc như người ở. Đã sử dụng nó để lập chỉ mục 150 triệu tài liệu cho chỉ mục tìm kiếm đàn hồi. readlinemô-đun là một nỗi đau. Nó không dừng lại và luôn gây ra thất bại sau 40-50 triệu. Lãng phí một ngày. Cảm ơn rất nhiều cho câu trả lời. Cái này hoạt động hoàn hảo
Mandeep Singh

3
event-stream đã bị xâm phạm: medium.com/intrinsic/… nhưng 4+ dường như là blog
John Vandivier

72

Bạn có thể sử dụng readlinegói có sẵn , xem tài liệu tại đây . Tôi sử dụng luồng để tạo luồng đầu ra mới.

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);
});

Các tệp lớn sẽ mất một thời gian để xử lý. Hãy cho biết nếu nó hoạt động.


2
Như đã viết, dòng thứ hai đến dòng cuối cùng không thành công vì cubestuff không được xác định.
Greg

2
Bằng cách sử dụng readline, có thể tạm dừng / tiếp tục luồng đã đọc để thực hiện các hành động không đồng bộ trong khu vực "làm nội dung" không?
jchook

1
@jchook readlineđã gây ra cho tôi rất nhiều vấn đề khi tôi thử tạm dừng / tiếp tục. Nó không tạm dừng dòng đúng cách tạo ra rất nhiều vấn đề nếu quá trình hạ lưu là chậm hơn
Mandeep Singh

31

Tôi thực sự thích câu trả lời @gerard mà thực sự xứng đáng là câu trả lời chính xác ở đây. Tôi đã thực hiện một số cải tiến:

  • Mã nằm trong một lớp (mô-đun)
  • Phân tích cú pháp được bao gồm
  • Khả năng tiếp tục được cung cấp cho bên ngoài trong trường hợp có một công việc không đồng bộ được liên kết để đọc CSV như chèn vào DB hoặc một yêu cầu HTTP
  • Đọc theo kích thước khối / lô mà người dùng có thể khai báo. Tôi cũng quan tâm đến việc mã hóa trong luồng, trong trường hợp bạn có các tệp ở mã hóa khác nhau.

Đây là mã:

'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

Về cơ bản, đây là cách bạn sẽ sử dụng nó:

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

Tôi đã thử nghiệm điều này với tệp CSV 35GB và nó phù hợp với tôi và đó là lý do tại sao tôi chọn xây dựng nó trên câu trả lời của @gerard , chúng tôi hoan nghênh phản hồi.


mất bao nhiêu thời gian
Z. Khullah

Rõ ràng, điều này thiếu pause()cuộc gọi, phải không?
Vanuan

Ngoài ra, điều này không gọi chức năng gọi lại khi kết thúc. Vì vậy, nếu kích thước batchS là 100, kích thước tệp là 150, chỉ 100 mục sẽ được xử lý. Liệu tôi có sai?
Vanuan

16

Tôi đã sử dụng https://www.npmjs.com/package/line-by-line để đọc hơn 1 000 000 dòng từ một tệp văn bản. Trong trường hợp này, dung lượng RAM bị chiếm dụng là khoảng 50-60 megabyte.

    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' tiết kiệm bộ nhớ hơn câu trả lời đã chọn. Đối với 1 triệu dòng trong csv, câu trả lời đã chọn có quá trình xử lý nút của tôi ở mức 800 megabyte thấp. Sử dụng 'line-by-line', nó đã nhất quán trong những năm 700 thấp. Mô-đun này cũng giữ cho mã sạch và dễ đọc. Tổng cộng tôi sẽ cần đọc khoảng 18 triệu để mỗi mb đều có giá trị!
Neo

Thật tiếc khi điều này sử dụng 'dòng' sự kiện của chính nó thay vì 'đoạn' tiêu chuẩn, nghĩa là bạn sẽ không thể sử dụng 'đường ống'.
Rene Wooller 21/09/17

Sau nhiều giờ thử nghiệm và tìm kiếm, đây là giải pháp duy nhất thực sự dừng lại trên lr.cancel()phương pháp. Đọc 1000 dòng đầu tiên của tệp 5Gig trong 1ms. Tuyệt vời!!!!
Perez Lamed van Niekerk

6

Ngoài việc đọc từng dòng một tệp lớn, bạn cũng có thể đọc từng đoạn một. Tham khảo thêm bài viết này

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);

Có thể nào, những điều sau đây phải là một phép so sánh thay vì một phép gán if(bytesRead = chunkSize):?
Stefan Rein

4

Tài liệu Node.js đưa ra một ví dụ rất dễ hiểu bằng cách sử dụng mô-đun Readline.

Ví dụ: Đọc dòng tệp theo từng dòng

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}`);
});

Lưu ý: chúng tôi sử dụng tùy chọn crlfDelay để nhận ra tất cả các trường hợp CR LF ('\ r \ n') là một ngắt dòng.


3

Tôi đã có cùng một vấn đề được nêu ra. Sau khi so sánh một số mô-đun dường như có tính năng này, tôi quyết định tự làm, nó đơn giản hơn tôi nghĩ.

ý chính: https://gist.github.com/deemstone/8279565

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

Nó bao gồm tệp được mở trong một lần đóng, được fetchBlock()trả về sẽ tìm nạp một khối từ tệp, kết thúc tách thành mảng (sẽ xử lý phân đoạn từ lần tìm nạp cuối cùng).

Tôi đã đặt kích thước khối thành 1024 cho mỗi thao tác đọc. Điều này có thể có lỗi, nhưng logic mã là rõ ràng, hãy tự mình thử.


2

node-byline sử dụng luồng, vì vậy tôi thích luồng đó hơn cho các tệp khổng lồ của bạn.

cho chuyển đổi ngày của bạn, tôi sẽ sử dụng moment.js .

để tối đa hóa thông lượng của bạn, bạn có thể nghĩ đến việc sử dụng một cụm phần mềm. có một số mô-đun đẹp bao bọc mô-đun cụm nút-gốc khá tốt. tôi thích cluster-master từ isaacs. Ví dụ: bạn có thể tạo một nhóm x worker mà tất cả đều tính toán một tệp.

để phân chia điểm chuẩn so với regexes, hãy sử dụng benchmark.js . Tôi đã thử nghiệm nó cho đến bây giờ. benchmark.js có sẵn dưới dạng mô-đun nút


2

Dựa trên câu trả lời câu hỏi này, tôi đã triển khai một lớp mà bạn có thể sử dụng để đọc đồng bộ từng dòng một tệp fs.readSync(). Bạn có thể thực hiện "tạm dừng" và "tiếp tục" này bằng cách sử dụng một Qlời hứa ( jQuerydường như yêu cầu DOM nên không thể chạy nó với 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

Tôi đã tạo một mô-đun nút để đọc không đồng bộ tệp lớn văn bản hoặc JSON. Đã thử nghiệm trên các tệp lớn.

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);
    }
};

Chỉ cần lưu tệp dưới dạng file-reader.js và sử dụng nó như sau:

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

7
Tôi có vẻ như bạn đã sao chép câu trả lời của Gerard. Bạn nên ghi công cho Gerard cho phần bạn đã sao chép.
Paul Lynch
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.