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



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

1114   9  

9 ответов:

обработать файл построчно, вам просто нужно отвязать чтение файла и код, который действует на этот вход. Вы можете сделать это путем буферизации ввода, пока вы не нажмете новую строку. Предполагая, что у нас есть один объект 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 строк менее чем за 15 мс.

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

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

  • JSONStream " потоковое JSON.разобрать и stringify"

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

он работает рассмотрим следующий CoffeeScript:

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

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

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

данные.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

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

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

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

во втором тесте я использовал ответ @arcseldon с jsonstream + event-stream. Я изменил запрос JSONPath, чтобы выбрать только то, что мне нужно. На этот раз память никогда не поднималась выше 82 МБ, однако теперь все это заняло 70 секунд!

у меня было аналогичное требование, мне нужно прочитать большой файл json в узле js и обработать данные в кусках и вызвать api и сохранить в mongodb. входной_файл.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();
    });
}

Я решил эту проблему с помощью разделить модуль НПМ. Направьте свой поток в Сплит, и он будет"разбейте поток и соберите его так, чтобы каждая строка была куском".

пример кода:

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

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

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

это все еще действительный JSON.

затем используйте узел.JS readline модуль для обработки их по одной строке за раз.

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! 
}

я написал модуль, который может это сделать, называется 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. объект options, указывающий, что вход является JSON с разделителями новой строки (это для обработки формата B из вопроса, это не требуется для формата A).

по вызову bfj.match сначала проанализирует JSON из глубины входного потока, вызывая предикат с каждым значением, чтобы определить, следует ли переместить этот элемент в поток результатов. Предикат передается три аргументы:

  1. ключ свойства или индекс массива (это будет undefined для элементов верхнего уровня).

  2. само значение.

  3. глубина элемента в структуре JSON (ноль для элементов верхнего уровня).

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

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

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

mongoimport --collection collection --file collection.json

Comments

    Ничего не найдено.