在 Nodejs 中解析大型 JSON 文件

IT技术 javascript json file node.js
2021-01-18 04:45:30

我有一个文件,它以 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!");
});*/

请注意,我希望防止将整个文件读入内存。时间效率对我来说并不重要。是的,我可以尝试一次读取多个对象并一次插入它们,但这是一个性能调整 - 我需要一种保证不会导致内存过载的方法,无论文件中包含多少个对象.

我可以选择使用FormatAFormatB其他东西,请在您的答案中指定。谢谢!

6个回答

要逐行处理文件,您只需要将文件的读取与作用于该输入的代码分离。您可以通过缓冲输入直到遇到换行符来完成此操作。假设我们每行有一个 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"}.

因为一次在内存中不会超过一大块数据,所以这将是非常高效的内存。它也会非常快。快速测试显示我在 15 毫秒内处理了 10,000 行。

这个答案现在是多余的。使用 JSONStream,您将获得开箱即用的支持。
2021-03-16 04:45:30
您知道,第三方库不是由魔法组成的。它们就像这个答案,精心制作的手卷解决方案,但只是打包并标记为程序。与盲目地将数据扔进图书馆期待结果相比,了解事物的运作方式更为重要和相关。只是说:)
2021-03-27 04:45:30
函数名称“进程”不好。'process' 应该是一个系统变量。这个错误让我困惑了几个小时。
2021-03-28 04:45:30
@arcseldon 我不认为有一个库可以做到这一点这一事实使这个答案变得多余。知道如何在没有module的情况下完成这项工作当然仍然很有用。
2021-04-09 04:45:30
我不确定这是否适用于缩小的 json 文件。如果整个文件都包含在一行中,并且无法使用任何此类分隔符怎么办?那我们怎么解决这个问题呢?
2021-04-10 04:45:30

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

原来有。

  • JSONStream “流式传输 JSON.parse 和 stringify”

因为我刚刚找到它,我显然没有使用过它,所以我不能评论它的质量,但我很想知道它是否有效。

考虑以下 Javascript 和_.isString

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

如果流是一个对象数组,这将在对象进入时记录它们。因此,唯一被缓冲的是一次一个对象。

这正是我 2 天以来一直在寻找的东西!非常感谢 :)
2021-04-10 04:45:30

截至 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

数据.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
在第一个代码块中,var getStream() = function () {应该删除第一组括号
2021-03-15 04:45:30
@JohnZwinck 谢谢,更新了答案,并添加了一个工作示例来充分展示它。
2021-03-26 04:45:30
这失败了 500mb json 文件的内存不足错误。
2021-03-28 04:45:30
这大部分是正确且有用的,但我认为您需要这样做,parse('*')否则您将无法获得任何数据。
2021-03-30 04:45:30

我有类似的需求,我需要在 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', ... )”“完成”/“关闭”流后应该发生什么。
2021-04-10 04:45:30
嘿 - 这是一个很好的解决方案,但您的代码中有一种类型。你有一个括号在 [code]function end ()[/code] 之前关闭 - 但你需要在之后移动它 - 否则 end () 不包含在 es.through() 中。
2021-04-12 04:45:30

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

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

在第一个测试中,我使用var data = require('./geo.json'). 这需要 3330 毫秒,然后打印出每个特征的属性需要 804 毫秒,总计 4134 毫秒。然而,node.js 似乎使用了 411MB 的内存。

在第二个测试中,我将@arcseldon 的答案与 JSONStream + 事件流一起使用。我修改了 JSONPath 查询以仅选择我需要的内容。这一次内存没有超过 82MB,然而,整个事情现在需要 70 秒才能完成!