2012-08-08 204 views
63

我有一个文件,它以JSON形式存储了许多JavaScript对象,我需要读取文件,创建每个对象,并对它们进行操作(在我的情况下将它们插入到数据库中) 。的JavaScript对象可被表示的格式:解析Nodejs中的大型JSON文件

格式答:

[{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或其他什么东西,请在您的答案中指定。谢谢!

+0

对于格式B,您可以通过块解析新行,并提取每个整行,如果在中间切断,则连接其余行。 虽然可能有更优雅的方式。我没有用过很多流。 – travis 2012-08-08 22:39:41

回答

57

要逐行处理文件,只需将文件读取和作用于该输入的代码分开。你可以通过缓冲你的输入来达到这个目的,直到你输入一个换行符。假设我们有每行一个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。如果存在,它将去掉回车符(以避免线结尾处的问题与LFLF相比较),然后调用JSON.parse作为一条线。在这一点上,你可以做任何你需要的东西与你的对象。

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

因为一次只有一块数据永远在内存中,这将是非常有效的内存。它也会非常快。一个快速测试表明我在15ms以内处理了10,000行。

+0

真的很好的答案,我发现这有用 - 谢谢。 – mrdnk 2012-12-15 11:17:14

+11

这个答案现在是多余的。使用JSONStream,你拥有开箱即用的支持。 – arcseldon 2014-07-12 05:45:44

+1

函数名称'process'不好。 'process'应该是一个系统变量。这个bug让我困惑了好几个小时。 – 2015-04-29 07:36:41

1

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

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

mongoimport --collection collection --file collection.json 
+0

这并不回答这个问题。请注意,问题的第二行表示他希望这样做*将数据导入数据库*。 – josh3736 2012-08-08 23:28:28

+1

josh3736,你是对的。我更新我的答案。 – 2012-08-08 23:39:56

27

正如我在想,这将是有趣的写流JSON解析器,我也想,也许我应该做一个快速的搜索,看看是否有一个已经可用。

原来有。

因为我只是觉得,我明明没有使用它,所以我不能在它的质量发表评论,但我有兴趣听听它是否有效。

它的工作考虑以下的CoffeeScript:

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

这将记录的对象,因为他们进来,如果流是对象的数组。因此,被缓冲的唯一东西是一次一个对象。

19

随着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 
+0

这大部分是真实有用的,但我认为你需要'解析('*')'或者你不会得到任何数据。 – 2014-10-02 02:42:16

+0

@JohnZwinck谢谢,已经更新了答案,并添加了一个工作示例来充分展示它。 – arcseldon 2014-10-02 11:23:04

+0

在第一个代码块中,第一组圆括号'var getStream()= function(){'应该被删除。 – givemesnacks 2015-07-30 16:20:37

3

我使用split npm module解决了这个问题。将流分成两部分,它将“分解流并重新组装,以便每行都是一个块”。

示例代码:

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);   
    // ... 
}); 
6

我有类似的要求,我需要读取数据块的节点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(); 
    }); 
} 
10

我知道你想避免读取整个JSON文件到内存如果可能的话,但是如果你有可用时,它可能不是一个坏主意性能明智的记忆。在json文件上使用node.js的require()可以非常快地将数据加载到内存中。

我运行了两个测试,看看在从81MB geojson文件中打印出每个功能的属性后,性能如何。

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

在第二个测试中,我使用了@ arcseldon的回答JSONStream +事件流。我修改了JSONPath查询来选择我所需要的。这次内存永远不会超过82MB,但是现在整个过程需要70秒才能完成!

1

如果您可以控制输入文件,并且它是一组对象,则可以更轻松地解决此问题。安排输出与每条记录在一条线上的文件,如下所示:

[ 
    {"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! 
}