misc.log

日常茶飯事とお仕事と

NPOIで.NET Framework/C#からエクセルファイルの中身をテキストに吐き出す

f:id:frontline:20211018214921p:plain

大量のエクセルファイルの中身について精査したり文字列調査する必要がありそうだったので、以前使った.NET Framework用のExcelオブジェクト操作ライブラリー「NPOI」を使って、エクセルファイルの中身をテキストにする処理を書いてました。XLSX形式(Office 2007以降)のエクセルファイルは実体がXMLで、ClosedXML形式を扱えるライブラリーでも操作可能ですが、NPOIは古いタイプの、拡張子が.xlsのエクセルファイルも取り扱えるのが強みです。実際、お客さんの環境ではなんだかんだいってXLS形式がのこっていたりしますので……。

しかし、久々にプログラム組むと楽しいですね。

NPOIの導入

NPOIは、今手元にあるVisual Studio 2019の場合、[プロジェクト]-[NuGetパッケージの管理]から、検索条件でNPOIと入力すると出てきます。後に何も付かない「NPOI」がおそらく本体ですので、それを指定すれば取り込めます。

f:id:frontline:20211018215116p:plain
NuGetでのNPOI検索と選択

サンプル処理: ファイルの読み込みとstring化

メインの処理はこんな感じ。自分が自分のために使うものなので特にエラー処理とかまだ入れていません。エクセルファイル名をフルパス指定すると、その内容をテキストデータとして返します。各列の区切りは「|」で、行の区切りは改行、というテキストデータが返却されるので、これをテキストファイルとして出力。この処理を、指定したフォルダーにある全エクセルファイルに対して実施すれば、ファイル数がどんなにあってもテキスト化が一気にできます。

private string ReadFile(string fileName)
{
    // XLSX形式のオブジェクトを生成
    XSSFWorkbook workbook;
    using (FileStream file = new FileStream(fileName, FileMode.Open, FileAccess.Read))
    {
        workbook = new XSSFWorkbook(file);
    }

    string sheetName = workbook.GetSheetName(0);
    ISheet sheet = workbook.GetSheet(sheetName);

    StringBuilder resultBuilder = new StringBuilder();
    ICell cellObject;
    // 行、列のループでセル情報を読み出してテキストを組み立てる
    for (int rowno = 0; rowno <= sheet.LastRowNum; rowno++)
    {
        if (sheet.GetRow(rowno) != null)
        {
            for (int colno = 0; colno <= Int32.Parse(ReadColsText.Text)-1; colno++)
            {
                if (sheet.GetRow(rowno).GetCell(colno) != null)
                {
                    cellObject = sheet.GetRow(rowno).GetCell(colno);
                    if (cellObject.CellType == CellType.String)
                    {
                        // セルが文字列の場合は文字として扱う
                        resultBuilder.Append(sheet.GetRow(rowno).GetCell(colno).StringCellValue + " | ");
                    }
                    else if (cellObject.CellType == CellType.Numeric)
                    {
                        // セルが数値の場合は文字変換して扱う
                        resultBuilder.Append(sheet.GetRow(rowno).GetCell(colno).NumericCellValue.ToString() + " | ");
                    }
                }
            }
            resultBuilder.Append("\r\n");
        }
    }
    return resultBuilder.ToString();
}

フォルダー内のファイル一覧取得

フォルダー内のファイル名を取得する処理とか、結構書くの面倒くさいなぁ……と思ってしまうのでサンプルを記載しておきます。

private List<string> GetFiles(string folderName)
{
    List<string> result = new List<string>();

    if (Directory.Exists(folderName))
    {
        result=Directory.GetFiles(folderName).Where(x => x.EndsWith(".xlsx", StringComparison.OrdinalIgnoreCase) ).ToList<string>();
    }

    return result;
}

上記のコードは、フォルダー名を指定するとstring型のListでファイル名一覧を返す処理です。拡張子xlsxのファイルだけが欲しいので、全ファイル一覧からxlsxに合致するものだけを抽出してListにしています。このListの内容それぞれについて、前述のテキスト化処理を掛け、戻ってきたstringデータをテキストファイルとして書き出せば、Excelファイルのテキスト化が完了です。

各処理の呼び出し部分

ついでなので、ファイル名一覧取得から、それぞれについてテキスト化の処理を読んで、結果をテキストファイルにする部分もメモしておきます。別途テキストボックスなどを用意して、そこに書いてあるパスを渡すようにしています。

private void ReadAndWriteAsText(List<string> fileNames)
{
    LogText.Text += "対象ファイル: " + fileNames.Count.ToString() + "\r\n";
    string outputFileName;
    foreach(string fileName in fileNames)
    {
        outputFileName = fileName + ".txt";
        StreamWriter writer = File.CreateText(outputFileName);
        writer.Write(ReadFile(fileName));
        writer.Close();
        LogText.Text += " ファイル: " + outputFileName + " 出力完了。\r\n";
    }
    LogText.Text += "全ファイル処理完了。\r\n";
}

オブジェクトの片付けとかエラー処理とか全然入れてないので、何か正式に使うものに流用される方はそのあたり、各自の責任で改造してくださいね。

既知の問題

なぜか、200ファイルほど読み込むと途中にExcelを開いているときに出来る作業用ファイル(ファイル名がチルダから始まるもの)が生成され、それを読み込んでしまってエラーになるという現象が。理由は不明(そもそもチルダ付ファイルは最初は無いのに、なぜかできる)。 読み込むファイルのサイズがゼロのものを弾く、などの処理を入れれば防げると思いますが、どうも指定したファイルは全て処理し終えた上でエラーが起きているので、とりあえず未解決のまま「自分用」として使うことにします。