Nodejs에서 큰 JSON 파일 구문 분석


98

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

참고로 전체 파일을 메모리로 읽는 것을 방지하고 싶습니다. 시간 효율성은 나에게 중요하지 않습니다. 예, 여러 개체를 한 번에 읽고 모두 한 번에 삽입하려고 할 수 있지만 이는 성능 조정입니다. 파일에 포함 된 개체 수에 관계없이 메모리 과부하를 일으키지 않는 방법이 필요합니다. .

나는 사용 FormatA하거나 FormatB또는 다른 것을 선택할 수 있습니다 . 대답에 지정하십시오. 감사!


형식 B의 경우 새 줄에 대한 청크를 구문 분석하고 각 전체 줄을 추출하여 중간에서 잘 리면 나머지 줄을 연결할 수 있습니다. 그래도 더 우아한 방법이있을 수 있습니다. 나는 스트림을 많이 사용하지 않았습니다.
travis

답변:


82

파일을 한 줄씩 처리하려면 파일 읽기와 해당 입력에 대해 작동하는 코드를 분리하기 만하면됩니다. 줄 바꿈을 칠 때까지 입력을 버퍼링하여이를 수행 할 수 있습니다. 한 줄에 하나의 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"}.

한 번에 한 덩어리의 데이터 만 메모리에 저장되므로 메모리 효율성이 매우 높습니다. 또한 매우 빠릅니다. 빠른 테스트에 따르면 15ms 이내에 10,000 개의 행을 처리했습니다.


12
이 대답은 이제 중복됩니다. JSONStream을 사용하면 즉시 사용할 수 있습니다.
arcseldon

2
함수 이름 'process'가 잘못되었습니다. '프로세스'는 시스템 변수 여야합니다. 이 버그는 몇 시간 동안 저를 혼란스럽게했습니다.
Zhigong Li

17
@arcseldon이 답변을 중복으로 만드는 라이브러리가 있다는 사실은 생각하지 않습니다. 모듈없이 이것이 어떻게 수행 될 수 있는지 아는 것은 여전히 ​​유용합니다.
Kevin B

3
이것이 축소 된 json 파일에서 작동하는지 확실하지 않습니다. 전체 파일이 한 줄로 묶여 있고 그러한 구분 기호를 사용할 수 없다면 어떻게 될까요? 그러면이 문제를 어떻게 해결합니까?
SLearner 2015-08-31

7
타사 라이브러리는 여러분이 알고있는 마법으로 만들어지지 않았습니다. 그것들은이 대답과 똑같습니다. 정교한 버전의 수작업 솔루션이지만 프로그램으로 포장되고 레이블이 지정됩니다. 일이 어떻게 작동하는지 이해하는 것은 결과를 기대하면서 맹목적으로 데이터를 라이브러리에 던지는 것보다 훨씬 더 중요하고 관련성이 있습니다. 그냥 말 :)
zanona

34

스트리밍 JSON 파서를 작성하는 것이 재미있을 것이라고 생각했던 것처럼, 이미 사용 가능한 파서가 있는지 빠른 검색을 수행해야 할 수도 있다고 생각했습니다.

거기에 있다고 밝혀졌습니다.

방금 발견했기 때문에 분명히 사용하지 않았기 때문에 품질에 대해서는 언급 할 수 없지만 작동하는지 듣고 싶습니다.

다음 Javascript를 고려하면 작동합니다 _.isString.

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

스트림이 객체의 배열 인 경우 들어오는 객체를 기록합니다. 따라서 버퍼링되는 유일한 것은 한 번에 하나의 객체입니다.


29

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

2
이것은 대부분 사실이고 유용하지만해야 할 일 parse('*')이 있으면 데이터를 얻지 못할 것입니다.
John Zwinck 2014 년

@JohnZwinck 감사합니다. 답변을 업데이트하고이를 완벽하게 설명하기위한 작업 예제를 추가했습니다.
arcseldon 2014 년

첫 번째 코드 블록에서 첫 번째 괄호 세트를 var getStream() = function () {제거해야합니다.
givemesnacks

1
500MB json 파일의 메모리 부족 오류로 인해 실패했습니다.
Keith John Hutchison

18

가능한 경우 전체 JSON 파일을 메모리로 읽는 것을 피하고 싶지만 메모리를 사용할 수 있다면 성능 측면에서 좋지 않을 수 있습니다. json 파일에서 node.js의 require ()를 사용하면 데이터를 메모리에 매우 빠르게로드합니다.

81MB geojson 파일에서 각 기능의 속성을 인쇄 할 때 성능이 어떤지 확인하기 위해 두 가지 테스트를 실행했습니다.

첫 번째 테스트에서는 .NET을 사용하여 전체 geojson 파일을 메모리로 읽었습니다 var data = require('./geo.json'). 3330 밀리 초가 걸렸고 각 기능의 속성을 인쇄하는 데 804 밀리 초가 걸렸고 총 4134 밀리 초였습니다. 그러나 node.js는 411MB의 메모리를 사용하는 것으로 보입니다.

두 번째 테스트에서는 JSONStream + event-stream과 함께 @arcseldon의 답변을 사용했습니다. 필요한 것만 선택하도록 JSONPath 쿼리를 수정했습니다. 이번에는 메모리가 82MB를 넘지 않았지만 전체 작업을 완료하는 데 70 초가 걸렸습니다!


18

비슷한 요구 사항이 있었기 때문에 노드 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', ...) ´로 '완료'/ '닫기'입니다.
nonNumericalFloat

6

나는 이것을 할 수있는 BFJ 라는 모듈을 작성했다 . 특히이 메서드 bfj.match를 사용하여 큰 스트림을 개별 JSON 청크로 분할 할 수 있습니다.

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

여기 bfj.match에서 파싱 된 데이터 항목을 수신하고 3 개의 인수가 전달되는 읽을 수있는 객체 모드 스트림을 반환합니다.

  1. 입력 JSON을 포함하는 읽기 가능한 스트림입니다.

  2. 구문 분석 된 JSON에서 결과 스트림으로 푸시 될 항목을 나타내는 조건 자입니다.

  3. 입력이 줄 바꿈으로 구분 된 JSON임을 나타내는 옵션 개체 (질문의 형식 B를 처리하기위한 것이며 형식 A에는 필요하지 않음).

호출 bfj.match되면는 입력 스트림 깊이에서 JSON을 구문 분석하여 각 값으로 조건자를 호출하여 해당 항목을 결과 스트림에 푸시할지 여부를 결정합니다. 술어에는 세 가지 인수가 전달됩니다.

  1. 속성 키 또는 배열 인덱스 ( undefined최상위 항목 용).

  2. 가치 그 자체.

  3. JSON 구조의 항목 깊이 (최상위 항목의 경우 0).

물론 필요에 따라 더 복잡한 술어를 사용할 수도 있습니다. 속성 키에 대해 단순 일치를 수행하려는 경우 조건 자 함수 대신 문자열 또는 정규식을 전달할 수도 있습니다.


4

분할 npm 모듈을 사용하여이 문제를 해결했습니다 . 스트림을 분할하면 "스트림을 분리 하고 각 라인이 덩어리가되도록 재 조립 "합니다.

샘플 코드 :

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

입력 파일을 제어 할 수 있고 이것이 객체 배열 인 경우이 문제를 더 쉽게 해결할 수 있습니다. 다음과 같이 각 레코드가있는 파일을 한 줄에 출력하도록 정렬합니다.

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

-1

데이터베이스를 사용해야한다고 생각합니다. MongoDB는 JSON과 호환되기 때문에이 경우 좋은 선택입니다.

업데이트 : mongoimport 도구를 사용 하여 JSON 데이터를 MongoDB로 가져올 수 있습니다 .

mongoimport --collection collection --file collection.json

1
이것은 질문에 대한 답이 아닙니다. 질문의 두 번째 줄은 데이터를 데이터베이스로 가져 오기 위해이 작업을 수행하고 싶다고 말합니다 .
josh3736

mongoimport는 최대 16MB의 파일 크기 만 가져옵니다.
Haziq Ahmed
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.