テキストファイルを分割したいときってありますよね。
例えばSQLのINSERT INTO hoge VALUESに続く行が数万行ある時とか (そんなにない)
個人的に上記をやりたいタイミングがあって、npmでいい感じのモジュールを探したんですが
意外とテキストファイルを「指定した行数で」区切ってくれるやつが無かったので泣く泣く自分で作りました。
あ、Nodeです。
Streamで順次処理してるので、でっかいファイルでもヒープアウトしないはず!
Usage
hoge.ts
import { FileSplitter } from "path/to/file-splitter"
// your-file.txtを100行ごとに分割する
const fileSplitter = new FileSplitter("/path/to/your-file.txt", 100)
// 分割開始!
fileSplitter.start()
御託は良いからコードを見せろ
file-splitter.ts
import fs from "fs"
import path from "path"
import readline from "readline"
export class FileSplitter {
  private maxLines: number
  private identifier = 0
  private curLine = 0
  private lineReader: readline.Interface
  private currentWriteStream: fs.WriteStream
  private outputPath: string
  constructor(readFrom: string, maxLines = 500) {
    // 分割したファイル群を保存する先のディレクトリを作り、そのpathを保存
    this.setOutputDir(readFrom)
    this.maxLines = maxLines
    // 最初のWriteStreamを作っておく
    this.replaceWriteStream()
    // readlineに指定ファイルのReadStreamを食わせて行リーダーを作る
    this.lineReader = readline.createInterface({
      input: fs.createReadStream(readFrom)
    })
  }
  start() {
    this.lineReader
      .on("line", (line) => {
        // 現在の行数を保持 (1 ~ 指定行数 + 1までの値を取る)
        this.curLine++
        // ここ、もう少しうまくやりたかった人生だった
        const isOn = this.curLine === this.maxLines
        const isOver = this.curLine > this.maxLines
        if (isOver) {
          // 指定行数を超えたら新しいファイル向けのWriteStreamに切り替え、行数を1にリセットする
          this.replaceWriteStream()
          this.curLine = 1
        }
        if (isOn) {
          // そのファイル最後の行は改行無しにしている (が、例えば100行のファイルを30行とかで区切られると、最後のファイルには改行が入っちゃう。めんどくさくてこれ以上考えなかった)
          this.currentWriteStream.write(line)
        } else {
          this.currentWriteStream.write(`${line}\n`)
        }
      })
      .on("close", () => {
        // 一応掃除する
        this.closeWriteStreamIfExists()
        console.info("Done!")
      })
  }
  private setOutputDir(readFrom: string) {
    const extension = path.extname(readFrom)
    const fileName = path.basename(readFrom, extension)
    const splitFileBaseDir = `${path.dirname(readFrom)}/split-files`
    this.outputPath = `${splitFileBaseDir}/${fileName}`
    if (!fs.existsSync(splitFileBaseDir)) {
      fs.mkdirSync(splitFileBaseDir)
    }
    if (!fs.existsSync(this.outputPath)) {
      fs.mkdirSync(this.outputPath)
    }
  }
  private replaceWriteStream(): void {
    this.identifier++
    this.closeWriteStreamIfExists()
    const writeTo = `${this.outputPath}/file_${this.identifier}.txt`
    console.info(`Start writing to ${writeTo}.\n`)
    this.currentWriteStream = fs
      .createWriteStream(writeTo)
  }
  private closeWriteStreamIfExists() {
    if (this.currentWriteStream) {
      this.currentWriteStream.close()
    }
  }
}
まとめ
readline標準モジュールにたどり着くまでの人生を無駄にした。
Streamをちゃんと考えて使ったことあんまりなかったのでちょっと楽しかった。