Nodejs'de büyük JSON dosyasını ayrıştırın


101

Birçok JavaScript nesnesini JSON biçiminde saklayan bir dosyam var ve dosyayı okumam, nesnelerin her birini oluşturmam ve onlarla bir şeyler yapmam gerekiyor (benim durumumda bunları bir db'ye ekleyin). JavaScript nesneleri bir formatta temsil edilebilir:

Biçim A:

[{name: 'thing1'},
....
{name: 'thing999999999'}]

veya B Biçimi:

{name: 'thing1'}         // <== My choice.
...
{name: 'thing999999999'}

...Çok sayıda JSON nesnesini gösterdiğini unutmayın . Dosyanın tamamını belleğe okuyabileceğimi ve ardından şu şekilde kullanabileceğimi biliyorum JSON.parse():

fs.readFile(filePath, 'utf-8', function (err, fileContents) {
  if (err) throw err;
  console.log(JSON.parse(fileContents));
});

Bununla birlikte, dosya gerçekten büyük olabilir, bunu başarmak için bir akış kullanmayı tercih ederim. Bir akışta gördüğüm sorun, dosya içeriklerinin herhangi bir noktada veri parçalarına bölünebilmesidir, bu nedenle bu JSON.parse()tür nesneler üzerinde nasıl kullanabilirim ?

İdeal olarak, her nesne ayrı bir veri parçası olarak okunacaktır, ancak bunu nasıl yapacağımdan emin değilim .

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!");
});*/

Not, tüm dosyanın hafızaya alınmasını engellemek istiyorum. Zaman verimliliği benim için önemli değil. Evet, aynı anda birkaç nesneyi okuyup hepsini birden yerleştirmeyi deneyebilirim, ancak bu bir performans ayarlaması - Dosyada kaç nesne olursa olsun, bellek aşırı yüklenmesine neden olmama garantili bir yönteme ihtiyacım var .

Ben kullanmayı seçebilir FormatAveya FormatBya da belki başka bir şey, sadece cevap belirtiniz. Teşekkürler!


B formatı için, yeni satırlar için öbekleri ayrıştırabilir ve her bir satırı, ortadan keserse geri kalanını birleştirerek tüm satırı ayıklayabilirsiniz. Yine de daha zarif bir yol olabilir. Akışlarla pek çalışmadım.
travis

Yanıtlar:


83

Bir dosyayı satır satır işlemek için, dosyanın okunmasını ve bu girdiye etki eden kodu ayırmanız yeterlidir. Bunu, bir satırsonu satırına gelene kadar girişinizi tamponlayarak gerçekleştirebilirsiniz. Her satırda bir JSON nesnemiz olduğunu varsayarsak (temelde B biçimi):

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!
    }
}

Dosya akışı dosya sisteminden her veri aldığında, bir arabellekte saklanır ve ardından pumpçağrılır.

Tamponda satırsonu yoksa, hiçbir pumpşey yapmadan geri döner. Akış bir dahaki sefere veri aldığında arabelleğe daha fazla veri (ve potansiyel olarak bir satırsonu) eklenecek ve ardından tam bir nesneye sahip olacağız.

Eğer bir satırsonu varsa pump, arabelleği baştan yeni satıra doğru keser ve ona aktarır process. Daha sonra tamponda başka bir satırsonu ( whiledöngü) olup olmadığını tekrar kontrol eder . Bu şekilde, mevcut parçada okunan tüm satırları işleyebiliriz.

Son olarak, processher giriş satırı için bir kez çağrılır. Varsa, satır sonu karakterini çıkarır (satır sonlarıyla ilgili sorunları önlemek için - LF - CRLF) ve ardından JSON.parsesatırdan birini çağırır . Bu noktada, nesnenizle ne yapmanız gerekiyorsa yapabilirsiniz.

JSON.parseGirdi olarak ne kabul ettiği konusunda katı olduğunu unutmayın ; tanımlayıcılarınızı ve dize değerlerinizi çift ​​tırnak içinde vermelisiniz . Başka bir deyişle, {name:'thing1'}bir hata atar; kullanmalısın {"name":"thing1"}.

Bir seferde bellekte bir veri yığınından fazlası olmayacağından, bu bellek açısından son derece verimli olacaktır. Aynı zamanda son derece hızlı olacak. Hızlı bir test, 15 ms'nin altında 10.000 satırı işlediğimi gösterdi.


12
Bu cevap artık gereksizdir. JSONStream kullanın ve kullanıma hazır bir desteğe sahip olun.
arcseldon

2
'İşlem' işlev adı kötü. 'süreç' bir sistem değişkeni olmalıdır. Bu hata saatlerce kafamı karıştırdı.
Zhigong Li

21
@arcseldon Bunu yapan bir kütüphane olduğu gerçeğinin bu yanıtı gereksiz kıldığını sanmıyorum. Bunun modül olmadan nasıl yapılacağını bilmek kesinlikle yararlıdır.
Mustafa B

3
Bunun küçültülmüş bir json dosyası için işe yarayıp yaramayacağından emin değilim. Ya tüm dosya tek bir satıra sarılmışsa ve bu tür sınırlayıcıları kullanmak mümkün değilse? O halde bu sorunu nasıl çözeriz?
SLearner

10
Üçüncü taraf kitaplıkları bildiğiniz sihirden yapılmamıştır. Tıpkı bu cevap gibiler, elle haddelenmiş çözümlerin ayrıntılı versiyonları, ancak bir program olarak paketlenmiş ve etiketlenmişler. İşlerin nasıl çalıştığını anlamak, sonuçları bekleyerek bir kütüphaneye körü körüne veri atmaktan çok daha önemlidir ve konuyla ilgilidir. Sadece :)
zanona

36

Bir akış JSON ayrıştırıcısı yazmanın eğlenceli olacağını düşündüğüm gibi, zaten mevcut olup olmadığını görmek için hızlı bir arama yapmam gerektiğini düşündüm.

Görünüşe göre var.

Onu yeni bulduğum için, belli ki kullanmadım, bu yüzden kalitesi hakkında yorum yapamam, ancak işe yarayıp yaramadığını öğrenmekle ilgileneceğim.

İşe yarıyor, aşağıdaki Javascript'i ve _.isString:

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

Bu, akış bir nesneler dizisi ise nesneleri geldiklerinde günlüğe kaydeder. Bu nedenle, arabelleğe alınan tek şey, her seferinde bir nesnedir.


30

Ekim 2014 itibariyle , aşağıdakine benzer bir şey yapabilirsiniz (JSONStream kullanarak) - 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
});

Çalışan bir örnekle göstermek için:

npm install JSONStream event-stream

data.json:

{
  "greeting": "hello world"
}

merhaba.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
Bu çoğunlukla doğru ve kullanışlıdır, ancak bence yapmanız gerekir, yoksa parse('*')herhangi bir veri alamazsınız.
John Zwinck

@JohnZwinck Teşekkürler, cevabı güncellediniz ve tam olarak göstermek için çalışan bir örnek eklediniz.
arcseldon

ilk kod bloğunda, ilk parantez seti var getStream() = function () {kaldırılmalıdır.
givemesnacks

1
Bu, 500mb'lik bir json dosyasında yetersiz bellek hatasıyla başarısız oldu.
Keith John Hutchison

19

Benzer bir gereksinimim vardı, js düğümünde büyük bir json dosyası okumam ve verileri yığınlar halinde işlemem ve bir api çağırıp mongodb'de kaydetmem gerekiyor. inputFile.json şuna benzer:

{
 "customers":[
       { /*customer data*/},
       { /*customer data*/},
       { /*customer data*/}....
      ]
}

Şimdi bunu eşzamanlı olarak başarmak için JsonStream ve EventStream kullandım.

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();
  });
}

Cevabınızı eklediğiniz için çok teşekkür ederim, benim durumum da bazı senkronize işlemlere ihtiyaç duydu. Ancak, test ettikten sonra, kanal bittikten sonra geri arama olarak "end ()" yi çağırmam mümkün olmadı. Yapılabilecek tek şeyin bir olay eklemek olduğuna inanıyorum, akış 'bittikten sonra' / 'fileStream.on (' close ', ...) ´ ile' kapatıldıktan sonra ne olması gerekir.
nonNumericalFloat

18

Mümkünse tüm JSON dosyasını belleğe okumaktan kaçınmak istediğinizi anlıyorum, ancak kullanılabilir belleğiniz varsa, performans açısından kötü bir fikir olmayabilir. Bir json dosyasında node.js'nin require () kullanılması, verileri belleğe gerçekten hızlı yükler.

81MB'lık bir geojson dosyasından her bir özellikten bir öznitelik yazdırırken performansın nasıl göründüğünü görmek için iki test yaptım.

1. testte tüm geojson dosyasını var data = require('./geo.json'). Kullanarak belleğe okudum . Bu 3330 milisaniye sürdü ve ardından her özellikten bir özniteliğin yazdırılması, toplamda 4134 milisaniye olmak üzere 804 milisaniye sürdü. Ancak, node.js 411 MB bellek kullanıyordu.

İkinci testte, @ arcseldon'un JSONStream + event-stream ile cevabını kullandım. JSONPath sorgusunu sadece ihtiyacım olanı seçecek şekilde değiştirdim. Bu sefer bellek hiçbir zaman 82MB'nin üzerine çıkmadı, ancak her şeyin tamamlanması 70 saniye sürdü!


6

Bunu yapabilen BFJ adında bir modül yazdım . Özellikle, yöntem bfj.matchbüyük bir akışı JSON'un ayrı parçalarına ayırmak için kullanılabilir:

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
  });

Burada, bfj.matchayrıştırılmış veri öğelerini alacak okunabilir, nesne modu akışını döndürür ve 3 bağımsız değişken geçirilir:

  1. JSON girişini içeren okunabilir bir akış.

  2. Ayrıştırılmış JSON'daki hangi öğelerin sonuç akışına gönderileceğini belirten bir yüklem.

  3. Girişin satırsonu ile ayrılmış JSON olduğunu belirten bir seçenekler nesnesi (bu, sorudaki B biçimini işlemek içindir, A biçimi için gerekli değildir).

Arandığında bfj.match JSON'u giriş akışından derinlik olarak ayrıştırır, bu öğeyi sonuç akışına itip göndermemeyi belirlemek için her bir değerle yüklemi çağırır. Koşul, üç bağımsız değişken olarak iletilir:

  1. Özellik anahtarı veya dizi dizini (bu, undefined en üst düzey öğeler için ).

  2. Değerin kendisi.

  3. JSON yapısındaki öğenin derinliği (üst düzey öğeler için sıfır).

Elbette, daha karmaşık bir yüklem de gereksinimlere göre gerektiği gibi kullanılabilir. Özellik anahtarlarına karşı basit eşleşmeler gerçekleştirmek istiyorsanız, bir yüklem işlevi yerine bir dize veya normal ifade de iletebilirsiniz.


4

Bu sorunu split npm modülünü kullanarak çözdüm . Akışınızı ikiye bölün ve " Bir akışı parçalayın ve her satırın bir yığın olması için onu yeniden birleştirin ".

Basit kod:

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

Girdi dosyası üzerinde kontrolünüz varsa ve bu bir dizi nesneyse, bunu daha kolay çözebilirsiniz. Dosyayı her kayıtla birlikte tek satırda şu şekilde çıktı olarak ayarlayın:

[
   {"key": value},
   {"key": value},
   ...

Bu hala geçerli bir JSON.

Ardından, her seferinde bir satır işlemek için node.js readline modülünü kullanın.

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

Bir veritabanı kullanmanız gerektiğini düşünüyorum. MongoDB, JSON uyumlu olduğu için bu durumda iyi bir seçimdir.

GÜNCELLEME : JSON verilerini MongoDB'ye aktarmak için mongoimport aracını kullanabilirsiniz .

mongoimport --collection collection --file collection.json

1
Bu soruya cevap vermiyor. Sorunun ikinci satırının, verileri bir veritabanına almak için bunu yapmak istediğini söylediğini unutmayın .
josh3736

mongoimport yalnızca 16MB'ye kadar dosya boyutunu içe aktarır.
Haziq Ahmed
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.