Phân tích cú pháp tệp JSON lớn trong Nodejs


98

Tôi có một tệp lưu trữ nhiều đối tượng JavaScript ở dạng JSON và tôi cần đọc tệp, tạo từng đối tượng và làm gì đó với chúng (chèn chúng vào db trong trường hợp của tôi). Các đối tượng JavaScript có thể được biểu diễn theo định dạng:

Định dạng A:

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

hoặc Định dạng B:

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

Lưu ý rằng ...dấu chỉ ra rất nhiều đối tượng JSON. Tôi biết rằng tôi có thể đọc toàn bộ tệp vào bộ nhớ và sau đó sử dụng JSON.parse()như sau:

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

Tuy nhiên, tệp có thể thực sự lớn, tôi muốn sử dụng luồng để thực hiện điều này. Vấn đề tôi gặp phải với luồng là nội dung tệp có thể bị chia thành các phần dữ liệu tại bất kỳ thời điểm nào, vậy làm cách nào để sử dụng JSON.parse()trên các đối tượng như vậy?

Lý tưởng nhất là mỗi đối tượng sẽ được đọc như một đoạn dữ liệu riêng biệt, nhưng tôi không chắc về cách thực hiện điều đó .

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!");
});*/

Lưu ý, tôi muốn ngăn việc đọc toàn bộ tệp vào bộ nhớ. Hiệu quả về thời gian đối với tôi không thành vấn đề. Có, tôi có thể cố gắng đọc một số đối tượng cùng một lúc và chèn tất cả chúng cùng một lúc, nhưng đó là một tinh chỉnh về hiệu suất - tôi cần một cách được đảm bảo không gây quá tải bộ nhớ, bất kể có bao nhiêu đối tượng trong tệp .

Tôi có thể chọn sử dụng FormatAhoặc FormatBhoặc có thể cái gì khác, chỉ cần bạn ghi rõ trong câu trả lời. Cảm ơn!


Đối với định dạng B, bạn có thể phân tích cú pháp qua từng đoạn cho các dòng mới và trích xuất từng dòng, nối phần còn lại nếu nó bị cắt ở giữa. Có thể có một cách thanh lịch hơn. Tôi chưa làm việc với các luồng nhiều.
travis

Câu trả lời:


82

Để xử lý từng dòng một tệp, bạn chỉ cần tách phần đọc của tệp và mã hoạt động trên đầu vào đó. Bạn có thể thực hiện điều này bằng cách đệm đầu vào của mình cho đến khi đạt được dòng mới. Giả sử chúng ta có một đối tượng JSON trên mỗi dòng (về cơ bản, định dạng 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!
    }
}

Mỗi khi luồng tệp nhận được dữ liệu từ hệ thống tệp, nó được lưu trữ trong bộ đệm và sau đó pumpđược gọi.

Nếu không có dòng mới trong bộ đệm, pumpchỉ cần quay lại mà không cần làm gì cả. Nhiều dữ liệu hơn (và có thể là một dòng mới) sẽ được thêm vào bộ đệm vào lần tới khi dòng lấy dữ liệu và khi đó chúng ta sẽ có một đối tượng hoàn chỉnh.

Nếu có dòng mới, hãy pumpcắt bộ đệm từ đầu đến dòng mới và chuyển nó sang process. Sau đó, nó sẽ kiểm tra lại nếu có một dòng mới khác trong bộ đệm ( whilevòng lặp). Bằng cách này, chúng tôi có thể xử lý tất cả các dòng đã được đọc trong đoạn hiện tại.

Cuối cùng, processđược gọi một lần trên mỗi dòng đầu vào. Nếu có, nó sẽ loại bỏ ký tự xuống dòng (để tránh các vấn đề với phần cuối dòng - LF vs CRLF), và sau đó gọi JSON.parsemột dòng. Tại thời điểm này, bạn có thể làm bất cứ điều gì bạn cần với đối tượng của mình.

Lưu ý rằng JSON.parsenghiêm ngặt về những gì nó chấp nhận làm đầu vào; bạn phải trích dẫn số nhận dạng và giá trị chuỗi bằng dấu ngoặc kép . Nói cách khác, {name:'thing1'}sẽ ném ra một lỗi; bạn phải sử dụng {"name":"thing1"}.

Bởi vì không quá một phần dữ liệu sẽ được lưu trong bộ nhớ tại một thời điểm, điều này sẽ cực kỳ hiệu quả về bộ nhớ. Nó cũng sẽ cực kỳ nhanh chóng. Một thử nghiệm nhanh cho thấy tôi đã xử lý 10.000 hàng trong thời gian dưới 15ms.


12
Câu trả lời này bây giờ là thừa. Sử dụng JSONStream và bạn có hỗ trợ ngoài hộp.
arcseldon,

2
Tên chức năng 'process' không hợp lệ. 'process' phải là một biến hệ thống. Lỗi này khiến tôi bối rối trong nhiều giờ.
Zhigong Li

17
@arcseldon Tôi không nghĩ rằng thực tế là có một thư viện làm việc này khiến câu trả lời này trở nên thừa. Chắc chắn vẫn hữu ích khi biết cách này có thể được thực hiện mà không cần mô-đun.
Kevin B

3
Tôi không chắc liệu điều này có hoạt động đối với tệp json được rút gọn hay không. Điều gì sẽ xảy ra nếu toàn bộ tệp được gói gọn trong một dòng và không thể sử dụng bất kỳ dấu phân tách nào như vậy? Làm thế nào để chúng tôi giải quyết vấn đề này sau đó?
SLearner

7
Thư viện của bên thứ ba không được làm bằng ma thuật mà bạn biết. Chúng giống như câu trả lời này, các phiên bản công phu của các giải pháp được cuộn bằng tay, nhưng chỉ được đóng gói và dán nhãn như một chương trình. Hiểu cách mọi thứ hoạt động quan trọng và phù hợp hơn nhiều so với việc ném dữ liệu vào thư viện một cách mù quáng để mong đợi kết quả. Chỉ nói :)
zanona

34

Cũng như tôi đã nghĩ rằng sẽ rất thú vị khi viết một trình phân tích cú pháp JSON phát trực tuyến, tôi cũng nghĩ rằng có lẽ tôi nên tìm kiếm nhanh để xem liệu đã có cái nào chưa.

Hóa ra là có.

  • JSONStream "phát trực tuyến JSON.parse and stringify"

Vì tôi vừa mới tìm thấy nó, rõ ràng là tôi chưa sử dụng nó, vì vậy tôi không thể nhận xét về chất lượng của nó, nhưng tôi sẽ quan tâm đến việc xem nó có hoạt động hay không.

Nó hoạt động xem xét Javascript sau và _.isString:

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

Điều này sẽ ghi lại các đối tượng khi chúng đi vào nếu luồng là một mảng các đối tượng. Do đó, thứ duy nhất được lưu vào bộ đệm là một đối tượng tại một thời điểm.


29

Kể từ tháng 10 năm 2014 , bạn chỉ có thể làm những việc như sau (sử dụng 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
});

Để chứng minh bằng một ví dụ làm việc:

npm install JSONStream event-stream

data.json:

{
  "greeting": "hello world"
}

xin chào.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
Điều này hầu hết đúng và hữu ích, nhưng tôi nghĩ bạn cần phải làm nếu không parse('*')bạn sẽ không nhận được bất kỳ dữ liệu nào.
John Zwinck

@JohnZwinck Cảm ơn bạn, đã cập nhật câu trả lời và thêm một ví dụ làm việc để minh họa đầy đủ.
arcseldon

trong khối mã đầu tiên, tập hợp các dấu ngoặc đơn đầu tiên var getStream() = function () {nên được loại bỏ.
givemesnacks

1
Việc này không thành công với lỗi hết bộ nhớ với tệp json 500mb.
Keith John Hutchison

18

Tôi nhận thấy rằng bạn muốn tránh đọc toàn bộ tệp JSON vào bộ nhớ nếu có thể, tuy nhiên nếu bạn có bộ nhớ thì đó có thể không phải là một ý tưởng tồi về hiệu suất. Việc sử dụng request () của node.js trên tệp json sẽ tải dữ liệu vào bộ nhớ rất nhanh.

Tôi đã chạy hai bài kiểm tra để xem hiệu suất trông như thế nào khi in ra một thuộc tính từ mỗi tính năng từ tệp geojson 81MB.

Trong thử nghiệm đầu tiên, tôi đọc toàn bộ tệp geojson vào bộ nhớ bằng cách sử dụng var data = require('./geo.json'). Điều đó mất 3330 mili giây và sau đó in ra một thuộc tính từ mỗi tính năng mất 804 mili giây trong tổng số 4134 mili giây. Tuy nhiên, có vẻ như node.js đang sử dụng 411MB bộ nhớ.

Trong thử nghiệm thứ hai, tôi đã sử dụng câu trả lời của @ arcseldon với JSONStream + event-stream. Tôi đã sửa đổi truy vấn JSONPath để chỉ chọn những gì tôi cần. Lần này bộ nhớ không bao giờ cao hơn 82MB, tuy nhiên, toàn bộ hiện tại mất 70 giây để hoàn thành!


18

Tôi có yêu cầu tương tự, tôi cần đọc một tệp json lớn trong nút js và xử lý dữ liệu theo từng phần và gọi một api và lưu trong mongodb. inputFile.json giống như:

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

Bây giờ tôi đã sử dụng JsonStream và EventStream để đạt được điều này một cách đồng bộ.

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

Cảm ơn bạn rất nhiều vì đã bổ sung câu trả lời của bạn, trường hợp của tôi cũng cần một số xử lý đồng bộ. Tuy nhiên, sau khi thử nghiệm, tôi không thể gọi "end ()" như một lệnh gọi lại sau khi đường ống kết thúc. Tôi tin rằng điều duy nhất có thể làm là thêm một sự kiện, những gì sẽ xảy ra sau khi luồng 'kết thúc' / 'đóng' với ´fileStream.on ('close', ...) ´.
nonNumericalFloat

6

Tôi đã viết một mô-đun có thể làm điều này, được gọi là BFJ . Cụ thể, phương pháp này bfj.matchcó thể được sử dụng để chia một luồng lớn thành các phần JSON rời rạc:

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

Tại đây, bfj.matchtrả về một luồng chế độ đối tượng, có thể đọc được sẽ nhận các mục dữ liệu đã phân tích cú pháp và được chuyển 3 đối số:

  1. Luồng có thể đọc được chứa JSON đầu vào.

  2. Vị từ cho biết những mục nào từ JSON được phân tích cú pháp sẽ được đẩy đến luồng kết quả.

  3. Một đối tượng tùy chọn chỉ ra rằng đầu vào là JSON được phân tách bằng dòng mới (đây là để xử lý định dạng B từ câu hỏi, nó không bắt buộc đối với định dạng A).

Khi được gọi, bfj.matchsẽ phân tích cú pháp JSON từ độ sâu luồng đầu vào trước tiên, gọi vị từ với mỗi giá trị để xác định có đẩy mục đó vào luồng kết quả hay không. Vị từ được chuyển qua ba đối số:

  1. Khóa thuộc tính hoặc chỉ mục mảng (điều này sẽ undefineddành cho các mục cấp cao nhất).

  2. Giá trị của chính nó.

  3. Độ sâu của mục trong cấu trúc JSON (không đối với các mục cấp cao nhất).

Tất nhiên, một vị từ phức tạp hơn cũng có thể được sử dụng khi cần thiết theo yêu cầu. Bạn cũng có thể chuyển một chuỗi hoặc một biểu thức chính quy thay vì một hàm vị từ, nếu bạn muốn thực hiện các đối sánh đơn giản với các khóa thuộc tính.


4

Tôi đã giải quyết vấn đề này bằng cách sử dụng mô-đun npm phân tách . Chia luồng của bạn thành từng đoạn và nó sẽ " Chia nhỏ một luồng và tập hợp lại để mỗi dòng là một đoạn ".

Mã mẫu:

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

Nếu bạn có quyền kiểm soát tệp đầu vào và đó là một mảng đối tượng, bạn có thể giải quyết vấn đề này dễ dàng hơn. Sắp xếp để xuất tệp với mỗi bản ghi trên một dòng, như sau:

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

Đây vẫn là JSON hợp lệ.

Sau đó, sử dụng mô-đun dòng đọc node.js để xử lý chúng từng dòng một.

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

Tôi nghĩ bạn cần sử dụng một cơ sở dữ liệu. MongoDB là một lựa chọn tốt trong trường hợp này vì nó tương thích với JSON.

CẬP NHẬT : Bạn có thể sử dụng công cụ mongoimport để nhập dữ liệu JSON vào MongoDB.

mongoimport --collection collection --file collection.json

1
Điều này không trả lời câu hỏi. Lưu ý rằng dòng thứ hai của câu hỏi nói rằng anh ta muốn làm điều này để đưa dữ liệu vào cơ sở dữ liệu .
josh3736

mongoimport chỉ nhập kích thước tệp tối đa 16MB.
Haziq Ahmed
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.