告别繁琐的手工删改!使用 Python + PyQt5 打造一款 Excel 正则替换神器

在日常的数据处理和办公中,我们经常会遇到这样的抓狂场景:系统导出的 Excel 数据里,某一列夹杂了大量的冗余信息,比如带有特定括号的备注、被特定符号包裹的无用代码等。如果只是简单的替换,Excel 自带的 Ctrl+H 还能应付;但如果是**“删除以 A 开头、以 B 结尾”**这种带有规则的动态内容,Excel 自身的功能就显得捉襟见肘了。
为了彻底解决这个痛点,释放我们的双手,我用 Python 配合 PyQt5 开发了一款轻量级的桌面小工具——Excel 内容正则替换工具。今天就把这款工具的设计思路和源码分享给大家!
完整代码1:Excel某列字符批量替换 - GitCode
完整代码2: 放在文章末尾
🌟 工具亮点
这款小工具虽然体积不大,但可以说是“麻雀虽小,五脏俱全”:
- 傻瓜式操作:支持直接拖拽 Excel 文件到窗口,或者点击按钮选择。
- 智能表头识别:自动读取 Excel 的第一行作为表头,并生成下拉菜单供你精准选择要处理的列。
- 强大的正则内核:底层基于 Python 的
re模块,你只需要输入“开头”和“结尾”的字符,它就能自动帮你生成正则表达式进行非贪婪匹配替换。 - 细节拉满的选项:提供了一个贴心的复选框——是否包含开头和结尾的字符。你可以自由决定是把这层“壳”一起删掉,还是只掏空里面的内容。
- 安全的数据预览:为了防止“一失足成千古恨”,工具内置了数据预览表格,点击预览即可查看前 20 行的处理结果。
- 一键无损导出:处理满意后,点击导出,瞬间生成一份干净清爽的全新 Excel 文件。
- 高颜值 UI:告别简陋的默认界面,注入了精心调配的 QSS 样式表,扁平化设计,赏心悦目。
📸 界面展示
(在这里插入一张你运行程序后的界面截图,展示其美观的 UI 和功能分区)
工具分为四个主要区块:
- 文件选择:支持按钮和拖拽。
- 列选择:自动加载所有列名。
- 规则设置:设定你的删除规则。
- 数据预览 (前20行):所见即所得。
🛠️ 核心技术原理解析
1. 为什么选 PyQt5 和 pandas?
- PyQt5 是 Python 中非常成熟且强大的 GUI 库,配合
QGridLayout和自定义的 QSS 样式,可以轻松画出现代化的界面。 - pandas 则是数据处理的王者。在这个工具中,无论是读取 Excel(
pd.read_excel)、提取列名,还是导出 Excel(to_excel),pandas 都提供了极简的 API。
2. 核心正则逻辑
整个工具最灵魂的代码在于数据的替换逻辑。我们来看核心处理方法:
import re
import pandas as pd
# 假设 df_copy 是读取的 DataFrame,column_name 是选中的列,start_str 和 end_str 是用户输入的头尾
if include:
# 包含首尾:使用 .*? 进行非贪婪匹配
pattern = f'{re.escape(start_str)}.*?{re.escape(end_str)}'
else:
# 不包含首尾:使用正则表达式的“零宽断言”(Lookbehind 和 Lookahead)
pattern = f'(?<={re.escape(start_str)}).*?(?={re.escape(end_str)})'
# 应用正则替换
df_copy[column_name] = df_copy[column_name].astype(str).str.replace(pattern, '', regex=True)
知识点解析:
re.escape():这是一个非常重要的安全措施。如果用户输入的开头或结尾包含正则表达式的特殊字符(如*,?,(,)),escape会自动对其进行转义,将其视为普通文本处理。.*?:非贪婪匹配,确保它只会匹配到最近的一个结束字符,而不会把两个括号之间所有的数据全部吞噬。(?<=...)和(?=...):也就是大名鼎鼎的断言。当用户选择“不包含首尾”时,我们需要用到它。它表示匹配的内容必须紧跟在start_str之后,且紧挨在end_str之前,但匹配结果中并不包含这两个边界本身。
🚀 如何在本地跑起来?
如果你想直接使用或者进行二次开发,非常简单。
-
环境准备:确保你的电脑安装了 Python 3。
-
安装依赖:
新建一个requirements.txt文件,写入:PyQt5>=5.15.0 pandas>=1.3.0 openpyxl>=3.0.0然后执行
pip install -r requirements.txt。(注:openpyxl 是 pandas 处理 Excel 必不可少的底层引擎) -
运行代码:将完整的 Python 代码保存为
main.py,然后在终端执行python main.py即可召唤出这个神器!
💡 总结
通过不到 200 行代码,我们将 Python 的 GUI 编程与数据处理能力完美结合,解决了一个高频的办公痛点。这个小项目非常适合初学者作为练手项目:它既包含了 PyQt5 布局、事件绑定、拖拽事件的进阶用法,又涵盖了 pandas 的基本操作和正则表达式的高级应用。
如果你觉得这个小工具对你有帮助,或者代码中的某些技巧给了你启发,欢迎点赞、收藏、转发!如果你有任何优化的想法,也欢迎在评论区交流讨论。
Life is short, you need Python. 😉
import sys
import re
import pandas as pd
from PyQt5.QtWidgets import (
QApplication, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,
QLineEdit, QComboBox, QCheckBox, QTableWidget, QFileDialog, QTableWidgetItem,
QGridLayout, QGroupBox, QMessageBox, QHeaderView
)
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.df = None
self.initUI()
def initUI(self):
self.setWindowTitle('Excel 内容替换工具')
self.setGeometry(100, 100, 900, 700)
self.setAcceptDrops(True)
# 界面美化:添加 QSS 样式表
self.setStyleSheet("""
QWidget {
font-family: 'Microsoft YaHei', 'Segoe UI', sans-serif;
font-size: 10pt;
color: #2f3640;
background-color: #f5f6fa;
}
QGroupBox {
font-weight: bold;
border: 1px solid #dcdde1;
border-radius: 8px;
margin-top: 16px;
padding-top: 16px;
background-color: #ffffff;
}
QGroupBox::title {
subcontrol-origin: margin;
subcontrol-position: top left;
left: 15px;
padding: 0 5px;
color: #00a8ff;
}
QPushButton {
background-color: #00a8ff;
color: white;
border: none;
border-radius: 6px;
padding: 8px 16px;
font-weight: bold;
min-height: 20px;
}
QPushButton:hover {
background-color: #0097e6;
}
QPushButton:pressed {
background-color: #0088cc;
}
QLineEdit, QComboBox {
border: 1px solid #dcdde1;
border-radius: 5px;
padding: 6px 12px;
background-color: #fdfdfd;
selection-background-color: #00a8ff;
}
QLineEdit:focus, QComboBox:focus {
border: 1px solid #00a8ff;
background-color: #ffffff;
}
QTableWidget {
border: 1px solid #dcdde1;
border-radius: 6px;
background-color: white;
gridline-color: #f1f2f6;
alternate-background-color: #fafbfc;
selection-background-color: #00a8ff;
selection-color: white;
}
QHeaderView::section {
background-color: #f5f6fa;
padding: 8px;
border: none;
border-right: 1px solid #dcdde1;
border-bottom: 1px solid #dcdde1;
font-weight: bold;
color: #2f3640;
}
""")
main_layout = QVBoxLayout()
main_layout.setContentsMargins(20, 20, 20, 20)
main_layout.setSpacing(15)
grid_layout = QGridLayout()
grid_layout.setSpacing(15)
# File selection group
file_group = QGroupBox('文件选择')
file_layout = QVBoxLayout()
self.label = QLabel('📁 请选择或拖拽 Excel 文件 (支持 .xlsx, .xls)')
self.label.setStyleSheet("color: #7f8fa6; font-style: italic;")
file_layout.addWidget(self.label)
file_btn_layout = QHBoxLayout()
self.btn_select_file = QPushButton('📂 浏览文件')
self.btn_select_file.clicked.connect(self.open_file_dialog)
file_btn_layout.addWidget(self.btn_select_file)
file_btn_layout.addStretch()
file_layout.addLayout(file_btn_layout)
file_group.setLayout(file_layout)
grid_layout.addWidget(file_group, 0, 0)
# Column selection group
column_group = QGroupBox('列选择')
column_layout = QVBoxLayout()
self.label_column = QLabel('请选择要处理的列')
column_layout.addWidget(self.label_column)
self.combo_columns = QComboBox()
column_layout.addWidget(self.combo_columns)
column_group.setLayout(column_layout)
grid_layout.addWidget(column_group, 0, 1)
# Regex inputs group
regex_group = QGroupBox('规则设置')
regex_layout = QGridLayout()
regex_layout.addWidget(QLabel('以xx开头(请填写)'), 0, 0)
self.edit_start = QLineEdit()
regex_layout.addWidget(self.edit_start, 0, 1)
regex_layout.addWidget(QLabel('以xx结尾(请填写)'), 1, 0)
self.edit_end = QLineEdit()
regex_layout.addWidget(self.edit_end, 1, 1)
self.check_include = QCheckBox('包含开头和结尾的字符(打钩将去除)')
regex_layout.addWidget(self.check_include, 2, 0, 1, 2)
regex_group.setLayout(regex_layout)
grid_layout.addWidget(regex_group, 1, 0, 1, 2)
# Action buttons group
action_group = QGroupBox('操作')
action_layout = QHBoxLayout()
self.btn_preview = QPushButton('✨ 预览数据')
self.btn_preview.setMinimumHeight(35)
self.btn_preview.clicked.connect(self.preview_data)
action_layout.addWidget(self.btn_preview)
self.btn_export = QPushButton('💾 导出为 Excel')
self.btn_export.setMinimumHeight(35)
self.btn_export.clicked.connect(self.export_data)
action_layout.addWidget(self.btn_export)
action_group.setLayout(action_layout)
grid_layout.addWidget(action_group, 2, 0, 1, 2)
main_layout.addLayout(grid_layout)
# Preview table
preview_group = QGroupBox('数据预览 (前20行)')
preview_layout = QVBoxLayout()
self.table_preview = QTableWidget()
self.table_preview.setAlternatingRowColors(True)
self.table_preview.horizontalHeader().setStretchLastSection(True)
self.table_preview.setEditTriggers(QTableWidget.NoEditTriggers) # 预览表格设为只读
preview_layout.addWidget(self.table_preview)
preview_group.setLayout(preview_layout)
main_layout.addWidget(preview_group)
self.setLayout(main_layout)
def dragEnterEvent(self, event):
if event.mimeData().hasUrls():
event.acceptProposedAction()
def dropEvent(self, event):
for url in event.mimeData().urls():
file_path = url.toLocalFile()
if file_path.endswith(('.xlsx', '.xls')):
self.label.setText(f'已选择文件: {file_path}')
self.load_excel_data(file_path)
def open_file_dialog(self):
options = QFileDialog.Options()
file_path, _ = QFileDialog.getOpenFileName(self, "选择 Excel 文件", "", "Excel Files (*.xlsx *.xls);;All Files (*)", options=options)
if file_path:
self.label.setText(f'已选择文件: {file_path}')
self.load_excel_data(file_path)
def load_excel_data(self, file_path):
try:
self.df = pd.read_excel(file_path)
self.combo_columns.clear()
self.combo_columns.addItems(self.df.columns)
except Exception as e:
self.label.setText(f'加载文件失败: {e}')
self.df = None
def preview_data(self):
processed_df = self.process_data()
self.update_preview(processed_df)
def process_data(self):
if self.df is None:
return None
column_name = self.combo_columns.currentText()
start_str = self.edit_start.text()
end_str = self.edit_end.text()
include = self.check_include.isChecked()
df_copy = self.df.copy()
if column_name in df_copy.columns and start_str and end_str:
df_copy[column_name] = df_copy[column_name].astype(str)
if include:
pattern = f'{re.escape(start_str)}.*?{re.escape(end_str)}'
else:
pattern = f'(?<={re.escape(start_str)}).*?(?={re.escape(end_str)})'
df_copy[column_name] = df_copy[column_name].str.replace(pattern, '', regex=True)
return df_copy
def export_data(self):
processed_df = self.process_data()
if processed_df is None:
return
options = QFileDialog.Options()
save_path, _ = QFileDialog.getSaveFileName(self, "保存文件", "", "Excel Files (*.xlsx);;All Files (*)", options=options)
if save_path:
try:
processed_df.to_excel(save_path, index=False)
QMessageBox.information(self, '成功', f'文件已保存到: {save_path}')
except Exception as e:
self.label.setText(f'保存文件失败: {e}')
def update_preview(self, df):
if df is None:
self.table_preview.clear()
self.table_preview.setRowCount(0)
self.table_preview.setColumnCount(0)
return
preview_df = df.head(20)
self.table_preview.setRowCount(preview_df.shape[0])
self.table_preview.setColumnCount(preview_df.shape[1])
self.table_preview.setHorizontalHeaderLabels(preview_df.columns)
for i in range(preview_df.shape[0]):
for j in range(preview_df.shape[1]):
item = str(preview_df.iloc[i, j])
self.table_preview.setItem(i, j, QTableWidgetItem(item))
if __name__ == '__main__':
app = QApplication(sys.argv)
mainWin = MainWindow()
mainWin.show()
sys.exit(app.exec_())