Проаналізуйте великий файл JSON у Nodejs


98

У мене є файл, який зберігає багато об’єктів 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щось інше, просто вкажіть у своїй відповіді. Дякую!


Для формату B ви можете проаналізувати фрагмент нових рядків і витягнути кожен цілий рядок, об'єднавши решту, якщо він відрізається посередині. Хоча може бути і більш елегантний спосіб. Я багато не працював із потоками.
travis

Відповіді:


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
Назва функції 'process' є поганою. 'процес' повинен бути системною змінною. Цей клоп годинами бентежить мене.
Чжигун Лі

17
@arcseldon Я не думаю, що факт, що існує бібліотека, робить це, робить цю відповідь зайвою. Безумовно, все-таки корисно знати, як це можна зробити без модуля.
Кевін Б

3
Я не впевнений, чи спрацювало б це для мінімізованого файлу json. Що робити, якщо весь файл було загорнуто в один рядок, а використання таких роздільників було неможливим? Як ми вирішуємо цю проблему тоді?
SLearner

7
Сторонні бібліотеки не створені магією, яку ви знаєте. Вони так само, як ця відповідь, розроблені версії рулонних рішень, але просто упаковані та позначені як програма. Розуміння того, як все працює, є набагато важливішим та релевантнішим, ніж сліпо перекидати дані в бібліотеку, очікуючи результатів. Просто кажу :)
zanona

34

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

Виявляється, є.

  • JSONStream "потокове JSON.parse та стримування"

Оскільки я щойно знайшов її, я, очевидно, не використовував її, тому не можу коментувати її якість, але мені буде цікаво почути, чи працює вона.

Він працює, враховуючи наступний Javascript і _.isString:

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

Це буде реєструвати об'єкти під час їх надходження, якщо потік є масивом об'єктів. Тому буферується лише один предмет за один раз.


29

Станом на жовтень 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

у першому кодовому блоці var getStream() = function () {слід видалити перший набір дужок .
подарунки

1
Це не вдалось із помилкою пам'яті, що зберігається з файлом json 500mb.
Кіт Джон Хатчісон

18

Я розумію, що ви хочете уникати читання всього файлу JSON в пам'яті, якщо це можливо, однак, якщо у вас є пам'ять, можливо, це не буде поганою ідеєю. Використання node.js's requ () у файлі json дуже швидко завантажує дані в пам'ять.

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

У першому тесті я зачитував весь файл 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 (« закрити », ...) '.
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 . Створіть потік у розкол, і він " Розбить потік і зібрати його так, щоб кожен рядок був шматок ".

Приклад коду:

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.

Потім використовуйте модуль для читання 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

mongoimport лише імпортувати розмір файлу до 16 Мб.
Хазік Ахмед
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.