2012-08-08 182 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! 
}