การแยกไฟล์บันทึกขนาดใหญ่ใน Node.js - อ่านทีละบรรทัด


126

ฉันต้องการแยกวิเคราะห์ไฟล์บันทึกขนาดใหญ่ (5-10 Gb) ใน Javascript / Node.js (ฉันใช้ Cube)

Logline มีลักษณะดังนี้:

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 ของพวกเขา

ประการแรกวิธีที่ยอมรับใน Node ในการอ่านไฟล์ทีละบรรทัดคืออะไร?

ดูเหมือนจะเป็นคำถามที่พบบ่อยทางออนไลน์:

คำตอบจำนวนมากดูเหมือนจะชี้ไปที่โมดูลของบุคคลที่สามจำนวนมาก:

อย่างไรก็ตามสิ่งนี้ดูเหมือนจะเป็นงานพื้นฐานที่ค่อนข้างแน่นอนมีวิธีง่ายๆใน stdlib ในการอ่านไฟล์ข้อความทีละบรรทัด?

ประการที่สองฉันต้องประมวลผลแต่ละบรรทัด (เช่นแปลงเวลาประทับเป็นวัตถุวันที่และแยกช่องที่มีประโยชน์)

วิธีใดที่ดีที่สุดในการทำเช่นนี้เพื่อเพิ่มปริมาณงานสูงสุด มีวิธีใดบ้างที่จะไม่บล็อกการอ่านในแต่ละบรรทัดหรือในการส่งไปยัง Cube?

ประการที่สาม - ฉันคาดเดาโดยใช้การแยกสตริงและ JS ที่เทียบเท่ากับมี (IndexOf! = -1?) จะเร็วกว่า regexes มาก? มีใครมีประสบการณ์ในการแยกวิเคราะห์ข้อมูลข้อความจำนวนมากใน Node.js บ้าง?

ไชโยวิคเตอร์


ฉันสร้างตัวแยกวิเคราะห์บันทึกในโหนดที่ใช้สตริง regex จำนวนมากที่มี 'การจับภาพ' ในตัวและส่งออกไปยัง JSON คุณยังสามารถเรียกใช้ฟังก์ชันในการจับภาพแต่ละครั้งหากคุณต้องการคำนวณ มันอาจทำในสิ่งที่คุณต้องการ: npmjs.org/package/logax
Jess

คำตอบ:


209

ฉันค้นหาวิธีแก้ปัญหาเพื่อแยกวิเคราะห์ไฟล์ขนาดใหญ่มาก (gbs) ทีละบรรทัดโดยใช้สตรีม ไลบรารีและตัวอย่างของบุคคลที่สามทั้งหมดไม่ตรงกับความต้องการของฉันเนื่องจากพวกเขาประมวลผลไฟล์ไม่ได้ทีละบรรทัด (เช่น 1, 2, 3, 4 .. ) หรืออ่านไฟล์ทั้งหมดไปยังหน่วยความจำ

โซลูชันต่อไปนี้สามารถแยกวิเคราะห์ไฟล์ที่มีขนาดใหญ่มากทีละบรรทัดโดยใช้สตรีมและไปป์ สำหรับการทดสอบฉันใช้ไฟล์ 2.1 gb ที่มีระเบียน 17.000.000 การใช้งาน 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
FYI รหัสนี้ไม่ซิงโครนัส มันไม่ตรงกัน หากคุณแทรกconsole.log(lineNr)หลังบรรทัดสุดท้ายของโค้ดโค้ดจะไม่แสดงจำนวนบรรทัดสุดท้ายเนื่องจากไฟล์ถูกอ่านแบบอะซิงโครนัส
jfriend00

4
ขอบคุณนี่เป็นทางออกเดียวที่ฉันพบว่าจริงๆแล้วหยุดชั่วคราวและกลับมาทำงานต่อเมื่อมันควรจะเป็น Readline ไม่ได้
Brent

3
ตัวอย่างที่ยอดเยี่ยมและมันหยุดชั่วคราว นอกจากนี้หากคุณตัดสินใจที่จะหยุดอ่านไฟล์ก่อนกำหนดคุณสามารถใช้ได้s.end();
zipzit

2
ทำงานอย่างมีเสน่ห์ ใช้เพื่อจัดทำดัชนีเอกสาร 150 ล้านฉบับไปยังดัชนีการค้นหาแบบยืดหยุ่น readlineโมดูลคือความเจ็บปวด มันไม่หยุดชั่วคราวและทำให้เกิดความล้มเหลวทุกครั้งหลังจาก 40-50 ล้าน เสียเวลาไปหนึ่งวัน. ขอบคุณมากสำหรับคำตอบ อันนี้ทำงานได้อย่างสมบูรณ์
Mandeep Singh

3
event-stream ถูกบุกรุก: medium.com/intrinsic/…แต่ 4+ ปลอดภัยblog.npmjs.org/post/180565383195/…
John Vandivier

72

คุณสามารถใช้ inbuilt 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
Greg

2
การใช้readlineเป็นไปได้ไหมที่จะหยุดชั่วคราว / เปิดสตรีมการอ่านต่อเพื่อดำเนินการ async ในพื้นที่ "do stuff"
jchook

1
@jchook readlineทำให้ฉันมีปัญหามากมายเมื่อฉันพยายามหยุดชั่วคราว / เล่นต่อ มันไม่ได้หยุดสตรีมชั่วคราวอย่างเหมาะสมสร้างปัญหามากมายหากกระบวนการดาวน์สตรีมช้าลง
Mandeep Singh

31

ฉันชอบคำตอบของ@gerardซึ่งสมควรเป็นคำตอบที่ถูกต้องที่นี่ ฉันได้ทำการปรับปรุงบางอย่าง:

  • รหัสอยู่ในคลาส (โมดูลาร์)
  • รวมการแยกวิเคราะห์
  • ความสามารถในการดำเนินการต่อจะถูกมอบให้กับภายนอกในกรณีที่มีงานอะซิงโครนัสถูกผูกไว้กับการอ่าน CSV เช่นการแทรกลงใน DB หรือคำขอ 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 35GB และมันใช้ได้ผลสำหรับฉันและนั่นคือเหตุผลที่ฉันเลือกที่จะสร้างมันขึ้นมาจาก คำตอบของ@gerardยินดีรับการตอบกลับ


ใช้เวลาเท่าไหร่?
Z. Khullah

เห็นได้ชัดว่าสิ่งนี้ขาดการpause()โทรใช่หรือไม่?
Vanuan

นอกจากนี้ยังไม่เรียกใช้ฟังก์ชันการโทรกลับเมื่อสิ้นสุด ดังนั้นหาก batchSize เท่ากับ 100 ขนาดของไฟล์คือ 150 ระบบจะประมวลผลเพียง 100 รายการ ฉันผิดเหรอ?
Vanuan

16

ฉันใช้https://www.npmjs.com/package/line-by-lineเพื่ออ่านมากกว่า 1,000,000 บรรทัดจากไฟล์ข้อความ ในกรณีนี้ความจุ 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' มีประสิทธิภาพหน่วยความจำมากกว่าคำตอบที่เลือก สำหรับ 1 ล้านบรรทัดใน csv คำตอบที่เลือกมีกระบวนการโหนดของฉันใน 800 เมกะไบต์ต่ำ การใช้ 'line-by-line' เป็นไปอย่างสม่ำเสมอในช่วงยุค 700 ต่ำ ๆ โมดูลนี้ยังช่วยให้โค้ดสะอาดและอ่านง่าย โดยรวมแล้วฉันจะต้องอ่านประมาณ 18 ล้านดังนั้นทุก ๆ mb จึงมีค่า!
นีโอ

เป็นเรื่องน่าเสียดายที่ใช้ 'เส้น' เหตุการณ์ของตัวเองแทนที่จะเป็น 'ชิ้นส่วน' มาตรฐานซึ่งหมายความว่าคุณจะไม่สามารถใช้ประโยชน์จาก 'ท่อ' ได้
Rene Wooller

หลังจากทดสอบและค้นหาหลายชั่วโมงนี่เป็นทางออกเดียวที่หยุดlr.cancel()วิธีการจริง อ่าน 1,000 บรรทัดแรกของไฟล์ 5Gig ใน 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

ตัวอย่าง: อ่าน File Stream ทีละบรรทัด

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 moment.js

สำหรับการเพิ่มปริมาณงานของคุณให้สูงสุดคุณอาจนึกถึงการใช้คลัสเตอร์ซอฟต์แวร์ มีโมดูลที่ดีบางโมดูลที่ห่อหุ้มโมดูลคลัสเตอร์โหนดเนทีฟไว้ค่อนข้างดี ฉันชอบคลัสเตอร์มาสเตอร์จาก isaacs เช่นคุณสามารถสร้างกลุ่มคนงาน x ซึ่งคำนวณไฟล์ทั้งหมด

สำหรับการเปรียบเทียบแยก VS regexes ใช้benchmark.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
ดูเหมือนว่าคุณคัดลอกมาจากคำตอบของเจอราร์ด คุณควรให้เครดิตเจอราร์ดสำหรับส่วนที่คุณคัดลอก
Paul Lynch
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.