[Python] 纯文本查看 复制代码
import sys
import os
import platform
# ===================== 跨平台错误提示函数(兼容麒麟Linux + Windows) =====================
def show_error_msg(msg: str):
# 判断操作系统
if platform.system() == "Windows":
# Windows使用弹窗
import ctypes
ctypes.windll.user32.MessageBoxW(0, msg, "运行环境缺失", 0x10)
else:
# 银河麒麟/Linux 终端彩色打印提示
print("\033[31m==================== 运行环境缺失 ====================\033[0m")
print(f"\033[33m{msg}\033[0m")
print("\033[31m======================================================\033[0m")
# ===================== 前置环境检测,缺失依赖跨平台提示 =====================
try:
import re
# 全部替换为PyQt5
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QTextEdit, QPushButton, QTabWidget,
QWidget, QVBoxLayout, QHBoxLayout, QFileDialog, QGroupBox, QStatusBar,
QCheckBox, QRadioButton, QLineEdit, QButtonGroup
)
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont, QColor, QPalette, QTextCursor, QIcon
import zhconv
except ModuleNotFoundError as e:
# 区分系统给出对应安装命令
if platform.system() == "Linux":
tip = f"检测到缺少运行依赖库:{e.name}\n银河麒麟系统请打开终端执行两条命令:\n1. sudo apt update\n2. sudo apt install python3-pip python3-pyqt5 libqt5core5a libqt5gui5 libqt5widgets5 qtbase5-dev qttools5-dev-tools libxcb-xinerama0 zhconv -y\n运行程序使用命令:python3 文本整理脚本.py"
else:
tip = f"检测到缺少运行依赖库:{e.name}\n请先安装Python,再执行命令:\npip install PyQt5 zhconv\n安装Python时务必勾选 Add Python to PATH"
show_error_msg(tip)
sys.exit(1)
# ===================== 文本处理函数 =====================
def remove_all_spaces(text: str) -> str:
return re.sub(r'\s+', '', text)
def trim_line_space(text: str) -> str:
lines = text.splitlines()
return '\n'.join([line.strip() for line in lines])
def remove_empty_lines(text: str) -> str:
return re.sub(r'\n+', '\n', text).strip('\n')
def traditional_to_simple(text: str) -> str:
return zhconv.convert(text, 'zh-cn')
def eng_punc_to_cn(text: str) -> str:
punc_map = {
",": ",", ".": "。", "?": "?", "!": "!",
":": ":", ";": ";", "(": "(", ")": ")",
"[": "【", "]": "】"
}
for en, cn in punc_map.items():
text = text.replace(en, cn)
return text
def replace_content(text: str, old_str: str, new_str: str, use_regex: bool = False, case_sensitive: bool = False) -> str:
if use_regex:
flags = 0 if case_sensitive else re.IGNORECASE
return re.sub(old_str, new_str, text, flags=flags)
else:
if case_sensitive:
return text.replace(old_str, new_str)
else:
pattern = re.compile(re.escape(old_str), re.IGNORECASE)
return pattern.sub(new_str, text)
def fullwidth_to_halfwidth(text: str) -> str:
result = []
for char in text:
code = ord(char)
if code == 0x3000:
result.append(chr(0x0020))
elif 0xFF01 <= code <= 0xFF5E:
result.append(chr(code - 0xFEE0))
else:
result.append(char)
return ''.join(result)
def count_text_stats(text: str) -> tuple:
total_chars = len(text)
pure_text = re.sub(r'\s+', '', text)
pure_chars = len(pure_text)
lines = text.count('\n') + 1 if text else 0
return total_chars, pure_chars, lines
# ===================== 主窗口 =====================
class TextToolMain(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("文本批量整理工具")
self.resize(1250, 750)
self.setFont(QFont())
self.status_bar = QStatusBar()
self.setStatusBar(self.status_bar)
self.find_pos = 0
# 跨系统加载图标(麒麟/Windows通用相对路径)
ico_path = os.path.join(os.getcwd(), "text.ico")
if os.path.exists(ico_path):
self.setWindowIcon(QIcon(ico_path))
# 全局统一所有分组框样式
self.setStyleSheet("""
QGroupBox {
border: 1px solid #888;
border-radius: 4px;
margin-top: 6px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 4px;
}
""")
self.init_ui()
def init_ui(self):
main_container = QWidget()
main_v_layout = QVBoxLayout(main_container)
main_v_layout.setContentsMargins(10, 10, 10, 10)
main_v_layout.setSpacing(10)
# 上方整体布局:左侧功能区 + 右侧标签编辑区
top_widget = QWidget()
top_h_layout = QHBoxLayout(top_widget)
top_h_layout.setSpacing(10)
# 左侧功能面板
left_widget = QWidget()
left_layout = QVBoxLayout(left_widget)
left_layout.setSpacing(10)
# 文件操作组
file_group = QGroupBox("文件操作")
file_layout = QVBoxLayout(file_group)
file_layout.setSpacing(6)
file_layout.setContentsMargins(10,10,10,10)
self.btn_open = QPushButton("打开文件")
self.btn_save = QPushButton("保存文件")
self.btn_save_as = QPushButton("另存为")
self.btn_save_all = QPushButton("全部保存")
file_layout.addWidget(self.btn_open)
file_layout.addWidget(self.btn_save)
file_layout.addWidget(self.btn_save_as)
file_layout.addWidget(self.btn_save_all)
# 文本整理组
func_group = QGroupBox("文本整理")
func_layout = QVBoxLayout(func_group)
func_layout.setSpacing(6)
func_layout.setContentsMargins(10,10,10,10)
self.btn_trim = QPushButton("行首尾去空格")
self.btn_all_space = QPushButton("清除所有空格")
self.btn_empty_line = QPushButton("删除空行")
self.btn_f2j = QPushButton("繁体转简体")
self.btn_punc = QPushButton("标点转中文")
self.btn_full2half = QPushButton("全角转半角")
func_layout.addWidget(self.btn_trim)
func_layout.addWidget(self.btn_all_space)
func_layout.addWidget(self.btn_empty_line)
func_layout.addWidget(self.btn_f2j)
func_layout.addWidget(self.btn_punc)
func_layout.addWidget(self.btn_full2half)
left_layout.addWidget(file_group)
left_layout.addWidget(func_group)
left_layout.addStretch()
# 右侧:标签页
self.tab_widget = QTabWidget()
# 右上角:撤消 / 重做 PyQt5枚举修正
corner_widget = QWidget()
corner_layout = QHBoxLayout(corner_widget)
corner_layout.setContentsMargins(0, 0, 5, 0)
corner_layout.setSpacing(5)
self.btn_undo = QPushButton("撤消")
self.btn_redo = QPushButton("重做")
self.btn_undo.setFixedSize(50, 23)
self.btn_redo.setFixedSize(50, 23)
corner_layout.addStretch()
corner_layout.addWidget(self.btn_undo)
corner_layout.addWidget(self.btn_redo)
# PyQt5 枚举:Qt.TopRightCorner 替代 Qt.Corner.TopRightCorner
self.tab_widget.setCornerWidget(corner_widget, Qt.TopRightCorner)
self.edit_list = []
for i in range(1, 5):
edit = QTextEdit()
edit.textChanged.connect(self.update_word_count)
palette = edit.palette()
palette.setColor(QPalette.Base, QColor(220, 240, 220))
edit.setPalette(palette)
edit.setUndoRedoEnabled(True)
self.tab_widget.addTab(edit, f"文档{i}")
self.edit_list.append(edit)
top_h_layout.addWidget(left_widget)
top_h_layout.addWidget(self.tab_widget, stretch=1)
main_v_layout.addWidget(top_widget, stretch=1)
# ===================== 底部横向双栏容器:查找替换 + 批量插入 =====================
bottom_h_container = QWidget()
bottom_h_layout = QHBoxLayout(bottom_h_container)
bottom_h_layout.setSpacing(12)
# ========== 左侧:查找替换区域 ==========
find_replace_group = QGroupBox("查找替换")
fr_layout = QVBoxLayout(find_replace_group)
fr_layout.setContentsMargins(10,10,10,10)
fr_layout.setSpacing(8)
# 查找行
fr_row1 = QHBoxLayout()
self.btn_find = QPushButton("查找")
self.edit_find = QLineEdit()
self.edit_find.setPlaceholderText("查找内容")
self.chk_regex = QCheckBox("正则表达式")
fr_row1.addWidget(self.btn_find)
fr_row1.addWidget(self.edit_find, stretch=1)
fr_row1.addWidget(self.chk_regex)
# 替换行
fr_row2 = QHBoxLayout()
self.btn_replace = QPushButton("替换")
self.edit_replace = QLineEdit()
self.edit_replace.setPlaceholderText("替换内容")
right_btn_v = QVBoxLayout()
self.chk_case = QCheckBox("区分大小写")
self.btn_replace_all = QPushButton("全部替换")
self.btn_replace_select = QPushButton("选区替换")
right_btn_v.addWidget(self.chk_case)
right_btn_v.addWidget(self.btn_replace_all)
right_btn_v.addWidget(self.btn_replace_select)
fr_row2.addWidget(self.btn_replace)
fr_row2.addWidget(self.edit_replace, stretch=1)
fr_row2.addLayout(right_btn_v)
fr_layout.addLayout(fr_row1)
fr_layout.addLayout(fr_row2)
# ========== 右侧:批量插入文本区域 ==========
insert_group = QGroupBox("批量插入文本")
insert_layout = QVBoxLayout(insert_group)
insert_layout.setContentsMargins(10,10,10,10)
insert_layout.setSpacing(8)
# 插入1行
row1 = QHBoxLayout()
self.btn_insert1 = QPushButton("插入1")
self.line_insert1 = QLineEdit()
self.line_insert1.setPlaceholderText("输入要插入的内容1")
row1.addWidget(self.btn_insert1)
row1.addWidget(self.line_insert1, stretch=1)
# 插入2行
row2 = QHBoxLayout()
self.btn_insert2 = QPushButton("插入2")
self.line_insert2 = QLineEdit()
self.line_insert2.setPlaceholderText("输入要插入的内容2")
row2.addWidget(self.btn_insert2)
row2.addWidget(self.line_insert2, stretch=1)
# 单选选项行
radio_row = QHBoxLayout()
self.radio_group = QButtonGroup()
self.radio_cur_start = QRadioButton("在当前段首")
self.radio_cur_end = QRadioButton("在当前段尾")
self.radio_all_start = QRadioButton("在所有段首")
self.radio_all_end = QRadioButton("在所有段尾")
self.radio_group.addButton(self.radio_cur_start, 1)
self.radio_group.addButton(self.radio_cur_end, 2)
self.radio_group.addButton(self.radio_all_start, 3)
self.radio_group.addButton(self.radio_all_end, 4)
self.radio_cur_start.setChecked(True)
radio_row.addWidget(self.radio_cur_start)
radio_row.addWidget(self.radio_cur_end)
radio_row.addWidget(self.radio_all_start)
radio_row.addWidget(self.radio_all_end)
radio_row.addStretch()
insert_layout.addLayout(row1)
insert_layout.addLayout(row2)
insert_layout.addLayout(radio_row)
# 将两个分组加入横向布局,左右平分宽度
bottom_h_layout.addWidget(find_replace_group, stretch=1)
bottom_h_layout.addWidget(insert_group, stretch=1)
main_v_layout.addWidget(bottom_h_container)
self.setCentralWidget(main_container)
self.bind_event()
self.update_word_count()
def bind_event(self):
# 原有按钮绑定
self.btn_trim.clicked.connect(self.do_trim_line)
self.btn_all_space.clicked.connect(self.do_clear_all_space)
self.btn_empty_line.clicked.connect(self.do_clear_empty_line)
self.btn_f2j.clicked.connect(self.do_f2j)
self.btn_punc.clicked.connect(self.do_convert_punc)
self.btn_full2half.clicked.connect(self.do_full2half)
self.btn_undo.clicked.connect(self.do_undo)
self.btn_redo.clicked.connect(self.do_redo)
self.btn_open.clicked.connect(self.open_file)
self.btn_save.clicked.connect(self.save_file)
self.btn_save_as.clicked.connect(self.save_file_as)
self.btn_save_all.clicked.connect(self.save_all_files)
self.btn_find.clicked.connect(self.do_find)
self.btn_replace.clicked.connect(self.do_single_replace)
self.btn_replace_all.clicked.connect(self.do_all_replace)
self.btn_replace_select.clicked.connect(self.do_select_replace)
self.tab_widget.currentChanged.connect(self.reset_find_pos)
self.tab_widget.currentChanged.connect(self.update_word_count)
# 新增插入按钮绑定
self.btn_insert1.clicked.connect(lambda: self.do_insert(self.line_insert1.text()))
self.btn_insert2.clicked.connect(lambda: self.do_insert(self.line_insert2.text()))
def get_current_edit(self) -> QTextEdit:
return self.edit_list[self.tab_widget.currentIndex()]
def get_current_text(self) -> str:
return self.get_current_edit().toPlainText()
# ===================== 核心修复:安全替换文本,保留撤销历史 =====================
def safe_set_text(self, new_text: str):
"""替代 setPlainText,操作会存入撤销栈,支持Ctrl+Z回退"""
edit = self.get_current_edit()
cursor = edit.textCursor()
# PyQt5 枚举修正:Document 直接用,移除SelectionType层级
cursor.select(QTextCursor.Document)
# 删除原有内容,插入新文本(整套操作作为1步撤销记录)
cursor.insertText(new_text)
# 重置光标到文档开头
cursor.movePosition(QTextCursor.Start)
edit.setTextCursor(cursor)
# 原废弃函数,改为调用safe_set_text
def set_current_text(self, text: str):
self.safe_set_text(text)
def reset_find_pos(self):
self.find_pos = 0
def update_word_count(self):
text = self.get_current_text()
total, pure, lines = count_text_stats(text)
self.status_bar.showMessage(f"总行数:{lines} | 总字符数:{total} | 纯文字数:{pure}")
def do_undo(self):
edit = self.get_current_edit()
if edit.document().isUndoAvailable():
edit.undo()
self.status_bar.showMessage("撤消成功")
self.update_word_count()
else:
self.status_bar.showMessage("无可撤消操作")
def do_redo(self):
edit = self.get_current_edit()
if edit.document().isRedoAvailable():
edit.redo()
self.status_bar.showMessage("重做成功")
self.update_word_count()
else:
self.status_bar.showMessage("无可重做操作")
def do_find(self):
keyword = self.edit_find.text().strip()
if not keyword:
return
edit = self.get_current_edit()
content = edit.toPlainText()
use_re = self.chk_regex.isChecked()
case_sensitive = self.chk_case.isChecked()
cursor = edit.textCursor()
cursor.movePosition(QTextCursor.Start)
if use_re:
try:
flags = 0 if case_sensitive else re.IGNORECASE
pattern = re.compile(keyword, flags)
match = pattern.search(content, self.find_pos)
if match:
start = match.start()
end = match.end()
cursor.setPosition(start)
cursor.setPosition(end, QTextCursor.KeepAnchor)
edit.setTextCursor(cursor)
self.find_pos = end
else:
self.find_pos = 0
self.status_bar.showMessage("已查找至文档末尾")
except re.error:
self.status_bar.showMessage("正则表达式格式错误")
self.find_pos = 0
else:
if case_sensitive:
idx = content.find(keyword, self.find_pos)
else:
idx = content.lower().find(keyword.lower(), self.find_pos)
if idx != -1:
cursor.setPosition(idx)
cursor.setPosition(idx + len(keyword), QTextCursor.KeepAnchor)
edit.setTextCursor(cursor)
self.find_pos = idx + len(keyword)
else:
self.find_pos = 0
self.status_bar.showMessage("已查找至文档末尾")
def do_single_replace(self):
old = self.edit_find.text()
new = self.edit_replace.text()
if not old:
return
edit = self.get_current_edit()
if edit.hasSelectedText():
edit.insertPlainText(new)
self.do_find()
def do_all_replace(self):
old = self.edit_find.text()
new = self.edit_replace.text()
use_re = self.chk_regex.isChecked()
case_sensitive = self.chk_case.isChecked()
if not old:
return
text = replace_content(self.get_current_text(), old, new, use_re, case_sensitive)
self.safe_set_text(text)
self.find_pos = 0
self.status_bar.showMessage("全部替换完成")
def do_select_replace(self):
old = self.edit_find.text()
new = self.edit_replace.text()
use_re = self.chk_regex.isChecked()
case_sensitive = self.chk_case.isChecked()
edit = self.get_current_edit()
selected_text = edit.selectedText()
if not old or not selected_text:
self.status_bar.showMessage("请先选中内容并填写查找关键词")
return
res = replace_content(selected_text, old, new, use_re, case_sensitive)
edit.insertPlainText(res)
self.status_bar.showMessage("选区替换完成")
def do_trim_line(self):
text = trim_line_space(self.get_current_text())
self.safe_set_text(text)
self.status_bar.showMessage("行首尾去空格完成")
def do_clear_all_space(self):
text = remove_all_spaces(self.get_current_text())
self.safe_set_text(text)
self.status_bar.showMessage("清除所有空格完成")
def do_clear_empty_line(self):
text = remove_empty_lines(self.get_current_text())
self.safe_set_text(text)
self.status_bar.showMessage("删除空行完成")
def do_f2j(self):
text = traditional_to_simple(self.get_current_text())
self.safe_set_text(text)
self.status_bar.showMessage("繁体转简体完成")
def do_convert_punc(self):
text = eng_punc_to_cn(self.get_current_text())
self.safe_set_text(text)
self.status_bar.showMessage("标点转中文完成")
def do_full2half(self):
text = fullwidth_to_halfwidth(self.get_current_text())
self.safe_set_text(text)
self.status_bar.showMessage("全角转半角完成")
# ===================== 插入文本核心逻辑 =====================
def do_insert(self, insert_str: str):
if not insert_str:
self.status_bar.showMessage("输入框内容为空,无需插入")
return
edit = self.get_current_edit()
full_text = edit.toPlainText()
lines = full_text.splitlines(keepends=True)
select_id = self.radio_group.checkedId()
# 1. 在当前段首
if select_id == 1:
cursor = edit.textCursor()
block_num = cursor.blockNumber()
if 0 <= block_num < len(lines):
lines[block_num] = insert_str + lines[block_num]
# 2. 在当前段尾
elif select_id == 2:
cursor = edit.textCursor()
block_num = cursor.blockNumber()
if 0 <= block_num < len(lines):
lines[block_num] = lines[block_num].rstrip('\n') + insert_str + '\n'
# 3. 在所有段首
elif select_id == 3:
new_lines = [insert_str + line for line in lines]
lines = new_lines
# 4. 在所有段尾
elif select_id == 4:
new_lines = [line.rstrip('\n') + insert_str + '\n' for line in lines]
lines = new_lines
# 写回编辑器(使用安全方法保留撤销)
new_text = ''.join(lines)
self.safe_set_text(new_text)
self.status_bar.showMessage(f"插入内容「{insert_str}」执行完成")
def open_file(self):
path, _ = QFileDialog.getOpenFileName(self, "打开文本文件", "", "Text Files (*.txt);;All Files (*)")
if not path:
return
try:
with open(path, "r", encoding="utf-8") as f:
content = f.read()
# 打开文件允许清空撤销栈,属于全新文档加载
self.get_current_edit().setPlainText(content)
self.tab_widget.setTabText(self.tab_widget.currentIndex(), os.path.basename(path))
self.reset_find_pos()
self.update_word_count()
except Exception as e:
self.status_bar.showMessage(f"打开失败:{str(e)}")
def save_file(self):
idx = self.tab_widget.currentIndex()
tab_text = self.tab_widget.tabText(idx)
if tab_text in ("文档1", "文档2", "文档3", "文档4"):
self.save_file_as()
return
path = tab_text if os.path.isabs(tab_text) else os.path.join(os.getcwd(), tab_text)
try:
with open(path, "w", encoding="utf-8") as f:
f.write(self.get_current_text())
self.status_bar.showMessage("保存成功")
except Exception as e:
self.status_bar.showMessage(f"保存失败:{str(e)}")
def save_file_as(self):
path, _ = QFileDialog.getSaveFileName(self, "另存为", "", "Text Files (*.txt);;All Files (*)")
if not path:
return
try:
with open(path, "w", encoding="utf-8") as f:
f.write(self.get_current_text())
self.tab_widget.setTabText(self.tab_widget.currentIndex(), os.path.basename(path))
self.status_bar.showMessage("另存为成功")
except Exception as e:
self.status_bar.showMessage(f"另存为失败:{str(e)}")
def save_all_files(self):
for i in range(self.tab_widget.count()):
self.tab_widget.setCurrentIndex(i)
self.save_file()
self.status_bar.showMessage("全部文档保存完成")
if __name__ == "__main__":
app = QApplication(sys.argv)
# 跨系统任务栏图标
ico_path = os.path.join(os.getcwd(), "text.ico")
if os.path.exists(ico_path):
app.setWindowIcon(QIcon(ico_path))
window = TextToolMain()
window.show()
sys.exit(app.exec_())