Розбір величезних журналів у Node.js - читати по черзі


126

Мені потрібно здійснити розбір великих (5-10 Gb) логістів у Javascript / Node.js (я використовую 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), а потім перекачати ці дані в Cube ( https://github.com/square/cube ) за допомогою свого клієнта JS.

По-перше, який канонічний спосіб у «Вузлі» читати у файлі, рядок за рядком?

Здається, досить поширене питання в Інтернеті:

Дуже багато відповідей вказують на купу сторонніх модулів:

Однак це здається досить базовим завданням - напевно, існує чистий простий спосіб stdlib для читання в текстовому файлі, рядок за рядком?

По-друге, мені потім потрібно обробити кожен рядок (наприклад, перетворити часову позначку в об’єкт Date і витягти корисні поля).

Який найкращий спосіб зробити це, максимізуючи пропускну здатність? Чи є якийсь спосіб, який не блокується ні при читанні в кожному рядку, ні надсиланні його на Cube?

По-третє - я здогадуюсь, що використовує розбиття рядків, і еквівалент JS містить (IndexOf! = -1?) Буде набагато швидше, ніж регулярні вирази? Хтось мав великий досвід розбору величезної кількості текстових даних у Node.js?

Ура, Вікторе


Я побудував аналізатор журналу у вузлі, який займає купу рядків регулярних виразів із вбудованими 'захопленнями' та виводить на JSON. Ви навіть можете зателефонувати за функціями під час кожного захоплення, якщо хочете зробити вирахування. Це може робити те, що ви хочете: npmjs.org/package/logax
Джесс

Відповіді:


208

Я шукав рішення для розбору дуже великих файлів (gbs) рядок за рядком за допомогою потоку. Усі сторонні бібліотеки та приклади не відповідали моїм потребам, оскільки вони обробляли файли не по черзі (наприклад, 1, 2, 3, 4 ..) або читали весь файл у пам'яті

Наступне рішення може проаналізувати дуже великі файли, по черзі, використовуючи stream & pipe. Для тестування я використав файл 2,1 ГБ із записами 17 000 000. Використання оперативної пам'яті не перевищувало 60 мб.

По-перше, встановіть пакет event-stream :

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
FYI, цей код не є синхронним. Це асинхронно. Якщо ви вставляєте console.log(lineNr)після останнього рядка свого коду, він не відображатиме остаточний підрахунок рядка, оскільки файл читається асинхронно.
jfriend00

4
Дякую, це було єдиним рішенням, яке я міг знайти, що насправді призупинився і відновився, коли треба було. Readline не став.
Brent

3
Дивовижний приклад, і він насправді робить паузу. Крім того, якщо ви вирішите зупинити читання файлів на початку, ви можете скористатисяs.end();
zipzit

2
Працював як шарм. Використовували його для індексації 150 мільйонів документів для індексу еластичного пошуку. readlineмодуль - це біль. Він не робив пауз і завдавав провалу щоразу після 40-50 мільйонів. Втрачений день. Дякую за відповідь. Цей чудово працює
Mandeep Singh

3
Подія-потік було зірвано : medium.com/intrinsic/…, але 4+, мабуть, є безпечним blog.npmjs.org/post/180565383195/…
Джон

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
Як написано, другий до останнього рядка виходить з ладу, оскільки куб не визначено.
Грег

2
Використовуючи readline, чи можна призупинити / відновити потік читання для виконання асинхронних дій у зоні "робити речі"?
jchook

1
@jchook доставляв readlineмені багато проблем, коли я спробував призупинити / відновити. Це не призупиняє потік належним чином, створюючи велику проблему, якщо процес нижче за течією буде повільнішим
Mandeep Singh

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

Я перевірив це на CSV-файлі розміром 35 Гб, і він працював на мене, і тому я вирішив створити його на відповідь @gerard , відгуки вітаються.


скільки часу пройшло?
З. Хулла

По- видимому, це не вистачає pause()виклик, чи не так?
Вануан

Крім того, ця функція не повертає дзвінки в кінці. Отже, якщо batchSize 100, розмір файлів - 150, оброблятиметься лише 100 елементів. Я помиляюся?
Вануан

16

Я використовував https://www.npmjs.com/package/line-by-line для читання понад 1 000 000 рядків з текстового файлу. При цьому зайнята ємність оперативної пам’яті становила близько 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.
    });

"рядок за рядком" ефективніше пам'яті, ніж обрана відповідь. Для 1 мільйона рядків у csv обрана відповідь мала процес мого вузла в низьких 800 мегабайт. Використовуючи "рядок за рядком", це було незмінно в низьких 700-х. Цей модуль також зберігає код чистим та легким для читання. Загалом мені потрібно буде прочитати близько 18 мільйонів, так що кожен mb рахує!
Нео

прикро, що він використовує власну подію 'рядок' замість стандартної «шматки», тобто ви не зможете скористатися «трубою».
Рене Вуллер

Після годин тестування та пошуку це єдине рішення, яке фактично зупиняється на lr.cancel()методі. Читає перші 1000 рядків файлу 5Gig за 1 мс. Дивовижно !!!!
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):?
Стефан Рейн

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 працівників, який обчислює файл.

для бенчмаркінгу розбиттів та регексів використовуйте бенчмарк.js . я не перевіряв це до цих пір. benchmark.js доступний у вигляді вузла-модуля


2

Виходячи з цієї відповіді на запитання, я реалізував клас, який ви можете використовувати для синхронного читання файлу по черзі fs.readSync(). Ви можете зробити цю "паузу" та "відновити", використовуючи Qобіцянку ( 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
Я схоже на те, що ти скопіював відповідь Жерара. Ви повинні надати Джерарду кредит за частину, яку ви скопіювали.
Пол Лінч
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.