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ファイルから エンコード指定して空シートに取り込みも可。