当然ですが、Excelファイルを読むには、Excelか互換アプリが必要になります。それで、ちょっとしたプログラムでExcelファイルを扱おうと思った時に不便に感じることもあります。そこで、Rustで気軽にExcelファイルをCSVに変換するコマンドラインツール(CLI)を作ってみましょう。

  • ExcelファイルをCSVとして出力するCLIツールを作ってみよう

    ExcelファイルをCSVとして出力するCLIツールを作ってみよう

ExcelファイルをCSVファイルに変換する際の注意

Excel自体にも、CSVファイルを出力する機能がついています。でも、出力できるのはアクティブなシートだけです。そのため、ExcelファイルをCSVファイルに変換するツールを作る時には、複数シートをどのように扱うかという点がポイントになります。

一つの選択肢としては、Excelアプリと挙動を合わせてアクティブなシートだけを出力する方法です。しかし、それでは重要なデータを出力できず不便な場面もあります。そのため、今回は、「元ファイル名_シート名.csv」という名前で出力します。

また、ExcelでCSVファイルを出力すると、文字エンコーディングがShift_JISになりますが、今回は、UTF-8で出力します。

Excelファイルを読むなら「calamine」クレートを使おう

それで、今回は、Excelファイルを読むのに特化したRustのライブラリ「calamine」を使って、CSVファイルを出力するツールを作ってみます。この「calamine」は、Rustのライブラリ(クレート)を登録できる「crates.io」で「Excel」と検索すると一番ダウンロード数が多いものです。対応ファイル形式は、xls, xlsx, xlsm, xlsb, xla, xlam, odsと、幅広いExcel形式/オープンドキュメント形式のファイルをサポートしています。

こちらの開発リポジトリを見ても定期的にメンテナンスが行われているので安心して利用できます。

  • Rustのクレート「calamine」の開発ページ

    Rustのクレート「calamine」の開発ページ

プロジェクトを作成しよう

それでは、Rustのプロジェクトを作成してみましょう。作成後に、今回利用するcalamineとchronoクレートをインストールします。

# プロジェクト用のライブラリを作成
mkdir xls2csv
cd xls2csv
# Rustプロジェクトを初期化
cargo init
# クレートcalamineとchronoを追加
cargo add calamine@0.34.0
cargo add chrono@0.4

上記のコマンドを実行すると、xls2csvフォルダに、Cargo.tomlというファイルが作成されます。テキストエディタでこのファイルを開いて、「calamine = "0.34.0"」の行を以下のように書き換えましょう。これで、Excelの日付の変換を行う機能が追加されます。

# calamine = "0.34.0"
calamine = { version = "0.34.0", features = ["dates"] } # ← このように書き換える

フォルダ構造のプロジェクトひな形は次のように作成されます。

[xls2csvフォルダ]
├── Cargo.lock
├── Cargo.toml
└── src
    └── main.rs

プログラムを作成しよう

それでは、メインプログラムを作成しましょう。src/main.rsを開いて、プログラムを作成しましょう。プログラム全体をこちらにアップロードしています。以下、少し長いのでポイントとなる部分だけ抜粋して紹介します。

以下は、コマンドラインの引数を解析して、変換処理を実行するメイン関数を定義した部分です。

// メイン処理 --- (*1)
fn main() {
    // コマンドライン引数を取得する --- (*2)
    let args: Vec<String> = env::args().collect();
    if args.len() < 2 {
        eprintln!("Usage: {} <excel_file>", args[0]);
        std::process::exit(1);
    }
    // 入力ファイルのパスを解析 --- (*3)
    // 入力ファイルのパスから「拡張子を除いたファイル名」と「親ディレクトリ」を取り出す
    // 出力ファイル名 "入力ファイル名_シート名.csv" を組み立てるために使う
    let input_path = &args[1];
    let path = Path::new(input_path);
    let stem = path
        .file_stem()
        .expect("Invalid input file name")
        .to_string_lossy()
        .to_string();
    let parent = path.parent().unwrap_or_else(|| Path::new(""));

    // 入力Excelをシートごとに分割してCSVへ出力する --- (*4)
    convert_workbook_to_csv(input_path, &stem, parent);
}

プログラムを確認しましょう。(*1)では、main関数を定義しています。Rustでは、このメイン関数からプログラムが実行されます。

(*2)では、コマンドライン引数を解析します。そして、引数がない場合(引数の個数が2未満)には、使い方を表示してプログラムを終了します。

(*3)では、入力ファイルのパスを解析して、拡張子を取り除いたファイル名と、親ディレクトリを取得しています。これらは、出力ファイル名を「入力ファイル名_シート名.csv」という形式にするために使います。

(*4)では、Excelファイルをシートごとに変換する関数convert_workbook_to_csvを呼び出します。

プログラム - 関数convert_workbook_to_csvを見てみよう

次に、関数convert_workbook_to_csvの定義を確認してみましょう。以下のようになっています。

// Excelファイルを開き、全シートをCSVへ書き出す --- (*5)
fn convert_workbook_to_csv(input_path: &str, stem: &str, parent: &Path) {
    // Excelファイルを開く (拡張子から自動でフォーマットを判定) --- (*6)
    let mut workbook = open_workbook_auto(input_path).expect("Cannot open Excel file");
    // 全シート名を取得する --- (*7)
    let sheet_names = workbook.sheet_names().to_owned();

    // シートごとに CSV ファイルを生成する --- (*8)
    for sheet_name in &sheet_names {
        // シートの内容を取得する --- (*9)
        let range = match workbook.worksheet_range(sheet_name) {
            Ok(r) => r,
            Err(e) => {
                // 取得失敗したシートはスキップ
                eprintln!("Skip sheet '{}': {}", sheet_name, e);
                continue;
            }
        };
        // 1シート分をCSVに書き出す --- (*10)
        write_sheet_to_csv(&range, stem, sheet_name, parent);
    }
}

プログラムの(*5)では、Excelファイルをシートごとに変換する関数convert_workbook_to_csvを定義しています。引数には、main関数から引き渡された「入力ファイル名」「拡張子を取り除いたファイル名」「親ディレクトリ」を指定します。

(*6)では、open_workbook_auto関数を呼び出して、Excelファイルを開きます。引数に拡張子を指定することなく、ファイルパスを指定するだけで開けるので便利です。

(*7)では、workbook.sheet_names()メソッドを呼び出して、開いているExcelファイルのシート名をすべて取得しています。そして、to_owned()メソッドを呼び出して、所有権を取得しています。

(*8)では、シート名を取得したリストをループ処理して、1シートずつCSVファイルを出力していきます。

(*9)では、worksheet_rangeメソッドを使って、各シートの内容をRange構造体として取得します。Rangeは、シートの範囲を表す構造体です。

(*10)では、write_sheet_to_csv関数を呼び出して、シートの内容をCSVファイルに書き出します。引数には、Range構造体、拡張子を取り除いたファイル名、シート名を指定します。

プログラム - 関数write_sheet_to_csvを見てみよう

引き続き、関数write_sheet_to_csvの定義を確認してみましょう。この関数では、シート内のデータを受け取って、CSVファイルを書き出します。

// 1シート分のセル範囲をCSVファイルに書き出す --- (*11)
fn write_sheet_to_csv(range: &Range<Data>, stem: &str, sheet_name: &str, parent: &Path) {
    // 出力ファイルのパスを「入力ファイル名_シート名.csv」で組み立てる --- (*12)
    let output_filename = format!("{}_{}.csv", stem, sheet_name);
    let output_path = parent.join(&output_filename);
    // 出力ファイルを作成し、バッファ付きで書き込む --- (*13)
    let file = File::create(&output_path).expect("Cannot create output file");
    let mut writer = BufWriter::new(file);

    // 1行ずつセルを文字列化&エスケープし、カンマ区切りで書き出す --- (*14)
    for row in range.rows() {
        let fields: Vec<String> = row
            .iter()
            .map(|cell| escape_csv_field(&cell_to_string(cell)))
            .collect();
        writeln!(writer, "{}", fields.join(",")).expect("Write failed");
    }

    // 出力したファイルパスを表示する
    println!("出力: {}", output_path.display());
}

(*11)では、write_sheet_to_csv関数を定義しています。引数には、Range構造体、拡張子を取り除いたファイル名、シート名、親ディレクトリを指定します。

(*12)では、出力ファイルのパスを「入力ファイル名_シート名.csv」という形式で組み立てています。

(*13)では、出力ファイルを作成し、バッファ付きで書き込みをします。バッファ付きで書き込みをすることで、書き込み回数を減らしてパフォーマンスを向上させることができます。

(*14)では、1行ずつセルを文字列化して、CSVファイルに書き出しています。この部分で、セルの値を文字列化して、カンマ区切りで書き出すという処理を行っています。

また、長くなるため本稿では省略しますが、CSV文字列として書き出すために、データをエスケープする関数escape_csv_fieldを定義しています。興味があれば、ぜひコード全体を読んでみてください。

プログラム - 関数cell_to_stringを見てみよう

(*16)では、Excelの各セルを文字列に変換する関数cell_to_stringを定義しています。以下が実際の定義です。

// Excelの1セル(Data型)を文字列に変換する --- (*16)
fn cell_to_string(cell: &Data) -> String {
    match cell {
        // 空セル
        Data::Empty => String::new(),
        // 文字列セル
        Data::String(s) => s.clone(),
        // 数値(浮動小数)セル
        Data::Float(f) => f.to_string(),
        // 整数セル
        Data::Int(i) => i.to_string(),
        // 真偽値セル
        Data::Bool(b) => b.to_string(),
        // 日時セル: Excel内部のシリアル値を NaiveDateTime に変換し YYYY-MM-DD で出力
        Data::DateTime(dt) => match dt.as_datetime() {
            Some(ndt) => ndt.format("%Y-%m-%d").to_string(),
            // 変換失敗時はシリアル値(数値)をそのまま出力する
            None => dt.as_f64().to_string(),
        },
        // ISO形式の日時文字列セル: パースして YYYY-MM-DD に整形する
        Data::DateTimeIso(s) => {
            if let Ok(ndt) = chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S") {
                ndt.format("%Y-%m-%d").to_string()
            } else if let Ok(nd) = chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d") {
                nd.format("%Y-%m-%d").to_string()
            } else {
                // 想定外のフォーマットならそのまま出力
                s.clone()
            }
        }
        // ISO形式の期間文字列セル(例: PT1H30M)はそのまま出力
        Data::DurationIso(s) => s.clone(),
        // エラーセル(#REF! など)はデバッグ表現で出力
        Data::Error(e) => format!("{:?}", e),
    }
}

calamineクレートでは、セルの値をData列挙型で取得できます。それで、上記のコードでは、Excelのセルの型に応じて、文字列に変換する処理を行っています。例えば、数値セルの場合は、to_string()メソッドを呼び出して、文字列に変換しています。数値セル以外にも、日付セルなど、文字列に変換するのに工夫が必要なセルの型もあります。日付セルは、Excel内部のシリアル値を文字列に変換して、YYYY-MM-DDという形式で出力するようにしています。

実行してみよう

それでは、作成したプログラムを実行してみましょう。例えば、invoice.xlsxというExcelファイルが、カレントディレクトリにあるとします。以下のコマンドを実行すると、Excelファイルを元にしてCSVファイルを出力できます。

cargo run invoice.xlsx

実行すると、以下のような出力が得られます。

  • コマンドラインでExcelファイル「invoice.xlsx」をCSVに変換したところ

    コマンドラインでExcelファイル「invoice.xlsx」をCSVに変換したところ

次に、実行ファイルにビルドするには、下記のコマンドを実行します。

cargo build --release

すると、target/release/xls2csv という実行ファイルが作成されます。Windowsであれば、この実行ファイルにExcelファイルをドラッグ&ドロップすることで、手軽にCSVファイルを生成できます。

まとめ

以上、 RustでExcelファイルをCSVファイルに変換するプログラムの作成方法について解説しました。「calamine」クレートを使うことで、とても簡単にExcelファイルを扱えることが分かったかと思います。Rustで実装されているので、一般的なExcelファイルなら、あっという間にCSVファイルに変換できます。RustでExcelファイルを扱う参考になれば幸いです。

なお、本連載のために作ったサンプルプログラムでしたが、実用的なツールになったので、こちらのGitHubでオープンソースとして公開することにしました。使ってみてください。このバージョンでは、デフォルトで出力するCSVファイルの文字エンコーディングをExcelと合わせてShift_JISで出力するようにするなど改良を加えています。

自由型プログラマー。くじらはんどにて、プログラミングの楽しさを伝える活動をしている。代表作に、日本語プログラミング言語「なでしこ」 、テキスト音楽「サクラ」など。2001年オンラインソフト大賞入賞、2004年度未踏ユース スーパークリエータ認定、2010年 OSS貢献者章受賞。これまで50冊以上の技術書を執筆した。直近では、「大規模言語モデルを使いこなすためのプロンプトエンジニアリングの教科書(マイナビ出版)」「Pythonでつくるデスクトップアプリ(ソシム)」「実践力を身につける Pythonの教科書 第2版」「シゴトがはかどる Python自動処理の教科書(マイナビ出版)」など。