はじめに 「ファイル分割」は“逃げ”ではなく戦略
業務をやっていると、だいたい一度はこうなります。
- ログがバカみたいに大きくなって、テキストエディタで開けない。
- 外部システムから「1 ファイル 10MB までで送って」と言われる。
- 巨大 CSV をそのまま処理するとメモリが死ぬので、小分けにしたい。
ここで必要になるのが「ファイル分割」です。
大きなファイルを「サイズごと」「行数ごと」などのルールで、複数の小さなファイルに切り分ける処理です。
ただ「適当に分ける」だけだと、後で結合できなかったり、ヘッダー行が消えたり、文字化けしたりと、地味に事故りやすいところでもあります。
ここでは、プログラミング初心者向けに、バイナリファイルの分割とテキストファイル(特に CSV)の分割を、実務目線でかみ砕いて解説します。
バイナリファイルの分割 「サイズで機械的に切る」
考え方:中身の意味は気にせず、バイト単位で切る
画像、PDF、ZIP、バックアップファイルなど、「中身の構造は意識せず、とにかくサイズで分けたい」ケースでは、
バイト単位で機械的に分割するのが基本です。
例えば、「1 ファイルを 10MB ごとに分割する」イメージです。
シンプルなバイナリ分割ユーティリティ
using System;
using System.IO;
public static class BinaryFileSplitter
{
public static void SplitBySize(string sourcePath, string outputDirectory, long partSizeBytes)
{
if (!File.Exists(sourcePath))
{
throw new FileNotFoundException("元ファイルが存在しません。", sourcePath);
}
if (!Directory.Exists(outputDirectory))
{
Directory.CreateDirectory(outputDirectory);
}
const int bufferSize = 1024 * 1024;
byte[] buffer = new byte[bufferSize];
using var input = new FileStream(sourcePath, FileMode.Open, FileAccess.Read, FileShare.Read);
int partIndex = 0;
long bytesWrittenInPart = 0;
FileStream? currentOutput = null;
void OpenNextPart()
{
currentOutput?.Dispose();
string partFileName = Path.Combine(
outputDirectory,
$"{Path.GetFileName(sourcePath)}.part{partIndex:D4}");
currentOutput = new FileStream(partFileName, FileMode.Create, FileAccess.Write, FileShare.None);
bytesWrittenInPart = 0;
partIndex++;
}
OpenNextPart();
int read;
while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
{
int offset = 0;
while (offset < read)
{
long remainingInPart = partSizeBytes - bytesWrittenInPart;
int toWrite = (int)Math.Min(remainingInPart, read - offset);
currentOutput!.Write(buffer, offset, toWrite);
offset += toWrite;
bytesWrittenInPart += toWrite;
if (bytesWrittenInPart >= partSizeBytes)
{
OpenNextPart();
}
}
}
currentOutput?.Dispose();
}
}
C#重要ポイントの解説
「バッファを使って少しずつ読む」
巨大ファイルを一気に読み込まず、1MB ずつ読みながら書き出しています。
これにより、ファイルサイズが何 GB でもメモリ使用量は一定に抑えられます。
「partSizeBytes を超えないように書き分ける」
1 回の Read で読んだデータを、そのまま 1 つの分割ファイルに書くのではなく、
「今の分割ファイルにあとどれだけ書けるか」を計算しながら、必要に応じて次のファイルを開いています。
「ファイル名に連番を付ける」元ファイル名.part0000, 元ファイル名.part0001 のように、
後で結合しやすい規則的な名前にしています。
テキストファイル(CSVなど)の分割 「行数で切る」
考え方:行の途中で切らない、ヘッダーをどうするか決める
テキストファイル、特に CSV のような「行単位のデータ」の場合は、
バイト単位ではなく「行数単位」で分割するのが自然です。
ここで大事になるのが、ヘッダー行の扱いです。
- 各分割ファイルの先頭にヘッダーを付けるのか。
- 最初のファイルだけヘッダーを持ち、残りはデータだけにするのか。
業務でよく使うのは「各ファイルにヘッダーを付ける」パターンです。
どのファイルを単体で渡しても、そのまま CSV として扱えるからです。
ヘッダー付き CSV を行数で分割するユーティリティ
using System;
using System.IO;
using System.Text;
public static class CsvFileSplitter
{
public static void SplitByLines(
string sourcePath,
string outputDirectory,
int maxDataLinesPerFile,
Encoding encoding)
{
if (!File.Exists(sourcePath))
{
throw new FileNotFoundException("元ファイルが存在しません。", sourcePath);
}
if (!Directory.Exists(outputDirectory))
{
Directory.CreateDirectory(outputDirectory);
}
using var reader = new StreamReader(sourcePath, encoding, detectEncodingFromByteOrderMarks: true);
string? header = reader.ReadLine();
if (header is null)
{
return;
}
int partIndex = 0;
int dataLinesInCurrentFile = 0;
StreamWriter? currentWriter = null;
StreamWriter OpenNextWriter()
{
currentWriter?.Dispose();
string partFileName = Path.Combine(
outputDirectory,
$"{Path.GetFileNameWithoutExtension(sourcePath)}_{partIndex:D4}{Path.GetExtension(sourcePath)}");
var writer = new StreamWriter(partFileName, false, encoding);
writer.WriteLine(header);
partIndex++;
dataLinesInCurrentFile = 0;
return writer;
}
currentWriter = OpenNextWriter();
string? line;
while ((line = reader.ReadLine()) is not null)
{
if (dataLinesInCurrentFile >= maxDataLinesPerFile)
{
currentWriter = OpenNextWriter();
}
currentWriter.WriteLine(line);
dataLinesInCurrentFile++;
}
currentWriter?.Dispose();
}
}
C#重要ポイントの解説
「最初の 1 行をヘッダーとして読む」ReadLine で 1 行目を読み、それを header として保持しています。
その後に開くすべての分割ファイルの先頭に、このヘッダーを書き出しています。
「データ行数で分割する」maxDataLinesPerFile は「ヘッダーを除いたデータ行数」です。
例えば 10,000 を指定すると、「ヘッダー 1 行+データ 10,000 行」の CSV が順に生成されます。
「エンコーディングを明示する」
読み込みと書き込みで同じ Encoding を使っています。
CSV のエンコーディング(UTF-8, Shift_JIS など)が決まっているなら、必ず指定しましょう。
分割ファイルの命名ルールをどうするか
規則的な名前にしておくと後処理が楽になる
分割ファイルの名前は、後で結合したり、別システムに渡したりするときに効いてきます。
よく使うパターンは次のような形です。
- 元ファイル名+
.part0000のような拡張子追加型(バイナリ向き) - 元ファイル名の末尾に
_0000のようなサフィックスを付ける(CSV など拡張子を残したい場合)
例えば、sales.csv を 3 つに分割した場合、
sales_0000.csvsales_0001.csvsales_0002.csv
のようにしておくと、ソート順もそのまま結合順になります。
命名ルールはチームで決めて、ユーティリティ側で統一してしまうと、後々楽です。
分割サイズ・行数の決め方
「上限ギリギリ」ではなく「余裕を持った値」にする
外部システムから「1 ファイル 10MB まで」と言われたとき、
本当に 10MB ぴったりを狙う必要はありません。
- 文字コードや改行コードの違いで微妙にサイズが変わる
- 後で列が増えて 1 行あたりのサイズが増える
といったことを考えると、例えば「8MB くらいを目安にする」ほうが安全です。
行数で分割する場合も同様で、「Excel で開きたいから 100 万行以下にしたい」なら、
90 万行など、少し余裕を持たせておくと安心です。
例題:巨大ログを日別・サイズ別に分割するイメージ
例えば、1 つの巨大ログファイル app.log があり、
これを「1 ファイル 50MB 以内」で分割したいとします。
バイナリ分割ユーティリティを使うと、次のように書けます。
string source = @"C:\logs\app.log";
string outputDir = @"C:\logs\split";
long partSize = 50L * 1024 * 1024;
BinaryFileSplitter.SplitBySize(source, outputDir, partSize);
C#これで、
app.log.part0000app.log.part0001- …
のようなファイルができます。
一方、CSV の売上データ sales.csv を「1 ファイル 5 万行(ヘッダー除く)」で分割したいなら、こうです。
string source = @"C:\data\sales.csv";
string outputDir = @"C:\data\sales_split";
CsvFileSplitter.SplitByLines(
sourcePath: source,
outputDirectory: outputDir,
maxDataLinesPerFile: 50_000,
encoding: Encoding.UTF8);
C#まとめ 実務で使える「ファイル分割」ユーティリティの考え方
ファイル分割は、「大きすぎて扱いづらいものを、扱いやすい単位にする」ための道具です。
ただ切るだけでなく、「後でどう使うか」「どう結合するか」までイメージして設計するのがポイントです。
押さえておきたいのは次のようなことです。
- バイナリファイルは「サイズ単位」、テキストファイルは「行数単位」で分割するのが基本。
- CSV などヘッダー付きのテキストは、「ヘッダーをどう扱うか」を最初に決める(各ファイルに付けるのが無難)。
- 読み書きのエンコーディングを揃えることで、文字化けやデータ破損を防ぐ。
- 分割ファイルの命名ルールを決めておくと、後処理(結合・転送)が楽になる。
- サイズや行数の上限は、仕様ギリギリではなく、少し余裕を持たせる。
ここまで押さえておけば、「とりあえず手で分ける」「エディタが固まるまで頑張る」といった世界から抜けて、
「業務で通用するファイル分割ユーティリティ」を自分の武器として使い回せるようになります。

