Created
October 14, 2025 10:08
-
-
Save kujirahand/9e6496446c9a953219847ccf675b7ef5 to your computer and use it in GitHub Desktop.
CSVをExcelファイルに変換
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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