C# Tips | ファイル・ディレクトリ操作:ファイル行数カウント

C sharp C#
スポンサーリンク

はじめに 「行数を数える」だけなのに、なぜユーティリティにするのか

「ファイルの行数カウント」って、一見すごく地味ですよね。
でも業務では、ログ解析、CSV のレコード数チェック、インポート前の件数確認、進捗表示など、いろんな場面でめちゃくちゃ使われます。

しかも、「小さいファイルならなんでもいい」けれど、「数百 MB のログ」「数百万行の CSV」になってくると、
安易な実装だとすぐに遅くなったり、メモリを食い潰したりします。

だからこそ、「行数カウント」をちゃんとユーティリティとして設計しておくと、
後々いろんなところで再利用できて、バグもパフォーマンス問題も減らせます。

ここでは、プログラミング初心者向けに、
シンプルな実装から、実務で通用する実装まで、段階的にかみ砕いて解説していきます。


一番シンプルなやり方 File.ReadLines を使う

ReadLines と ReadAllLines の違いをまず押さえる

C# には、テキストファイルを行単位で扱うためのメソッドがいくつかありますが、
行数カウントでまず覚えてほしいのは File.ReadLines です。

File.ReadAllLines(path)
ファイル全体を一気に読み込んで、string[] として返す。
小さいファイルならいいけれど、大きいファイルだとメモリを大量に消費する。

File.ReadLines(path)
必要になった行だけ、順番に読み出す「遅延読み込み」。
巨大ファイルでも、メモリをほとんど消費せずに行単位処理ができる。

行数カウントは「全行を一度にメモリに持つ必要がない」処理なので、
ReadLines を使うのが圧倒的に相性がいいです。

ReadLines を使ったシンプルな行数カウント

まずは一番素直な実装を見てみましょう。

using System;
using System.IO;
using System.Linq;

public static class LineCountUtil
{
    public static long CountLines(string path)
    {
        if (!File.Exists(path))
        {
            throw new FileNotFoundException("ファイルが存在しません。", path);
        }

        return File.ReadLines(path).LongCount();
    }
}
C#

使い方はとてもシンプルです。

string filePath = @"C:\data\log.txt";

long lineCount = LineCountUtil.CountLines(filePath);

Console.WriteLine($"行数: {lineCount}");
C#

ここでの重要ポイントは、File.ReadLines(path).LongCount() という書き方です。
ReadLines は 1 行ずつ読み出し、LongCount は列挙しながら件数を数えるだけなので、
巨大ファイルでもメモリをほとんど使いません。


LINQ を使わない「ループ版」の行数カウント

ループで書くと中身がイメージしやすい

LINQ にまだ慣れていない場合は、StreamReader と while ループで書いたほうがイメージしやすいかもしれません。

using System;
using System.IO;

public static class LineCountUtil
{
    public static long CountLinesManual(string path)
    {
        if (!File.Exists(path))
        {
            throw new FileNotFoundException("ファイルが存在しません。", path);
        }

        long count = 0;

        using (var reader = new StreamReader(path))
        {
            while (reader.ReadLine() is not null)
            {
                count++;
            }
        }

        return count;
    }
}
C#

このコードがやっていることは、とても単純です。

1 行読む → null でなければカウントを 1 増やす → 次の行へ、を繰り返す。
ReadLine が null を返したら「もう行がない(EOF)」なので終了、という流れです。

ReadLines 版も内部的にはほぼ同じことをやっているので、
「行数カウントってこういう仕組みなんだ」という理解には、このループ版が役立ちます。


空行や条件付きカウントにも対応する

「空行は数えたくない」という要件

業務では、「空行は行数に含めないでほしい」という要件もよくあります。
例えば、CSV の「実データ行数」を知りたいときなどです。

その場合は、ReadLines に対して条件を付けてカウントします。

using System;
using System.IO;
using System.Linq;

public static class LineCountUtil
{
    public static long CountNonEmptyLines(string path)
    {
        if (!File.Exists(path))
        {
            throw new FileNotFoundException("ファイルが存在しません。", path);
        }

        return File
            .ReadLines(path)
            .LongCount(line => !string.IsNullOrWhiteSpace(line));
    }
}
C#

ここでは、LongCount に「条件付きカウント」のラムダ式を渡しています。
!string.IsNullOrWhiteSpace(line) が true の行だけを数える、という意味です。

コメント行を除外したい場合

設定ファイルやスクリプトなどでは、「コメント行(例: #// で始まる行)は除外したい」ということもあります。

例えば、「空行と # で始まる行を除外してカウントする」ユーティリティは次のように書けます。

public static long CountEffectiveLines(string path)
{
    if (!File.Exists(path))
    {
        throw new FileNotFoundException("ファイルが存在しません。", path);
    }

    return File
        .ReadLines(path)
        .Select(line => line.Trim())
        .LongCount(line =>
            !string.IsNullOrWhiteSpace(line) &&
            !line.StartsWith("#"));
}
C#

ここでのポイントは、Trim で前後の空白を削ってから判定していることです。
" # comment" のように、前にスペースがあってもコメント行として扱えるようになります。


パフォーマンスを意識した「超大容量ファイル」の行数カウント

ReadLines でも十分速いが、さらに低レベルに書くこともできる

File.ReadLines は内部的に StreamReader を使っているので、
ほとんどのケースではこれで十分です。

ただ、「数 GB クラスのログファイル」「1,000 万行以上の CSV」など、
本当に巨大なファイルを相手にする場合、
「1 行ずつ ReadLine するより、バイト配列をまとめて読みながら改行コードを数える」ほうが速いこともあります。

イメージとしては、「テキストとして読む」のではなく、「生のバイト列として読み、'\n' を数える」というやり方です。

using System;
using System.IO;
using System.Text;

public static class FastLineCountUtil
{
    public static long CountLinesByBytes(string path, Encoding encoding)
    {
        if (!File.Exists(path))
        {
            throw new FileNotFoundException("ファイルが存在しません。", path);
        }

        const int bufferSize = 1024 * 1024;

        long count = 0;

        using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
        using (var reader = new StreamReader(stream, encoding, detectEncodingFromByteOrderMarks: true, bufferSize: bufferSize))
        {
            char[] buffer = new char[bufferSize];

            int read;
            while ((read = reader.Read(buffer, 0, buffer.Length)) > 0)
            {
                for (int i = 0; i < read; i++)
                {
                    if (buffer[i] == '\n')
                    {
                        count++;
                    }
                }
            }
        }

        return count;
    }
}
C#

ここでは、Read で大量の文字を一気に読み込み、その中で '\n' を数えています。
改行コードが \r\n の場合でも、'\n' の数を数えれば行数と一致します。

ただし、この方法は「最後の行が改行で終わっていない場合」の扱いなど、細かい仕様を自分で決める必要があります。
初心者のうちは、まずは ReadLines ベースで十分です。


行数カウントを「業務ユーティリティ」としてどう設計するか

戻り値の型は int ではなく long にする

行数カウントの戻り値は、int ではなく long にしておくのがおすすめです。
int の最大値は約 21 億ですが、ログや CSV の世界では「数千万行」「1 億行」も普通にあり得ます。

LongCount を使っているのもそのためで、
「最初から long で設計しておけば、後から溢れ対策をする必要がない」という安心感があります。

例外を投げるか、0 を返すか

ファイルが存在しない場合や、読み取り権限がない場合をどう扱うかも、ユーティリティ設計のポイントです。

パターンとしては、次のような考え方があります。

ファイルがないのは異常 → 例外を投げる(FileNotFoundException など)
ファイルがないのは「行数 0」とみなしてよい → 0 を返す

どちらが正しいかは業務要件次第ですが、
「ユーティリティの中で勝手に握りつぶさない」ことだけは意識しておくとよいです。

例えば、「例外を投げる版」と「安全に 0 を返す版」を分けておくのも一つの手です。

public static long TryCountLines(string path)
{
    try
    {
        return CountLines(path);
    }
    catch
    {
        return 0;
    }
}
C#

呼び出し側が「失敗したら 0 でいい」と割り切れる場面では、こちらを使う、というように使い分けられます。


例題 実務での具体的な使いどころ

CSV インポート前の件数チェック

例えば、「売上データ CSV をインポートする前に、レコード数をログに出したい」というケースを考えてみます。
1 行目がヘッダーで、2 行目以降がデータだとします。

string csvPath = @"C:\import\sales.csv";

long totalLines = LineCountUtil.CountLines(csvPath);

long dataLines = Math.Max(0, totalLines - 1);

Console.WriteLine($"ファイル: {csvPath}");
Console.WriteLine($"総行数: {totalLines}");
Console.WriteLine($"データ行数(ヘッダー除く): {dataLines}");
C#

これをログに残しておけば、「インポート前に何件あったか」「インポート後に何件成功したか」を比較できるので、
トラブル時の調査がぐっと楽になります。

ログローテーションの判断材料として使う

ログファイルが「何行以上になったらローテーションする」といった要件もよくあります。

string logPath = @"C:\logs\app.log";

long lines = LineCountUtil.CountLines(logPath);

if (lines > 1_000_000)
{
    Console.WriteLine("ログが 100 万行を超えたのでローテーションします。");
    // ローテーション処理(リネーム、圧縮など)を書く
}
C#

サイズ(バイト数)ではなく行数で管理したい場合、
このように「行数カウントユーティリティ」が素直に使えます。


まとめ 実務で使える「ファイル行数カウント」ユーティリティの考え方

「行数を数えるだけ」の処理でも、ちゃんと設計しておくと業務でかなり役に立ちます。

押さえておきたいポイントは次の通りです。

行数カウントには File.ReadLines を使うと、巨大ファイルでもメモリをほとんど使わずに済む。
LINQ の LongCount でシンプルに書けるが、StreamReader+while ループで中身を理解しておくと応用が効く。
空行やコメント行を除外したい場合は、「条件付きカウント」にするだけで対応できる。
戻り値は long にしておくと、大量行ファイルでも安心して扱える。
例外の扱い(存在しないとき・権限がないとき)をどうするかは、ユーティリティの設計方針として最初に決めておく。

ここまで押さえておけば、「とりあえず ReadAllLines で数えました」から卒業して、
「業務でそのまま使える行数カウントユーティリティ」を自信を持って書けるようになります。

タイトルとURLをコピーしました