Разбираем большой файл JSON в Nodejs


99

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

Ответы:


82

Чтобы обработать файл построчно, вам просто нужно разделить чтение файла и код, который действует на этот ввод. Вы можете добиться этого, буферизовав ввод, пока не достигнете новой строки. Предполагая, что у нас есть один объект 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 против CRLF), а затем вызывает JSON.parseодну строку. На этом этапе вы можете делать со своим объектом все, что вам нужно.

Обратите внимание, что JSON.parseэто строго в отношении того, что он принимает в качестве входных данных; вы должны заключать свои идентификаторы и строковые значения в двойные кавычки . Другими словами, {name:'thing1'}выдаст ошибку; вы должны использовать {"name":"thing1"}.

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


12
Этот ответ теперь является избыточным. Используйте JSONStream, и у вас будет готовая поддержка.
arcseldon

2
Имя функции "процесс" неправильное. "процесс" должен быть системной переменной. Этот баг сбивал меня с толку часами.
Чжигун Ли

19
@arcseldon Я не думаю, что тот факт, что есть библиотека, которая делает это, делает этот ответ излишним. Конечно, все еще полезно знать, как это можно сделать без модуля.
Кевин Б.

3
Я не уверен, сработает ли это для уменьшенного файла json. Что, если бы весь файл был заключен в одну строку и использование таких разделителей было бы невозможно? Как же тогда решить эту проблему?
SLearner

8
Как вы знаете, сторонние библиотеки не созданы по волшебству. Они точно такие же, как этот ответ, разработанные версии решений, скрученных вручную, но просто упакованные и помеченные как программа. Понимание того, как все работает, гораздо важнее и актуальнее, чем слепое добавление данных в библиотеку в ожидании результатов. Просто говорю :)
zanona

36

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

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

  • JSONStream "потоковая передача JSON.parse and 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"
}

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

2
В основном это верно и полезно, но я думаю, вам нужно это сделать, parse('*')иначе вы не получите никаких данных.
Джон Цвинк

@JohnZwinck Спасибо, обновили ответ и добавили рабочий пример, чтобы полностью продемонстрировать его.
arcseldon 02

в первом блоке кода var getStream() = function () {следует убрать первый набор скобок .
givemesnacks

1
Это не удалось из-за ошибки нехватки памяти с файлом json размером 500 МБ.
Кейт Джон Хатчисон,

18

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

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

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

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


18

У меня было аналогичное требование: мне нужно прочитать большой файл 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

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

Я решил эту проблему с помощью модуля split npm . Разделите свой поток на split, и он «разбивает поток и собирает его так, чтобы каждая строка была фрагментом ».

Образец кода:

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.

ОБНОВЛЕНИЕ : вы можете использовать инструмент mongoimport для импорта данных JSON в MongoDB.

mongoimport --collection collection --file collection.json

1
Это не отвечает на вопрос. Обратите внимание, что во второй строке вопроса говорится, что он хочет сделать это, чтобы получить данные в базе данных .
josh3736 08

mongoimport импортирует только файлы размером до 16 МБ.
Haziq Ahmed
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.