Typescript - 빅데이터 배치 프로그램
in DEV on Typescript
이 포스트는 빅데이터 배치 프로그램을 만들어볼 것이다.
50만건의 가짜 데이터를 csv 파일 포맷으로 저장한 뒤, 이를 다시 읽어내는 프로그램을 구현할 것이다. node.js 환경에서 CSV 파일 형식의 데이터를 MySQL 이나 PostgreSQL 과 같은 데이터베이스 시스템에 batch 작업으로 데이터를 저장할 때 유용하다.
소스는 assu10/typescript.git 에 있습니다.
- 1. 프로젝트 구성
- 2. CSV 파일과 생성기
- 3. node.js 에서 프로그램 명령 줄 인수 읽기
- 4. 파일 처리 비동기 함수를 프로미스로 구현
- 5. 가짜 데이터 생성
- 6.
Object.keys
와Object.values
함수 사용 - 7. CSV 파일 생성
- 8. 데이터를 CSV 파일에 쓰기
- 9. zip 함수 생성
- 10. 생성기 코드 구현 시 주의점
- 11. CSV 파일 데이터 읽기
- 참고 사이트 & 함께 보면 좋은 사이트
아래와 같은 순서로 진행 예정이다.
- node.js 의 fs 패키지가 제공하는 비동기 방식 API 들의 Promise 방식 구현
- range, zip 같은 유틸리티 함수 구현
- chance 패키지를 사용해 그럴듯한 가짜 데이터 생성 코드 구현
- CSV 파일 포맷 데이터를 읽고 쓰는 코드 구현
1. 프로젝트 구성
> npm init --y
> npm i mkdirp rimraf chance
> npm i -D typescript ts-node @types/node @types/mkdirp @types/rimraf @types/chance
> tsc --init
> mkdir -p src/fileApi
> mkdir src/fake
> mkdir src/csv
> mkdir src/utils
> mkdir src/test
mkdirp
은 디렉터리를 생성하는 패키지이고, rimraf
는 디렉터리를 삭제하는 기능이다.
package.json
{
"name": "chap12-big-data-batch",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "ts-node src",
"build": "tsc && node dist"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"chance": "^1.1.8",
"mkdirp": "^1.0.4",
"rimraf": "^3.0.2"
},
"devDependencies": {
"@types/chance": "^1.1.3",
"@types/mkdirp": "^1.0.2",
"@types/node": "^16.10.2",
"@types/rimraf": "^3.0.2",
"ts-node": "^10.2.1",
"typescript": "^4.4.3"
}
}
tsconfig.json
{
"compilerOptions": {
"module": "commonjs",
"esModuleInterop": true,
"target": "ES2019",
"moduleResolution": "node",
"outDir": "dist",
"baseUrl": ".",
"sourceMap": true,
"downlevelIteration": true,
"strict": true,
"noImplicitAny": false,
"strictNullChecks": false,
"paths": { "*": ["node_modules/*"] }
},
"include": ["src/**/*"]
}
2. CSV 파일과 생성기
자바스크립트나 타입스크립트는 파일에 데이터를 저장할 때 JSON 포맷을 많이 사용하는데 저장할 데이터의 분량이 많아지면 JSON 파일 포맷은 시스템 메모리를 많이 사용한다.
예를 들어 50만 건의 데이터가 담긴 JSON 파일 포맷은 물리적인 구조상 이 데이터를 한꺼번에 읽어들여야 하므로 시스템 메모리를 많이 사용한다.
CSV 파일 형식은 맨 첫 줄을 읽어 쉼표로 구분된 항목의 의미를 파악한 다음, 파일의 끝까지 한 줄씩 계속 데이터를 읽어서 시스템 자원을 적게 소비한다.
Typescript - 반복기, 생성기 의 2. 생성기 (generator
) 에서 설명한 생성기는 시스템 자원을 매우 적게 소모하면서도 엄청량 분량의 데이터를 처리할 수 있다.
예를 들어 50만 건의 데이터가 담긴 CSV 파일을 읽을 때는 한꺼번에 읽지 않고 한 줄씩 읽고, 읽은 데이터를 타입스크립트 객체로 변환한 후 yield
문으로 for...of
문에 넘겨주면 생성기가 전달한 객체 한 개를 대상으로만 작업을 진행한다.
3. node.js 에서 프로그램 명령 줄 인수 읽기
프로그램을 실행할 때 외부에서 입력된 값을 명령 줄 인수 (command line arguments) 라고 한다.
node.js 에서는 process
라는 내장 객체를 제공하는데 프로그램의 명령 줄 인수는 이 객체의 argv
배열 속성에서 얻을 수 있다.
process.argv.forEach((val: string, index: number) => {
console.log(index + ': ', val);
});
> ts-node src/index.ts data/aa.csv 500000
0: /usr/local/bin/ts-node
1: /Users/assu/myhome/02_Study/03_typescript/mytypescript/chap12-big-data-batch/src/index.ts
2: data/aa.csv
3: 500000
src/utils/getFileNameAndNumber.ts
export type FileNameAndNumber = [string, number];
export const getFileNameAndNumber = (defaultFilename: string, defaultNumberOfFakeData: number): FileNameAndNumber => {
const [bin, node, filename, numberOfFakeData] = process.argv;
return [filename || defaultFilename, numberOfFakeData ? parseInt(numberOfFakeData, 10) : defaultNumberOfFakeData];
};
const [filename, numberOfFakeItems] = getFileNameAndNumber('data/fake.csv', 500000);
console.log(filename, numberOfFakeItems);
> ts-node src/utils/getFileNameAndNumber.ts data/fake2.csv 500001
data/fake2.csv 500001
4. 파일 처리 비동기 함수를 프로미스로 구현
4.1. fs.access
API 로 디렉터리와 파일 확인
fs.access
- 파일이나 디렉터리가 현재 있는지 확인
아래는 fs.access
함수를 사용하여 파일이나 디렉터리가 있는지 확인하는 코드를 프로미스 형태로 구현한 것이다.
src/fileApi/fileExists.ts
import * as fs from "fs";
export const fileExists = (filepath: string): Promise<boolean> =>
new Promise<boolean>(resolve => fs.access(filepath, error => resolve(error ? false : true)));
const exist = async(filepath) => {
const result = await fileExists(filepath);
console.log(`${filepath} ${result ? 'exists': 'not exits'}`);
};
exist('./package.json'); // ./package.json exists
exist('./package'); // ./package not exits
node.js 에서는 package.json 파일이 있는 위치가 현재 디렉터리이다.
4.2. mkdirp
패키지로 디렉터리 생성 함수 생성
node.js 는 mkdir
이라는 API 를 제공하는데 이 API 는 ‘./src/aaa/bbb’ 와 같은 여러 경로의 디렉터리를 한번에 만들지 못한다.
mkdirp
는 여러 디렉터리를 한번에 만드는 명령인 mkdir -p
처럼 동작하는 API 를 제공한다.
아래는 디렉터리가 있는지 판단하여 없을 때만 mkdirp
함수로 디렉터리를 생성한다.
src/fileApi/mkdir.ts
import mkdirp from 'mkdirp'
import {fileExists} from "./fileExists";
// .then(resolve) 에서 아래와 같은 오류가 나면 tsconfig.json 의 "strictNullChecks": false 설정
// TS2345: Argument of type '(value: string | PromiseLike<string>) => void'
// is not assignable to parameter of type '(value: string | undefined) => void | PromiseLike<void>'.
export const mkdir = (dirname: string): Promise<string> =>
new Promise(async (resolve, reject) => {
const alreadyExists = await fileExists(dirname);
alreadyExists ? resolve(dirname) : mkdirp(dirname).then(resolve).catch(reject);
});
const makeDataDir = async (dirname: string) => {
let result = await mkdir(dirname);
console.log(`${result} dir created.`); // /Users/mytypescript/chap12-big-data-batch/data dir created.
}
makeDataDir('./data/today');
.then(resolve) 에서 아래와 같은 오류 발생 시 tsconfig.json 의 “strictNullChecks”: false 설정
TS2345: Argument of type ‘(value: string | PromiseLike) => void' is not assignable to parameter of type '(value: string | undefined) => void | PromiseLike '.
4.3. rimraf
패키지로 디렉터리 삭제 함수 생성
node.js 는 fs.rmdir
함수를 제공하지만 이 함수는 비어있지 않은 디렉터리는 삭제하지 못한다.
rimraf
패키지를 이용하면 비어있지 않은 디렉터리도 삭제가 가능하다.
src/fileApi/rmdir.ts
import rimraf from "rimraf";
import {fileExists} from "./fileExists";
export const rmdir = (dirname: string): Promise<string> =>
new Promise<string>(async (resolve, reject) => {
const alreadyExists = await fileExists(dirname);
!alreadyExists ? resolve(dirname) :
rimraf(dirname, error => error ? reject(error) : resolve(dirname));
});
const deleteDataDir = async (dir) => {
const result = await rmdir(dir);
console.log(`${result} dir deleted.`); // ./data/today dir deleted.
}
deleteDataDir('./data/today'); // today 디렉터리 삭제
4.4. fs.writeFile
API 로 파일 생성
node.js 환경에서 파일의 데이터를 읽거나 쓸 때는 대부분 text 데이터를 대상으로 하는데 이 때 그 데이터는 유니코드로 처리해야 한다.
fs.writeFile(filepath, data, 'utf8', callback)
src/fileApi/writeFile.ts
import * as fs from "fs";
import {mkdir} from "./mkdir";
export const writeFile = (filename: string, data: any): Promise<any> =>
new Promise<any>((resolve, reject) => {
fs.writeFile(filename, data, 'utf8', (error: Error) => {
error? reject(error) : resolve(data);
})
});
const writeTest = async (filename: string, data: any) => {
const result = await writeFile(filename, data);
console.log(`write ${result} to ${filename}`);
};
mkdir('./data')
.then(s => writeTest('./data/hello.txt', 'hello world!'))
.then(s => writeTest('./data/test.json', JSON.stringify({name: 'assu', age: 20}, null, 2)))
.catch((e: Error) => console.log(e.message));
/*
write hello world! to ./data/hello.txt
write {
"name": "assu",
"age": 20
} to ./data/test.json
*/
타입스크립트나 자바스크립트는 객체 object 를 JSON.stringify(object)
를 통해 JSON 문자열로 바꿔준다. 사람이 좀 더 읽기 편한 형식으로는 JSON.stringify(object, null, 2)
로 사용하면 된다. 숫자 2는 들여쓰기를 위해 공백문자 2개를 사용하라는 의미이다.
4.5. fs.readFile
API 로 파일 내용 읽기
파일에 담긴 데이터를 읽을 때는 fs.readFile
API 를 사용한다.
파일에 담긴 데이터는 텍스트 포맷으로 읽을 수도 있고, 바이너리 포맷으로 읽을 수도 있다.
텍스트 포맷은 텍스트가 영문으로만 이루어졌다고 가정하는 ANSI 포맷과 비영어권 문자도 있다고 가정하는 유니코드 포맷 두 가지가 존재한다.
그리고 유니코드 포맷은 ANSI 문자열 체계를 확장한 utf8 포맷과 원래의 유니코드 포맷 두 가지가 있다.
node.js 에서 파일의 데이터를 읽거나 쓸 때는 기본으로 utf8 포맷을 사용한다.
fs.readFile(filepath, 'utf8', callback)
src/fileApi/readFile.ts
import * as fs from "fs";
export const readFile = (filename: string): Promise<any> =>
new Promise<any>((resolve, reject) => {
fs.readFile(filename, 'utf8', (e: Error, data: any) => {
e ? reject(e) : resolve(data);
})
});
const readTest = async (filename: string) => {
const result = await readFile(filename);
console.log(`read ${result} from ${filename} file.`);
};
readTest('./data/hello.txt')
.then(s => readTest('./data/test.json'))
.catch((e: Error) => console.log(e.message));
/*
read hello world! from ./data/hello.txt file.
read {
"name": "assu",
"age": 20
} from ./data/test.json file.
*/
4.6. fs.appendFile
API 로 파일에 내용 추가
fs.writeFile
은 파일이 이미 존재하면 기존 파일 내용을 모두 지우고 새로운 데이터를 쓴다.
기존 내용을 보존하면서 새로운 데이터를 파일 끝에 삽입할 때는 fs.appendFile
API 를 이용한다.
fs.appendFile(filepath, data, 'utf8', callback)
src/fileApi/appendFile.ts
import * as fs from "fs";
import {mkdir} from "./mkdir";
export const appendFile = (filename: string, data: any): Promise<any> =>
new Promise<any>((resolve, reject) => {
fs.appendFile(filename, data, 'utf8', (error: Error) => {
error ? reject(error) : resolve(data)
})
});
const appendTest = async (filename: string, data: any) => {
const result = await appendFile(filename, data);
console.log(`append ${result} to ${filename}`);
};
mkdir('./data')
.then(s => appendTest('./data/hello.txt', '\nhi, there'))
.catch((e: Error) => console.log(e.message));
/*
append
hi, there to ./data/hello.txt
*/
4.7. fs.unlink
API 로 파일 삭제
fs.unlink
는 파일을 삭제하는 API 이다.
fs.unlink(filepath, callback)
src/fileApi/deleteFile.ts
import {fileExists} from "./fileExists";
import * as fs from "fs";
import {rmdir} from "./rmdir";
export const deleteFile = (filename: string): Promise<string> =>
new Promise<string>(async (resolve, reject) => {
const alreadyExists = await fileExists(filename);
!alreadyExists ? resolve(filename) :
fs.unlink(filename, (error: Error) => error ? reject(error) : resolve(filename));
});
const deleteTest = async (filename: string) => {
const result = await deleteFile(filename);
console.log(`delete ${result} file.`);
}
Promise.all([deleteTest('./data/hello.txt'), deleteTest('./data/test.json')])
.then(s => rmdir('./data'))
.then(dirname => console.log(`delete ${dirname} dir`))
.catch((e: Error) => console.log(e.message));
/*
delete ./data/hello.txt file.
delete ./data/test.json file.
delete ./data dir
*/
Promise.all 은 Typescript - Promise, async/await 의 2.4.
Promise.all
메서드 와 3.6. async 함수와Promise.all
을 참고하세요.
4.8. src/fileApi/index.ts 파일 생성
앞에서 만든 기능들을 모두 export 해주는 파일을 만든다. 이 파일을 앞으로 src/fileApi 디렉터리의 함수들을 아래처럼 사용할 수 있도록 해준다.
import {fileExists, mkdir, rmdir} from './src/fileApi'
src/fileApi/index.ts
import {fileExists} from './fileExists'
import {mkdir} from './mkdir'
import {rmdir} from './rmdir'
import {writeFile} from './writeFile'
import {readFile} from './readFile'
import {appendFile} from './appendFile'
import {deleteFile} from './deleteFile'
export {fileExists, mkdir, rmdir, writeFile, readFile, appendFile, deleteFile}
5. 가짜 데이터 생성
chance
패키지를 사용하여 가짜 데이터를 만든다.
IFake 라는 인터페이스를 만들고 이름, 이메일 주소, 간단한 프로필(sentence) 등을 속성으로 포함한다.
src/fake/IFake.ts
export interface IFake {
name: string,
email: string,
sentence: string,
profession: string,
birthday: Date
}
이제 IFake 형태의 데이터를 만들자.
src/fake/makeFakeData.ts
import Chance from 'chance'
import {IFake} from "./IFake";
const c = new Chance();
export const makeFakeData = (): IFake => ({
name: c.name(),
email: c.email(),
profession: c.profession(),
birthday: c.birthday(),
sentence: c.sentence()
});
export { IFake }
사용하는 법은 아래와 같다.
src/test/makeFakeData-test.ts
import {makeFakeData, IFake} from "../fake/makeFakeData";
const fakeData: IFake = makeFakeData();
console.log(fakeData);
/*
{
name: 'Ida Fuller',
email: 'nap@huc.sv',
profession: 'City Manager',
birthday: 1967-02-12T00:30:53.300Z,
sentence: 'Li po vevmad getire modde lu fekural ig if fimo wocef kisodcil famateme.'
}
*/
src/fake/index.ts
import {IFake, makeFakeData} from './makeFakeData'
export {IFake, makeFakeData }
이제 이렇게 만들어진 가짜 데이터를 CSV 파일에 쓸 차례인데 이를 위해서는 한 가지 먼저 만들어둬야 할 함수가 있다.
6. Object.keys
와 Object.values
함수 사용
CSV 파일을 만드려면 객체의 속성과 값을 분리해야 하는데 자바스크립트는 이를 위해 Object.keys
와 Object.values
함수를 제공한다.
src/test/keys-values-test.ts
import {IFake, makeFakeData} from "../fake";
const data: IFake = makeFakeData();
const keys = Object.keys(data);
console.log(keys); // [ 'name', 'email', 'profession', 'birthday', 'sentence' ]
const values = Object.values(data);
console.log(values);
// ['Leah Ortega', 'tuglu@tuji.sj', 'Production Engineer', 1967-11-01T01:45:26.062Z,
// 'Se unuijogo kuweju cipa lazeuf samew guv jet cejzah vorol linrejvo pe ti.']
7. CSV 파일 생성
이제 가짜 데이터를 여러 개 생성하여 CSV 파일에 써보자.
src/utils/range.ts
export function* range(max: number, min: number = 0) {
while (min < max) {
yield min++;
}
}
src/utils/index.ts
import {getFileNameAndNumber, FileNameAndNumber} from './getFileNameAndNumber'
import {range} from './range'
export {getFileNameAndNumber, FileNameAndNumber, range}
생성기
function*
은 Typescript - 반복기, 생성기 의 2. 생성기 (generator
) 를 참고하세요.
이제 makeFakeData 를 사용하여 numberOfItems 만큼 IFake 객체를 생성하고, 속성명과 속성값의 배열을 각각 추출하여 filename 파일을 만든다.
src/fake/writeCsvFakeData.ts
import path from "path";
import {IFake, makeFakeData} from './makeFakeData'
import { mkdir, writeFile, appendFile } from '../fileApi'
import {range} from "../utils";
export const writeCsvFormatFakeData = async (filename: string, numberOfItems: number): Promise<string> => {
const dirname = path.dirname(filename);
console.log('dirname: ', dirname);
await mkdir(dirname);
const comma = ',';
const newLine = '\n';
for (let n of range(numberOfItems)) {
const fake: IFake = makeFakeData();
if (n == 0) {
const keys = Object.keys(fake).join(comma);
await writeFile(filename, keys);
}
const values = Object.values(fake).join(comma);
await appendFile(filename, newLine + values);
}
return `write ${numberOfItems} items to ${filename} file.`
}
src/fake/index.ts
import {IFake, makeFakeData} from './makeFakeData'
import {writeCsvFormatFakeData } from './writeCsvFormatFakeData'
export {IFake, makeFakeData, writeCsvFormatFakeData }
8. 데이터를 CSV 파일에 쓰기
이제 CSV 포맷으로 IFake 타입 객체를 저장하는 기능을 만들도록 한다.
src/writeCsv.ts
import {getFileNameAndNumber} from "./utils";
import {writeCsvFormatFakeData} from "./fake";
const [filename2, numberOfFakeData2] = getFileNameAndNumber('./data/fake', 100000);
const csvFilename = `${filename2}-${numberOfFakeData2}.csv`;
writeCsvFormatFakeData(csvFilename, numberOfFakeData2)
.then(result => console.log(result))
.catch((e: Error) => console.log(e.message));
// write 1 items to ./data/fake-1.csv file.
> ts-node src/writeCsv.ts
> ts-node src/writeCsv.ts data/fake 10
9. zip 함수 생성
이제 CSV 포맷 파일을 읽는 코드를 작성하자.
CSV 파일은 첫 줄에 객체의 속성명들이 있고, 두 번째 줄부터는 속성값들만 있기 때문에 객체의 속성명 배열과 속성값 배열을 결합하여 객체를 만드는 함수가 필요하다.
이런 기능을 하는 함수는 보통 zip 이라는 이름으로 구현한다.
src/utils/zip.ts
export const zip = (keys: string[], values: any[]): object => {
const makeObject = (key: string, value: any) => ({[key]: value});
const mergeObject = (a: any[]) => a.reduce((accu, val) => ({...accu, ...val}), {});
let tmp = keys.map((key: string, index: number) => [key, values[index]])
.filter(a => a[0] && a[1])
.map(a => makeObject(a[0], a[1]));
return mergeObject(tmp);
}
src/utils/index.ts
import {getFileNameAndNumber, FileNameAndNumber} from './getFileNameAndNumber'
import {range} from './range'
import {zip} from './zip'
export {getFileNameAndNumber, FileNameAndNumber, range, zip}
위에서 만든 zip 함수에 대한 테스트 코드를 작성해보자.
아래는 makeFakeData 를 호출해 가짜 데이터를 만든 다음 Object.keys 와 Object.values 를 각각 호출해 속성명 배열과 속성값 배열을 만든다. 그리고 zip 를 이용해 다시 가짜 데이터를 IFake 타입 객체로 만든다.
src/test/zip-test.ts
import {IFake, makeFakeData} from "../fake";
import {zip} from "../utils";
const data = makeFakeData();
const keys = Object.keys(data), values = Object.values(data);
const fake: IFake = zip(keys, values) as IFake;
console.log(data);
console.log(fake);
/*
{
name: 'Gerald Carter',
email: 'mu@og.bn',
profession: 'Fast Food Manager',
birthday: 1965-11-04T17:29:29.370Z,
sentence: 'Zogtuvvu zotrajni kewi ki tecros mozub wuw gi si lel azafule sah.'
}
{
name: 'Gerald Carter',
email: 'mu@og.bn',
profession: 'Fast Food Manager',
birthday: 1965-11-04T17:29:29.370Z,
sentence: 'Zogtuvvu zotrajni kewi ki tecros mozub wuw gi si lel azafule sah.'
}
*/
10. 생성기 코드 구현 시 주의점
아래 코드를 보자.
import {readFile} from '../fileApi'
import * as fs from "fs";
function* readFileGen() {
yield 1;
fs.readFile('./package.json', (err: Error, data: any) => {
yield data; // TS1163: A 'yield' expression is only allowed in a generator body.
})
}
yield 가 총 2 군데 있는데 yield data 는 fs.readFile 의 콜백 함수 내부에 있다. 즉, 생성기 본문에 있지 않다.
생성기를 구현할 때는 fs.readFile 과 같은 비동기 함수를 사용할 수 없다.
11. CSV 파일 데이터 읽기
위에서 본 것처럼 파일 읽기는 ‘생성기 방식’ 으로 구현할 때 fs.readFile
을 사용하지 못하기 때문에 ‘그냥’ fs.readFile
을 이용하는 방법을 생각해볼 수 있다.
하지만 fs.readFile
을 이용하면 시스템 메모리를 많이 사용하는 문제가 발생한다. 즉, fs.readFile
의 물리적인 동작을 고려할 때 엄청난 용량의 데이터가 담겨 있을지도 모르는 CSV 파일을 한꺼번에 읽는 것은 바람직하지 못하다.
결론적으로 파일을 한 줄씩 읽는 방식으로 생성기를 구현해야 한다.
아래는 1,024 Byte 의 Buffer 타입 객체를 생성하여 파일을 1,024 Byte 씩 읽으면서 한 줄씩 찾은 후, 찾을 줄 (=\n 으로 끝난 줄) 의 데이터를 yield 문으로 발생시키는 예시이다.
src/fileApi/readFileGenerator.ts
import * as fs from "fs";
export function* readFileGenerator(filename: string): any {
let fd: any;
try {
fd = fs.openSync(filename, 'rs'); // rs: 동기 모드를 사용하여 파일을 열고 읽습니다. 운영 체제가 로컬 파일 시스템 캐시를 무시하도록 지시합니다.
const stats = fs.fstatSync(fd); // Getting information for a file or directory
// Using methods of the Stats object
console.log("Path is file:", stats.isFile());
console.log("Path is directory:", stats.isDirectory());
const bufferSize = Math.min(stats.size, 1024);
const buffer = Buffer.alloc(bufferSize + 4);
let filepos = 0;
let line: string;
while (filepos > -1) {
[line, filepos] = readLine(fd, buffer, bufferSize, filepos);
if (filepos > -1) {
yield line;
}
}
yield buffer.toString(); // yield last line (마지막 줄)
} catch (e) {
console.error('readline:', e);
} finally {
fd && fs.closeSync(fd);
}
}
function readLine (fd: any, buffer: Buffer, bufferSize: number, position: number): [string, number] {
let line = '';
let readSize;
const crSize = '\n'.length;
console.log('crSize: ', crSize);
while (true) {
readSize = fs.readSync(fd, buffer, 0, bufferSize, position);
if (readSize > 0) {
const tmp = buffer.toString('utf8', 0, readSize);
const index = tmp.indexOf('\n');
if (index > -1) {
line += tmp.substr(0, index);
position += index + crSize;
break;
} else {
line += tmp;
position += tmp.length;
}
} else {
position = -1; // end of file
break;
}
}
return [line.trim(), position];
}
src/fileApi/index.ts
import {fileExists} from './fileExists'
import {mkdir} from './mkdir'
import {rmdir} from './rmdir'
import {writeFile} from './writeFile'
import {readFile} from './readFile'
import {appendFile} from './appendFile'
import {deleteFile} from './deleteFile'
import {readFileGenerator} from './readFileGenerator'
export {fileExists, mkdir, rmdir, writeFile, readFile, appendFile, deleteFile, readFileGenerator}
실제로 CSV 파일을 잘 읽는지 확인해보자.
src/test/readFileGenerator-test.ts
import {readFileGenerator} from "../fileApi";
for (let value of readFileGenerator('data/fake-10.csv')) {
console.log('<line>', value, '</line>\n');
}
/*
<line> name,email,profession,birthday,sentence </line>
<line> Carrie Lloyd,hewfuc@or.mu,Aerospace Engineer,Sun Jul 15 2001 16:21:59 GMT+0900 (Korean Standard Time),Nu comwa comu maw pez wukibuz ul dollasek sujo sof ohizuz vab oza. </line>
*/
readFileGenerator 는 단순히 파일을 한 줄 한 줄 읽는다.
이번엔 CSV 파일을 해석하면서 읽는 코드를 만들어보자.
src/csv/csvFileReaderGenerator.ts
import {readFileGenerator} from "../fileApi";
import {zip} from "../utils";
export function* csvFileReaderGenerator(filename: string, delim: string = ',') {
let header: string[] = [];
for (let line of readFileGenerator(filename)) {
if (!header.length) {
header = line.split(delim);
} else {
yield zip(header, line.split(delim));
}
}
}
src/readCsv.ts
import {getFileNameAndNumber} from "./utils";
import {csvFileReaderGenerator} from "./csv/csvFileReaderGenerator";
const [filename] = getFileNameAndNumber('./data/fake-100000.csv', 1);
let line = 1;
for (let object of csvFileReaderGenerator(filename)) {
console.log(`[${line++}] ${JSON.stringify(object)}`);
}
console.log('\n read completed.');
/*
[99999] {"name":"Harry Morris","email":"werfuksot@tuhho.zw","profession":"Payroll Specialist","birthday":"Fri Aug 01 1969 02:41:44 GMT+0900 (Korean Standard Time)","sentence":"Wum nipwilas witgok asa uge nopotwi korjor lic ludhocmik fasawohek owu de."}
[100000] {"name":"Myra Schultz","email":"labjal@anagawu.uy","profession":"Traffic Manager","birthday":"Fri Mar 18 1983 23:27:21 GMT+0900 (Korean Standard Time)","sentence":"Gi cubessod wagkif dakim osiwikkiw kurogtun do jihdonfi ra ibsa kowarov fahe dewwow ma kuju.labjal@anagawu.uy"}
read completed.
*/
시스템 자원을 거의 소비하지 않으면서 이와 같은 대용량 데이터를 처리할 수 있게 하는 것이 생성기의 진정한 위력이다.
본 포스트는 전예홍 저자의 Do it! 타입스크립트 프로그래밍을 기반으로 스터디하며 정리한 내용들입니다.