Analisi di enormi file di log in Node.js: lettura riga per riga


126

Ho bisogno di analizzare alcuni file di log di grandi dimensioni (5-10 Gb) in Javascript / Node.js (sto usando Cube).

La logline ha un aspetto simile a:

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

Dobbiamo leggere ogni riga, fare qualche analisi (es nudo fuori 5, 7e SUCCESS), poi la pompa questi dati in cubo ( https://github.com/square/cube ) usando il loro client JS.

In primo luogo, qual è il modo canonico in Node di leggere un file, riga per riga?

Sembra essere una domanda abbastanza comune online:

Molte delle risposte sembrano indicare un gruppo di moduli di terze parti:

Tuttavia, questo sembra un compito abbastanza semplice: sicuramente c'è un modo semplice all'interno di stdlib per leggere in un file di testo, riga per riga?

In secondo luogo, devo quindi elaborare ogni riga (ad esempio convertire il timestamp in un oggetto Date ed estrarre campi utili).

Qual è il modo migliore per farlo, massimizzando il throughput? C'è un modo che non si blocchi sulla lettura di ogni riga o sull'invio a Cube?

Terzo: immagino che l'uso di divisioni di stringhe e l'equivalente JS di contiene (IndexOf! = -1?) Sarà molto più veloce delle regex? Qualcuno ha avuto molta esperienza nell'analisi di enormi quantità di dati di testo in Node.js?

Salute, Victor


Ho creato un parser di log nel nodo che accetta un mucchio di stringhe di espressioni regolari con "acquisizioni" integrate e output in JSON. Puoi anche chiamare funzioni su ogni acquisizione se vuoi eseguire un calcolo. Potrebbe fare quello che vuoi: npmjs.org/package/logax
Jess

Risposte:


209

Ho cercato una soluzione per analizzare riga per riga file molto grandi (gbs) utilizzando un flusso. Tutte le librerie e gli esempi di terze parti non soddisfacevano le mie esigenze poiché elaboravano i file non riga per riga (come 1, 2, 3, 4 ..) o leggevano l'intero file in memoria

La seguente soluzione può analizzare file molto grandi, riga per riga, utilizzando stream e pipe. Per i test ho utilizzato un file da 2.1 GB con 17.000.000 di record. L'utilizzo della RAM non ha superato i 60 MB.

Innanzitutto, installa il pacchetto event-stream :

npm install event-stream

Poi:

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

inserisci qui la descrizione dell'immagine

Per favore fammi sapere come va!


6
Cordiali saluti, questo codice non è sincrono. È asincrono. Se si inserisce console.log(lineNr)dopo l'ultima riga del codice, non verrà visualizzato il conteggio delle righe finali perché il file viene letto in modo asincrono.
jfriend00

4
Grazie, questa è stata l'unica soluzione che sono riuscito a trovare che in realtà è stata messa in pausa e ripresa quando avrebbe dovuto. Readline no.
Brent

3
Fantastico esempio, e in realtà si ferma. Inoltre, se decidi di interrompere la lettura del file in anticipo, puoi utilizzares.end();
zipzit

2
Ha funzionato come un fascino. Utilizzato per indicizzare 150 milioni di documenti nell'indice elasticsearch. readlineil modulo è un dolore. Non si interrompe e causava guasti ogni volta dopo 40-50 milioni. Ho sprecato un giorno. Grazie mille per la risposta. Questo funziona perfettamente
Mandeep Singh

3
event-stream è stato compromesso: medium.com/intrinsic/… ma 4+ è apparentemente sicuro blog.npmjs.org/post/180565383195/…
John Vandivier

72

Puoi usare il readlinepacchetto integrato , vedi la documentazione qui . Uso stream per creare un nuovo flusso di output.

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

L'elaborazione dei file di grandi dimensioni richiederà del tempo. Dimmi se funziona.


2
Come scritto, la penultima riga fallisce perché cubestuff non è definito.
Greg

2
Utilizzando readline, è possibile mettere in pausa / riprendere il flusso di lettura per eseguire azioni asincrone nell'area "fare le cose"?
jchook

1
@jchook readlinemi stava dando molti problemi quando ho provato a mettere in pausa / riprendere. Non interrompe correttamente lo streaming creando molti problemi se il processo a valle è più lento
Mandeep Singh

31

Mi è davvero piaciuta la risposta di @gerard che in realtà merita di essere la risposta corretta qui. Ho apportato alcuni miglioramenti:

  • Il codice è in una classe (modulare)
  • L'analisi è inclusa
  • La possibilità di riprendere è data all'esterno nel caso in cui ci sia un lavoro asincrono è concatenato alla lettura del CSV come l'inserimento nel DB, o una richiesta HTTP
  • Lettura in blocchi / dimensioni di batche che l'utente può dichiarare. Mi sono occupata anche della codifica nello stream, nel caso avessi file con codifiche diverse.

Ecco il codice:

'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

Quindi, in pratica, ecco come lo userai:

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

L'ho testato con un file CSV da 35 GB e ha funzionato per me ed è per questo che ho scelto di costruirlo sulla risposta di @gerard , i feedback sono i benvenuti.


quanto tempo ci è voluto?
Z. Khullah,

A quanto pare, questo manca di pause()chiamata, vero?
Vanuan

Inoltre, questo non chiama la funzione di callback alla fine. Quindi, se batchSize è 100, la dimensione dei file è 150, verranno elaborati solo 100 elementi. Ho sbagliato?
Vanuan

16

Ho usato https://www.npmjs.com/package/line-by-line per leggere più di 1.000.000 di righe da un file di testo. In questo caso, la capacità occupata della RAM era di circa 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.
    });

'riga per riga' è più efficiente in termini di memoria rispetto alla risposta selezionata. Per 1 milione di righe in un csv la risposta selezionata aveva il mio processo di nodo negli 800 di megabyte. Usando "riga per riga" era costantemente nei 700 bassi. Questo modulo mantiene anche il codice pulito e di facile lettura. In totale avrò bisogno di leggere circa 18 milioni quindi ogni MB conta!
Neo

è un peccato che questo usi la propria "linea" di eventi invece del "blocco" standard, il che significa che non sarà possibile utilizzare la "pipe".
Rene Wooller,

Dopo ore di test e ricerche questa è l'unica soluzione che si ferma effettivamente sul lr.cancel()metodo. Legge le prime 1000 righe di un file 5Gig in 1 ms. Eccezionale!!!!
Perez Lamed van Niekerk il

6

Oltre a leggere il file grande riga per riga, puoi anche leggerlo pezzo per pezzo. Per ulteriori informazioni fare riferimento a questo articolo

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

Potrebbe essere che il seguente dovrebbe essere un confronto invece di un compito if(bytesRead = chunkSize):?
Stefan Rein,

4

La documentazione di Node.js offre un esempio molto elegante utilizzando il modulo Readline.

Esempio: lettura del flusso di file riga per riga

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

Nota: utilizziamo l'opzione crlfDelay per riconoscere tutte le istanze di CR LF ('\ r \ n') come una singola interruzione di riga.


3

Ho ancora avuto lo stesso problema. Dopo aver confrontato diversi moduli che sembrano avere questa caratteristica, ho deciso di farlo da solo, è più semplice di quanto pensassi.

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

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

Copre il file aperto in una chiusura, quello fetchBlock() restituito recupererà un blocco dal file, terminerà diviso in array (tratterà il segmento dall'ultimo recupero).

Ho impostato la dimensione del blocco su 1024 per ogni operazione di lettura. Potrebbe avere bug, ma la logica del codice è ovvia, provalo tu stesso.


2

node-byline utilizza flussi, quindi preferirei quello per i tuoi file enormi.

per le tue conversioni di data userei moment.js .

per massimizzare il tuo throughput potresti pensare di utilizzare un software-cluster. ci sono alcuni bei moduli che avvolgono abbastanza bene il modulo cluster nativo del nodo. mi piace cluster-master di isaacs. ad esempio potresti creare un cluster di x worker che calcolano tutti un file.

per il benchmarking di split vs regex usa benchmark.js . non l'ho provato fino ad ora. benchmark.js è disponibile come modulo nodo


2

Sulla base di questa risposta alle domande ho implementato una classe che puoi utilizzare per leggere un file in modo sincrono riga per riga con fs.readSync(). Puoi fare questa "pausa" e "riprendere" utilizzando una Qpromessa ( jQuerysembra che richieda un DOM quindi non posso eseguirlo con 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

Ho creato un modulo nodo per leggere file di grandi dimensioni in modo asincrono, testo o JSON. Testato su file di grandi dimensioni.

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

Basta salvare il file come file-reader.js e usarlo in questo modo:

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

7
Sembra che tu abbia copiato dalla risposta di Gerard. Dovresti dare credito a Gerard per la parte che hai copiato.
Paul Lynch
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.