目次
概要
NPOIを使用して、Excelシートに含まれるすべての行とセルを順番に走査し、その値を文字列として取得する実装です。
行数や列数が決まっていない可変長のデータを扱う場合に有効で、C#のイテレータ(IEnumerable / yield return)を活用することで、簡潔かつメモリ効率の良い読み込み処理を実現します。
仕様(入出力)
- 入力: Excelファイルパス、対象シート番号
- 出力: コンソールへの全セル値のタブ区切り出力
- ライブラリ: NPOI (NuGetパッケージ)
実装メソッド
| メソッド名 | 説明 |
GetRows | シート内のデータが存在する最初の行から最後の行までを順に走査し、行オブジェクト(IRow)を返します。 |
GetCells | 指定された行に含まれるセルを先頭列から末尾列まで順に走査し、セルオブジェクト(ICell)を返します。 |
基本の使い方
var book = MyExcelIterator.Open("data.xlsx");
book.SelectSheet(0);
// すべての行をループ
foreach (var row in book.GetRows())
{
// 行内のすべてのセルをループ
foreach (var cell in book.GetCells(row))
{
// 値を表示(NPOIのToStringは簡易的な値の文字列表現を返します)
Console.Write($"{cell?.ToString()}\t");
}
Console.WriteLine();
}
コード全文
using System;
using System.Collections.Generic;
using System.IO;
using NPOI.SS.UserModel;
using NPOI.XSSF.UserModel;
class Program
{
static void Main()
{
// テスト用ファイルパス
string filePath = "example.xlsx";
if (!File.Exists(filePath))
{
Console.WriteLine("File not found.");
return;
}
try
{
// 1. ファイルを開く
var xls = MyExcelIterator.Open(filePath);
// 2. 最初のシートを選択
xls.SelectSheet(0);
// 3. 全行・全セルを走査して出力
Console.WriteLine("--- Excel Data Dump ---");
foreach (var row in xls.GetRows())
{
// 行ごとの値を格納するリスト
var lineValues = new List<string>();
foreach (var cell in xls.GetCells(row))
{
// cellがnullの場合は空文字、それ以外はToString()で値を取得
// ※NPOIのToString()は型に応じて適切な文字列を返してくれます
lineValues.Add(cell?.ToString() ?? "");
}
// タブ区切りで結合して表示
Console.WriteLine(string.Join("\t", lineValues));
}
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
}
}
/// <summary>
/// NPOIを使用したExcel全走査クラス
/// </summary>
public sealed class MyExcelIterator
{
private XSSFWorkbook _workbook;
private ISheet _sheet;
// コンストラクタ隠蔽
private MyExcelIterator() { }
/// <summary>
/// ファイルを開いてインスタンスを生成
/// </summary>
public static MyExcelIterator Open(string filePath)
{
var obj = new MyExcelIterator();
// 読み込みモードで開く
using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
obj._workbook = new XSSFWorkbook(stream);
return obj;
}
/// <summary>
/// 対象シートを選択
/// </summary>
public void SelectSheet(int index)
{
_sheet = _workbook.GetSheetAt(index);
}
/// <summary>
/// シート内の有効な全行を列挙するイテレータ
/// </summary>
public IEnumerable<IRow> GetRows()
{
if (_sheet == null) yield break;
// FirstRowNum: データがある最初の行インデックス
// LastRowNum : データがある最後の行インデックス
for (int i = _sheet.FirstRowNum; i <= _sheet.LastRowNum; i++)
{
// GetRowは行データがない場合nullを返すため、nullチェック推奨
var row = _sheet.GetRow(i);
if (row != null)
{
yield return row;
}
}
}
/// <summary>
/// 行内の全セルを列挙するイテレータ
/// </summary>
public IEnumerable<ICell> GetCells(IRow row)
{
if (row == null) yield break;
// LastCellNumは「最後の列インデックス + 1」を返す仕様
// (例: データが3列目(Index 2)まであれば、LastCellNumは 3)
for (int i = 0; i < row.LastCellNum; i++)
{
// セルが未入力の場合はnullが返る
var cell = row.GetCell(i);
yield return cell;
}
}
}
カスタムポイント
- 空行の扱い:現在の GetRows は row != null の場合のみを返していますが、空白行も詰めずに処理したい場合は yield return row; とし、呼び出し側で null 判定を行うように変更してください。
- 厳密な値取得:cell.ToString() は便利ですが、日付などが意図しない書式になる場合があります。厳密な型制御が必要な場合は、前回の記事で紹介した CellType に基づく値取得ロジックを GetCells の内部や呼び出し側で組み合わせてください。
注意点
- LastCellNumの罠:NPOIの row.LastCellNum プロパティは「最後の列のインデックス」ではなく、「セルの個数(1ベース)」のような値を返します。そのため、ループ条件は i <= LastCellNum ではなく i < LastCellNum が正解です。ここを間違えると範囲外アクセスや意図しない挙動の原因になります。
- Nullセルの存在:Excel上で値が入っていないセルや、書式設定のみされて値がないセルは GetCell(i) で null が返ってきます。必ず null チェックを行うか、?. 演算子を使用してください。
- パフォーマンス:IEnumerable と yield return を使用しているため、一度に全てのデータをリスト化するよりもメモリ効率は良いですが、巨大なファイルを扱う場合はストリーミング処理が必要になることもあります。
応用
特定の条件に一致する行だけを抽出する
LINQと組み合わせることで、特定のキーワードを含む行だけを簡単に抽出できます。
using System.Linq;
// A列(Index 0)に "Target" という文字が含まれる行だけを取得
var targetRows = xls.GetRows()
.Where(row => row.GetCell(0)?.ToString() == "Target");
foreach (var row in targetRows)
{
// 処理...
}
まとめ
NPOIにおける全セルの走査は、FirstRowNum から LastRowNum までの行ループと、0 から LastCellNum までのセルループという二重構造が基本となります。この繰り返し処理をクラス内にイテレータとして隠蔽することで、利用側のコードは foreach 文だけの読みやすい形になり、行ごとのデータ処理ロジックに集中できるようになります。
