最近、CSVファイルをExcelで開こうとすると、文字化けすることが増えたと感じます。というのも、文字エンコーディングがUTF-8のCSVファイルを出力するツールが増えてきたからです。多言語化が当然の時代になり、Web上のデータはUTF-8を採用することが当たり前になってきました。そこで今回は、文字エンコーディング判定機能を持ったExcelファイルへの変換ツールを作ってみましょう。

  • 文字エンコーディングを自動判定してExcelに変換するツールを作ろう

    文字エンコーディングを自動判定してExcelに変換するツールを作ろう

最近増えた?! Excelで開くと文字化けしている現象

あるアプリで作成したデータを別のアプリに取り込んで使う場面というのは意外と多いものです。その中でも多いのが、CSVファイルのインポートとエクスポートです。CSVであればExcelやGoogleスプレッドシートなどで開いて手軽に編集できます。

しかし、その際、困るのが文字エンコーディングの問題です。次の画面のように、UTF-8のCSVファイルをダブルクリックしてExcelを開くと、たいてい文字化けしてしまいます。

  • CSVファイルを関連付けで開くと文字化けすることがある

    CSVファイルを関連付けで開くと文字化けすることがある

そうなった時は、テキストエディタを利用して、CSVファイルを開いて、文字エンコーディングをShift_JISに変換して保存するか、Excelの「データ」タブの「テキストまたはCSVから」をクリックして、文字エンコーディングなどを指定すれば正しくCSVファイルを読み込めます。

しかし、毎回そのような手順で修正するのは面倒です。そこで、RustでCSVファイルをExcelファイルに変換するツールを作ってみましょう。文字エンコーディングも自動で判定する機能も付けましょう。

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

まずは、プロジェクトを作成しましょう。ここでは「csv2excel」というツール名にします。ターミナル(WindowsならPowerShell、macOS/Linuxならターミナル)を起動して、下記のコマンドを実行しましょう。

# プロジェクトフォルダを作成
mkdir csv2excel
cd csv2excel

# cargoでプロジェクトを初期化
cargo init

# 今回利用するライブラリをインストール
cargo add csv xlsxwriter chardet encoding_rs anyhow

文字エンコーディングを判定してみよう

今回、文字エンコーディングを判定するのに「chardet」クレートを利用し、文字エンコーディングを変換するのに「encoding_rs」クレートを使っています。

最初に、コマンドラインで指定したファイルの文字エンコーディングを調べる部分を作ってみましょう。

use std::env;
use std::fs;
use std::io::Read;
use anyhow::{Context, Result};
use encoding_rs::{Encoding};

fn main() -> Result<()> {
    // コマンドライン引数を取得する --- (*1)
    let args: Vec<String> = env::args().collect();    
    if args.len() < 2 {
        println!("ファイルを指定してください。");
        return Ok(());
    }
    // ファイルを読み出してエンコーディングと内容を表示 ---- (*2)
    let input_file = &args[1];
    let (content, encoding) = read_and_detect_encoding(&input_file)?;
    println!("encoding={}", encoding);
    println!("content={}", content);
    Ok(())
}

// エンコーディングを自動判定してテキストファイルを読み込む --- (*3)
fn read_and_detect_encoding(file_path: &str) -> Result<(String, String)> {
    // ファイルをバイナリで読み込み --- (*4)
    let mut file = fs::File::open(file_path)
        .with_context(|| format!("ファイルを開けません: {}", file_path))?;
    let mut buffer = Vec::new();
    file.read_to_end(&mut buffer)?;

    // chardetを使用してエンコーディングを検出 --- (*5)
    let charset_match = chardet::detect(&buffer);
    let detected_encoding = charset_match.0;
    // エンコーディングを指定してencoding_rs::Encodingを取得
    let encoding = find_supported_encoding(&detected_encoding)
        .unwrap_or(encoding_rs::UTF_8);
    // エンコーディングを使用してデコード --- (*6)
    let (decoded_content, _, had_errors) = encoding.decode(&buffer);
    if had_errors {
        println!("警告: デコード中にエラーが発生しました。一部の文字が正しく表示されない可能性があります。");
    }

    Ok((decoded_content.into_owned(), detected_encoding))
}

// サポートする4つのエンコーディングのみを検索する関数 --- (*7)
fn find_supported_encoding(label: &str) -> Option<&'static Encoding> {
    let label_lower = label.to_lowercase();
    match label_lower.as_str() {
        "shift_jis" | "shift-jis" | "sjis" | "windows-31j" | "cp932" => Some(encoding_rs::SHIFT_JIS),
        "euc-jp" | "eucjp" => Some(encoding_rs::EUC_JP),
        "utf-8" | "utf8" => Some(encoding_rs::UTF_8),
        "utf-16" | "utf16" | "utf-16le" | "utf-16be" => Some(encoding_rs::UTF_16LE),
        _ => None,
    }
}

プログラムを実行するには、ターミナル上で、「cargo run (ファイル名)」のように指定して実行します。例えば「test_shiftjis.csv」というファイルを対象にする場合は、下記のようなコマンドを実行します。

cargo run test_shiftjis.csv

すると、文字エンコーディング名とテキストの内容が出力されます。

  • 引数として与えたファイルを読み込んでエンコーディングを判定してテキストを表示する

    引数として与えたファイルを読み込んでエンコーディングを判定してテキストを表示する

プログラムを確認してみましょう。(*1)では、コマンドライン引数を取得しています。env::args() でプログラムの実行時引数をベクタにまとめ、引数が1つも指定されていない場合は「ファイルを指定してください」と表示して終了します。

(*2)では、指定されたファイルを読み込み、検出したエンコーディングと内容を表示します。そのために、関数read_and_detect_encodingを呼び出して (content, encoding) のタプルを受け取り、結果を println! で出力しています。

(*3)では、ファイルの内容を読み取り、文字コードを自動判定して文字列に変換する関数を定義しています。この関数は(テキスト内容, 文字コード名)を返すものです。

(*4)では、ファイルをバイナリとして開き、全内容をメモリ上の Vec に読み込みます。テキストとして読む前にバイナリで扱うのは、文字コードを自動判定するために「生のバイト列」が必要だからです。

(*5)では、chardetクレートを用いてエンコーディングを検出しています。chardet::detect(&buffer) が返すタプルの第1要素(例: "SHIFT_JIS" や "UTF-8")を detected_encoding に格納しています。

(*6)では、検出したエンコーディングをもとにして、デコード処理を行います。そのために、encoding_rsクレートのEncoding構造体を取得し、そのメソッドdecodeを使います。もしデコード中に不正なバイトが見つかった場合は had_errors が true になるので警告を表示します。

(*7)では、chardetクレートとencoding_rsクレートのエンコーディング名の差異を埋めるために、主要な日本語エンコーディング(Shift_JIS、EUC-JP、UTF-8、UTF-16)を対象にして、Encoding 構造体を返します。

CSVファイルをExcelに変換しよう

続いて、CSVをパースして、Excel形式で保存するプログラムを作ってみましょう。

CSVファイルをパースするのに「csv」クレートを使い、Excelファイルの作成には「xlsxwriter」クレートを使います。それでは、プログラムを確認してみましょう。

fn convert_csv_to_excel(content: &str, output_path: &str, delimiter: char) -> Result<()> {
    // Excelワークブックを作成 --- (*1)
    let workbook = Workbook::new(output_path)
        .with_context(|| format!("Excelファイルを作成できません: {}", output_path))?;

    let mut worksheet = workbook.add_worksheet(Some("Sheet1"))
        .with_context(|| "ワークシートを追加できません")?;

    // CSVリーダーを設定 --- (*2)
    let mut csv_reader = csv::ReaderBuilder::new()
        .delimiter(delimiter as u8)
        .has_headers(false)  // ヘッダーの存在を仮定しない
        .from_reader(content.as_bytes());

    // CSVの各行を処理 --- (*3)
    let mut row_index = 0u32; // 0行目を表す
    for result in csv_reader.records() {
        let record = result.with_context(|| format!("CSV行の読み込みエラー: {}", row_index + 1))?;

        for (col_index, field) in record.iter().enumerate() {
            // 数値として解析可能かチェック --- (*4)
            if let Ok(number) = field.parse::<f64>() {
                worksheet.write_number(row_index, col_index as u16, number, None)?;
            } else {
                worksheet.write_string(row_index, col_index as u16, field, None)?;
            }
        }
        row_index += 1; // 次の行に
    }

    // ワークブックを保存 --- (*5)
    workbook.close()
        .with_context(|| "Excelファイルの保存に失敗しました")?;

    Ok(())
}

プログラムを確認しましょう。(*1)ではExcelファイル(ワークブック)の作成を行っています。Workbook::new(output_path) により、指定されたパスに .xlsx ファイルを新規作成します。with_context により、ファイル作成に失敗した場合にエラーメッセージを出力するようにします。さらに add_worksheet(Some("Sheet1")) で最初のシート(Sheet1)を追加しています。

(*2)では、CSVデータの読み取り準備をしています。csv::ReaderBuilder により、区切り文字(delimiter)を指定し、ヘッダー行の有無を「仮定しない(false)」設定にしています。from_reader(content.as_bytes()) によって、文字列 content をCSV入力として扱います。 これにより、ファイルを使わずに文字列ベースで処理できます。

(*3)では、CSVの各行を順に読み取り、Excelに書き込む処理を行います。csv_reader.records() で1行ずつ StringRecord として取得し、row_index に基づいてExcelの行に対応させます。もしCSVの行が正しく読み込めなければ、with_context で行番号付きのエラーメッセージを出すようにしています。

(*4)では、各セルの内容を型に応じてExcelに書き込みます。なお、数値に変換できそうなら、数値としてセルに書き込みます。

そして最後に、(*5)ではワークブックを閉じて保存します。

プログラムを完成させよう

上記の関数convert_csv_to_excelを、先ほどのmain.rsに組み込めば、文字エンコーディング判定付き、CSVからExcelファイルへの保存ツールが完成です。プログラム全体は少し長くなったので、こちらのGistにアップロードしました。

完成したプログラムの使い方も同じで、ターミナルで「cargo run (CSVファイル名)」のようなコマンドを実行すると、同名のExcelファイルを生成します。なお、Windowsなら実行ファイルにドラッグ&ドロップするだけで、CSVファイルをExcelファイルに変換できます。
  • CSVファイルをExcelファイルに変換したところ

    CSVファイルをExcelファイルに変換したところ

まとめ

以上、今回は、CSVファイルをExcelファイルに変換するプログラムを作りました。特に、CSVファイルの文字エンコーディングを判定することで、Excelをダブルクリックで開いた時のように、データが文字化けすることを防ぐようにしました。

文字エンコーディングの判定や変換には、chardetクレートや、encoding_rsクレートを利用できるので、それほど難しい処理は必要ないことが分かったことでしょう。テキスト処理のプログラムを作成する参考にしてください。

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