python

关注公众号 jb51net

关闭
首页 > 脚本专栏 > python > Python转换csv文件编码

Python实现CSV文件编码转换工具(转成UTF-8格式)

作者:小庄-Python办公

这篇文章主要为大家详细介绍了如何使用Python实现CSV文件编码转换工具,即转成UTF-8格式,文中的示例代码讲解详细感兴趣的小伙伴可以了解一下

背景

一个支持拖拽操作的 CSV 文件批量编码转换工具,可以将各种编码的 CSV 文件统一转换为 UTF-8 编码,解决中文乱码问题。

功能特点

多种文件添加方式

智能文件管理

强大的编码兼容性

现代化界面

系统要求

Python 版本:3.7 或更高版本

操作系统:Windows、macOS、Linux(推荐 Windows)

可选依赖:tkinterdnd2(用于拖拽功能)

安装与运行

基础运行

python main.py

启用拖拽功能(推荐)

pip install tkinterdnd2
python main.py

提示:如果未安装 tkinterdnd2,程序会自动降级运行,并在界面提示安装方法。

使用指南

步骤 1:添加文件

有三种方式添加需要转换的 CSV 文件:

1.拖拽方式(推荐)

2.按钮选择

3.列表管理

步骤 2:设置输出目录

步骤 3:开始转换

技术实现

编码检测策略

程序采用多重编码尝试机制:

encodings = ['mbcs', 'ansi', 'utf-8', 'utf-8-sig']

按优先级依次尝试读取,一旦成功即停止尝试,确保最大兼容性。

文件处理流程

拖拽功能实现

使用 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 App:
    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.files = []  # 去重使用
        self.files_set = set()

        self.output_folder_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 = ttk.Frame(self.root)
        btns.pack(fill=tk.X, padx=12, pady=(0, 6))

        ttk.Button(btns, text="添加文件…", command=self.add_files_dialog).pack(side=tk.LEFT, padx=(0, 6))
        ttk.Button(btns, text="添加文件夹…", command=self.add_folder_dialog).pack(side=tk.LEFT, padx=(0, 6))
        ttk.Button(btns, text="移除选中", command=self.remove_selected).pack(side=tk.LEFT, padx=(0, 6))
        ttk.Button(btns, text="清空列表", command=self.clear_list).pack(side=tk.LEFT)

        # ========== 输出设置 ==========
        out_frame = ttk.LabelFrame(self.root, text="输出设置")
        out_frame.pack(fill=tk.X, padx=12, pady=6)

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

        ttk.Label(row, text="输出文件夹:").pack(side=tk.LEFT)
        self.out_entry = ttk.Entry(row, textvariable=self.output_folder_var)
        self.out_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 = ttk.Frame(self.root)
        bottom.pack(fill=tk.X, padx=12, pady=10)

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

        ttk.Button(bottom, text="开始转换", command=self.convert_files).pack(side=tk.RIGHT)

        # 状态栏
        self.status_var = tk.StringVar(value="准备就绪")
        status = ttk.Label(self.root, textvariable=self.status_var, relief=tk.SUNKEN, anchor='w')
        status.pack(fill=tk.X, side=tk.BOTTOM)

    # ---------- 文件源管理 ----------
    def _add_paths(self, paths):
        added = 0
        for p in paths:
            if os.path.isdir(p):
                for f in collect_csv_from_dir(p):
                    if f not in self.files_set:
                        self.files.append(f)
                        self.files_set.add(f)
                        self.listbox.insert(tk.END, f)
                        added += 1
            else:
                if is_csv_file(p) and p not in self.files_set:
                    self.files.append(p)
                    self.files_set.add(p)
                    self.listbox.insert(tk.END, p)
                    added += 1
        self.status_var.set(f"已添加 {added} 个文件,当前总数:{len(self.files)}")

    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.files_set:
                self.files_set.remove(path)
            if path in self.files:
                self.files.remove(path)
        self.status_var.set(f"已移除,当前总数:{len(self.files)}")

    def clear_list(self):
        self.listbox.delete(0, tk.END)
        self.files.clear()
        self.files_set.clear()
        self.status_var.set("列表已清空")

    def select_output_folder(self):
        folder = filedialog.askdirectory(title="请选择保存转换后的文件夹")
        if folder:
            self.output_folder_var.set(folder)

    # ---------- 转换逻辑 ----------
    def _read_rows_with_encodings(self, file_path):
        # 按顺序尝试不同编码读取
        encodings = ['mbcs', 'ansi', 'utf-8', 'utf-8-sig']
        last_err = None
        for enc in encodings:
            try:
                with open(file_path, newline='', encoding=enc) as csvfile:
                    reader = csv.reader(csvfile, delimiter=',', quotechar='"')
                    for row in reader:
                        yield row
                return
            except Exception as e:
                last_err = e
                continue
        # 如果都失败,再抛出最后的异常
        raise last_err if last_err else UnicodeDecodeError("codec", b"", 0, 1, "无法解码")

    def convert_files(self):
        if not self.files:
            messagebox.showwarning("提示", "请先添加需要转换的 CSV 文件或文件夹!")
            return

        output_folder = self.output_folder_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.files)
        self.progress.config(maximum=total, value=0)
        success, failed = 0, 0

        for i, src in enumerate(self.files, start=1):
            dst = os.path.join(output_folder, os.path.basename(src))
            try:
                with open(dst, 'w', newline='', encoding='utf-8') as f_w:
                    writer = csv.writer(f_w)
                    for row in self._read_rows_with_encodings(src):
                        writer.writerow(row)
                success += 1
                self.status_var.set(f"转换成功:{os.path.basename(src)}")
            except Exception as e:
                failed += 1
                self.status_var.set(f"转换失败:{os.path.basename(src)} -> {e}")
            finally:
                self.progress['value'] = i
                self.root.update_idletasks()

        msg = f"转换完成!成功:{success},失败:{failed}。输出目录:{output_folder}"
        if failed:
            messagebox.showwarning("完成(部分失败)", msg)
        else:
            messagebox.showinfo("完成", msg)


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 = App(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 进行自动检测。

使用建议

最佳实践

性能优化

扩展功能规划

开发者: 专注于提供简单可靠的数据处理工具

到此这篇关于Python实现CSV文件编码转换工具(转成UTF-8格式)的文章就介绍到这了,更多相关Python转换csv文件编码内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文