fragmentary notesITエンジニアの気まぐれメモ

Excel-CSV変換を、.NET・JavaScript・Pythonでやってみた

Introduction

最近、隙間タスクとして人間が都度 手で実施している作業を自動化・簡易化する活動を進めていますが、その際 度々発生リクエストが、InputとしてExcelを使いたい、というもの。隙間タスクであまりがっつりとプログラムは書きたくなく(後々 誰がメンテするんだ問題が発生するため)、なるべくWindows batやShellで簡易に済ませたいと思っている身としてはExcel Inputはなかなか手ごわい。

結局 いつもプレーンテキストとして扱いやすいCSVファイルに人が変換して処理するというひと手間を挟んでいるわけだが、これも正直煩雑。なので、今回はあまり複雑になりすぎない範囲内でExcelファイルをCSVに変換するプログラムを組めないか実践してみたので、その備忘メモとしてしたためてみる。結論から書くと、結局は環境準備も含めそれなりの手数をかけてしまった。。。

成果物イメージ

今回、作成するものとしては、引数にInputとなるExcelファイル(.xlsx)とOutputフォルダパスを渡して実行すると、指定されたOutputフォルダに、Inputファイルのシート毎にCSVファイルを出力するコンソールアプリケーションを想定する。

例えば、「Sheet1」「Sheet2」という2つのシートで構成されるExcelファイル "input.xlsx" をInputとして、Outputフォルダを"CSV"として指定し実行すると、Outputフォルダ配下に、「input_Sheet1.csv」「input_Sheet2.csv」の2つのファイルができるようにする。

このようなコンソールアプリケーションを、個人的に好きな(得意な?)2つの言語 C#(.NET)とJavaScript(Node.js)、世間一般的に最近メジャーなPythonでそれぞれ作成してみた。

C#(.NET)で作成してみた

最初にC#(.NET)にて作成。処理の流れは、引数チェック → 出力フォルダ作成 → ExcelブックOpen → シートのデータ読込 → テキストファイル出力 となります。Excelファイルの処理には、NPOIというライブラリを使用しています。これはJavaでExcelを操作するライブラリとして有名なApache POIを、.NET向けに移植されたもの。

作成したサンプルコードは下記の通り、GitHubにも載せています。

今回作成した3つの中では最もリッチな言語(その代わりコンパイルが必要)で個人的には一番書きやすく、環境構築も.NET SDKをインストールするだけ。ただし.NETというリッチな環境故にオーバーヘッドが大きく、単独実行可能な形で出力した際のファイルサイズは大き目。。。またNPOIにはこの後に試した2つと異なり調べた限り1発でCSVに変換する術がなさそうなので、1行・1セル毎にループして処理を書いてあげる手間はあり、初心者向きとは言えないかも。

//----------------------------------------------------------
// Excel-CSV変換
//
//  概要:
//    xlsxファイルパスをinputに、対象ファイルを展開し、
//    シートごとに出力フォルダパスで指定された場所にcsv出力する
//
//  引数:
//    ・入力xlsxファイルパス
//    ・出力フォルダパス
// 
//  リターンコード:
//    ・81:引数不足
//    ・82:入力ファイルパス不正
//    ・83:出力フォルダ作成失敗
//    ・98:ファイル入出力エラー
//    ・99:その他例外
//----------------------------------------------------------
using System.Text;
using NPOI.SS.UserModel;
using NPOI.XSSF.UserModel;

namespace Excel2CSv;

class Program
{
  // 入力ファイルパス
  private static string inFile = "";
  // 出力フォルダパス
  private static string outPath = "";

  /// <summary>
  /// メイン関数
  /// </summary>
  /// <param name="args">第1引数:入力xlsxファイルパス、第2引数:出力フォルダパス</param>
  /// <returns>リターンコード</returns>
  static int Main(string[] args)
  {
    int check = 0;

    // 引数チェック
    check = checkImportArgs(args);
    if(check != 0)
    {
      return check;
    }

    // 事前処理
    check = preProc();
    if(check != 0)
    {
      return check;
    }

    // 開始メッセージ
    Console.WriteLine("Excel-CSV変換処理を開始します。");
    Console.WriteLine("入力ファイル:" + inFile);

    Console.WriteLine("...");

    // 変換処理
    List<string> outFiles;
    check = excel2CSv(out outFiles);
    if(check != 0)
    {
      return check;
    }

    //  終了メッセージ
    Console.WriteLine("Excel-CSV変換処理が完了しました。\n出力ファイル:");
    foreach(string outFile in outFiles)
    {
      Console.WriteLine("・" + outFile);
    }

    return 0;
  }

  /// <summary>
  /// 引数チェック・引数取込み
  /// </summary>
  /// <param name="args">引数配列</param>
  /// <returns>リターンコード、81:引数不足、82:入力ファイルパス不正</returns>
  static private int checkImportArgs(string[] args)
  {
    // 引数不足
    if(args.Length < 2)
    {
      Console.WriteLine("引数が不足しています、第1引数:入力xlsxファイル、第2引数:出力フォルダパス");
      return 81;
    }

    // 入力ァイル存在チェック
    if(!File.Exists(args[0]))
    {
      Console.WriteLine("入力xlsxファイルが存在しません。");
      return 82;
    }

    // 引数をフィールド変数に取り込み
    inFile = args[0];
    outPath = args[1];

    return 0;
  }

  /// <summary>
  /// 事前処理
  /// </summary>
  /// <returns>リターンコード、83:フォルダ作成失敗</returns>
  static private int preProc()
  {
    try{
      // 出力先フォルダが存在しない場合、作成する
      if(!Directory.Exists(outPath))
      {
        Directory.CreateDirectory(outPath);
      }
    }
    catch(Exception e)
    {
      Console.WriteLine("出力フォルダの作成に失敗しました。");
      Console.WriteLine(e.Message);
      return 83;
    }
    return 0;
  }

  /// <summary>
  /// エクセル-CSV変換
  /// </summary>
  /// <param name="OutFiles">出力ファイルパス</param>
  /// <returns>リターンコード、98:ファイル入出力エラー、99:その他例外</returns>
  static private int excel2CSv(out List<string> OutFiles)
  {
    // 出力ファイルパスListの初期化
    OutFiles= new List<string>();

    try{
      // エクセルファイル読み込み
      using(FileStream fs = new FileStream(inFile, FileMode.Open, FileAccess.Read))
      {
        // エクセルブックオブジェクト
        IWorkbook excelBook = new XSSFWorkbook(fs);

        // エクセルファイル名prefix
        string prefix = System.IO.Path.GetFileNameWithoutExtension(inFile);

        // エクセルブックに含まれるシートを走査
        foreach(ISheet sheet in excelBook)
        {
          // 出力csvファイルパス
          string outFile = outPath + "\\" + prefix + "_" + sheet.SheetName + ".csv";

          using(StreamWriter sw = new StreamWriter(outFile, false, Encoding.UTF8))
          {
            // 行を走査
            foreach(IRow row in sheet)
            {
              StringBuilder lineBuilder = new StringBuilder();
              // 列(セル)を走査
              foreach(ICell cell in row)
              {
                lineBuilder.Append('"').Append(cell.ToString()).Append('"').Append(',');
              }

              // ファイル出力
              sw.WriteLine(lineBuilder.ToString().TrimEnd(','));
            }
          }
          // 出力先を格納
          OutFiles.Add(outFile);
        }
      }
    }
    catch(IOException ex)
    {
      Console.WriteLine("ファイル入出力に失敗しました");
      Console.WriteLine(ex.Message);
      return 98;
    }
    catch(Exception ex)
    {
      Console.WriteLine("Excel-CSV変換処理で例外が発生しました");
      Console.WriteLine(ex.Message);
      return 99;
    }

    return 0;
  }
}

JavaScript(Node.js)で作成してみた

続いて、JavaScript。スクリプト言語なのでC#と違いコンパイルは不要。Excel処理にはxlsxというパッケージを使用。大まかな処理の流れはC#のコードと同じですが、読み取ったExcelデータからCSVに変換する関数が用意されていて、この関数1発で変換が実行可能。1行・1セル毎にループを回す必要はありません。

作成したサンプルコードは下記の通り、GitHubにも載せています。

C#と比べると若干コード量は少なくなったかな。

//----------------------------------------------------------
// Excel-CSV変換
//
//  概要:
//    xlsxファイルパスをinputに、対象ファイルを展開し、
//    シートごとに出力フォルダパスで指定された場所にcsv出力する
//
//  引数:
//    ・入力xlsxファイルパス
//    ・出力フォルダパス
// 
//  リターンコード:
//    ・81:引数不足
//    ・82:入力ファイルパス不正
//    ・83:出力フォルダ作成失敗
//    ・99:その他例外
//----------------------------------------------------------
const XLSX = require('xlsx');
const FS = require('fs');
const PATH = require('path');

// メイン処理呼び出し
main(process.argv);

// --------------------------------------
// メイン処理
// 
// 引数:実行時引数
// 戻り値:リターンコード
// --------------------------------------
async function main(args) {
  let check = 0;
  
  // 引数チェック
  check = CheckArgs(args);
  if(check != 0)
  {
    return check;
  }

  // 入力ファイルパス
  const inFile = args[2];
  // 出力フォルダパス
  const outPath = args[3];

  // 事前処理
  check = PreProc(outPath);

  // 開始メッセージ
  console.log("Excel-CSV変換処理を開始します。");
  console.log("入力ファイル:" + inFile);

  console.log("...");

  // 変換処理
  const outFiles = Excel2Csv(inFile, outPath);
  if(outFiles == null)
  {
    return 99;
  }

  // 終了メッセージ
  console.log("Excel-CSV変換処理が完了しました。\n出力ファイル:");
  outFiles.forEach(outFile => {
    console.log("・" + outFile)
  });

  return 0;
}

// --------------------------------------
// 引数チェック
// 
// 引数:実行時引数
// 戻り値:リターンコード
//  81:引数不足、82:入力ファイルパス不正
// --------------------------------------
function CheckArgs(args){
  if(args.length < 4)
  {
    console.log("引数が不足しています、第1引数:入力xlsxファイル、第2引数:出力フォルダパス");
    return 81;
  }

  // 入力ファイル存在チェック
  if(!FS.existsSync(args[2]))
  {
    console.log("入力xlsxファイルが存在しません。");
    return 82;
  }

  return 0;
}

// --------------------------------------
// 事前処理
// 
// 引数:出力フォルダパス
// 戻り値:リターンコード
//  83:出力フォルダ作成失敗
// --------------------------------------
function PreProc(outPath)
{
  try{
    // 出力フォルダ先フォルダが存在しない場合、作成
    if(!FS.existsSync(outPath))
    {
      FS.mkdirSync(outPath);
    }
  }
  catch(e)
  {
    console.log("出力フォルダの作成に失敗しました。");
    console.log(e.message);
    return 83;
  }

  return 0;
}

// --------------------------------------
// エクセル-CSV変換
// 
// 引数:
// ・入力xlsxファイルパス
// ・出力フォルダパス
// 戻り値:出力ファイル一覧
// --------------------------------------
function Excel2Csv(inFile, outPath)
{
  const outFiles = [];

  try{
    // エクセルファイル読み込み
    const excelBook = XLSX.readFile(inFile);

    // エクセルファイル名prefix
    const xlsxFNM = PATH.basename(inFile);
    const prefix = PATH.parse(xlsxFNM).name;

    // エクセルブックに含まれるシートを走査
    excelBook.SheetNames.forEach(sheetName => {
      // 出力ファイル名
      const outFile = outPath + "\\" + prefix + "_" + sheetName + ".csv";

      // シート
      const sheet = excelBook.Sheets[sheetName];

      // CSVオプションを設定 
      const csvOptions = {
        FS: ',',
        RS:'\r\n',
        forceQuotes: true
      };
      // CSV変換
      const csv = XLSX.utils.sheet_to_csv(sheet, csvOptions);

      // ファイル出力
      FS.writeFileSync(outFile, csv);
      // 出力先を格納
      outFiles.push(outFile);
    });

    return outFiles;
  }
  catch(e)
  {
    console.log("Excel-CSV変換に失敗しました。");
    console.log(e.message);
    return null;
  }
}

Pythonで作成してみた

最後にPython。ここ10年くらいすごく人気が出ている言語みたいですが、個人的には苦手意識が。。。{ } を使わずインデントで処理を区切る点等のお作法がC#やJavaScriptとはだいぶ異なり、C言語がベースとなっている私のコーディングスキル・癖とはあわず。。。今回も結構苦戦しました(Copilot君に助けてもらいながら)。

Excel処理には、Pandasを使用、するといいよとCopilot君におすすめされたのこちらを使用。JavaScriptバント同様、読み取ったExcelデータからCSVに変換する関数が用意されていて、この関数1発で変換が実行可能。1行・1セル毎にループを回す必要はありません。

作成したサンプルコードは下記の通り、GitHubにも載せています。

今回作成した3本の中では一番コード量が少なさそうね。

# ----------------------------------------------------------
#  Excel-CSV変換
# 
# 概要:
#   xlsxファイルパスをinputに、対象ファイルを展開し、
#   シートごとに出力フォルダパスで指定された場所にcsv出力する
# 
# 引数:
# ・入力xlsxファイルパス
# ・出力フォルダパス
#  
# リターンコード:
# ・81:引数不足
# ・82:入力ファイルパス不正
# ・83:出力フォルダ作成失敗
# ・99:その他例外
# ----------------------------------------------------------

import sys
import os
import pandas as pd
import csv

# %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
# % 関数定義 %
# %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

# --------------------------------------
# 引数チェック
# 
# 引数:実行時引数
# 戻り値:リターンコード
#  81:引数不足、82:入力ファイルパス不正
# --------------------------------------
def CheckArgs(args):
  # 引数チェック
  if(len(args) < 3):
    print("引数が不足しています、第1引数:入力xlsxファイル、第2引数:出力フォルダパス")
    return 81
  
  # 入力ファイル存在チェック
  if not os.path.isfile(args[1]):
    print ("入力xlsxファイルが存在しません。")
    return 82

  return 0

#  --------------------------------------
#  事前処理
#  
#  引数:出力フォルダパス
#  戻り値:リターンコード
#   83:出力フォルダ作成失敗
#  --------------------------------------
def PreProc(outPath):
  try:
    # 出力フォルダが存在しない場合、作成
    if not os.path.isdir(outPath):
      os.mkdir(outPath)
  except:
    return 83
  
  return 0

#  --------------------------------------
#  エクセル-CSV変換
#  
#  引数:
#  ・入力xlsxファイルパス
#  ・出力フォルダパス
#  戻り値:出力ファイル一覧
#  --------------------------------------
def Excel2Csv(inFile, outPath):
  outFiles = []
  try:
    # エクセルファイル名
    inFileNm = os.path.basename(inFile)
    prefix = os.path.splitext(inFileNm)[0]

    # エクセルブック読込
    excelBook = pd.ExcelFile(inFile)

    # シートを走査
    for sheetName in excelBook.sheet_names:
      # シート読込
      sheet = pd.read_excel(inFile, sheet_name=sheetName)
      # 出力ファイル名作成
      outFile = outPath + "/" + prefix + "_" + sheetName + ".csv"
      # CSVファイル出力
      sheet.to_csv(outFile, index=False, quoting=csv.QUOTE_ALL)

      # 出力ファイル名を格納
      outFiles.append(outFile)

  except:
    print("Excel-CSV変換に失敗しました。")
    return None
  
  return outFiles

#  --------------------------------------
#  メイン関数
#  
#  引数:実行時引数
#  戻り値:リターンコード
#  --------------------------------------
def main(args):
  check = 0
  
  # 引数チェック
  check = CheckArgs(args)
  if(check != 0):
    return check
  
  # 入力ファイルパス
  IN_FILE = args[1]
  # 出力ファイルパス
  OUT_PATH = args[2]

  # 事前処理
  check = PreProc(OUT_PATH)
  if(check != 0):
    return check
  
  # 開始メッセージ
  print("Excel-CSV変換処理を開始します。")
  print("入力ファイル:" + IN_FILE)

  print("...")

  # 変換処理
  outFiles = Excel2Csv(IN_FILE, OUT_PATH)
  if(outFile is None):
    return 99

  # 終了メッセージ
  print("Excel-CSV変換処理が完了しました。\n出力ファイル:")
  for outFile in outFiles:
    print("・" + outFile)

  return 0


# %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
# % メイン関数呼び出し %
# %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
main(sys.argv)

終わりに

結論は3つとも結構なコード量になり、片手間でサクッという範囲を超えているように思えました。自身のPCで動かすだけならまだしも、仮に他の人に展開して実行してもらおうと思うと。。。環境構築不要で単独実行可能なexeを簡単に吐き出せるC#がいいのかしらね。でもexeだといろいろセキュリティ的なところでブロックされる可能性もあるのよね、めんどくさい。もっと良い方法がないか継続検討。

ちなみに、同じ処理を別の言語で3つ書いてみるという試みは今回 私は初めてでしたが、結構面白いね。実践してみると手順を読んでいるだけでは気づけない気づき・発見もあり勉強になりました。

  • Home
  • /
  • Posts
  • /
  • Excel-CSV変換を、.NET・JavaScript・Pythonでやってみた
Tech-TIPS

Comments

© 2024 shunya_wisteria