Node에서 대용량 CSV 파일 읽기

2020년 8월 11일

Papa Parse를 사용해 대용량의 CSV 파일을 열어서 작업을 수행을 하는데 파일이 너무 커서 파일을 읽다가 JavaScript heap out of memory 에러로 죽어버리는 문제가 생겼습니다.

그래서 다음과 같이 stream을 전달하고 async하게 시도해 보았지만, 그래도 JavaScript heap out of memory를 동일한 오류가 발생했습니다.

const stream = fs.createReadStream(path.join(__dirname, './data.csv'));

let count = 0;
process.stdout.write('start\n');

Papa.parse(stream, {
  header: true,
  delimiter: ',',
  skipEmptyLines: true,
	worker: true,
	step: async (record) => {
		process.stdout.write(`${count++} ${Object.values(record).join(',')}\n`);
    // Fake asynchronous operation
    await new Promise((resolve) => setTimeout(resolve, 100));
	}
  complete: function(results) {
		process.stdout.write('...done\n');
	}
});

그래서 Papa Parse의 API 문서에서 이 문제를 해결할 마땅한 방법이 보이지 않아 다른 CSV 라이브러리를 찾아보았습니다. csv-parse를 보다 보니 대용량 CSV 파일을 Generator로 읽어오면 해당 이슈를 해결할 수 있을 것 같다는 생각이 들었습니다.

csv-parse API 문서에 다음과 같은 예제가 있어서 csv-generate에 File Stream을 전달해서 사용할 것이라 생각이 들었지만, 해당 라이브러리는 임의의 값을 생성해 주는 유틸이어서 용도가 맞지 않았습니다.

const assert = require('assert');
const generate = require('csv-generate');
const parse = require('..');

(async () => {
  // Initialise the parser by generating random records
  const parser = generate({
    high_water_mark: 64 * 64,
    length: 1000
  }).pipe(
    parse()
  )
  // Intialise count
  let count = 0;
  // Report start
  process.stdout.write('start\n')
  // Iterate through each records
  for await (const record of parser) {
    // Report current line
    process.stdout.write(`${count++} ${record.join(',')}\n`)
    // Fake asynchronous operation
    await new Promise((resolve) => setTimeout(resolve, 100))
  }
  // Report end
  process.stdout.write('...done\n')
  // Validation
  assert.strictEqual(count, 10000)
})()

API 문서를 여기저기 뒤져봤지만, 마땅한 예제가 없어서 직접 시도해본 방법을 글로 공유하면 같은 고민을 하는 분들께 도움이 될 것 같아 포스트를 작성하였고 해당 코드는 아래와 같습니다.

const fs = require('fs');
const path = require('path');
const parse = require('csv-parse');

(async () => {
  const stream = fs.createReadStream(path.join(__dirname, './data.csv'));
  const parser = stream
    .pipe(
      parse({
        columns: true,
        delimiter: ',',
        trim: true,
        skip_empty_lines: true,
      })
    );

  let count = 0;
  process.stdout.write('start\n');

  for await (const record of parser) {
    process.stdout.write(`${count++} ${Object.values(record).join(',')}\n`);
    // Fake asynchronous operation
    await new Promise((resolve) => setTimeout(resolve, 100));
  }
  process.stdout.write('...done\n');
})();
Recently posts
© 2016-2023 smilecat.dev