import
tkinter as tk
from
tkinter
import
filedialog, scrolledtext, messagebox, ttk
import
re
import
os
import
pathlib
import
datetime
class
NovelEditorApp:
def
__init__(
self
, master):
self
.master
=
master
master.title(
"小说文本章节&内容修改器"
)
master.geometry(
"750x650"
)
self
.original_file_path
=
None
self
.current_file_path
=
None
self
.modification_count
=
0
top_frame
=
ttk.Frame(master, padding
=
"10"
)
top_frame.pack(fill
=
tk.X)
ttk.Button(top_frame, text
=
"选择小说 TXT 文件"
, command
=
self
.select_file).pack(side
=
tk.LEFT, padx
=
5
)
self
.file_label
=
ttk.Label(top_frame, text
=
"尚未选择文件"
, width
=
70
, relief
=
"sunken"
, anchor
=
"w"
)
self
.file_label.pack(side
=
tk.LEFT, fill
=
tk.X, expand
=
True
, padx
=
5
)
main_frame
=
ttk.Frame(master, padding
=
"10"
)
main_frame.pack(fill
=
tk.BOTH, expand
=
True
)
left_frame
=
ttk.Frame(main_frame, padding
=
"10"
, relief
=
"groove"
, borderwidth
=
2
)
left_frame.pack(side
=
tk.LEFT, fill
=
tk.Y, padx
=
(
0
,
10
))
chapter_frame
=
ttk.LabelFrame(left_frame, text
=
"章节标题统一化"
, padding
=
"10"
)
chapter_frame.pack(fill
=
tk.X, pady
=
10
)
ttk.Label(chapter_frame, text
=
"识别模式 (正则表达式):"
).pack(anchor
=
"w"
)
self
.chapter_find_pattern
=
tk.StringVar(value
=
r
"^\s*(?:第?\s*(\d+)\s*章?\.?)\s*(.*)"
)
ttk.Entry(chapter_frame, textvariable
=
self
.chapter_find_pattern, width
=
35
).pack(fill
=
tk.X, pady
=
(
0
,
5
))
ttk.Label(chapter_frame, text
=
"说明: 使用正则表达式匹配章节标题行。\n必须包含一个捕获组 `(\d+)` 来提取章节数字。\n `(.*)` 可捕获标题数字后的内容(可选)。"
, justify
=
tk.LEFT, foreground
=
"gray"
).pack(anchor
=
"w"
)
ttk.Label(chapter_frame, text
=
"替换格式:"
).pack(anchor
=
"w"
, pady
=
(
10
,
0
))
self
.chapter_replace_format
=
tk.StringVar(value
=
"第{num}章 {title}"
)
ttk.Entry(chapter_frame, textvariable
=
self
.chapter_replace_format, width
=
35
).pack(fill
=
tk.X, pady
=
(
0
,
5
))
ttk.Label(chapter_frame, text
=
"说明: 使用 `{num}` 代表捕获的章节数字。\n使用 `{title}` 代表捕获的章节数字后的内容。\n例如: `第{num}章` 或 `第{num}章 {title}`"
, justify
=
tk.LEFT, foreground
=
"gray"
).pack(anchor
=
"w"
)
ttk.Button(chapter_frame, text
=
"执行章节标题修改"
, command
=
self
.modify_chapters).pack(pady
=
10
)
remove_frame
=
ttk.LabelFrame(left_frame, text
=
"批量内容删除"
, padding
=
"10"
)
remove_frame.pack(fill
=
tk.X, pady
=
10
)
ttk.Label(remove_frame, text
=
"要删除的内容 (可以是文字、数字、符号或正则表达式):"
).pack(anchor
=
"w"
)
self
.remove_pattern
=
tk.StringVar(value
=
"")
ttk.Entry(remove_frame, textvariable
=
self
.remove_pattern, width
=
35
).pack(fill
=
tk.X, pady
=
(
0
,
5
))
ttk.Label(remove_frame, text
=
"说明: 输入你想删除的精确文本或一个正则表达式。\n例如,删除所有数字 54646: 输入 `54646`\n删除所有连续的数字: 输入 `\d+`\n删除所有空白行: 输入 `^\s*$`"
, justify
=
tk.LEFT, foreground
=
"gray"
).pack(anchor
=
"w"
)
ttk.Button(remove_frame, text
=
"执行内容删除"
, command
=
self
.remove_content).pack(pady
=
10
)
right_frame
=
ttk.Frame(main_frame, padding
=
"10"
)
right_frame.pack(side
=
tk.LEFT, fill
=
tk.BOTH, expand
=
True
)
ttk.Label(right_frame, text
=
"操作说明和日志:"
, font
=
(
"Arial"
,
12
,
"bold"
)).pack(anchor
=
"w"
)
self
.log_text
=
scrolledtext.ScrolledText(right_frame, wrap
=
tk.WORD, height
=
25
, width
=
50
)
self
.log_text.pack(fill
=
tk.BOTH, expand
=
True
, pady
=
5
)
self
.log_text.insert(tk.END,
self
.get_instructions())
self
.log_text.config(state
=
tk.DISABLED)
def
get_instructions(
self
):
return
def
_log(
self
, message):
now
=
datetime.datetime.now().strftime(
"%H:%M:%S"
)
self
.log_text.config(state
=
tk.NORMAL)
self
.log_text.insert(tk.END, f
"[{now}] {message}\n"
)
self
.log_text.see(tk.END)
self
.log_text.config(state
=
tk.DISABLED)
def
select_file(
self
):
filepath
=
filedialog.askopenfilename(
title
=
"请选择一个 TXT 文件"
,
filetypes
=
((
"Text files"
,
"*.txt"
), (
"All files"
,
"*.*"
))
)
if
filepath:
self
.original_file_path
=
pathlib.Path(filepath)
self
.current_file_path
=
self
.original_file_path
self
.modification_count
=
0
self
.file_label.config(text
=
str
(
self
.original_file_path))
self
._log(f
"已选择文件: {self.original_file_path}"
)
self
._log(
"现在可以执行修改操作了。后续操作将基于此文件或其修改版本。"
)
else
:
self
._log(
"未选择文件。"
)
def
_generate_output_path(
self
):
if
not
self
.original_file_path:
return
None
self
.modification_count
+
=
1
base_name
=
self
.original_file_path.stem
dir_name
=
self
.original_file_path.parent
new_filename
=
f
"{base_name}_modified_{self.modification_count}.txt"
return
dir_name
/
new_filename
def
modify_chapters(
self
):
if
not
self
.current_file_path
or
not
self
.current_file_path.exists():
messagebox.showerror(
"错误"
,
"请先选择一个有效的文件!"
)
self
._log(
"错误:未选择文件或文件不存在。"
)
return
find_pattern_str
=
self
.chapter_find_pattern.get()
replace_format_str
=
self
.chapter_replace_format.get()
if
not
find_pattern_str
or
not
replace_format_str:
messagebox.showerror(
"错误"
,
"请提供章节识别模式和替换格式!"
)
self
._log(
"错误:章节识别模式或替换格式为空。"
)
return
try
:
find_regex
=
re.
compile
(find_pattern_str)
if
find_regex.groups <
1
:
messagebox.showerror(
"错误"
,
"识别模式的正则表达式必须至少包含一个捕获组 `(\d+)` 来提取章节数字!"
)
self
._log(
"错误:识别模式缺少捕获组 `(\d+)`。"
)
return
except
re.error as e:
messagebox.showerror(
"正则表达式错误"
, f
"章节识别模式无效: {e}"
)
self
._log(f
"错误:章节识别模式正则表达式无效 - {e}"
)
return
output_path
=
self
._generate_output_path()
if
not
output_path:
return
self
._log(f
"开始处理章节标题..."
)
self
._log(f
"读取文件: {self.current_file_path}"
)
self
._log(f
"识别模式: {find_pattern_str}"
)
self
._log(f
"替换格式: {replace_format_str}"
)
self
._log(f
"写入文件: {output_path}"
)
lines_processed
=
0
chapters_found
=
0
try
:
with
open
(
self
.current_file_path,
'r'
, encoding
=
'utf-8'
, errors
=
'ignore'
) as infile, \
open
(output_path,
'w'
, encoding
=
'utf-8'
) as outfile:
for
line
in
infile:
lines_processed
+
=
1
match
=
find_regex.match(line)
if
match:
chapters_found
+
=
1
try
:
num_str
=
match.group(
1
)
num
=
int
(num_str)
title
=
""
if
find_regex.groups >
1
:
title
=
match.group(
2
).strip()
if
match.group(
2
)
else
""
format_args
=
{
'num'
: num,
'title'
: title}
new_title
=
replace_format_str.
format
(
*
*
format_args)
outfile.write(new_title
+
'\n'
)
except
ValueError:
self
._log(f
"警告: 在行 {lines_processed} 找到匹配,但捕获组 '{num_str}' 不是有效数字,该行已跳过修改。"
)
outfile.write(line)
except
IndexError:
self
._log(f
"警告: 正则表达式配置可能不正确,无法在行 {lines_processed} 提取所需内容,该行已跳过修改。"
)
outfile.write(line)
except
KeyError as ke:
messagebox.showerror(
"格式化错误"
, f
"替换格式 '{replace_format_str}' 中的占位符 {ke} 无法被匹配!\n请确保识别模式能捕获对应内容,或调整替换格式。"
)
self
._log(f
"错误:替换格式中的占位符 {ke} 无法匹配。"
)
outfile.close()
os.remove(output_path)
self
.modification_count
-
=
1
return
else
:
outfile.write(line)
self
._log(f
"处理完成!共处理 {lines_processed} 行,找到并修改了 {chapters_found} 个章节标题。"
)
self
._log(f
"结果已保存到: {output_path}"
)
self
.current_file_path
=
output_path
except
FileNotFoundError:
messagebox.showerror(
"错误"
, f
"文件未找到: {self.current_file_path}"
)
self
._log(f
"错误:文件 {self.current_file_path} 未找到。"
)
self
.modification_count
-
=
1
except
Exception as e:
messagebox.showerror(
"处理失败"
, f
"处理文件时发生错误: {e}"
)
self
._log(f
"错误:处理文件时发生异常 - {e}"
)
if
output_path
and
output_path.exists():
try
:
os.remove(output_path)
self
._log(f
"已删除不完整的输出文件: {output_path}"
)
except
OSError:
self
._log(f
"警告:无法删除不完整的输出文件: {output_path}"
)
self
.modification_count
-
=
1
def
remove_content(
self
):
if
not
self
.current_file_path
or
not
self
.current_file_path.exists():
messagebox.showerror(
"错误"
,
"请先选择一个有效的文件!"
)
self
._log(
"错误:未选择文件或文件不存在。"
)
return
pattern_to_remove
=
self
.remove_pattern.get()
if
not
pattern_to_remove:
messagebox.showwarning(
"提示"
,
"请输入要删除的内容或模式。"
)
self
._log(
"提示:删除内容为空,操作未执行。"
)
return
output_path
=
self
._generate_output_path()
if
not
output_path:
return
self
._log(f
"开始删除内容..."
)
self
._log(f
"读取文件: {self.current_file_path}"
)
self
._log(f
"删除模式/文本: {pattern_to_remove}"
)
self
._log(f
"写入文件: {output_path}"
)
lines_processed
=
0
removals_made
=
0
try
:
try
:
remove_regex
=
re.
compile
(pattern_to_remove)
is_regex
=
True
self
._log(
"删除模式被解释为正则表达式。"
)
except
re.error:
is_regex
=
False
self
._log(
"删除模式被解释为普通文本。"
)
with
open
(
self
.current_file_path,
'r'
, encoding
=
'utf-8'
, errors
=
'ignore'
) as infile, \
open
(output_path,
'w'
, encoding
=
'utf-8'
) as outfile:
for
line
in
infile:
lines_processed
+
=
1
original_line
=
line
if
is_regex:
modified_line, count
=
remove_regex.subn("", line)
removals_made
+
=
count
else
:
modified_line
=
line.replace(pattern_to_remove, "")
if
modified_line !
=
original_line:
removals_made
+
=
original_line.count(pattern_to_remove)
outfile.write(modified_line)
self
._log(f
"处理完成!共处理 {lines_processed} 行。"
)
if
removals_made >
0
:
self
._log(f
"大约进行了 {removals_made} 次内容删除。"
)
else
:
self
._log(
"未找到或删除任何匹配的内容。"
)
self
._log(f
"结果已保存到: {output_path}"
)
self
.current_file_path
=
output_path
except
FileNotFoundError:
messagebox.showerror(
"错误"
, f
"文件未找到: {self.current_file_path}"
)
self
._log(f
"错误:文件 {self.current_file_path} 未找到。"
)
self
.modification_count
-
=
1
except
Exception as e:
messagebox.showerror(
"处理失败"
, f
"处理文件时发生错误: {e}"
)
self
._log(f
"错误:处理文件时发生异常 - {e}"
)
if
output_path
and
output_path.exists():
try
:
os.remove(output_path)
self
._log(f
"已删除不完整的输出文件: {output_path}"
)
except
OSError:
self
._log(f
"警告:无法删除不完整的输出文件: {output_path}"
)
self
.modification_count
-
=
1
if
__name__
=
=
"__main__"
:
root
=
tk.Tk()
app
=
NovelEditorApp(root)
root.mainloop()