在Node.js中解析大型JSON文件


98

我有一个文件,该文件以JSON形式存储许多JavaScript对象,我需要读取该文件,创建每个对象并对其进行处理(以我为例将其插入到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,您可以解析块中的新行,然后提取每一行,如果其余部分在中间截断,则将其余行连接起来。不过,可能会有更优雅的方式。我没有太多使用流。
特拉维斯

Answers:


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 vs CRLF),然后将JSON.parse其中之一称为行。此时,您可以对对象执行任何操作。

注意,JSON.parse它接受什么作为输入是严格的。您必须用双引号将标识符和字符串值引起。换句话说,{name:'thing1'}会抛出错误;您必须使用{"name":"thing1"}

因为一次最多只能有一块数据存储在内存中,所以这将大大提高存储效率。这也将非常快。快速测试显示,我在15毫秒内处理了10,000行。


12
这个答案现在是多余的。使用JSONStream,您将获得开箱即用的支持。
arcseldon 2014年

2
函数名称“ process”错误。“过程”应该是系统变量。这个错误使我困惑了几个小时。
Zhigong Li

17
@arcseldon我不认为有一个库可以做到这一点,这一事实是多余的。知道在没有模块的情况下如何完成此操作当然仍然有用。
凯文B

3
我不确定这是否适用于缩小的json文件。如果将整个文件包装在一行中,并且无法使用任何此类定界符怎么办?那我们该如何解决这个问题呢?
SLearner 2015年

7
第三方库不是由您知道的魔术制成的。它们就像这个答案,手工解决方案的详尽版本,只是打包并标记为程序。与盲目地将数据放入期望结果的库相比,了解事物的工作原理更为重要和相关。只是
说说

34

正如我以为编写流式JSON解析器会很有趣一样,我也认为也许应该快速搜索一下是否已有一个可用的。

原来有。

自从我刚找到它以来,我显然没有使用过它,因此我无法评论其质量,但是我很想知道它是否有效。

考虑以下Javascript并可以正常工作_.isString

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

如果流是对象数组,则将记录对象进入时的状态。因此,唯一要缓冲的是一次仅一个对象。


29

2014年10月的,你可以做类似如下(使用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('*')否则您将无法获得任何数据。
John Zwinck

@JohnZwinck谢谢,已经更新了答案,并添加了一个工作示例来充分演示它。
arcseldon 2014年

在第一个代码块中,var getStream() = function () {应删除第一组括号。
givemesnacks

1
此操作失败,并显示500mb json文件出现内存不足错误。
基思·约翰·哈奇森

18

我意识到,如果可能的话,您要避免将整个JSON文件读取到内存中,但是,如果您有可用的内存,那么就性能而言可能不是一个坏主意。在json文件上使用node.js的require()可以将数据真正快速地加载到内存中。

我进行了两次测试,以查看从81MB geojson文件中的每个功能打印出属性时的性能如何。

在第一个测试中,我使用读取了整个geojson文件到内存中var data = require('./geo.json')。这花费了3330毫秒,然后从每个功能中打印出一个属性花费了804毫秒,总计4134毫秒。但是,似乎node.js正在使用411MB的内存。

在第二个测试中,我将@arcseldon的答案与JSONStream + event-stream一起使用。我修改了JSONPath查询以仅选择我需要的内容。这次的内存从未超过82MB,但是现在整个过程花了70秒!


18

我有类似的要求,我需要在node js中读取一个大的json文件,并分块处理数据,然后调用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. 一个options对象,指示输入为换行符分隔的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 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! 
}

-1

我认为您需要使用数据库。在这种情况下,MongoDB是一个不错的选择,因为它与JSON兼容。

更新:您可以使用mongoimport工具将JSON数据导入MongoDB。

mongoimport --collection collection --file collection.json

1
这不能回答问题。请注意,问题的第二行说他想这样做是为了将数据输入数据库
josh3736

mongoimport仅导入文件大小最大为16MB。
哈齐格·艾哈迈德
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.