はじめに
おはようございます、もきゅりんです。
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; });
さいごに
結果的にお勉強にはなりましたが、検索ワードはもう少しいろいろとバリエーションを換えて粘り強くトライするようにします。
以上です。
どなたかの参考になれば幸いです。