Разбор большого файла 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 или, может быть, что-то еще, просто укажите в своем ответе. Спасибо!
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 аргумента:
читаемый поток, содержащий входной JSON.
предикат, который указывает, какие элементы из разобранного JSON будет передвинут в поток результатов.
объект options, указывающий, что вход является JSON с разделителями новой строки (это для обработки формата B из вопроса, это не требуется для формата A).
по вызову
bfj.matchсначала проанализирует JSON из глубины входного потока, вызывая предикат с каждым значением, чтобы определить, следует ли переместить этот элемент в поток результатов. Предикат передается три аргументы:
ключ свойства или индекс массива (это будет
undefinedдля элементов верхнего уровня).само значение.
глубина элемента в структуре JSON (ноль для элементов верхнего уровня).
конечно, более сложный предикат также может быть использован по мере необходимости в соответствии с требованиями. Вы также можете передать строку или регулярное выражение вместо функции сказуемого, если вы хотите выполнить простые совпадения с ключами свойств.
Я думаю, что вам нужно использовать базу данных. MongoDB является хорошим выбором в этом случае, потому что он совместим с JSON.
обновление: Вы можете использовать mongoimport инструмент для импорта данных JSON в MongoDB.
mongoimport --collection collection --file collection.json
Comments