Skip to content

Instantly share code, notes, and snippets.

@kujirahand
Created October 14, 2025 10:08
Show Gist options
  • Save kujirahand/9e6496446c9a953219847ccf675b7ef5 to your computer and use it in GitHub Desktop.
Save kujirahand/9e6496446c9a953219847ccf675b7ef5 to your computer and use it in GitHub Desktop.
CSVをExcelファイルに変換
use std::env;
use std::fs;
use std::io::Read;
use std::path::Path;
use anyhow::{Context, Result};
use xlsxwriter::Workbook;
fn main() -> Result<()> {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
print_usage(&args[0]);
return Ok(());
}
let input_file = &args[1];
// ヘルプの表示
if input_file == "-h" || input_file == "--help" {
print_usage(&args[0]);
return Ok(());
}
// 入力ファイルの存在確認
if !Path::new(input_file).exists() {
eprintln!("エラー: ファイルが見つかりません: {}", input_file);
return Ok(());
}
// 出力ファイル名を生成(拡張子をxlsxに変更)
let output_file = generate_output_filename(input_file);
println!("入力ファイル: {}", input_file);
println!("出力ファイル: {}", output_file);
// CSVファイルを読み込んで文字エンコーディングを自動判定
let (content, encoding) = read_and_detect_encoding(input_file)?;
println!("検出されたエンコーディング: {}", encoding);
// CSVをパースしてExcelに変換
convert_csv_to_excel(&content, &output_file, ',')?;
println!("変換完了: {} -> {}", input_file, output_file);
Ok(())
}
fn print_usage(program_name: &str) {
println!("CSVファイルをExcelファイルに変換するツール");
println!();
println!("使用方法:");
println!(" {} <CSVファイル>", program_name);
println!();
println!("例:");
println!(" {} data.csv", program_name);
println!(" {} /path/to/file.csv", program_name);
println!();
println!("機能:");
println!(" - 文字エンコーディング自動判定 (UTF-8, Shift_JIS, EUC-JP, GBK, Big5, EUC-KR等)");
println!(" - 出力ファイル名は入力ファイル名の拡張子を.xlsxに変更");
println!(" - 数値の自動認識とフォーマット");
}
fn generate_output_filename(input_file: &str) -> String {
let path = Path::new(input_file);
if let Some(stem) = path.file_stem() {
if let Some(parent) = path.parent() {
if parent == Path::new("") {
format!("{}.xlsx", stem.to_string_lossy())
} else {
format!("{}/{}.xlsx", parent.display(), stem.to_string_lossy())
}
} else {
format!("{}.xlsx", stem.to_string_lossy())
}
} else {
format!("{}.xlsx", input_file)
}
}
fn read_and_detect_encoding(file_path: &str) -> Result<(String, String)> {
// ファイルをバイナリで読み込み
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を使用してエンコーディングを検出
let charset_match = chardet::detect(&buffer);
let detected_encoding = charset_match.0;
// encoding_rsでサポートされているエンコーディングを検索
let encoding = find_encoding_by_label(&detected_encoding)
.or_else(|| {
// 検出に失敗した場合、よく使われるエンコーディングを順番に試す
try_common_encodings(&buffer)
})
.unwrap_or(encoding_rs::UTF_8); // 最終的にUTF-8をデフォルトとする
// エンコーディングを使用してデコード
let (decoded_content, _, had_errors) = encoding.decode(&buffer);
if had_errors {
println!("警告: デコード中にエラーが発生しました。一部の文字が正しく表示されない可能性があります。");
}
Ok((decoded_content.into_owned(), format!("{} (detected: {})", encoding.name(), detected_encoding)))
}
fn find_encoding_by_label(label: &str) -> Option<&'static encoding_rs::Encoding> {
// encoding_rs::Encoding::for_labelを使用してエンコーディングを検索
let normalized_label = label.to_lowercase().replace(['_', '-'], "");
// まず、そのままの形で検索
if let Some(encoding) = encoding_rs::Encoding::for_label(label.as_bytes()) {
return Some(encoding);
}
// 正規化した形で検索
if let Some(encoding) = encoding_rs::Encoding::for_label(normalized_label.as_bytes()) {
return Some(encoding);
}
// 一般的な別名もチェック
match normalized_label.as_str() {
"shiftjis" | "sjis" | "mskanji" | "shift_jis" => Some(encoding_rs::SHIFT_JIS),
"eucjp" | "ujis" | "euc_jp" => Some(encoding_rs::EUC_JP),
"utf8" | "utf_8" => Some(encoding_rs::UTF_8),
"utf16" | "utf_16" => Some(encoding_rs::UTF_16LE),
"utf16be" | "utf_16be" => Some(encoding_rs::UTF_16BE),
"iso88591" | "latin1" | "iso_8859_1" => Some(encoding_rs::WINDOWS_1252),
"gb2312" | "gbk" => Some(encoding_rs::GBK),
"big5" => Some(encoding_rs::BIG5),
"ksc56011987" | "euckr" | "euc_kr" => Some(encoding_rs::EUC_KR),
"windows1252" | "cp1252" => Some(encoding_rs::WINDOWS_1252),
"iso2022jp" | "iso_2022_jp" => Some(encoding_rs::ISO_2022_JP),
_ => None,
}
}
fn try_common_encodings(buffer: &[u8]) -> Option<&'static encoding_rs::Encoding> {
// よく使用されるエンコーディングを順番に試す
let common_encodings = [
encoding_rs::UTF_8,
encoding_rs::SHIFT_JIS,
encoding_rs::EUC_JP,
encoding_rs::WINDOWS_1252,
encoding_rs::GBK,
encoding_rs::BIG5,
encoding_rs::EUC_KR,
encoding_rs::ISO_2022_JP,
encoding_rs::UTF_16LE,
encoding_rs::UTF_16BE,
];
for &encoding in &common_encodings {
let (_, _, had_errors) = encoding.decode(buffer);
if !had_errors {
return Some(encoding);
}
}
None
}
fn convert_csv_to_excel(content: &str, output_path: &str, delimiter: char) -> Result<()> {
// Excelワークブックを作成
let workbook = Workbook::new(output_path)
.with_context(|| format!("Excelファイルを作成できません: {}", output_path))?;
let mut worksheet = workbook.add_worksheet(Some("Sheet1"))
.with_context(|| "ワークシートを追加できません")?;
// CSVリーダーを設定
let mut csv_reader = csv::ReaderBuilder::new()
.delimiter(delimiter as u8)
.has_headers(false) // ヘッダーの存在を仮定しない
.from_reader(content.as_bytes());
let mut row_index = 0u32;
// CSVの各行を処理
for result in csv_reader.records() {
let record = result.with_context(|| format!("CSV行の読み込みエラー: {}", row_index + 1))?;
for (col_index, field) in record.iter().enumerate() {
// 数値として解析可能かチェック
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;
}
// ワークブックを保存
workbook.close()
.with_context(|| "Excelファイルの保存に失敗しました")?;
Ok(())
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment