Rust エクセルで認識するCSVを出力する。

CSVファイルは、単純にカンマ区切りと、改行さえあれば成り立ちます。
しかし、エクセルファイルで開く場合は、それだけだと文字化けしたり、開けないケースがあります。
それは、CSVファイルのエンコードが関係している問題です。

CSVファイルのエンコード

CSVファイルをダブルクリックしてエクセルアプリケーションで開いた際、
エクセルには優先認識する言語設定があります。

日本語を優先設定にしている場合に、文字化けなく、正しく認識してくれるCSVファイルというのは、UTF-8のBOM付か、ShiftJISで書かれたファイルです。

ちなみに、UTF-8 BOM付であれば優先言語がなんであれ、エクセルは正しくエンコードを認識してくれます。
なので、CSVファイルを出力する際のオススメはUTF-8 BOM付で書き出すことです。
逆に言えば、Shift-JISはエクセルの優先言語設定によっては正しく表示してくれません。

この記事では、Rustを使った、CSVファイルの出力方法と、
エクセルアプリケーションで正しく認識してもらうための、出力方法について紹介いたします。

この記事で説明する出力形式

  • UTF-8 BOM付
  • Shift-JIS

Rustで単純にCSV出力してみる

Rustで単純にCSVファイルを出力する場合は、カンマ区切りでファイルへ書き込むだけです。

use std::fs::{create_dir_all, File};
use std::io::{BufWriter, Write};
use std::path::Path;

use anyhow::Result;

fn main() -> Result<()> {
    let output_dir = Path::new("out");
    create_dir_all(&output_dir)?;

    let mut w = BufWriter::new(File::create(output_dir.join("fruits.csv"))?);

    writeln!(w, "name,quantity")?;
    writeln!(w, "りんご,2")?;
    writeln!(w, "みかん,10")?;
    writeln!(w, "さくらんぼ,40")?;

    Ok(())
}

出力されたファイルはこんな感じ。

out/fruits.csv

name,quantity
りんご,2
みかん,10
さくらんぼ,40

プログラムで読み込んで使うだけなら、全然これでOKです。
むしろ余計なものがないので、こちらのほうが都合がよいでしょう。

しかし、このCSVファイルをダブルクリックして、エクセルアプリケーションで開こうとすると、
残念ながら文字化けしたような表示になる場合があります。

この例のように、Rustで単純に出力した場合には、文字エンコード自体はUTF-8で出力されています。(Rustは標準でUTF-8で文字列を扱っているため)

しかし、エクセルで認識するUTF-8エンコードのCSVファイルは、
BOMと呼ばれる目印がついていないと認識ができないのです。

UTF-8 BOM付とは

BOMとは、Byte order markのことで、
UTF-8, UTF-16, UTF-32 などのユニコードのエンコード形式を示すために、
ファイルの先頭に決まったバイト列でマーキングするというものです。

UTF-8の場合は、先頭に決まった3バイト [0xEF, 0xBB, 0xBF] を書き込んでおくのがルールです。
このバイト列で始まっていたら、UTF-8と判断していいよとマーキングしているわけです。

RustでUTF-8 BOM付でCSV出力する方法

先ほどの例では、ファイルの先頭にBOMがついていないため、エクセルがUTF-8で書かれたCSVであると認識してくれなかったために、文字化けが発生しています。

そこで、BOMも出力するように書き換えてみたのがこちら。

use std::fs::{create_dir_all, File};
use std::io::{BufWriter, Write};
use std::path::Path;

use anyhow::Result;

// Byte order mark
const BOM: &[u8; 3] = &[0xEF, 0xBB, 0xBF]; // UTF-8

fn main() -> Result<()> {
    let output_dir = Path::new("out");
    create_dir_all(&output_dir)?;

    let mut w = BufWriter::new(File::create(output_dir.join("fruits.csv"))?);

    // BOMをファイルの先頭に追加
    w.write_all(BOM)?;

    writeln!(w, "name,quantity")?;
    writeln!(w, "りんご,2")?;
    writeln!(w, "みかん,10")?;
    writeln!(w, "さくらんぼ,40")?;

    Ok(())
}

出力されたCSVファイルをエクセルアプリケーションで開いてみましょう。
BOM付きのファイルでは、文字化けもなく正しく表示されているのがわかります。
これが、UTF-8 BOM付ファイルというものです。

csvクレート

あと、Rustでは、csv という有名なクレートがCSVファイル出力をサポートしています。
しかし、BOMに関しての出力サポートまではしていないようなので、
csvクレートを使った場合にも、BOMをつける方法をここで紹介しておきます。

csv というクレートを使うと、改行の指定や、
ダブルクォーテーションが必要な場合は、自動クォートしてくれるので便利です。

csvクレートを使った、UTF-8 BOM付の出力方法。

use std::fs::{create_dir_all, File};
use std::io::{BufWriter, Write};
use std::path::Path;

use anyhow::Result;
use csv::Terminator;

// Byte order mark
const BOM: &[u8; 3] = &[0xEF, 0xBB, 0xBF]; // UTF-8

fn main() -> Result<()> {
    let output_dir = Path::new("out");
    create_dir_all(&output_dir)?;

    // csv::WriterBuilderに渡す前にBOMを書き込んでおく。
    let mut w = BufWriter::new(File::create(output_dir.join("fruits.csv"))?);
    w.write_all(BOM)?;

    let mut w = csv::WriterBuilder::new()
        .terminator(Terminator::CRLF)
        .from_writer(w);

    // ダブルクォーテーションが必要な場合は自動で囲んで出力される。
    w.write_record(["name", "quantity"])?;
    w.write_record(["りんご", "2"])?;
    w.write_record(["みかん", "10"])?;
    w.write_record(["さくらんぼ", "40"])?;

    Ok(())
}

csv::WriterBuilderに渡す前に、BufWriterでBOM書き込みをしているのがミソです。

RustでShift JIS でCSV出力する方法

今では、Shift-JISで出力してくれ!と言われることは少ないと思いますが、
Shift-JISエンコードで出力するケースも紹介しておきます。

Shift-JISの場合は、BOMなどは必要ありません。
単純に出力する内容をShift-JISエンコードして出力するだけです。

encoding_rs クレート

エンコード処理には、encoding_rsクレートを使います。
encoding_rsで用意されているエンコーダーに文字列を渡すだけですね。

use encoding_rs::SHIFT_JIS;

let (encoded, encoder, had_errors) = SHIFT_JIS.encode("おはよう");

println!("encoded: {:02X?}", encoded);
println!("encoder: {:?}", encoder);
println!("had_errors: {:?}", had_errors);

// ==> encoded: [82, A8, 82, CD, 82, E6, 82, A4]
// ==> encoder: Encoding { Shift_JIS }
// ==> had_errors: false

SHIFT_JIS#encode に文字列を指定すると、Shift-JISエンコード後のバイト配列を返してくれます。

タプルになっていて、

  • 1つ目の要素がバイト配列 (Cow<[u8]>)
  • 2つ目はShift-JISの場合は同じShift-JISのエンコーダーの参照が返るだけです。
  • 3つ目が処理に失敗があればtrueが返ります。

csv::WriterBuilder にメソッド拡張する形で、Shift-JIS出力に対応させてみました。

Shift-JISでCSVファイル出力する

use std::fs::create_dir_all;
use std::io;
use std::path::Path;

use anyhow::Result;
use csv::Terminator;
use encoding_rs::SHIFT_JIS;

/// csv::Writer を拡張するためのトレイト
trait ShiftJISWrite {
    fn write_record_with_shift_jis(&mut self, columns: &[&str]) -> Result<()>;
}

impl<W> ShiftJISWrite for csv::Writer<W>
    where W: io::Write
{
    /// 指定レコードをShift-JISにエンコードしたうえで書き込む。
    fn write_record_with_shift_jis(&mut self, columns: &[&str]) -> Result<()> {
        // 要素ごとにエンコード
        let columns: Vec<_> = columns.iter()
            .map(|&s| SHIFT_JIS.encode(s).0)
            .collect();

        let bytes = csv::ByteRecord::from(columns);
        self.write_byte_record(&bytes)?;

        Ok(())
    }
}

fn main() -> Result<()> {
    let output_dir = Path::new("out");
    create_dir_all(&output_dir)?;

    let mut w = csv::WriterBuilder::new()
        .terminator(Terminator::CRLF)
        .from_path(output_dir.join("fruits.csv"))?;

    w.write_record_with_shift_jis(&["name", "quantity"])?;
    w.write_record_with_shift_jis(&["りんご", "2"])?;
    w.write_record_with_shift_jis(&["みかん", "10"])?;

    Ok(())
}

こちらも同様にエクセルアプリケーションで正しく認識できると思いますが、
日本語優先設定になっていないエクセルでは文字化けしますので、そこだけご注意ください。

エクセルの優先言語設定

最後に、エクセルアプリケーションの優先言語設定について紹介しておきます。

エクセルの優先言語設定はここで設定ができます。

ファイル > オプション > 言語

優先言語の設定によって、どのエンコードで認識するのか順番が変わってきます。

日本語優先になっている場合、

UTF-8 BOMなしで出力したファイルは、Shift-JISで表示されようとして文字化けが発生します。
この場合、UTF-8 BOMありで出力するか、Shift-JISで出力されたファイルじゃないと認識してくれません。

英語優先になっている場合、

日本語が含まれる、UTF-8 BOMなしのファイルでもこの場合正しく表示されます。
しかしShift-JISで出力されたファイルは、認識してくれません。

なので、どちらの設定でも日本語を含むCSVを正しく表示させたい場合は、
UTF-8 BOM付で出力するのが一番良い方法です。

CSVファイルのインポート

もし、それでもうまくエクセルがCSVファイルを認識してくれない場合には、
エンコード形式を指定して、空のエクセルシートにインポートするという方法もあります。

CSVファイルをダブルクリックをして開くことはできませんが、エンコード形式を指定して取り込むことはできるので、覚えておくと良いでしょう。

まとめ

RustのCSVファイル出力

  • Rustは単純にUTF-8で出力するだけ。
  • BOM付は、ファイル先頭に UTF-8を表す [0xEF, 0xBB, 0xBF] バイト列を出力すればよい。
  • Shift-JIS変換する場合は、encoding_rs クレートを使う。
  • csvクレートを使うと、便利に出力できるがBOMやShift-JIS変換までは面倒みてくれない。

エクセルのCSVエンコード認識

  • 日本語優先のエクセルでは、UTF-8はBOM付じゃないと認識しない。
  • 日本語優先のエクセルじゃないと、Shift-JISは認識しない。
  • 英語優先のエクセルでは、UTF-8はBOMなしでも認識する。
  • データ > データの取得 > CSVファイルから エンコード指定して空シートに取り込みも可。
タイトルとURLをコピーしました