Parsing file JSON besar di Nodejs

98

Saya memiliki file yang menyimpan banyak objek JavaScript dalam bentuk JSON dan saya perlu membaca file tersebut, membuat setiap objek, dan melakukan sesuatu dengannya (masukkan ke dalam db dalam kasus saya). Objek JavaScript dapat direpresentasikan dalam format:

Format A:

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

atau Format B:

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

Perhatikan bahwa ...menunjukkan banyak objek JSON. Saya sadar saya bisa membaca seluruh file ke dalam memori dan kemudian menggunakan JSON.parse()seperti ini:

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

Namun, filenya bisa sangat besar, saya lebih suka menggunakan aliran untuk melakukannya. Masalah yang saya lihat dengan aliran adalah bahwa konten file dapat dipecah menjadi potongan data kapan saja, jadi bagaimana saya dapat menggunakannya JSON.parse()pada objek seperti itu?

Idealnya, setiap objek akan dibaca sebagai potongan data yang terpisah, tetapi saya tidak yakin bagaimana melakukannya .

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

Catatan, saya ingin mencegah membaca seluruh file ke dalam memori. Efisiensi waktu tidak penting bagi saya. Ya, saya dapat mencoba membaca sejumlah objek sekaligus dan memasukkan semuanya sekaligus, tetapi itu adalah tweak kinerja - Saya memerlukan cara yang dijamin tidak menyebabkan kelebihan memori, tidak peduli berapa banyak objek yang ada di file .

Saya dapat memilih untuk menggunakan FormatAatau FormatBatau mungkin yang lain, harap sebutkan dalam jawaban Anda. Terima kasih!

dgh
sumber
Untuk format B Anda dapat mengurai melalui potongan untuk baris baru, dan mengekstrak setiap baris, menggabungkan sisanya jika terpotong di tengah. Mungkin ada cara yang lebih elegan. Saya belum terlalu banyak bekerja dengan streaming.
travis

Jawaban:

82

Untuk memproses file baris demi baris, Anda hanya perlu memisahkan pembacaan file dan kode yang bertindak atas input itu. Anda dapat melakukannya dengan menyangga masukan Anda sampai Anda mencapai baris baru. Dengan asumsi kita memiliki satu objek JSON per baris (pada dasarnya, format 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!
    }
}

Setiap kali aliran file menerima data dari sistem file, itu disimpan di buffer, dan kemudian pumpdipanggil.

Jika tidak ada baris baru di buffer, pumpcukup kembali tanpa melakukan apa pun. Lebih banyak data (dan kemungkinan baris baru) akan ditambahkan ke buffer saat berikutnya stream mendapatkan data, dan kemudian kita akan memiliki objek yang lengkap.

Jika ada baris baru, pumppotong buffer dari awal ke baris baru dan serahkan ke process. Kemudian memeriksa kembali apakah ada baris baru lain di buffer ( whileloop). Dengan cara ini, kami dapat memproses semua baris yang telah dibaca di bagian saat ini.

Akhirnya, processdipanggil sekali per baris masukan. Jika ada, ini akan menghapus karakter carriage return (untuk menghindari masalah dengan akhiran baris - LF vs CRLF), dan kemudian memanggil JSON.parsesalah satu baris. Pada titik ini, Anda dapat melakukan apa pun yang Anda perlukan dengan objek Anda.

Perhatikan bahwa JSON.parseketat tentang apa yang diterima sebagai input; Anda harus mengutip pengenal dan nilai string Anda dengan tanda kutip ganda . Dengan kata lain, {name:'thing1'}akan melempar kesalahan; Anda harus menggunakan {"name":"thing1"}.

Karena tidak lebih dari sepotong data yang akan ada dalam memori pada satu waktu, ini akan sangat efisien dalam menggunakan memori. Ini juga akan sangat cepat. Tes cepat menunjukkan saya memproses 10.000 baris di bawah 15ms.

josh3736
sumber
12
Jawaban ini sekarang berlebihan. Gunakan JSONStream, dan Anda memiliki dukungan di luar kotak.
arcseldon
2
Nama fungsi 'proses' buruk. 'proses' harus menjadi variabel sistem. Bug ini membuatku bingung selama berjam-jam.
Zhigong Li
17
@arcseldon Saya tidak berpikir fakta bahwa ada perpustakaan yang melakukan ini membuat jawaban ini berlebihan. Tentu masih berguna untuk mengetahui bagaimana hal ini dapat dilakukan tanpa modul.
Kevin B
3
Saya tidak yakin apakah ini akan berfungsi untuk file json yang diperkecil. Bagaimana jika seluruh file dibungkus dalam satu baris, dan penggunaan pembatas seperti itu tidak memungkinkan? Lalu bagaimana kita mengatasi masalah ini?
SLearner
7
Perpustakaan pihak ketiga tidak terbuat dari sihir lho. Mereka hanya seperti jawaban ini, versi solusi yang dielaborasi secara manual, tetapi hanya dikemas dan diberi label sebagai program. Memahami bagaimana sesuatu bekerja jauh lebih penting dan relevan daripada membuang data secara membabi buta ke perpustakaan mengharapkan hasil. Hanya mengatakan :)
zanona
34

Saat saya berpikir bahwa akan menyenangkan untuk menulis pengurai JSON streaming, saya juga berpikir bahwa mungkin saya harus melakukan pencarian cepat untuk melihat apakah sudah ada yang tersedia.

Ternyata ada.

Karena saya baru menemukannya, saya jelas tidak menggunakannya, jadi saya tidak dapat mengomentari kualitasnya, tetapi saya akan tertarik untuk mengetahui apakah ini berfungsi.

Itu berhasil, pertimbangkan Javascript berikut dan _.isString:

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

Ini akan mencatat objek saat mereka masuk jika aliran adalah larik objek. Oleh karena itu, satu-satunya hal yang disangga adalah satu objek pada satu waktu.

pengguna1106925
sumber
29

Mulai Oktober 2014 , Anda hanya dapat melakukan sesuatu seperti berikut (menggunakan 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
});

Untuk mendemonstrasikan dengan contoh kerja:

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
arcseldon.dll
sumber
2
Ini sebagian besar benar dan berguna, tetapi saya pikir Anda perlu melakukannya parse('*')atau Anda tidak akan mendapatkan data apa pun.
John Zwinck
@JohnZwinck Terima kasih, telah memperbarui jawabannya, dan menambahkan contoh yang berfungsi untuk mendemonstrasikannya sepenuhnya.
arcseldon
di blok kode pertama, kumpulan tanda kurung pertama var getStream() = function () {harus dihilangkan.
givemesnacks
1
Ini gagal dengan kesalahan kehabisan memori dengan file json 500mb.
Keith John Hutchison
18

Saya menyadari bahwa Anda ingin menghindari membaca seluruh file JSON ke dalam memori jika memungkinkan, namun jika Anda memiliki memori yang tersedia, itu mungkin bukan ide yang buruk dari segi kinerja. Menggunakan node.js's require () pada file json memuat data ke dalam memori dengan sangat cepat.

Saya menjalankan dua tes untuk melihat seperti apa kinerja saat mencetak atribut dari setiap fitur dari file geojson 81MB.

Pada tes pertama, saya membaca seluruh file geojson ke dalam memori menggunakan var data = require('./geo.json'). Itu membutuhkan waktu 3330 milidetik dan kemudian mencetak atribut dari setiap fitur membutuhkan waktu 804 milidetik dengan total keseluruhan 4134 milidetik. Namun, ternyata node.js menggunakan memori 411MB.

Dalam tes kedua, saya menggunakan jawaban @ arcseldon dengan JSONStream + event-stream. Saya memodifikasi kueri JSONPath untuk memilih hanya yang saya butuhkan. Kali ini memorinya tidak pernah melebihi 82MB, namun, semuanya sekarang membutuhkan 70 detik untuk diselesaikan!

Evan Siroky
sumber
18

Saya memiliki persyaratan serupa, saya perlu membaca file json besar di node js dan memproses data dalam potongan dan memanggil api dan simpan di mongodb. inputFile.json seperti:

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

Sekarang saya menggunakan JsonStream dan EventStream untuk mencapai ini secara sinkron.

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();
  });
}
karthick N
sumber
Terima kasih banyak telah menambahkan jawaban Anda, kasus saya juga membutuhkan penanganan yang sinkron. Namun setelah pengujian tidak mungkin bagi saya untuk memanggil "end ()" sebagai callback setelah pipa selesai. Saya percaya satu-satunya hal yang bisa dilakukan adalah menambahkan acara, apa yang akan terjadi setelah streaming adalah 'selesai' / 'tutup' dengan ´fileStream.on ('close', ...) ´.
nonNumericalFloat
6

Saya menulis modul yang dapat melakukan ini, yang disebut BFJ . Secara khusus, metode bfj.matchini dapat digunakan untuk memecah aliran besar menjadi beberapa bagian JSON yang terpisah:

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

Di sini, bfj.matchmengembalikan aliran mode objek yang dapat dibaca yang akan menerima item data yang diurai, dan diteruskan 3 argumen:

  1. Aliran yang dapat dibaca yang berisi masukan JSON.

  2. Predikat yang menunjukkan item mana dari JSON yang diurai akan didorong ke aliran hasil.

  3. Objek opsi yang menunjukkan bahwa input adalah JSON yang dibatasi baris baru (ini untuk memproses format B dari pertanyaan, tidak diperlukan untuk format A).

Setelah dipanggil, bfj.matchakan mengurai JSON dari input stream depth-first, memanggil predikat dengan setiap nilai untuk menentukan apakah akan mendorong item itu ke aliran hasil atau tidak. Predikat diberikan tiga argumen:

  1. Kunci properti atau indeks larik (ini undefineduntuk item tingkat atas).

  2. Nilai itu sendiri.

  3. Kedalaman item dalam struktur JSON (nol untuk item level teratas).

Tentu saja predikat yang lebih kompleks juga bisa digunakan sesuai kebutuhan sesuai kebutuhan. Anda juga dapat meneruskan string atau ekspresi reguler sebagai ganti fungsi predikat, jika Anda ingin melakukan pencocokan sederhana terhadap kunci properti.

Phil Booth
sumber
4

Saya memecahkan masalah ini menggunakan modul npm split . Pipa aliran Anda menjadi beberapa bagian, dan itu akan " Hancurkan aliran dan pasang kembali sehingga setiap baris menjadi potongan ".

Kode sampel:

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);           
    // ...
});
Brian Leathem
sumber
4

Jika Anda memiliki kendali atas file input, dan itu adalah larik objek, Anda dapat menyelesaikannya dengan lebih mudah. Atur untuk mengeluarkan file dengan setiap catatan dalam satu baris, seperti ini:

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

Ini masih JSON yang valid.

Kemudian, gunakan modul readline node.js untuk memprosesnya satu baris dalam satu waktu.

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! 
}
Steve Hanov
sumber
-1

Saya pikir Anda perlu menggunakan database. MongoDB adalah pilihan yang baik dalam hal ini karena kompatibel dengan JSON.

UPDATE : Anda dapat menggunakan alat mongoimport untuk mengimpor data JSON ke MongoDB.

mongoimport --collection collection --file collection.json
Vadim Baryshev
sumber
1
Ini tidak menjawab pertanyaan itu. Perhatikan bahwa baris kedua pertanyaan mengatakan dia ingin melakukan ini untuk memasukkan data ke dalam database .
josh3736
mongoimport hanya mengimpor ukuran file hingga 16MB.
Haziq Ahmed