Python 实战:打造 CSV 与 Excel 互转工具
在数据处理的日常工作中,我们经常需要在 CSV 和 Excel 格式之间进行转换。虽然 Pandas 提供了强大的 API,但对于不熟悉编程的同事来说,一个简单易用的图形界面(GUI)工具会更加友好。
本文将带你从零开始,使用 Python + PyQt5 + Pandas 打造一个界面美观、功能实用的格式转换工具。

完整代码1:点击直达
完整代码2:
import sys
import os
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QPushButton, QLabel, QFileDialog,
QTabWidget, QMessageBox, QFrame, QLineEdit, QListWidget, QAbstractItemView)
from PyQt5.QtCore import Qt, QSize, pyqtSignal, QUrl
from PyQt5.QtGui import QFont, QIcon, QColor, QPalette, QDragEnterEvent, QDropEvent
import pandas as pd
def csv_to_excel(csv_path, excel_path):
"""
Convert CSV file to Excel file.
"""
try:
if not os.path.exists(csv_path):
raise FileNotFoundError(f"File not found: {csv_path}")
# Read CSV file
# Attempt to detect encoding or default to utf-8
try:
df = pd.read_csv(csv_path, encoding='utf-8')
except UnicodeDecodeError:
df = pd.read_csv(csv_path, encoding='gbk') # Try GBK if utf-8 fails
# Write to Excel
df.to_excel(excel_path, index=False)
return True, "转换成功"
except Exception as e:
return False, str(e)
def excel_to_csv(excel_path, csv_path):
"""
Convert Excel file to CSV file.
"""
try:
if not os.path.exists(excel_path):
raise FileNotFoundError(f"File not found: {excel_path}")
# Read Excel file
df = pd.read_excel(excel_path)
# Write to CSV
df.to_csv(csv_path, index=False, encoding='utf-8-sig') # use utf-8-sig for Excel compatibility
return True, "转换成功"
except Exception as e:
return False, str(e)
class ModernButton(QPushButton):
def __init__(self, text, parent=None):
super().__init__(text, parent)
self.setCursor(Qt.PointingHandCursor)
self.setFixedHeight(40)
class DropArea(QFrame):
files_dropped = pyqtSignal(list)
def __init__(self, allowed_extensions, parent=None):
super().__init__(parent)
self.allowed_extensions = allowed_extensions
self.setAcceptDrops(True)
self.init_ui()
def init_ui(self):
self.layout = QVBoxLayout(self)
self.label = QLabel("拖拽文件或文件夹到此处\n支持多个文件")
self.label.setAlignment(Qt.AlignCenter)
self.label.setFont(QFont("Microsoft YaHei", 12))
self.label.setStyleSheet("color: #7f8c8d;")
self.layout.addWidget(self.label)
self.setStyleSheet("""
DropArea {
border: 2px dashed #bdc3c7;
border-radius: 10px;
background-color: #f9f9f9;
}
""")
def dragEnterEvent(self, event: QDragEnterEvent):
if event.mimeData().hasUrls():
event.acceptProposedAction()
self.setStyleSheet("""
DropArea {
border: 2px dashed #2196F3;
border-radius: 10px;
background-color: #e3f2fd;
}
""")
else:
event.ignore()
def dragLeaveEvent(self, event):
self.setStyleSheet("""
DropArea {
border: 2px dashed #bdc3c7;
border-radius: 10px;
background-color: #f9f9f9;
}
""")
def dropEvent(self, event: QDropEvent):
self.setStyleSheet("""
DropArea {
border: 2px dashed #bdc3c7;
border-radius: 10px;
background-color: #f9f9f9;
}
""")
files = []
for url in event.mimeData().urls():
path = url.toLocalFile()
if os.path.isdir(path):
files.extend(self.scan_directory(path))
else:
if self.is_valid_file(path):
files.append(path)
if files:
self.files_dropped.emit(files)
def scan_directory(self, directory):
valid_files = []
for root, _, files in os.walk(directory):
for file in files:
file_path = os.path.join(root, file)
if self.is_valid_file(file_path):
valid_files.append(file_path)
return valid_files
def is_valid_file(self, file_path):
_, ext = os.path.splitext(file_path)
return ext.lower() in self.allowed_extensions
class ConverterTab(QWidget):
def __init__(self, conversion_type, parent=None):
super().__init__(parent)
self.conversion_type = conversion_type
self.file_list = []
self.init_ui()
def init_ui(self):
layout = QVBoxLayout(self)
layout.setSpacing(15)
layout.setContentsMargins(20, 20, 20, 20)
# Title
title_text = "CSV 转 Excel" if self.conversion_type == "CSV to Excel" else "Excel 转 CSV"
title = QLabel(title_text)
title.setFont(QFont("Microsoft YaHei", 16, QFont.Bold))
title.setAlignment(Qt.AlignCenter)
layout.addWidget(title)
# Drop Area
if self.conversion_type == "CSV to Excel":
extensions = ['.csv']
else:
extensions = ['.xlsx', '.xls']
self.drop_area = DropArea(extensions)
self.drop_area.setFixedHeight(120)
self.drop_area.files_dropped.connect(self.add_files)
layout.addWidget(self.drop_area)
# File List
self.list_widget = QListWidget()
self.list_widget.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.list_widget.setAlternatingRowColors(True)
layout.addWidget(self.list_widget)
# Action Buttons
btn_layout = QHBoxLayout()
self.clear_btn = ModernButton("清空列表")
self.clear_btn.setStyleSheet("background-color: #e74c3c;")
self.clear_btn.clicked.connect(self.clear_files)
self.convert_btn = ModernButton("开始批量转换")
self.convert_btn.clicked.connect(self.start_conversion)
btn_layout.addWidget(self.clear_btn)
btn_layout.addWidget(self.convert_btn)
layout.addLayout(btn_layout)
# Status Label
self.status_label = QLabel("等待文件...")
self.status_label.setAlignment(Qt.AlignCenter)
self.status_label.setStyleSheet("color: #7f8c8d;")
layout.addWidget(self.status_label)
def add_files(self, files):
count = 0
for file_path in files:
if file_path not in self.file_list:
self.file_list.append(file_path)
self.list_widget.addItem(file_path)
count += 1
if count > 0:
self.status_label.setText(f"已添加 {count} 个文件,共 {len(self.file_list)} 个文件等待转换")
self.status_label.setStyleSheet("color: #2196F3;")
else:
self.status_label.setText("没有添加新文件(可能已存在或格式不符)")
def clear_files(self):
self.file_list.clear()
self.list_widget.clear()
self.status_label.setText("列表已清空")
self.status_label.setStyleSheet("color: #7f8c8d;")
def start_conversion(self):
if not self.file_list:
QMessageBox.warning(self, "警告", "请先添加文件!")
return
success_count = 0
fail_count = 0
total = len(self.file_list)
self.status_label.setText("正在转换...")
self.status_label.setStyleSheet("color: #2196F3;")
self.convert_btn.setEnabled(False)
self.clear_btn.setEnabled(False)
QApplication.processEvents()
for file_path in self.file_list:
directory = os.path.dirname(file_path)
filename = os.path.basename(file_path)
name, _ = os.path.splitext(filename)
try:
if self.conversion_type == "CSV to Excel":
target_file = os.path.join(directory, f"{name}.xlsx")
success, _ = csv_to_excel(file_path, target_file)
else:
target_file = os.path.join(directory, f"{name}.csv")
success, _ = excel_to_csv(file_path, target_file)
if success:
success_count += 1
else:
fail_count += 1
except Exception:
fail_count += 1
# Update progress (optional, could be improved with a progress bar)
self.status_label.setText(f"正在转换... ({success_count + fail_count}/{total})")
QApplication.processEvents()
self.convert_btn.setEnabled(True)
self.clear_btn.setEnabled(True)
result_msg = f"转换完成!\n成功:{success_count}\n失败:{fail_count}"
self.status_label.setText(result_msg.replace("\n", " "))
if fail_count == 0:
self.status_label.setStyleSheet("color: #4CAF50; font-weight: bold;")
QMessageBox.information(self, "成功", result_msg)
else:
self.status_label.setStyleSheet("color: #F44336;")
QMessageBox.warning(self, "部分完成", result_msg)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("CSV 与 Excel 互转工具专业版 作者:小庄-Python学习")
self.setMinimumSize(700, 500)
# Main Layout
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QVBoxLayout(central_widget)
main_layout.setContentsMargins(0, 0, 0, 0)
# Tabs
self.tabs = QTabWidget()
self.tabs.addTab(ConverterTab("CSV to Excel"), "CSV 转 Excel")
self.tabs.addTab(ConverterTab("Excel to CSV"), "Excel 转 CSV")
main_layout.addWidget(self.tabs)
self.apply_styles()
def apply_styles(self):
style_sheet = """
QMainWindow {
background-color: #f5f5f7;
}
QTabWidget::pane {
border: 1px solid #e0e0e0;
background: white;
border-radius: 8px;
margin: 20px;
}
QTabBar::tab {
background: #e0e0e0;
color: #555;
padding: 10px 20px;
margin-right: 5px;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
font-family: "Microsoft YaHei";
font-size: 14px;
}
QTabBar::tab:selected {
background: white;
color: #333;
font-weight: bold;
border-bottom: 2px solid #2196F3;
}
QListWidget {
border: 1px solid #ccc;
border-radius: 4px;
padding: 5px;
background: #fff;
font-family: "Microsoft YaHei";
font-size: 13px;
}
QListWidget::item {
padding: 5px;
}
QListWidget::item:selected {
background-color: #e3f2fd;
color: #333;
}
QPushButton {
background-color: #2196F3;
color: white;
border: none;
border-radius: 4px;
font-family: "Microsoft YaHei";
font-size: 14px;
}
QPushButton:hover {
background-color: #1976D2;
}
QPushButton:pressed {
background-color: #0D47A1;
}
QLabel {
color: #333;
font-family: "Microsoft YaHei";
}
"""
self.setStyleSheet(style_sheet)
if __name__ == "__main__":
app = QApplication(sys.argv)
# Set app font
font = QFont("Microsoft YaHei", 10)
app.setFont(font)
window = MainWindow()
window.show()
sys.exit(app.exec_())
1. 项目简介
本工具主要包含以下功能:
- CSV 转 Excel:将
.csv文件转换为.xlsx文件。 - Excel 转 CSV:将
.xlsx或.xls文件转换为.csv文件。 - 拖拽支持:支持拖拽单个文件、多个文件甚至整个文件夹。
- 批量处理:一次性转换列表中的所有文件。
- 现代化界面:采用扁平化设计,操作直观。
- 智能处理:自动处理文件编码(支持 UTF-8 和 GBK),防止乱码。
2. 技术选型
- Python 3.x:开发语言。
- PyQt5:强大的桌面 GUI 框架,用于构建界面。
- Pandas:数据处理库,用于读取和写入文件。
- Openpyxl/Xlrd:Pandas 处理 Excel 文件的依赖库。
3. 环境搭建
首先,我们需要安装必要的依赖库。在项目根目录下创建一个 requirements.txt 文件:
PyQt5
pandas
openpyxl
xlrd
使用 pip 安装:
pip install -r requirements.txt
4. 核心逻辑实现
为了方便分发和使用,我们将所有代码合并在一个文件中。以下是核心转换函数的实现:
import pandas as pd
import os
def csv_to_excel(csv_path, excel_path):
"""
CSV 转 Excel
自动尝试 utf-8 和 gbk 编码
"""
try:
if not os.path.exists(csv_path):
raise FileNotFoundError(f"File not found: {csv_path}")
# 尝试读取 CSV,处理编码问题
try:
df = pd.read_csv(csv_path, encoding='utf-8')
except UnicodeDecodeError:
df = pd.read_csv(csv_path, encoding='gbk')
df.to_excel(excel_path, index=False)
return True, "转换成功"
except Exception as e:
return False, str(e)
def excel_to_csv(excel_path, csv_path):
"""
Excel 转 CSV
使用 utf-8-sig 编码,确保 Excel 打开不乱码
"""
try:
if not os.path.exists(excel_path):
raise FileNotFoundError(f"File not found: {excel_path}")
df = pd.read_excel(excel_path)
# 使用 utf-8-sig 编码写入 CSV
df.to_csv(csv_path, index=False, encoding='utf-8-sig')
return True, "转换成功"
except Exception as e:
return False, str(e)
关键点解析:
- 编码处理:读取 CSV 时,先尝试
utf-8,如果失败则尝试gbk,这能有效解决中文乱码问题。 - Excel 兼容性:写入 CSV 时使用
utf-8-sig编码,这样生成的 CSV 文件在 Excel 中打开时不会出现乱码。
5. 界面设计与实现
为了让界面看起来“好看一点”,我们使用了 PyQt5 的 QSS(Qt Style Sheets)进行样式定制,采用了扁平化的设计风格。
5.1 自定义拖拽组件
我们封装了一个 DropArea 组件,支持拖拽文件和文件夹,并自动递归扫描文件夹中的有效文件。
class DropArea(QFrame):
files_dropped = pyqtSignal(list)
def __init__(self, allowed_extensions, parent=None):
super().__init__(parent)
self.allowed_extensions = allowed_extensions
self.setAcceptDrops(True)
# ... 初始化 UI ...
def dropEvent(self, event: QDropEvent):
files = []
for url in event.mimeData().urls():
path = url.toLocalFile()
if os.path.isdir(path):
files.extend(self.scan_directory(path))
else:
if self.is_valid_file(path):
files.append(path)
if files:
self.files_dropped.emit(files)
5.2 样式美化 (QSS)
这是让界面变好看的关键。我们在 MainWindow 中定义了全局样式:
style_sheet = """
QMainWindow {
background-color: #f5f5f7; /* 柔和的背景色 */
}
QTabWidget::pane {
border: 1px solid #e0e0e0;
background: white;
border-radius: 8px; /* 圆角设计 */
margin: 20px;
}
QTabBar::tab {
background: #e0e0e0;
color: #555;
padding: 10px 20px;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
}
QTabBar::tab:selected {
background: white;
color: #333;
font-weight: bold;
border-bottom: 2px solid #2196F3; /* 选中状态的高亮 */
}
QPushButton {
background-color: #2196F3; /* 材质蓝 */
color: white;
border: none;
border-radius: 4px;
font-size: 14px;
}
QPushButton:hover {
background-color: #1976D2;
}
"""
5.3 主窗口逻辑
主窗口使用 QTabWidget 分为两个标签页,分别对应两种转换功能。
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("CSV 与 Excel 互转工具专业版")
self.setMinimumSize(600, 400)
# ... 初始化布局 ...
self.tabs = QTabWidget()
self.tabs.addTab(ConverterTab("CSV to Excel"), "CSV 转 Excel")
self.tabs.addTab(ConverterTab("Excel to CSV"), "Excel 转 CSV")
self.apply_styles()
6. 使用说明
- 运行程序:
python main.py - 选择转换模式(CSV 转 Excel 或 Excel 转 CSV)。
- 拖拽文件:将一个或多个文件/文件夹拖拽到虚线区域内。
- 点击 "开始批量转换" 按钮。
- 转换成功后,状态栏会显示成功和失败的数量统计。
7. 总结
通过不到 300 行代码,我们完成了一个支持批量拖拽转换的实用工具。这个项目展示了:
- 如何实现 PyQt5 的拖拽事件处理 (
QDragEnterEvent,QDropEvent)。 - 如何递归扫描目录下的特定文件。
- 如何使用 Pandas 进行高效的数据格式转换。
你可以在此基础上继续扩展,例如添加转换进度条、自定义输出目录等。