แยกวิเคราะห์ไฟล์ JSON ขนาดใหญ่ใน Nodejs


101

ฉันมีไฟล์ที่เก็บออบเจ็กต์ JavaScript จำนวนมากในรูปแบบ JSON และฉันต้องการอ่านไฟล์สร้างอ็อบเจ็กต์แต่ละรายการและทำอะไรบางอย่างกับพวกเขา (แทรกลงในฐานข้อมูลในกรณีของฉัน) วัตถุ JavaScript สามารถแสดงในรูปแบบ:

รูปแบบ A:

[{name: 'thing1'},
....
{name: 'thing999999999'}]

หรือรูปแบบ B:

{name: 'thing1'}         // <== My choice.
...
{name: 'thing999999999'}

สังเกตว่า...อ็อบเจ็กต์ JSON ระบุจำนวนมาก ฉันทราบว่าฉันสามารถอ่านไฟล์ทั้งหมดลงในหน่วยความจำได้แล้วใช้JSON.parse()ดังนี้:

fs.readFile(filePath, 'utf-8', function (err, fileContents) {
  if (err) throw err;
  console.log(JSON.parse(fileContents));
});

อย่างไรก็ตามไฟล์อาจมีขนาดใหญ่มากฉันต้องการใช้สตรีมเพื่อทำสิ่งนี้ให้สำเร็จ ปัญหาที่ฉันพบในสตรีมคือเนื้อหาของไฟล์อาจแตกออกเป็นส่วนข้อมูล ณ จุดใดก็ได้ดังนั้นฉันจะใช้JSON.parse()กับวัตถุดังกล่าวได้อย่างไร

จะเป็นการดีที่แต่ละวัตถุจะอ่านเป็นก้อนข้อมูลที่แยกกัน แต่ผมไม่แน่ใจว่าเกี่ยวกับวิธีการทำที่

var importStream = fs.createReadStream(filePath, {flags: 'r', encoding: 'utf-8'});
importStream.on('data', function(chunk) {

    var pleaseBeAJSObject = JSON.parse(chunk);           
    // insert pleaseBeAJSObject in a database
});
importStream.on('end', function(item) {
   console.log("Woot, imported objects into the database!");
});*/

หมายเหตุฉันต้องการป้องกันไม่ให้อ่านไฟล์ทั้งหมดลงในหน่วยความจำ ประสิทธิภาพของเวลาไม่สำคัญสำหรับฉัน ใช่ฉันสามารถลองอ่านวัตถุจำนวนหนึ่งพร้อมกันและแทรกทั้งหมดได้ในคราวเดียว แต่นั่นเป็นการปรับแต่งประสิทธิภาพ - ฉันต้องการวิธีที่รับประกันว่าจะไม่ทำให้หน่วยความจำมากเกินไปไม่ว่าจะมีวัตถุอยู่ในไฟล์กี่ชิ้นก็ตาม .

ฉันสามารถเลือกใช้FormatAหรือFormatBหรืออย่างอื่นได้โปรดระบุในคำตอบของคุณ ขอบคุณ!


สำหรับรูปแบบ B คุณสามารถแยกวิเคราะห์ผ่านกลุ่มสำหรับบรรทัดใหม่และแยกแต่ละบรรทัดออกจากกันโดยนำส่วนที่เหลือมาต่อกันหากตัดตรงกลางออก อาจมีวิธีที่สง่างามกว่านี้แม้ว่า ฉันไม่ได้ทำงานกับสตรีมมากนัก
travis

คำตอบ:


83

ในการประมวลผลไฟล์ทีละบรรทัดคุณเพียงแค่ต้องแยกการอ่านไฟล์และรหัสที่ทำงานกับอินพุตนั้น คุณสามารถทำได้โดยการบัฟเฟอร์อินพุตของคุณจนกว่าคุณจะขึ้นบรรทัดใหม่ สมมติว่าเรามีออบเจ็กต์ JSON หนึ่งรายการต่อบรรทัด (โดยทั่วไปคือรูปแบบ B):

var stream = fs.createReadStream(filePath, {flags: 'r', encoding: 'utf-8'});
var buf = '';

stream.on('data', function(d) {
    buf += d.toString(); // when data is read, stash it in a string buffer
    pump(); // then process the buffer
});

function pump() {
    var pos;

    while ((pos = buf.indexOf('\n')) >= 0) { // keep going while there's a newline somewhere in the buffer
        if (pos == 0) { // if there's more than one newline in a row, the buffer will now start with a newline
            buf = buf.slice(1); // discard it
            continue; // so that the next iteration will start with data
        }
        processLine(buf.slice(0,pos)); // hand off the line
        buf = buf.slice(pos+1); // and slice the processed data off the buffer
    }
}

function processLine(line) { // here's where we do something with a line

    if (line[line.length-1] == '\r') line=line.substr(0,line.length-1); // discard CR (0x0D)

    if (line.length > 0) { // ignore empty lines
        var obj = JSON.parse(line); // parse the JSON
        console.log(obj); // do something with the data here!
    }
}

ทุกครั้งที่สตรีมไฟล์ได้รับข้อมูลจากระบบไฟล์จะถูกเก็บไว้ในบัฟเฟอร์จากนั้นpumpจะถูกเรียก

หากไม่มีการขึ้นบรรทัดใหม่ในบัฟเฟอร์pumpเพียงแค่ส่งคืนโดยไม่ต้องทำอะไรเลย ข้อมูลเพิ่มเติม (และอาจขึ้นบรรทัดใหม่) จะถูกเพิ่มลงในบัฟเฟอร์ในครั้งต่อไปที่สตรีมได้รับข้อมูลจากนั้นเราจะมีวัตถุที่สมบูรณ์

หากมีการขึ้นบรรทัดใหม่ชิ้นปิดบัฟเฟอร์จากจุดเริ่มต้นที่จะขึ้นบรรทัดใหม่และมือมันออกไปpump processจากนั้นตรวจสอบอีกครั้งว่ามีการขึ้นบรรทัดใหม่ในบัฟเฟอร์ ( whileลูป) หรือไม่ ด้วยวิธีนี้เราสามารถประมวลผลบรรทัดทั้งหมดที่อ่านในกลุ่มปัจจุบัน

สุดท้ายprocessเรียกหนึ่งครั้งต่อบรรทัดอินพุต หากมีอยู่มันจะตัดอักขระส่งคืนแคร่ (เพื่อหลีกเลี่ยงปัญหาเกี่ยวกับการสิ้นสุดบรรทัด - LF vs CRLF) จากนั้นเรียกJSON.parseหนึ่งบรรทัด ณ จุดนี้คุณสามารถทำอะไรก็ได้กับวัตถุของคุณ

โปรดทราบว่าJSON.parseเคร่งครัดเกี่ยวกับสิ่งที่ยอมรับเป็นอินพุต คุณต้องพูดตัวบ่งชี้และค่าสายของคุณด้วยคำพูดสอง กล่าวอีกนัยหนึ่ง{name:'thing1'}จะทำให้เกิดข้อผิดพลาด {"name":"thing1"}คุณต้องใช้

เนื่องจากข้อมูลจะอยู่ในหน่วยความจำไม่เกินครั้งละจำนวนมากจึงทำให้หน่วยความจำมีประสิทธิภาพอย่างมาก นอกจากนี้ยังจะเร็วมาก การทดสอบอย่างรวดเร็วแสดงให้เห็นว่าฉันประมวลผล 10,000 แถวภายใต้ 15ms


12
คำตอบนี้ซ้ำซ้อน ใช้ JSONStream และคุณมีการสนับสนุนนอกกรอบ
arcseldon

2
ชื่อฟังก์ชัน "กระบวนการ" ไม่ถูกต้อง "กระบวนการ" ควรเป็นตัวแปรของระบบ ข้อผิดพลาดนี้ทำให้ฉันสับสนเป็นเวลาหลายชั่วโมง
Zhigong Li

21
@arcseldon ฉันไม่คิดว่าข้อเท็จจริงที่ว่ามีห้องสมุดที่ทำให้คำตอบนี้ซ้ำซ้อน ยังคงมีประโยชน์อย่างแน่นอนที่จะทราบว่าสามารถทำได้โดยไม่ต้องใช้โมดูล
Kevin B

3
ฉันไม่แน่ใจว่ามันจะใช้ได้กับไฟล์ json ที่ถูกย่อขนาดหรือไม่ จะเกิดอะไรขึ้นถ้าไฟล์ทั้งหมดถูกรวมไว้ในบรรทัดเดียวและไม่สามารถใช้ตัวคั่นดังกล่าวได้ แล้วเราจะแก้ปัญหานี้อย่างไร?
SLearner

10
ห้องสมุดของบุคคลที่สามไม่ได้สร้างจากเวทมนตร์ที่คุณรู้จัก พวกเขาเหมือนกับคำตอบนี้ซึ่งเป็นโซลูชันแบบรีดด้วยมือในเวอร์ชันที่ซับซ้อน แต่บรรจุและระบุว่าเป็นโปรแกรม การทำความเข้าใจว่าสิ่งต่างๆทำงานอย่างไรมีความสำคัญและเกี่ยวข้องมากกว่าการโยนข้อมูลลงในห้องสมุดแบบสุ่มสี่สุ่มห้าโดยหวังผล แค่พูด :)
zanona

36

เช่นเดียวกับที่ฉันคิดว่าการเขียนตัวแยกวิเคราะห์ JSON แบบสตรีมมิงจะเป็นเรื่องสนุกฉันก็คิดว่าฉันควรค้นหาอย่างรวดเร็วเพื่อดูว่ามีตัวแยกวิเคราะห์ที่พร้อมใช้งานหรือไม่

ปรากฎว่ามี

  • JSONStream "สตรีมมิง JSON.parse และ stringify"

เนื่องจากฉันเพิ่งค้นพบเห็นได้ชัดว่าฉันไม่ได้ใช้มันดังนั้นฉันจึงไม่สามารถแสดงความคิดเห็นเกี่ยวกับคุณภาพของมันได้ แต่ฉันจะสนใจที่จะฟังว่ามันใช้งานได้หรือไม่

ใช้งานได้พิจารณา Javascript ต่อไปนี้และ_.isString:

stream.pipe(JSONStream.parse('*'))
  .on('data', (d) => {
    console.log(typeof d);
    console.log("isString: " + _.isString(d))
  });

สิ่งนี้จะบันทึกวัตถุเมื่อเข้ามาหากสตรีมเป็นอาร์เรย์ของวัตถุ ดังนั้นสิ่งเดียวที่ถูกบัฟเฟอร์คือทีละวัตถุ


30

ในเดือนตุลาคม 2014คุณสามารถทำสิ่งต่อไปนี้ (โดยใช้ JSONStream) - https://www.npmjs.org/package/JSONStream

var fs = require('fs'),
    JSONStream = require('JSONStream'),

var getStream() = function () {
    var jsonData = 'myData.json',
        stream = fs.createReadStream(jsonData, { encoding: 'utf8' }),
        parser = JSONStream.parse('*');
    return stream.pipe(parser);
}

getStream().pipe(MyTransformToDoWhateverProcessingAsNeeded).on('error', function (err) {
    // handle any errors
});

เพื่อแสดงให้เห็นด้วยตัวอย่างการทำงาน:

npm install JSONStream event-stream

data.json:

{
  "greeting": "hello world"
}

สวัสดี js:

var fs = require('fs'),
    JSONStream = require('JSONStream'),
    es = require('event-stream');

var getStream = function () {
    var jsonData = 'data.json',
        stream = fs.createReadStream(jsonData, { encoding: 'utf8' }),
        parser = JSONStream.parse('*');
    return stream.pipe(parser);
};

getStream()
    .pipe(es.mapSync(function (data) {
        console.log(data);
    }));
$ node hello.js
// hello world

2
ส่วนใหญ่เป็นความจริงและมีประโยชน์ แต่ฉันคิดว่าคุณต้องทำมิparse('*')ฉะนั้นคุณจะไม่ได้รับข้อมูลใด ๆ
John Zwinck

@JohnZwinck ขอบคุณอัปเดตคำตอบและเพิ่มตัวอย่างการทำงานเพื่อแสดงให้เห็นอย่างเต็มที่
arcseldon

ในบล็อกโค้ดแรกvar getStream() = function () {ควรลบวงเล็บชุดแรกออก
givemesnacks

1
สิ่งนี้ล้มเหลวโดยมีข้อผิดพลาดหน่วยความจำไม่เพียงพอกับไฟล์ json ขนาด 500mb
Keith John Hutchison

19

ฉันมีข้อกำหนดที่คล้ายกันฉันต้องอ่านไฟล์ json ขนาดใหญ่ในโหนด js และประมวลผลข้อมูลเป็นกลุ่มและเรียกใช้ api และบันทึกใน mongodb inputFile.json เป็นเหมือน:

{
 "customers":[
       { /*customer data*/},
       { /*customer data*/},
       { /*customer data*/}....
      ]
}

ตอนนี้ฉันใช้ JsonStream และ EventStream เพื่อทำสิ่งนี้พร้อมกัน

var JSONStream = require("JSONStream");
var es = require("event-stream");

fileStream = fs.createReadStream(filePath, { encoding: "utf8" });
fileStream.pipe(JSONStream.parse("customers.*")).pipe(
  es.through(function(data) {
    console.log("printing one customer object read from file ::");
    console.log(data);
    this.pause();
    processOneCustomer(data, this);
    return data;
  }),
  function end() {
    console.log("stream reading ended");
    this.emit("end");
  }
);

function processOneCustomer(data, es) {
  DataModel.save(function(err, dataModel) {
    es.resume();
  });
}

ขอบคุณมากสำหรับการเพิ่มคำตอบกรณีของฉันยังต้องการการจัดการแบบซิงโครนัส อย่างไรก็ตามหลังจากการทดสอบแล้วฉันไม่สามารถเรียก "end ()" เป็นการเรียกกลับได้หลังจากที่ท่อเสร็จสิ้น ฉันเชื่อว่าสิ่งเดียวที่ทำได้คือการเพิ่มเหตุการณ์สิ่งที่ควรเกิดขึ้นหลังจากสตรีม 'เสร็จสิ้น' / 'ปิด' ด้วย´fileStream.on ('close', ... ) ´
nonNumericalFloat

18

ฉันตระหนักดีว่าคุณต้องการหลีกเลี่ยงการอ่านไฟล์ JSON ทั้งหมดลงในหน่วยความจำหากเป็นไปได้อย่างไรก็ตามหากคุณมีหน่วยความจำที่พร้อมใช้งานอาจไม่ใช่ความคิดที่ไม่ดีสำหรับประสิทธิภาพ การใช้ node.js require () บนไฟล์ json จะโหลดข้อมูลลงในหน่วยความจำเร็วมาก

ฉันทำการทดสอบสองครั้งเพื่อดูว่าประสิทธิภาพการทำงานเป็นอย่างไรในการพิมพ์แอตทริบิวต์จากแต่ละคุณลักษณะจากไฟล์ geojson ขนาด 81MB

ในการทดสอบครั้งที่ 1 ฉันอ่านไฟล์ geojson ทั้งหมดลงในหน่วยความจำโดยใช้ไฟล์var data = require('./geo.json'). ซึ่งใช้เวลา 3330 มิลลิวินาทีจากนั้นการพิมพ์แอตทริบิวต์จากแต่ละคุณลักษณะใช้เวลา 804 มิลลิวินาทีรวมเป็น 4134 มิลลิวินาที อย่างไรก็ตามดูเหมือนว่า node.js ใช้หน่วยความจำ 411MB

ในการทดสอบครั้งที่สองฉันใช้คำตอบของ @ arcseldon กับ JSONStream + event-stream ฉันแก้ไขแบบสอบถาม JSONPath เพื่อเลือกเฉพาะสิ่งที่ฉันต้องการ คราวนี้หน่วยความจำไม่เคยสูงเกิน 82MB แต่ตอนนี้ใช้เวลาทั้งหมด 70 วินาที!


6

ผมเขียนโมดูลที่สามารถทำเช่นนี้เรียกว่าBFJ โดยเฉพาะอย่างยิ่งวิธีนี้bfj.matchสามารถใช้เพื่อแยกสตรีมขนาดใหญ่ออกเป็นส่วนที่ไม่ต่อเนื่องของ JSON:

const bfj = require('bfj');
const fs = require('fs');

const stream = fs.createReadStream(filePath);

bfj.match(stream, (key, value, depth) => depth === 0, { ndjson: true })
  .on('data', object => {
    // do whatever you need to do with object
  })
  .on('dataError', error => {
    // a syntax error was found in the JSON
  })
  .on('error', error => {
    // some kind of operational error occurred
  })
  .on('end', error => {
    // finished processing the stream
  });

ที่นี่bfj.matchส่งคืนสตรีมโหมดออบเจ็กต์ที่อ่านได้ซึ่งจะรับรายการข้อมูลที่แยกวิเคราะห์และส่งผ่าน 3 อาร์กิวเมนต์:

  1. สตรีมที่อ่านได้ที่มีอินพุต JSON

  2. เพรดิเคตที่ระบุว่ารายการใดจาก JSON ที่แยกวิเคราะห์จะถูกพุชไปยังสตรีมผลลัพธ์

  3. อ็อบเจ็กต์อ็อพชันที่ระบุว่าอินพุตเป็น JSON ที่คั่นด้วยขึ้นบรรทัดใหม่ (นี่คือการประมวลผลรูปแบบ B จากคำถามซึ่งไม่จำเป็นสำหรับรูปแบบ A)

เมื่อถูกเรียกbfj.matchจะแยกวิเคราะห์ JSON จากความลึกของสตรีมอินพุตก่อนโดยเรียกเพรดิเคตพร้อมค่าแต่ละค่าเพื่อพิจารณาว่าจะพุชรายการนั้นไปยังสตรีมผลลัพธ์หรือไม่ เพรดิเคตถูกส่งผ่านสามอาร์กิวเมนต์:

  1. คีย์คุณสมบัติหรือดัชนีอาร์เรย์ (จะใช้undefinedสำหรับรายการระดับบนสุด)

  2. ค่าตัวเอง

  3. ความลึกของรายการในโครงสร้าง JSON (ศูนย์สำหรับรายการระดับบนสุด)

แน่นอนว่ายังสามารถใช้เพรดิเคตที่ซับซ้อนกว่านี้ได้ตามความจำเป็น คุณยังสามารถส่งสตริงหรือนิพจน์ทั่วไปแทนฟังก์ชันเพรดิเคตได้หากคุณต้องการทำการจับคู่แบบง่ายกับคีย์คุณสมบัติ


4

ฉันจะแก้ไขปัญหานี้โดยใช้โมดูลแยก NPM ต่อสายสตรีมของคุณให้แยกเป็นส่วน ๆ และมันจะ " แยกสตรีมและประกอบใหม่เพื่อให้แต่ละบรรทัดรวมกัน "

โค้ดตัวอย่าง:

var fs = require('fs')
  , split = require('split')
  ;

var stream = fs.createReadStream(filePath, {flags: 'r', encoding: 'utf-8'});
var lineStream = stream.pipe(split());
linestream.on('data', function(chunk) {
    var json = JSON.parse(chunk);           
    // ...
});

4

หากคุณสามารถควบคุมไฟล์อินพุตและเป็นอาร์เรย์ของออบเจ็กต์คุณสามารถแก้ปัญหานี้ได้ง่ายขึ้น จัดเรียงเพื่อส่งออกไฟล์พร้อมกับแต่ละระเบียนในหนึ่งบรรทัดดังนี้:

[
   {"key": value},
   {"key": value},
   ...

นี่ยังคงเป็น JSON ที่ถูกต้อง

จากนั้นใช้โมดูล readline node.js เพื่อประมวลผลทีละบรรทัด

var fs = require("fs");

var lineReader = require('readline').createInterface({
    input: fs.createReadStream("input.txt")
});

lineReader.on('line', function (line) {
    line = line.trim();

    if (line.charAt(line.length-1) === ',') {
        line = line.substr(0, line.length-1);
    }

    if (line.charAt(0) === '{') {
        processRecord(JSON.parse(line));
    }
});

function processRecord(record) {
    // Process the records one at a time here! 
}

-1

ฉันคิดว่าคุณต้องใช้ฐานข้อมูล MongoDB เป็นตัวเลือกที่ดีในกรณีนี้เนื่องจากเข้ากันได้กับ JSON

UPDATE : คุณสามารถใช้เครื่องมือmongoimportเพื่อนำเข้าข้อมูล JSON ไปยัง MongoDB

mongoimport --collection collection --file collection.json

1
สิ่งนี้ไม่ตอบคำถาม โปรดทราบว่าบรรทัดที่สองของคำถามที่เขาบอกว่าเขาต้องการที่จะทำเช่นนี้เพื่อให้ได้ข้อมูลลงในฐานข้อมูล
josh3736

mongoimport นำเข้าไฟล์ที่มีขนาดไม่เกิน 16MB เท่านั้น
Haziq Ahmed
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.