Node.js で大きいファイルを指定行数で分割する
はじめに
おはようございます、もきゅりんです。
Shall we stream ?
タイトルのような要件があって、その際に雑にG先生で検索したところ、いい感じの見当たらないなーと思ったため、いそいそと書いたのでした。
ところが、このブログにする前に色々とワードを変えて検索したらそれっぽいのが出てきました。
- split-file - npm
- Implementation of a File Splitter Using Node.js | by Michelle Wiginton | May, 2022 | Level Up Coding
- TypeScript(Node.js)でテキストファイルを指定行数ごとに分割する - Qiita
うん、このブログいらねーじゃん、と思いましたが、アプローチはそれぞれ違ったりすると思うので、お焚き上げさせて下さい。
作ったもの
分割したいファイルを配置するディレクトリをよしなに設定して、環境変数 NUMBER_TO_DIVIDED_LINES
に分割したい行数(e.g. 10000)を設定してから実行します。
import * as util from 'util'; import * as fs from 'fs'; import * as readline from 'readline'; import * as childProcess from 'child_process'; // 分割したいファイルを配置するディレクトリを指定 const targetDir = `${__dirname}/target-files`; const targetFileNameList = fs.readdirSync(targetDir); const exec = util.promisify(childProcess.exec); const fileLineCount = async ({ fileLocation }: FileLocation) => { const { stdout } = await exec(`cat ${fileLocation} | wc -l`); return parseInt(stdout, 10); }; const execFileLineCount = async (targetFile: string) => { const lineCount = await fileLineCount({ fileLocation: targetFile, }); return lineCount; }; // 拡張子の前にファイル番号を付与させる const prefixNumberFileExtension = (targetFile: string, i: number) => { const fileExtension = /\.*\.\w{3}$/.exec(targetFile)![0]; return targetFile.replace(fileExtension, `_${i}${fileExtension}`); }; const writeLiner = ( outputFile: fs.WriteStream, documentSrc: fs.ReadStream, numberToDivideLines: number, turn: number ) => { const startLine = (turn - 1) * numberToDivideLines + 1; let readCounter = 1; const reader = readline.createInterface({ input: documentSrc }); // 書き出しポイントから分割する行数×周回までファイルに出力 reader.on('line', (data) => { if (startLine <= readCounter && readCounter < turn * numberToDivideLines) { outputFile.write(`${data.trim()}\n`); } else if ( startLine <= readCounter && readCounter === turn * numberToDivideLines ) { outputFile.write(`${data.trim()}`); } readCounter += 1; }); outputFile.on('error', (err) => { if (err) console.log(err.message); }); }; type FileLocation = { fileLocation: string; }; const main = async () => { const numberToDivideLines: number | undefined = Number( process.env.NUMBER_TO_DIVIDED_LINES ); try { if (!numberToDivideLines) throw Error(`${numberToDivideLines}is undefined`); for (const targetFileName of targetFileNameList) { const targetFile = `${targetDir}/${targetFileName}`; const documentSrc = fs.createReadStream(targetFile, 'utf8'); const fileLines = await execFileLineCount(targetFile); // 最終行÷分割行数の商に余りがあればファイル1つ増やして余り分を書き込む const dividedFiles: number = fileLines % numberToDivideLines === 0 ? fileLines / numberToDivideLines : fileLines / numberToDivideLines + 1; for (let i = 1; i <= dividedFiles; i += 1) { const dividedFileName = /\.*\.\w{3}$/.test(targetFile) ? prefixNumberFileExtension(targetFile, i) : `${targetFile}_${i}`; const dividedFile = fs.createWriteStream(dividedFileName); writeLiner(dividedFile, documentSrc, numberToDivideLines, i); } console.info(`${targetFileName}を分割しました`); } } catch (err) { console.log(err); } }; main();
やってること
読めばすぐ理解できてしまうと思いますが、要点だけまとめておきます。
- シェルコマンド実行してファイル最終行を同期的に取得
const exec = util.promisify(childProcess.exec); const fileLineCount = async ({ fileLocation }: FileLocation) => { const { stdout } = await exec(`cat ${fileLocation} | wc -l`); return parseInt(stdout, 10); }; const execFileLineCount = async (targetFile: string) => { const lineCount = await fileLineCount({ fileLocation: targetFile, }); return lineCount; };
- 最終行を指定行で割ったファイル数を用意する、最終行を指定した行数で割り切れなかったら、追加の1ファイルを用意する
const fileLines = await execFileLineCount(targetFile); // 最終行÷分割行数の商に余りがあればファイル1つ増やして余り分を書き込む const dividedFiles: number = fileLines % numberToDivideLines === 0 ? fileLines / numberToDivideLines : fileLines / numberToDivideLines + 1;
- txt, tsv, csvなどの3文字の拡張子が付いている場合、拡張子前にナンバリングを付与する
// 拡張子の前にファイル番号を付与させる const prefixNumberFileExtension = (targetFile: string, i: number) => { const fileExtension = /\.*\.\w{3}$/.exec(targetFile)![0]; return targetFile.replace(fileExtension, `_${i}${fileExtension}`); }; ... for (let i = 1; i <= dividedFiles; i += 1) { const dividedFileName = /\.*\.\w{3}$/.test(targetFile) ? prefixNumberFileExtension(targetFile, i) : `${targetFile}_${i}`; const dividedFile = fs.createWriteStream(dividedFileName);
- 対応するファイル枚数目と指定行数とで、始点行と終止行を readCounter で捕捉してファイルに行を書き込む
const startLine = (turn - 1) * numberToDivideLines + 1; let readCounter = 1; const reader = readline.createInterface({ input: documentSrc }); // 書き出しポイントから分割する行数×周回までファイルに出力 reader.on('line', (data) => { if (startLine <= readCounter && readCounter < turn * numberToDivideLines) { outputFile.write(`${data.trim()}\n`); } else if ( startLine <= readCounter && readCounter === turn * numberToDivideLines ) { outputFile.write(`${data.trim()}`); } readCounter += 1; });
さいごに
結果的にお勉強にはなりましたが、検索ワードはもう少しいろいろとバリエーションを換えて粘り強くトライするようにします。
以上です。
どなたかの参考になれば幸いです。