Разбор большого файла JSON в Nodejs

У меня есть файл, который хранит много объектов JavaScript в форме JSON, и мне нужно прочитать файл, создать каждый из объектов и сделать с ними что-то (вставьте их в db в моем случае). Объекты 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 или, возможно, что-то еще, просто укажите в своем ответе. Спасибо!

Ответ 1

Чтобы обрабатывать файл по строкам, вам просто нужно отделить чтение файла и код, который действует на этот вход. Вы можете выполнить это, буферизируя ввод, пока не нажмете новую строку. Предполагая, что у нас есть один объект 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 a newline somewhere in the buffer
        if (pos == 0) { // if there 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 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 строк в возрасте до 15 мс.

Ответ 2

Так же, как я думал, что было бы интересно написать потоковый анализатор JSON, я также подумал, что, возможно, мне следует выполнить быстрый поиск, чтобы увидеть, есть ли уже доступный.

Оказывается, есть.

  • JSONStream "потоковая передача JSON.parse и stringify"

Так как я только что нашел его, я, очевидно, не использовал его, поэтому я не могу комментировать его качество, но мне будет интересно узнать, работает ли он.

Это работает, рассмотрите следующий Javascript и _.isString:

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

Это будет регистрировать объекты по мере их поступления, если поток является массивом объектов. Поэтому единственное, что буферизуется, это один объект за раз.

Ответ 3

По состоянию на октябрь 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"
}

hello.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

Ответ 4

У меня было похожее требование, мне нужно прочитать большой файл 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();
  });
}

Ответ 5

Я понимаю, что вы хотите избежать чтения всего файла JSON в память, если это возможно, однако, если у вас есть доступная память, это может быть плохой идеей. Использование node.js require() в json файле загружает данные в память очень быстро.

Я провел два теста, чтобы увидеть, как выглядела производительность при печати атрибута из каждой функции из файла geojson 81MB.

В первом тесте я прочитал весь файл geojson в памяти, используя var data = require('./geo.json'). Это заняло 3330 миллисекунд, а затем распечатка атрибута из каждой функции заняла 804 миллисекунды в общей сложности 4134 миллисекунды. Однако оказалось, что node.js использует 411 МБ памяти.

Во втором тесте я использовал @arcseldon ответ с потоком событий JSONStream+. Я изменил запрос JSONPath, чтобы выбрать только то, что мне нужно. На этот раз память никогда не выходила выше 82 МБ, однако все это заняло 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 (ноль для элементов верхнего уровня).

Конечно, более сложный предикат также может быть использован по мере необходимости в соответствии с требованиями. Вы также можете передать строку или регулярное выражение вместо функции предиката, если хотите выполнить простое сопоставление с ключами свойства.

Ответ 7

Я решил эту проблему, используя split npm module. Труба вашего потока в раскол, и он будет "Разбить поток и собрать его, чтобы каждая строка была куском".

Пример кода:

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

Ответ 8

Если у вас есть контроль над входным файлом, и это массив объектов, вы можете решить это более легко. Расположите файл с каждой записью на одной строке, например:

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

Это все еще действует JSON.

Затем используйте модуль read 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! 
}

Ответ 9

Я думаю, вам нужно использовать базу данных. MongoDB является хорошим выбором в этом случае, потому что он совместим с JSON.

UPDATE: Вы можете использовать инструмент mongoimport для импорта данных JSON в MongoDB.

mongoimport --collection collection --file collection.json