C#: 演習:LINQ とファイル
1. 学生の成績分析ツール
学生データをJSONファイルにシリアライズし、それをデシリアライズして、LINQを用いて多次元統計分析(平均点、上位3名、合格者数、学年ごとのグループ分け)を行います。
(1) 要件
Studentクラスを定義し、そのプロパティとしてNameとScoreを設定する- 学生リストをJSON形式でシリアライズし、一時ファイルに保存する
- ファイルから読み込み、デシリアライズする
- LINQ を使用して、平均スコア、上位 3 位、合格者数(60 以上)、および学年ごとの集計を算出する
▶ サンプル
CSHARP
📖 参照専用
using System;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Collections.Generic;
public class Student
{
public string Name { get; set; }
public int Score { get; set; }
}
class Program
{
static void Main()
{
var students = new List<Student>
{
new Student { Name = "Zhang San", Score = 92 },
new Student { Name = "Li Si", Score = 45 },
new Student { Name = "Wang Wu", Score = 78 },
new Student { Name = "Zhao Liu", Score = 88 },
new Student { Name = "Qian Qi", Score = 55 },
new Student { Name = "Sun Ba", Score = 95 },
new Student { Name = "Zhou Jiu", Score = 33 },
new Student { Name = "Wu Shi", Score = 71 }
};
string tempDir = Path.GetTempPath();
string filePath = Path.Combine(tempDir, "students.json");
var options = new JsonSerializerOptions { WriteIndented = true };
string json = JsonSerializer.Serialize(students, options);
File.WriteAllText(filePath, json);
Console.WriteLine("Written to: " + filePath);
string readJson = File.ReadAllText(filePath);
var loaded = JsonSerializer.Deserialize<List<Student>>(readJson);
double average = loaded.Average(s => s.Score);
Console.WriteLine($"Average score: {average:F1}");
var top3 = loaded.OrderByDescending(s => s.Score).Take(3);
Console.WriteLine("Top three:");
foreach (var s in top3)
{
Console.WriteLine($" {s.Name} - {s.Score}");
}
int passCount = loaded.Count(s => s.Score >= 60);
Console.WriteLine($"Pass count: {passCount}");
var grouped = loaded.GroupBy(s => s.Score >= 90 ? "Excellent"
: s.Score >= 60 ? "Pass" : "Fail");
Console.WriteLine("Grade groups:");
foreach (var group in grouped)
{
Console.WriteLine($" {group.Key}: {string.Join(", ", group.Select(s => s.Name))}");
}
File.Delete(filePath);
}
}
TEXT
📖 参照専用
Written to: /tmp/students.json
Average score: 69.6
Top three:
Sun Ba - 95
Zhang San - 92
Zhao Liu - 88
Pass count: 5
Grade groups:
Excellent: Zhang San, Sun Ba
Fail: Li Si, Qian Qi, Zhou Jiu
Pass: Wang Wu, Zhao Liu, Wu Shi
2. シンプルなログ解析ツール
テキスト形式のログファイルを読み込み、各行を解析し、LINQ を使用してエラーや警告の件数をカウントし、最も頻度の高いエラーを特定し、日付範囲でフィルタリングします。
(1) 要件
- ログ形式:
[Date] [Level] Message、例:[2025-01-15] [ERROR] Disk full - レベルごとのエントリ数を数える
- 最も頻繁に表示されるエラーメッセージを見つける
- 日付範囲でログをフィルタリングする
▶ サンプル
CSHARP
📖 参照専用
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
class Program
{
static void Main()
{
string tempDir = Path.GetTempPath();
string logPath = Path.Combine(tempDir, "app.log");
var lines = new List```<string>```
{
"[2025-01-10] [INFO] System started",
"[2025-01-10] [WARN] Memory usage 80%",
"[2025-01-11] [ERROR] Database connection failed",
"[2025-01-11] [ERROR] Disk full",
"[2025-01-12] [INFO] User login",
"[2025-01-12] [ERROR] Database connection failed",
"[2025-01-13] [WARN] CPU usage 90%",
"[2025-01-13] [ERROR] Network timeout",
"[2025-01-14] [INFO] Scheduled task completed",
"[2025-01-14] [ERROR] Database connection failed"
};
File.WriteAllLines(logPath, lines);
var logEntries = File.ReadAllLines(logPath)
.Select(line =>
{
var parts = line.Split(']');
return new
{
Date = parts[0].TrimStart('[').Trim(),
Level = parts[1].TrimStart('[').Trim(),
Message = parts[2].Trim()
};
})
.ToList();
var levelCounts = logEntries
.GroupBy(e => e.Level)
.Select(g => new { Level = g.Key, Count = g.Count() })
.OrderByDescending(x => x.Count);
Console.WriteLine("Level counts:");
foreach (var item in levelCounts)
{
Console.WriteLine($" {item.Level}: {item.Count}");
}
var topError = logEntries
.Where(e => e.Level == "ERROR")
.GroupBy(e => e.Message)
.OrderByDescending(g => g.Count())
.First();
Console.WriteLine($"Most frequent error: {topError.Key} ({topError.Count()} times)");
var filtered = logEntries
.Where(e => string.Compare(e.Date, "2025-01-11") >= 0
&& string.Compare(e.Date, "2025-01-13") <= 0)
.ToList();
Console.WriteLine("Logs from Jan 11-13:");
foreach (var entry in filtered)
{
Console.WriteLine($" [{entry.Date}] [{entry.Level}] {entry.Message}");
}
File.Delete(logPath);
}
}
TEXT
📖 参照専用
Level counts:
ERROR: 5
INFO: 3
WARN: 2
Most frequent error: Database connection failed (3 times)
Logs from Jan 11-13:
[2025-01-11] [ERROR] Database connection failed
[2025-01-11] [ERROR] Disk full
[2025-01-12] [INFO] User login
[2025-01-12] [ERROR] Database connection failed
[2025-01-13] [WARN] CPU usage 90%
[2025-01-13] [ERROR] Network timeout
3. バッチファイル名変更ツール
ディレクトリ内のファイルを一覧表示し、LINQ を使用して拡張子でフィルタリングした後、接頭辞または接尾辞を一括で追加します。
(1) 要件
- 一時ディレクトリにサンプルファイルを作成する
- ファイルの拡張子で絞り込む
- ファイル名に接頭辞と接尾辞を追加する(拡張子はそのまま維持する)
- 名前変更のロジックを表示する(旧名称と新名称の比較結果を出力する)
▶ サンプル
CSHARP
📖 参照専用
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
class Program
{
static void Main()
{
string workDir = Path.Combine(Path.GetTempPath(), "rename_demo");
Directory.CreateDirectory(workDir);
var demoFiles = new[] { "photo1.jpg", "photo2.jpg", "doc.txt", "notes.txt", "data.csv" };
foreach (var f in demoFiles)
{
File.WriteAllText(Path.Combine(workDir, f), "demo");
}
Console.WriteLine("Original files:");
foreach (var f in Directory.GetFiles(workDir))
{
Console.WriteLine(" " + Path.GetFileName(f));
}
string targetExt = ".txt";
string prefix = "backup_";
var filesToRename = Directory.GetFiles(workDir)
.Where(f => Path.GetExtension(f).Equals(targetExt, StringComparison.OrdinalIgnoreCase))
.ToList();
Console.WriteLine($"\nFiltering extension {targetExt}:");
foreach (var f in filesToRename)
{
string dir = Path.GetDirectoryName(f);
string nameNoExt = Path.GetFileNameWithoutExtension(f);
string newName = prefix + nameNoExt + targetExt;
string newPath = Path.Combine(dir, newName);
Console.WriteLine($" {Path.GetFileName(f)} -> {newName}");
File.Move(f, newPath);
}
Console.WriteLine("\nAfter renaming:");
foreach (var f in Directory.GetFiles(workDir))
{
Console.WriteLine(" " + Path.GetFileName(f));
}
Directory.Delete(workDir, true);
}
}
TEXT
📖 参照専用
Original files:
photo1.jpg
photo2.jpg
doc.txt
notes.txt
data.csv
Filtering extension .txt:
doc.txt -> backup_doc.txt
notes.txt -> backup_notes.txt
After renaming:
photo1.jpg
photo2.jpg
backup_doc.txt
backup_notes.txt
data.csv
❓ よくある質問
Q JSONのデシリアライズ時にプロパティ名が一致しない場合はどうすればよいですか?
A
[JsonPropertyName]属性を使用して名前をマッピングするか、JsonSerializerOptions内のPropertyNamingPolicyをカスタマイズしてください。Q GroupBy の結果の順序は保証されていますか?
A いいえ、GroupBy ではグループの順序は保証されません。反復処理を行う前に、OrderBy を使用して並べ替えてください。
Q File.Move を使用してファイル名を変更する際、対象のファイルがすでに存在している場合はどうなりますか?
A IOException が発生します。まず対象のパスが存在するかどうかを確認するか、File.Replace を使用してください。
Q ReadAllLines は、大容量のログファイルを読み込むのに適していますか?
A いいえ、ReadAllLines はコンテンツ全体を一度にメモリに読み込みます。大容量のファイルの場合は、File.ReadLines を使用して、行単位で遅延読み込みを行ってください。
📖 まとめ
- データの永続化および統計分析のために、LINQとJSONシリアライゼーションを包括的に活用する
- ログの解析、LINQによるグループ化と集計、および日付によるフィルタリングの手法を習得する
- ファイルのフィルタリングやバッチ処理には、Directory クラスと Path クラスを LINQ と組み合わせて使用する
- すべてのファイル操作の例では、クロスプラットフォームでの互換性を確保するため、Path.GetTempPath() を使用しています
- 一時ファイルやディレクトリは、使用後に削除され、残骸が残らないようにしています
📝 練習問題
- 学生の成績分析機能を拡張し、得点範囲(0-59/60-79/80-89/90-100)ごとに学生数を集計し、棒グラフを出力するようにする(
*文字を使用)。 - ログアナライザーに以下の機能を追加する:1日あたりの「ERROR」エントリ数をカウントし、「ERROR」が最も連続して発生した日付範囲を出力する
- ファイル名変更ツールをリファクタリングし、ディレクトリパス、拡張子フィルター、プレフィックスを指定するためのコマンドライン引数を受け入れられるようにするとともに、実行せずにプレビューのみを表示する
--dry-runモードを追加する