【Pythonオフィス自動化】CSVファイルのエンコーディング変換ツール(UTF-8形式)完全コード付き+GUIパッケージング

背景

様々なエンコーディングのCSVファイルをUTF-8形式に一括変換するドラッグ&ドロップ対応ツール。中国語の文字化け問題を解決します。

機能特徴

複数のファイル追加方法

  • ドラッグ&ドロップ対応: CSVファイルまたはCSVファイルを含むフォルダを直接プログラムウィンドウにドラッグ
  • 単一ファイル選択: 1つまたは複数のCSVファイルを選択可能
  • フォルダ一括処理: フォルダを選択すると、内部のすべてのCSVファイルを自動収集

スマートファイル管理

  • 自動重複排除: 重複して追加されたファイルは自動的にフィルタリング
  • リスト管理: 選択ファイルの削除、リストのクリアなどの操作をサポート
  • リアルタイドフィードバック: ステータスバーに現在のファイル数と操作結果を表示

高度なエンコーディング互換性

  • 多エンコーディング対応: MBCS、ANSI、UTF-8、UTF-8-SIGなどのエンコーディングを自動で試行
  • スマート認識: 優先順位に従って順次試行し、文字化けを最大限回避
  • 統一出力: すべてのファイルをUTF-8エンコーディングに統一変換

モダンなインターフェース

  • 美しいレイアウト: ttkコントロールを使用し、インターフェースはクリアで美しい
  • 進捗の可視化: リアルタイムプログレスバーで変換進捗を表示
  • ステータス表示: 底部ステータスバーに現在の操作状態を表示

システム要件

  • Pythonバージョン: 3.7以上
  • オペレーティングシステム: Windows、macOS、Linux(Windows推奨)
  • オプション依存: tkinterdnd2(ドラッグ機能用)

インストールと実行

基本的な実行

python main.py

ドラッグ機能の有効化(推奨)

pip install tkinterdnd2
python main.py

ヒント: tkinterdnd2がインストールされていない場合、プログラムは自動でデグレード動作し、インターフェースにインストール方法を提示します。

使用ガイド

ステップ1: ファイルを追加

  • 変換するCSVファイルを追加する3つの方法:
  1. ドラッグ方式(推奨)
  • CSVファイルを直接ファイルリストエリアにドラッグ
  • CSVファイルを含むフォルダをドラッグすることも可能
  1. ボタン選択
  • 「ファイルを追加...」をクリックして1つまたは複数のCSVファイルを選択
  • 「フォルダを追加...」をクリックしてCSVを含むフォルダを選択
  1. リスト管理
  • ファイルを選択して「選択を削除」をクリックして不要なファイルを削除
  • 「リストをクリア」をクリックしてすべてのファイルをクリア

ステップ2: 出力ディレクトリの設定

  • 「出力設定」エリアで「参照...」ボタンをクリック
  • 変換後のファイルを保存するディレクトリを選択
  • 同名ファイルの上書きを避けるために、空のディレクトリを選択することを推奨

ステップ3: 変換開始

  • 「変換開始」ボタンをクリック
  • プログレスバーとステータスバーのリアルタイムフィードバックを確認
  • 変換完了後に結果通知ポップアップが表示される

技術的実装

エンコーディング検出戦略

プログラムは多重エンコーディング試行メカニズムを採用:
encodings = ['mbcs', 'ansi', 'utf-8', 'utf-8-sig']

優先順位に従って順次読み取りを試行し、成功した時点で試行を停止し、最大限の互換性を確保します。

ファイル処理フロー

  1. ファイル収集: 指定されたパスをスキャンし、.csvファイルをフィルタリング
  2. エンコーディング検出: 戦略に従って異なるエンコーディングで読み取りを試行
  3. コンテンツ変換: 行ごとに読み取りUTF-8ファイルに書き込み
  4. 進捗フィードバック: インターフェースの状態をリアルタイムで更新

ドラッグ機能の実装

  • tkinterdnd2ライブラリを使用してクロスプラットフォームのドラッグ&ドロップサポートを実装:
self.listbox.drop_target_register(DND_FILES)
self.listbox.dnd_bind('<<Drop>>', self._on_drop)

完全なコード

import csv
import os
import sys
import tkinter as tk
from tkinter import filedialog, messagebox
from tkinter import ttk

# ドラッグサポートの有効化(インストールが必要: pip install tkinterdnd2)
try:
    from tkinterdnd2 import DND_FILES, TkinterDnD  # type: ignore
    DND_AVAILABLE = True
except Exception:
    DND_AVAILABLE = False
    TkinterDnD = None  # 未使用の場合はプレースホルダ


def is_csv_file(path: str) -> bool:
    return os.path.isfile(path) and path.lower().endswith('.csv')


def collect_csv_from_dir(folder: str):
    files = []
    if os.path.isdir(folder):
        for f in os.listdir(folder):
            full = os.path.join(folder, f)
            if is_csv_file(full):
                files.append(full)
    return files


def parse_dnd_event_data(root: tk.Tk, data: str):
    # ドラッグデータの解析、スペースを含むパスに対応
    try:
        parts = root.tk.splitlist(data)
    except Exception:
        parts = data.split()
    return [p.strip('{').strip('}') for p in parts]


class CSVConverterApp:
    def __init__(self, root: tk.Tk):
        self.root = root
        self.root.title("CSVファイルUTF-8変換ツール(ドラッグ/選択/フォルダ対応)")
        self.root.geometry('760x560')
        self.root.minsize(680, 480)

        self.file_list = []  # 重複排除用
        self.file_set = set()

        self.output_dir_var = tk.StringVar()

        self._build_ui()

    def _build_ui(self):
        # ========== 入力エリア(リスト + ドラッグ) ==========
        input_frame = ttk.LabelFrame(self.root, text="変換対象ファイル(CSVファイルまたはフォルダをここにドラッグ)")
        input_frame.pack(fill=tk.BOTH, expand=True, padx=12, pady=(12, 6))

        list_frame = ttk.Frame(input_frame)
        list_frame.pack(fill=tk.BOTH, expand=True, padx=8, pady=8)

        self.listbox = tk.Listbox(list_frame, selectmode=tk.EXTENDED)
        self.listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

        scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.listbox.yview)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        self.listbox.config(yscrollcommand=scrollbar.set)

        hint_text = "ヒント: 下のボタンでファイル/フォルダを追加できます。CSVファイルまたはフォルダを直接リストにドラッグすることも可能です。"
        self.hint_label = ttk.Label(input_frame, text=hint_text, foreground="#666")
        self.hint_label.pack(anchor='w', padx=10, pady=(0, 8))

        # ドラッグサポート
        if DND_AVAILABLE and isinstance(self.root, TkinterDnD.Tk):
            try:
                self.listbox.drop_target_register(DND_FILES)
                self.listbox.dnd_bind('<<Drop>>', self._on_drop)
            except Exception:
                pass
        else:
            if not DND_AVAILABLE:
                warn = "(tkinterdnd2が未インストールのため、ドラッグ機能は使用できません。実行: pip install tkinterdnd2)"
                self.hint_label.configure(text=f"{hint_text}\n{warn}")

        # ========== 操作ボタン ==========
        btns_frame = ttk.Frame(self.root)
        btns_frame.pack(fill=tk.X, padx=12, pady=(0, 6))

        ttk.Button(btns_frame, text="ファイルを追加...", command=self.add_files_dialog).pack(side=tk.LEFT, padx=(0, 6))
        ttk.Button(btns_frame, text="フォルダを追加...", command=self.add_folder_dialog).pack(side=tk.LEFT, padx=(0, 6))
        ttk.Button(btns_frame, text="選択を削除", command=self.remove_selected).pack(side=tk.LEFT, padx=(0, 6))
        ttk.Button(btns_frame, text="リストをクリア", command=self.clear_list).pack(side=tk.LEFT)

        # ========== 出力設定 ==========
        output_frame = ttk.LabelFrame(self.root, text="出力設定")
        output_frame.pack(fill=tk.X, padx=12, pady=6)

        row = ttk.Frame(output_frame)
        row.pack(fill=tk.X, padx=10, pady=8)

        ttk.Label(row, text="出力フォルダ:").pack(side=tk.LEFT)
        self.output_entry = ttk.Entry(row, textvariable=self.output_dir_var)
        self.output_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=6)
        ttk.Button(row, text="参照...", command=self.select_output_folder).pack(side=tk.LEFT)

        # ========== 変換 & 進捗 ==========
        bottom_frame = ttk.Frame(self.root)
        bottom_frame.pack(fill=tk.X, padx=12, pady=10)

        self.progress_bar = ttk.Progressbar(bottom_frame, mode='determinate')
        self.progress_bar.pack(fill=tk.X, expand=True, side=tk.LEFT, padx=(0, 10))

        ttk.Button(bottom_frame, text="変換開始", command=self.convert_files).pack(side=tk.RIGHT)

        # ステータスバー
        self.status_var = tk.StringVar(value="準備完了")
        status_label = ttk.Label(self.root, textvariable=self.status_var, relief=tk.SUNKEN, anchor='w')
        status_label.pack(fill=tk.X, side=tk.BOTTOM)

    # ---------- ファイルソース管理 ----------
    def _add_paths(self, paths):
        added_count = 0
        for path in paths:
            if os.path.isdir(path):
                for file in collect_csv_from_dir(path):
                    if file not in self.file_set:
                        self.file_list.append(file)
                        self.file_set.add(file)
                        self.listbox.insert(tk.END, file)
                        added_count += 1
            else:
                if is_csv_file(path) and path not in self.file_set:
                    self.file_list.append(path)
                    self.file_set.add(path)
                    self.listbox.insert(tk.END, path)
                    added_count += 1
        self.status_var.set(f"{added_count}個のファイルを追加しました。合計: {len(self.file_list)}")

    def _on_drop(self, event):
        paths = parse_dnd_event_data(self.root, event.data)
        self._add_paths(paths)

    def add_files_dialog(self):
        paths = filedialog.askopenfilenames(title="CSVファイルを選択", filetypes=[("CSVファイル", "*.csv"), ("すべてのファイル", "*.*")])
        if paths:
            self._add_paths(paths)

    def add_folder_dialog(self):
        folder = filedialog.askdirectory(title="CSVファイルを含むフォルダを選択")
        if folder:
            self._add_paths([folder])

    def remove_selected(self):
        indices = list(self.listbox.curselection())
        if not indices:
            return
        # 後ろから削除してインデックスのずれを回避
        for idx in reversed(indices):
            path = self.listbox.get(idx)
            self.listbox.delete(idx)
            if path in self.file_set:
                self.file_set.remove(path)
            if path in self.file_list:
                self.file_list.remove(path)
        self.status_var.set(f"削除完了。合計: {len(self.file_list)}")

    def clear_list(self):
        self.listbox.delete(0, tk.END)
        self.file_list.clear()
        self.file_set.clear()
        self.status_var.set("リストをクリアしました")

    def select_output_folder(self):
        folder = filedialog.askdirectory(title="変換後のファイルを保存するフォルダを選択")
        if folder:
            self.output_dir_var.set(folder)

    # ---------- 変換ロジック ----------
    def _read_rows_with_encodings(self, file_path):
        # 順不同のエンコーディングで読み取りを試行
        encodings = ['mbcs', 'ansi', 'utf-8', 'utf-8-sig']
        last_error = None
        for enc in encodings:
            try:
                with open(file_path, newline='', encoding=enc) as csv_file:
                    reader = csv.reader(csv_file, delimiter=',', quotechar='"')
                    for row in reader:
                        yield row
                return
            except Exception as e:
                last_error = e
                continue
        # すべて失敗した場合、最後の例外をスロー
        raise last_error if last_error else UnicodeDecodeError("codec", b"", 0, 1, "デコードできません")

    def convert_files(self):
        if not self.file_list:
            messagebox.showwarning("注意", "変換するCSVファイルまたはフォルダを先に追加してください!")
            return

        output_folder = self.output_dir_var.get().strip()
        if not output_folder:
            messagebox.showerror("エラー", "変換後のファイルを保存するフォルダを選択してください!")
            return
        if not os.path.exists(output_folder):
            try:
                os.makedirs(output_folder, exist_ok=True)
            except Exception as e:
                messagebox.showerror("エラー", f"出力フォルダを作成できません: {e}")
                return

        total = len(self.file_list)
        self.progress_bar.config(maximum=total, value=0)
        success_count, fail_count = 0, 0

        for i, source in enumerate(self.file_list, start=1):
            destination = os.path.join(output_folder, os.path.basename(source))
            try:
                with open(destination, 'w', newline='', encoding='utf-8') as output_file:
                    writer = csv.writer(output_file)
                    for row in self._read_rows_with_encodings(source):
                        writer.writerow(row)
                success_count += 1
                self.status_var.set(f"変換成功: {os.path.basename(source)}")
            except Exception as e:
                fail_count += 1
                self.status_var.set(f"変換失敗: {os.path.basename(source)} -> {e}")
            finally:
                self.progress_bar['value'] = i
                self.root.update_idletasks()

        message = f"変換完了!成功: {success_count}、失敗: {fail_count}。出力ディレクトリ: {output_folder}"
        if fail_count:
            messagebox.showwarning("完了(一部失敗)", message)
        else:
            messagebox.showinfo("完了", message)


def main():
    # 優先的にDnD対応のTkを使用
    if DND_AVAILABLE:
        root = TkinterDnD.Tk()
    else:
        root = tk.Tk()
    # Windows上ではttkのデフォルトテーマがより美しい
    try:
        ttk.Style().theme_use('clam')
    except Exception:
        pass

    app = CSVConverterApp(root)
    root.mainloop()


if __name__ == '__main__':
    main()

よくある質問

Q: なぜドラッグ機能が使えないのですか?

A: tkinterdnd2ライブラリをインストールする必要があります。pip install tkinterdnd2を実行してからプログラムを再起動してください。

Q: 変換後にExcelを開くと依然として文字化けしますか?

A: このツールは標準のUTF-8形式を出力します。一部の古いバージョンのExcelではUTF-8-BOM形式が必要な場合があります。出力エンコーディングをutf-8-sigに変更することを検討してください。

Q: 出力ファイルは元のファイルを上書きしますか?

A: しません。プログラムは指定された出力ディレクトリに新しいファイルを作成し、元のファイルは変更しません。しかし、出力ディレクトリに同名のファイルが存在する場合は上書きされます。

Q: どのような区切り文字をサポートしていますか?

A: 現在はデフォルトでカンマ区切り文字をサポートしています。セミコロンやタブなどの他の区切り文字をサポートする必要がある場合は、csv.Snifferを使用して自動検出を拡張できます。

使用上のアドバイス

ベストプラクティス

  1. 一括処理: 同じタイプのCSVファイルを同じフォルダに配置し、全体をドラッグして追加
  2. 出力管理: 常に空の出力ディレクトリを選択し、ファイルの上書きを避ける
  3. エンコーディング互換性: 特定のソフトウェアとの互換性が必要な場合は、出力エンコーディング形式を調整

パフォーマンスの最適化

  • 大ファイル処理時にリアルタイム進捗が表示されます
  • 操作の中断が可能(プログラムウィンドウを閉じる)
  • メモリ使用量の最適化で、大ファイルのメモリ溢れを回避

拡張機能計画

  • 出力ファイル名に自動でサフィックスを追加して上書きを回避
  • 自動区切り文字検出と設定
  • 変換レポートのエクスポート(成功/失敗統計)
  • 再帰的サブディレクトリスキャンオプション
  • エンコーディング検出結果のプレビュー
  • 一括リネーム機能

タグ: Python csv エンコーディング変換 GUI ドラッグ&ドロップ

7月30日 17:45 投稿