吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 3011|回复: 44
上一主题 下一主题
收起左侧

[Windows] epub转txt转docx转PDF格式工具

[复制链接]
跳转到指定楼层
楼主
zwjtr93 发表于 2025-4-2 15:23 回帖奖励
本帖最后由 zwjtr93 于 2025-4-2 22:42 编辑

epub转txt转docx转PDF格式工具,功能如图:


请注意转换PDF时间较长,并且需要系统安装 Microsoft Word (Windows) 或 LibreOffice,转换为PDF的逻辑是先转换为DOCX格式在根据DOCX格式转换为PDF,原EPUB上的格式不一定能完整转换请自行分辨。
如果实在无法运行可以建议转换为DOCX用自己系统的WORD或WPS等转换为PDF即可。
可以批量选择epub文件,选择文件夹则默认为将文件夹内的所有epub格式文件都转换。


下载地址:
链接: https://pan.baidu.com/s/18AtRIe1D2jtfcHBXnSx0cA?pwd=7qzq 提取码: 7qzq
没办法文件超过100M没法免费传蓝奏只能传度云了。
下楼分享python代码

免费评分

参与人数 12吾爱币 +10 热心值 +12 收起 理由
welkin2000 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
DanHuang1016 + 1 谢谢@Thanks!
aria1983 + 1 + 1 用心讨论,共获提升!
huiker231 + 1 + 1 感谢分享,还分享源码
fengwang + 1 + 1 热心回复!
wht1301 + 1 + 1 谢谢@Thanks!
jiangchun + 1 + 1 谢谢@Thanks!
锋霜 + 1 + 1 我很赞同!
czliwx + 1 + 1 谢谢@Thanks!
wangmin + 1 + 1 用心讨论,共获提升!
helh0275 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
hscxdesign + 1 谢谢@Thanks!

查看全部评分

本帖被以下淘专辑推荐:

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

来自 #
 楼主| zwjtr93 发表于 2025-4-2 20:04 |楼主
本帖最后由 zwjtr93 于 2025-4-2 22:14 编辑

这是源码(能转txt/pdf/docx)版本:
[Python] 纯文本查看 复制代码
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
# Optimized Code
# --- Optimized Python Code Snippet (EPUB -> TXT/DOCX/PDF + Fixes) ---
import tkinter as tk
from tkinter import filedialog, messagebox, scrolledtext, ttk
import os
import threading
import ebooklib
from ebooklib import epub
from bs4 import BeautifulSoup
import html
import sys
import traceback
import tempfile
import io                     # For redirecting stdout/stderr
from contextlib import redirect_stdout, redirect_stderr # For redirecting
 
# Import necessary libraries for format conversion
from docx import Document as DocxDocument
try:
    import docx2pdf
except ImportError:
    # Make sure messagebox can show even if Tk root isn't fully ready
    try:
        root_check = tk.Tk()
        root_check.withdraw() # Hide the dummy root window
        messagebox.showerror("依赖错误", "缺少 'docx2pdf' 库。\n请运行 'pip install docx2pdf' 进行安装。\n\n注意: 此库还需要系统安装 Microsoft Word (Windows) 或 LibreOffice (Linux/macOS) 才能将 DOCX 转换为 PDF。")
        root_check.destroy()
    except tk.TclError: # Fallback if even dummy root fails
         print("ERROR: Missing 'docx2pdf' library. Please run 'pip install docx2pdf'.", file=sys.stderr)
    sys.exit(1)
 
# Import TkinterDnD2
from tkinterdnd2 import DND_FILES, TkinterDnD
 
# --- Constants (No changes needed) ---
SUPPORTED_INPUT_FORMATS = [("EPUB 文件", "*.epub"), ("所有文件", "*.*")]
SUPPORTED_OUTPUT_FORMATS = ["TXT", "DOCX", "PDF"]
 
# --- Helper Functions (No changes needed) ---
def resource_path(relative_path):
    try: base_path = sys._MEIPASS
    except Exception: base_path = os.path.abspath(".")
    return os.path.join(base_path, relative_path)
 
def extract_text_from_html(html_content):
    try:
        soup = BeautifulSoup(html_content, 'lxml')
        for script_or_style in soup(["script", "style"]): script_or_style.decompose()
        text = soup.get_text(separator=' ', strip=True)
        text = html.unescape(text)
        return text
    except Exception as e: print(f"Error extracting text: {e}"); return ""
 
def get_epub_items_in_order(book):
    items_in_order = []
    try:
        all_html_items = list(book.get_items_of_type(ebooklib.ITEM_DOCUMENT))
        if not all_html_items: all_html_items = list(book.get_items_of_type(ebooklib.ITEM_XHTML))
        if not all_html_items: print("Warning: No processable content found."); return []
        items_map = {item.id: item for item in all_html_items}
        spine_ids = [item_id for item_id, _ in book.spine]
        ordered_items = []; seen_ids = set()
        for item_id in spine_ids:
            if item_id in items_map: item = items_map[item_id]; ordered_items.append(item); seen_ids.add(item_id)
        other_items = [item for item_id, item in items_map.items() if item_id not in seen_ids]
        items_in_order = ordered_items + other_items
    except Exception as e:
        print(f"Warning: Cannot determine EPUB order: {e}. Falling back...");
        items_in_order = list(book.get_items_of_type(ebooklib.ITEM_DOCUMENT))
        if not items_in_order: items_in_order = list(book.get_items_of_type(ebooklib.ITEM_XHTML))
    return items_in_order
 
# --- Individual Conversion Functions ---
 
# EPUB -> TXT (No changes needed)
def convert_epub_to_txt_internal(epub_path, txt_path, status_callback):
    try:
        # ... (same code as before) ...
        status_callback(f"  正在读取 EPUB: {os.path.basename(epub_path)}...")
        book = epub.read_epub(epub_path)
        full_text = []
        items_to_process = get_epub_items_in_order(book)
        if not items_to_process: status_callback("警告: EPUB 中无文档项目。生成空 TXT。"); open(txt_path, 'w', encoding='utf-8').close(); return
        status_callback(f"  找到 {len(items_to_process)} 个 EPUB 内容项,正在提取文本...")
        for i, item in enumerate(items_to_process):
            content = item.get_body_content();
            if not content: continue
            try:
                decoded_content = ""
                try: decoded_content = content.decode('utf-8')
                except UnicodeDecodeError:
                    status_callback(f"    警告: {item.get_name()} UTF-8 解码失败,尝试忽略错误...");
                    try: decoded_content = content.decode('utf-8', errors='ignore')
                    except Exception as decode_err: status_callback(f"    严重警告: {item.get_name()} 解码彻底失败: {decode_err}"); continue
                text = extract_text_from_html(decoded_content)
                if text: full_text.append(text); full_text.append("\n\n")
            except Exception as e: status_callback(f"警告: 处理项目 {item.get_name()} 时出错: {e}"); continue
        status_callback("  正在写入 TXT 文件...");
        with open(txt_path, 'w', encoding='utf-8') as f: f.write("".join(full_text))
        status_callback("  TXT 文件写入完成。")
    except epub.EpubException as e: raise epub.EpubException(f"读取 EPUB 失败: {e}")
    except FileNotFoundError: raise FileNotFoundError(f"输入 EPUB 未找到: {epub_path}")
    except Exception as e: raise Exception(f"转换 EPUB->TXT 时未知错误: {e}\n{traceback.format_exc()}")
 
 
# EPUB -> DOCX (No changes needed)
def convert_epub_to_docx_internal(epub_path, docx_path, status_callback):
    try:
        # ... (same code as before) ...
        status_callback(f"  正在读取 EPUB: {os.path.basename(epub_path)}...")
        book = epub.read_epub(epub_path); doc = DocxDocument()
        items_to_process = get_epub_items_in_order(book)
        if not items_to_process: status_callback("警告: EPUB 中无文档项目。生成空 DOCX。"); doc.save(docx_path); return
        status_callback(f"  找到 {len(items_to_process)} 个 EPUB 内容项,正在提取内容到 DOCX...")
        for i, item in enumerate(items_to_process):
            content = item.get_body_content()
            if not content: continue
            try:
                decoded_content = ""
                try: decoded_content = content.decode('utf-8')
                except UnicodeDecodeError:
                    status_callback(f"    警告: {item.get_name()} UTF-8 解码失败,尝试忽略错误...");
                    try: decoded_content = content.decode('utf-8', errors='ignore')
                    except Exception as decode_err: status_callback(f"    严重警告: {item.get_name()} 解码彻底失败: {decode_err}"); continue
                text = extract_text_from_html(decoded_content)
                if text:
                    paragraphs = text.split('\n')
                    for para_text in paragraphs:
                        if para_text.strip(): doc.add_paragraph(para_text.strip())
                    doc.add_paragraph("") # Separator
            except Exception as e: status_callback(f"警告: 处理项目 {item.get_name()} 时出错: {e}"); continue
        status_callback("  正在保存 DOCX 文件..."); doc.save(docx_path); status_callback("  DOCX 文件保存完成。")
    except epub.EpubException as e: raise epub.EpubException(f"读取 EPUB 失败: {e}")
    except FileNotFoundError: raise FileNotFoundError(f"输入 EPUB 未找到: {epub_path}")
    except Exception as e: raise Exception(f"转换 EPUB->DOCX 时未知错误: {e}\n{traceback.format_exc()}")
 
 
# DOCX -> PDF (Modified to redirect stdout/stderr)
def convert_docx_to_pdf_internal(docx_path, pdf_path, status_callback):
    """DOCX -> PDF (using docx2pdf library), redirects stdout/stderr."""
    try:
        status_callback(f"  正在将 DOCX 转换为 PDF: {os.path.basename(docx_path)} -> {os.path.basename(pdf_path)}...")
 
        # --- Redirect stdout/stderr during conversion ---
        # This prevents 'NoneType' error when run packaged without console
        with io.StringIO() as buf_out, io.StringIO() as buf_err:
            with redirect_stdout(buf_out), redirect_stderr(buf_err):
                # Actual conversion happens here
                docx2pdf.convert(docx_path, pdf_path)
 
            # Optional: Log suppressed output if needed (useful for debugging docx2pdf issues)
            suppressed_stdout = buf_out.getvalue().strip()
            suppressed_stderr = buf_err.getvalue().strip()
            if suppressed_stdout: status_callback(f"    (docx2pdf stdout: {suppressed_stdout})")
            if suppressed_stderr: status_callback(f"    (docx2pdf stderr: {suppressed_stderr})") # Log errors from docx2pdf even if suppressed
        # --- End Redirection ---
 
        status_callback("  PDF 文件转换完成。")
        return True # Indicate success
    except FileNotFoundError:
        status_callback(f"错误: 输入 DOCX 文件未找到: {docx_path}")
        raise FileNotFoundError(f"输入 DOCX 文件未找到: {docx_path}")
    except Exception as e:
        # Provide more context if possible
        error_msg = f"错误: 使用 docx2pdf 将 DOCX 转换为 PDF 时失败: {e}"
        # Include suppressed stderr if it contains relevant info
        if 'suppressed_stderr' in locals() and suppressed_stderr:
            error_msg += f"\n    (底层错误信息可能为: {suppressed_stderr})"
        status_callback(error_msg)
        status_callback("  请确保已安装 Microsoft Word (Windows) 或 LibreOffice (Linux/macOS),并且它们可以正常工作。")
        raise RuntimeError(f"DOCX 到 PDF 转换失败: {e}")
 
 
# Master Conversion Dispatcher (No significant changes needed)
def convert_file(input_path, output_folder, output_format, status_callback):
    intermediate_docx_path = None
    try:
        input_filename = os.path.basename(input_path); base_name = os.path.splitext(input_filename)[0]
        input_ext = os.path.splitext(input_filename)[1].lower(); output_ext = f".{output_format.lower()}"
        if input_ext != ".epub": status_callback(f"跳过: 只支持 EPUB 输入 - {input_filename}"); return False
        if not os.path.isdir(output_folder):
             try: os.makedirs(output_folder, exist_ok=True); status_callback(f"  创建了输出文件夹: {output_folder}")
             except Exception as e: status_callback(f"错误: 输出文件夹无效且无法创建: {output_folder} ({e})"); return False
        final_output_path = os.path.join(output_folder, f"{base_name}{output_ext}")
        status_callback(f"开始转换: {input_filename} -> {output_format}...")
        if output_format == "PDF":
            status_callback("  执行两步转换 (EPUB -> DOCX -> PDF)")
            intermediate_docx_filename = f"{base_name}_intermediate_for_pdf.docx"
            intermediate_docx_path = os.path.join(output_folder, intermediate_docx_filename)
            # Step 1: EPUB -> DOCX
            status_callback(f"  步骤 1: 正在转换 EPUB 到临时 DOCX ({intermediate_docx_filename})...")
            try: convert_epub_to_docx_internal(input_path, intermediate_docx_path, status_callback); status_callback("  步骤 1: 临时 DOCX 文件创建成功。")
            except Exception as e: status_callback(f"错误: 步骤 1 (EPUB -> DOCX) 失败: {e}"); return False
            # Step 2: DOCX -> PDF
            status_callback(f"  步骤 2: 正在转换临时 DOCX 到 PDF...")
            try: convert_docx_to_pdf_internal(intermediate_docx_path, final_output_path, status_callback); status_callback("  步骤 2: PDF 文件创建成功。"); status_callback(f"成功完成 (PDF): {os.path.basename(final_output_path)}"); return True
            except Exception as e: status_callback(f"错误: 步骤 2 (DOCX -> PDF) 失败: {e}"); return False # Cleanup in finally
        elif output_format == "TXT": convert_epub_to_txt_internal(input_path, final_output_path, status_callback); status_callback(f"成功完成 (TXT): {os.path.basename(final_output_path)}"); return True
        elif output_format == "DOCX": convert_epub_to_docx_internal(input_path, final_output_path, status_callback); status_callback(f"成功完成 (DOCX): {os.path.basename(final_output_path)}"); return True
        else: status_callback(f"跳过: 不支持的目标输出格式 '{output_format}'"); return False
    except Exception as e:
        status_callback(f"严重错误: 转换 {os.path.basename(input_path)} -> {output_format} 时发生意外失败 - {str(e)}")
        print(f"--- Conversion Error Traceback ({os.path.basename(input_path)}) ---"); traceback.print_exc(); print("--- End Traceback ---")
        return False
    finally: # Cleanup Intermediate File
        if intermediate_docx_path and os.path.exists(intermediate_docx_path):
            try: status_callback(f"  清理: 正在删除临时 DOCX 文件 ({os.path.basename(intermediate_docx_path)})..."); os.remove(intermediate_docx_path); status_callback("  清理: 临时文件删除成功。")
            except Exception as e: status_callback(f"警告: 无法删除临时 DOCX 文件 {intermediate_docx_path}: {e}")
 
 
# --- GUI Application Class (Added graceful shutdown) ---
class FileConverterApp:
    def __init__(self, root):
        self.root = root
        self.root.title("EPUB 转换器 (转 TXT/DOCX/PDF)")
        self.root.geometry("700x650")
        self.input_files = []
        self.input_folder = ""
        self.output_folder = ""
        self.selected_output_format = tk.StringVar()
        self.default_output_folder = self._get_default_output_dir()
 
        # --- Additions for Graceful Shutdown ---
        self.conversion_thread = None # Keep track of the conversion thread
        self.shutdown_event = threading.Event() # Signal for thread to stop (optional use here)
        self.is_closing = False # Flag to prevent issues during shutdown
 
        # --- GUI Elements (Same as before) ---
        # Input Frame
        input_frame = tk.LabelFrame(root, text="输入文件 (可拖拽 EPUB 文件/文件夹)", padx=10, pady=10); input_frame.pack(padx=10, pady=10, fill="x")
        self.btn_select_files = tk.Button(input_frame, text="选择 EPUB 文件", command=self.select_files); self.btn_select_files.grid(row=0, column=0, padx=5, pady=5)
        self.btn_select_folder = tk.Button(input_frame, text="选择 EPUB 文件夹", command=self.select_folder); self.btn_select_folder.grid(row=0, column=1, padx=5, pady=5)
        self.lbl_input_selection = tk.Label(input_frame, text="未选择 EPUB 文件或文件夹", wraplength=650, justify="left", height=3, anchor="nw"); self.lbl_input_selection.grid(row=1, column=0, columnspan=2, padx=5, pady=5, sticky="we")
        # Output Frame
        output_frame = tk.LabelFrame(root, text="输出设置", padx=10, pady=10); output_frame.pack(padx=10, pady=5, fill="x")
        self.btn_select_output = tk.Button(output_frame, text="选择输出文件夹", command=self.select_output_folder); self.btn_select_output.grid(row=0, column=0, padx=5, pady=5, sticky='w')
        self.output_folder = self.default_output_folder; self.lbl_output_folder = tk.Label(output_frame, text=f"默认: {self.output_folder}", wraplength=450, justify="left"); self.lbl_output_folder.grid(row=0, column=1, padx=5, pady=5, sticky='we')
        lbl_output_format = tk.Label(output_frame, text="选择输出格式:"); lbl_output_format.grid(row=1, column=0, padx=5, pady=5, sticky='w')
        self.combo_output_format = ttk.Combobox(output_frame, textvariable=self.selected_output_format, values=SUPPORTED_OUTPUT_FORMATS, state="readonly"); self.combo_output_format.grid(row=1, column=1, padx=5, pady=5, sticky='we')
        if SUPPORTED_OUTPUT_FORMATS: self.combo_output_format.set(SUPPORTED_OUTPUT_FORMATS[0])
        self.selected_output_format.trace_add("write", self.check_ready_to_convert)
        output_frame.columnconfigure(1, weight=1)
        # Action Frame
        action_frame = tk.Frame(root, padx=10, pady=5); action_frame.pack(fill="x")
        self.btn_convert = tk.Button(action_frame, text="开始转换", command=self.start_conversion_thread, state="disabled"); self.btn_convert.pack(side="left", padx=5)
        self.btn_clear = tk.Button(action_frame, text="清空选择", command=self.clear_selections); self.btn_clear.pack(side="left", padx=5)
        # Log Frame
        log_frame = tk.LabelFrame(root, text="状态和日志", padx=10, pady=10); log_frame.pack(padx=10, pady=10, fill="both", expand=True)
        self.log_area = scrolledtext.ScrolledText(log_frame, wrap=tk.WORD, height=10, state="disabled", bd=0, relief="sunken"); self.log_area.pack(fill="both", expand=True)
 
        # Drag and Drop Setup
        self.root.drop_target_register(DND_FILES)
        self.root.dnd_bind('<<Drop>>', self.handle_drop)
 
        # Initial Status Messages
        self.update_log(f"默认输出文件夹设置为: {self.default_output_folder}"); self.update_log("支持的输入格式: EPUB"); self.update_log(f"支持的输出格式: {', '.join(SUPPORTED_OUTPUT_FORMATS)}"); self.update_log("将 EPUB 文件或包含 EPUB 文件的文件夹拖拽到窗口,或使用按钮选择。"); self.update_log("注意: 转换为 PDF 需要系统安装 Microsoft Word (Windows) 或 LibreOffice (Linux/macOS)。")
        self.check_ready_to_convert()
 
        # --- Set Window Closing Protocol ---
        self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
 
    # --- Graceful Shutdown Handler ---
    def on_closing(self):
        """Handles the window close event."""
        if self.is_closing: # Prevent double execution
            return
        self.is_closing = True # Set flag
 
        if self.conversion_thread and self.conversion_thread.is_alive():
            # If a conversion is running, show a message and maybe wait briefly
            # A more robust solution would involve signaling the thread to cancel,
            # but given the blocking nature of file conversion, this is complex.
            # For now, we'll rely on the daemon thread property but inform the user.
            self.update_log("正在关闭窗口... 如果转换正在进行,可能需要稍等片刻。")
            messagebox.showinfo("关闭", "正在尝试关闭应用程序。\n如果转换正在进行,后台任务可能仍在运行,请稍候。")
            # Optional: Signal the thread (if thread checks the event)
            # self.shutdown_event.set()
            # Optional: Wait a short time for the thread (might block GUI)
            # self.conversion_thread.join(timeout=2.0) # Wait max 2 seconds
        else:
            self.update_log("正在关闭窗口...")
 
        # Proceed to destroy the window
        try:
            if self.root and self.root.winfo_exists():
                self.root.destroy()
        except tk.TclError:
             pass # Ignore errors if widget is already gone
 
 
    # --- Other Methods (No major changes, ensure safety checks) ---
 
    def _get_default_output_dir(self):
        try:
            if getattr(sys, 'frozen', False): app_path = os.path.dirname(sys.executable)
            else: app_path = os.path.dirname(os.path.abspath(__file__))
            return app_path
        except NameError: return os.getcwd()
        except Exception: return os.getcwd()
 
    def update_log(self, message):
        if self.is_closing or not hasattr(self, 'root') or not self.root.winfo_exists():
            print(f"Log (GUI closing/closed): {message}")
            return
        def _update():
            # Double check existence before widget interaction
            if hasattr(self, 'log_area') and self.log_area.winfo_exists():
                try:
                    self.log_area.config(state="normal")
                    self.log_area.insert(tk.END, str(message) + "\n")
                    self.log_area.see(tk.END)
                    self.log_area.config(state="disabled")
                except tk.TclError:
                    print(f"Log (TclError on update): {message}") # Log error if widget destroyed mid-update
            else:
                 print(f"Log (GUI closing/closed): {message}")
        # Check root existence before scheduling 'after'
        if hasattr(self, 'root') and self.root.winfo_exists():
            self.root.after(0, _update)
        else:
            print(f"Log (GUI closing/closed): {message}")
 
    def _parse_dnd_data(self, data_string):
        import re; paths = re.findall(r'\{[^{}]*\}|\S+', data_string); cleaned_paths = [p.strip('{}').strip() for p in paths]; return [p for p in cleaned_paths if p and os.path.exists(p)]
 
    def handle_drop(self, event):
        if self.is_closing: return
        try:
            dropped_items = self._parse_dnd_data(event.data);
            if not dropped_items: self.update_log("拖拽操作未识别有效路径。"); return
            added_files = []; folder = None; valid_ext = (".epub",)
            for item in dropped_items:
                if os.path.isdir(item): folder = item; added_files = []; self.update_log(f"拖拽文件夹: {folder} (处理 EPUB)"); break
                elif os.path.isfile(item):
                    if item.lower().endswith(valid_ext):
                        if item not in self.input_files: added_files.append(item)
                    else: self.update_log(f"忽略拖拽 (非 EPUB): {os.path.basename(item)}")
                else: self.update_log(f"忽略拖拽 (非文件/文件夹): {os.path.basename(item)}")
            if folder: self.input_folder = folder; self.input_files = []; self.lbl_input_selection.config(text=f"已选文件夹: {folder}")
            elif added_files:
                if self.input_folder: self.input_folder = ""; self.input_files = []
                self.input_files.extend(added_files)
                names = [os.path.basename(f) for f in self.input_files]; n = len(self.input_files); max_disp = 5
                disp_txt = f"已选 {n} 个 EPUB:\n" + "\n".join(names[:max_disp]) + ("\n..." if n > max_disp else "")
                self.lbl_input_selection.config(text=disp_txt); self.update_log(f"拖拽添加 {len(added_files)} 个 EPUB。总计 {n} 个。")
            elif not folder and not added_files: self.update_log("拖拽内容无有效 EPUB。")
            self.check_ready_to_convert()
        except Exception as e: self.update_log(f"处理拖拽出错: {e}"); traceback.print_exc()
 
    def select_files(self):
        if self.is_closing: return
        try:
            files = filedialog.askopenfilenames(title="选择 EPUB 输入文件", filetypes=SUPPORTED_INPUT_FORMATS)
            if files:
                epub_files = [f for f in files if f.lower().endswith('.epub')]; ignored = len(files) - len(epub_files)
                if ignored > 0: self.update_log(f"注意: 忽略了 {ignored} 个非 EPUB 文件。")
                if epub_files:
                     self.input_files = list(epub_files); self.input_folder = ""
                     names=[os.path.basename(f) for f in self.input_files]; n=len(self.input_files); max_disp=5
                     disp_txt=f"已选 {n} 个 EPUB:\n" + "\n".join(names[:max_disp]) + ("\n..." if n > max_disp else "")
                     self.lbl_input_selection.config(text=disp_txt); self.update_log(f"已选 {n} 个 EPUB 文件。")
                     self.check_ready_to_convert()
                elif not self.input_files: self.lbl_input_selection.config(text="未选择 EPUB"); self.update_log("未选有效 EPUB。"); self.check_ready_to_convert()
        except Exception as e: self.update_log(f"选择文件出错: {e}"); traceback.print_exc()
 
    def select_folder(self):
        if self.is_closing: return
        try:
            folder = filedialog.askdirectory(title="选择包含 EPUB 文件的文件夹")
            if folder: self.input_folder = folder; self.input_files = []; self.lbl_input_selection.config(text=f"已选文件夹: {folder}"); self.update_log(f"已选文件夹: {folder} (处理 EPUB)"); self.check_ready_to_convert()
        except Exception as e: self.update_log(f"选择文件夹出错: {e}"); traceback.print_exc()
 
    def select_output_folder(self):
        if self.is_closing: return
        try:
            init_dir = self.output_folder if os.path.isdir(self.output_folder) else self.default_output_folder
            folder = filedialog.askdirectory(title="选择保存转换后文件的文件夹", initialdir=init_dir)
            if folder: self.output_folder = folder; self.lbl_output_folder.config(text=f"{folder}"); self.update_log(f"输出文件夹设为: {folder}"); self.check_ready_to_convert()
        except Exception as e: self.update_log(f"选择输出文件夹出错: {e}"); traceback.print_exc()
 
    def clear_selections(self):
        if self.is_closing: return
        self.input_files = []; self.input_folder = ""; self.output_folder = self.default_output_folder
        self.lbl_input_selection.config(text="未选择 EPUB 文件或文件夹"); self.lbl_output_folder.config(text=f"默认: {self.output_folder}")
        try:
            if hasattr(self, 'log_area') and self.log_area.winfo_exists():
                self.log_area.config(state="normal"); self.log_area.delete(1.0, tk.END); self.log_area.config(state="disabled")
        except tk.TclError: pass
        if hasattr(self, 'btn_convert'): self.btn_convert.config(state="disabled")
        self.update_log("选择已清除。输出已重置为默认。")
 
    def check_ready_to_convert(self, *args):
        if self.is_closing: return
        try:
            output_ok = self.output_folder and os.path.isdir(self.output_folder); input_ok = bool(self.input_files or self.input_folder); format_ok = bool(self.selected_output_format.get())
            # Check widget existence before config
            can_convert = input_ok and output_ok and format_ok
            if hasattr(self, 'btn_convert'):
                 # Also check if controls are generally enabled (not during conversion)
                 controls_active = hasattr(self, 'btn_select_files') and self.btn_select_files['state'] == 'normal'
                 self.btn_convert.config(state="normal" if can_convert and controls_active else "disabled")
        except Exception: # Catch potential TclError if widgets destroyed unexpectedly
             pass
 
    def start_conversion_thread(self):
        if self.is_closing: return
        output_format = self.selected_output_format.get()
        # ...(rest of the checks are the same)...
        if not (self.input_files or self.input_folder): messagebox.showerror("错误", "请先选择 EPUB 输入。"); return
        if not self.output_folder or not os.path.isdir(self.output_folder):
            try: os.makedirs(self.output_folder, exist_ok=True); self.update_log(f"创建输出文件夹: {self.output_folder}")
            except Exception as e: messagebox.showerror("错误", f"输出文件夹无效且无法创建: {e}"); return
        if not output_format: messagebox.showerror("错误", "请先选择输出格式。"); return
 
        # --- Start Thread ---
        self.toggle_controls(enabled=False); self.update_log(f"开始转换 EPUB 到 {output_format}...")
        thread_in_files = list(self.input_files); thread_in_folder = self.input_folder; thread_out_folder = self.output_folder
        # Assign thread to instance variable
        self.conversion_thread = threading.Thread(target=self.run_conversion, args=(output_format, thread_in_files, thread_in_folder, thread_out_folder), daemon=True)
        self.conversion_thread.start()
 
    def toggle_controls(self, enabled=True):
        if self.is_closing: return # Don't change state if closing
        state = "normal" if enabled else "disabled"
        try: # Wrap in try-except for TclError
            if hasattr(self, 'btn_select_files'): self.btn_select_files.config(state=state)
            if hasattr(self, 'btn_select_folder'): self.btn_select_folder.config(state=state)
            if hasattr(self, 'btn_select_output'): self.btn_select_output.config(state=state)
            if hasattr(self, 'btn_clear'): self.btn_clear.config(state=state)
            if hasattr(self, 'combo_output_format'): self.combo_output_format.config(state="readonly" if enabled else "disabled")
            if hasattr(self, 'btn_convert'):
                if not enabled: self.btn_convert.config(state="disabled")
                else: self.check_ready_to_convert() # Re-check conditions when enabling
        except tk.TclError:
             self.update_log("警告: 尝试配置已销毁的控件。") # Log if widgets are gone
 
    def run_conversion(self, output_format, input_files, input_folder, output_folder):
        #...(Same file gathering logic as before)...
        files_to_process = []; valid_extensions = (".epub",)
        if input_files: files_to_process = [f for f in input_files if f.lower().endswith(valid_extensions)]
        elif input_folder:
            try:
                self.update_log(f"扫描文件夹: {input_folder}"); count = 0
                for entry in os.scandir(input_folder):
                    if entry.is_file() and entry.name.lower().endswith(valid_extensions): files_to_process.append(entry.path); count += 1
                self.update_log(f"找到 {count} 个 EPUB。")
            except FileNotFoundError: self.update_log(f"错误: 输入文件夹未找到 - {input_folder}"); self.conversion_finished(success=False, message="输入文件夹未找到。"); return
            except Exception as e: self.update_log(f"错误: 扫描文件夹出错 - {e}"); self.conversion_finished(success=False, message="扫描文件夹出错。"); return
        if not files_to_process: self.update_log("错误: 未找到有效 EPUB 转换。"); self.conversion_finished(success=False, message="未找到 EPUB 转换。"); return
 
        total_files = len(files_to_process); success_count = 0; fail_count = 0
        for i, file_path in enumerate(files_to_process):
            # --- Add check for shutdown signal (optional but good) ---
            # if self.shutdown_event.is_set():
            #    self.update_log("...转换被用户中断...")
            #    break # Exit loop early
 
            if not os.path.exists(file_path): self.update_log(f"--- [{i+1}/{total_files}] 跳过: 文件不存在 {os.path.basename(file_path)} ---"); fail_count += 1; continue
            self.update_log(f"--- [{i+1}/{total_files}] ---")
            result = convert_file(file_path, output_folder, output_format, self.update_log)
            if result is True: success_count += 1
            else: fail_count += 1
 
        # --- Finished ---
        # Don't reset shutdown event here, let the app close
        summary = f"转换完成。\n总计 EPUB: {total_files}, 成功: {success_count}, 失败/跳过: {fail_count}"
        self.update_log("--- " + summary.replace('\n', ' ') + " ---"); overall_success = (fail_count == 0 and success_count > 0)
        self.conversion_finished(success=overall_success, message=summary)
 
    def conversion_finished(self, success=True, message="转换完成"):
        # Ensure GUI updates happen only if window exists
        if self.is_closing or not hasattr(self, 'root') or not self.root.winfo_exists():
             print(f"Conversion finished but GUI closed. Message: {message}")
             return
        def _update_gui():
            if hasattr(self, 'root') and self.root.winfo_exists() and not self.is_closing: # Double check again
                self.toggle_controls(enabled=True) # Re-enable controls safely
                if success: messagebox.showinfo("完成", message)
                elif "未找到" in message or "扫描" in message: messagebox.showwarning("提示", message)
                else: messagebox.showwarning("完成", message + "\n部分文件可能转换失败或跳过,请查看日志。")
            else:
                 print(f"Conversion finished but GUI closed before final update. Message: {message}")
        # Schedule update only if root exists
        if hasattr(self, 'root') and self.root.winfo_exists():
            self.root.after(10, _update_gui)
 
 
# --- Main Execution Block (Added basic exception handling) ---
if __name__ == "__main__":
    # Redirect stderr globally for startup issues when running without console (optional, might hide other issues)
    # if not sys.stderr: # Only if running without console
    #    try:
    #        log_file_path = os.path.join(os.path.dirname(sys.executable) if getattr(sys, 'frozen', False) else '.', 'app_startup_error.log')
    #        sys.stderr = open(log_file_path, 'w')
    #    except Exception:
    #        pass # Ignore if redirection fails
 
    try:
        root = TkinterDnD.Tk()
        app = FileConverterApp(root)
        root.mainloop()
    except Exception as main_err:
        print(f"FATAL ERROR DURING STARTUP OR MAIN LOOP: {main_err}", file=sys.stderr)
        traceback.print_exc(file=sys.stderr)
        try: # Attempt to show error box if possible
             root_err = tk.Tk(); root_err.withdraw()
             messagebox.showerror("程序严重错误", f"应用程序遇到严重错误无法继续:\n{main_err}\n详细信息请查看控制台或日志。")
             root_err.destroy()
        except Exception: pass # Ignore if messagebox fails
        sys.exit(1)
 
# /-- End Optimized Code Snippet ---

免费评分

参与人数 1吾爱币 +1 热心值 +1 收起 理由
wapjsx + 1 + 1 谢谢@Thanks!

查看全部评分

推荐
 楼主| zwjtr93 发表于 2025-4-2 15:24 |楼主
本帖最后由 zwjtr93 于 2025-4-2 22:43 编辑

以下源码为最早版本只能转换为txt,并附带打包好的下载地址https://wwlo.lanzouo.com/iGX962sgeqyb
密码:fge2
[Python] 纯文本查看 复制代码
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
import tkinter as tk
from tkinter import filedialog, messagebox, scrolledtext
import os
import threading
import ebooklib
from ebooklib import epub
from bs4 import BeautifulSoup
import html
import sys # 用于获取脚本路径
 
# 导入 TkinterDnD2
from tkinterdnd2 import DND_FILES, TkinterDnD
 
# --- Core Conversion Logic (与之前相同) ---
 
def extract_text_from_html(html_content):
    """从 HTML 内容中提取纯文本"""
    soup = BeautifulSoup(html_content, 'lxml') # 使用 lxml 解析器
    for script_or_style in soup(["script", "style"]):
        script_or_style.decompose()
    text = soup.get_text(separator=' ', strip=True)
    text = html.unescape(text)
    return text
 
def convert_epub_to_txt(epub_path, output_folder, status_callback):
    """将单个 EPUB 文件转换为 TXT 文件"""
    try:
        status_callback(f"正在处理: {os.path.basename(epub_path)}")
        book = epub.read_epub(epub_path)
 
        base_name = os.path.splitext(os.path.basename(epub_path))[0]
        txt_filename = os.path.join(output_folder, f"{base_name}.txt")
 
        full_text = []
        items_in_order = [item for item in book.get_items() if isinstance(item, ebooklib.epub.EpubHtml)]
        try:
            spine_ids = [item_id for item_id, _ in book.spine]
            ordered_items_map = {item.id: item for item in items_in_order}
            ordered_items = [ordered_items_map[item_id] for item_id in spine_ids if item_id in ordered_items_map]
            other_items = [item for item in items_in_order if item.id not in spine_ids]
            items_to_process = ordered_items + other_items
        except Exception:
            items_to_process = items_in_order # 回退
 
        for item in items_to_process:
            content = item.get_body_content()
            try:
                decoded_content = content.decode('utf-8', errors='ignore')
            except AttributeError:
                decoded_content = str(content)
            except Exception as decode_err:
                 status_callback(f"警告: 文件 {os.path.basename(epub_path)} 中解码项目 {item.get_name()} 时出错: {decode_err}")
                 continue
 
            text = extract_text_from_html(decoded_content)
            if text:
                full_text.append(text)
                full_text.append("\n\n") # 章节间隔
 
        with open(txt_filename, 'w', encoding='utf-8') as txt_file:
            txt_file.write("".join(full_text))
 
        status_callback(f"完成: {os.path.basename(txt_filename)}")
        return True
 
    except FileNotFoundError:
        status_callback(f"错误: EPUB 文件未找到 - {epub_path}")
        return False
    except epub.EpubException as e:
        status_callback(f"错误: 处理 EPUB 文件 {os.path.basename(epub_path)} 时出错 - {e}")
        return False
    except Exception as e:
        status_callback(f"错误: 转换 {os.path.basename(epub_path)} 时发生未知错误 - {e}")
        return False
 
# --- GUI Application Class ---
 
class EpubConverterApp:
    def __init__(self, root):
        self.root = root
        self.root.title("EPUB 到 TXT 转换器 (支持拖拽)")
        self.root.geometry("650x600") # 稍微调大窗口以容纳拖拽提示
 
        self.input_files = []
        self.input_folder = ""
        self.output_folder = ""
        self.default_output_folder = self._get_default_output_dir()
 
        # --- Input Selection Frame ---
        input_frame = tk.LabelFrame(root, text="输入 (可拖拽文件/文件夹到此窗口)", padx=10, pady=10)
        input_frame.pack(padx=10, pady=10, fill="x")
 
        self.btn_select_files = tk.Button(input_frame, text="选择文件", command=self.select_files)
        self.btn_select_files.grid(row=0, column=0, padx=5, pady=5)
 
        self.btn_select_folder = tk.Button(input_frame, text="选择文件夹", command=self.select_folder)
        self.btn_select_folder.grid(row=0, column=1, padx=5, pady=5)
 
        self.lbl_input_selection = tk.Label(input_frame, text="未选择输入", wraplength=550, justify="left", height=3, anchor="nw") # 增加高度
        self.lbl_input_selection.grid(row=1, column=0, columnspan=2, padx=5, pady=5, sticky="w"+"e") # 宽度填充
 
        # --- Output Selection Frame ---
        output_frame = tk.LabelFrame(root, text="输出文件夹", padx=10, pady=10)
        output_frame.pack(padx=10, pady=5, fill="x")
 
        self.btn_select_output = tk.Button(output_frame, text="更改输出文件夹", command=self.select_output_folder) # 更改按钮文字
        self.btn_select_output.pack(side="left", padx=5, pady=5)
 
        # 设置并显示默认输出文件夹
        self.output_folder = self.default_output_folder
        self.lbl_output_folder = tk.Label(output_frame, text=f"默认: {self.output_folder}", wraplength=450, justify="left")
        self.lbl_output_folder.pack(side="left", padx=5, pady=5, fill="x", expand=True)
 
 
        # --- Action Frame ---
        action_frame = tk.Frame(root, padx=10, pady=5)
        action_frame.pack(fill="x")
 
        self.btn_convert = tk.Button(action_frame, text="开始转换", command=self.start_conversion_thread, state="disabled")
        self.btn_convert.pack(side="left", padx=5)
 
        self.btn_clear = tk.Button(action_frame, text="清空选择", command=self.clear_selections)
        self.btn_clear.pack(side="left", padx=5)
 
 
        # --- Status/Log Area ---
        log_frame = tk.LabelFrame(root, text="状态和日志", padx=10, pady=10)
        log_frame.pack(padx=10, pady=10, fill="both", expand=True)
 
        self.log_area = scrolledtext.ScrolledText(log_frame, wrap=tk.WORD, height=10, state="disabled")
        self.log_area.pack(fill="both", expand=True)
 
        # --- 启用拖拽 ---
        # 需要在能接收拖拽的控件上注册,这里我们让整个窗口都能接收
        self.root.drop_target_register(DND_FILES)
        self.root.dnd_bind('<<Drop>>', self.handle_drop) # 绑定拖拽事件
 
        # 初始化时检查是否可以转换 (因为现在有默认输出了)
        self.update_log(f"默认输出文件夹设置为: {self.default_output_folder}")
        self.check_ready_to_convert()
 
 
    def _get_default_output_dir(self):
        """获取默认输出目录(脚本所在目录或打包后的可执行文件目录)"""
        try:
            # 检查是否通过 PyInstaller 等工具打包
            if getattr(sys, 'frozen', False):
                application_path = os.path.dirname(sys.executable)
            else:
                # 否则获取脚本文件所在目录
                application_path = os.path.dirname(os.path.abspath(__file__))
            return application_path
        except NameError:
            # 如果 __file__ 未定义 (例如在某些交互式环境中),使用当前工作目录
            return os.getcwd()
        except Exception:
            # 其他未知错误,也回退到当前工作目录
            return os.getcwd()
 
 
    def update_log(self, message):
        """线程安全地更新日志区域"""
        def _update():
            self.log_area.config(state="normal")
            self.log_area.insert(tk.END, message + "\n")
            self.log_area.see(tk.END) # 滚动到底部
            self.log_area.config(state="disabled")
        self.root.after(0, _update)
 
    def _parse_dnd_data(self, data_string):
        """解析从拖拽事件获取的可能包含带空格路径的字符串"""
        # TkinterDnD2 通常返回一个空格分隔的列表,
        # 其中带空格的路径可能被花括号 {} 包裹。
        import re
        # 正则表达式查找被{}包裹的内容或不含空格的连续字符
        paths = re.findall(r'\{[^{}]*\}|\S+', data_string)
        # 去除路径两端可能存在的花括号
        cleaned_paths = [p.strip('{}') for p in paths]
        return [p for p in cleaned_paths if os.path.exists(p)] # 确保路径存在
 
 
    def handle_drop(self, event):
        """处理拖拽到窗口的文件或文件夹"""
        dropped_items = self._parse_dnd_data(event.data)
        if not dropped_items:
            self.update_log("拖拽操作未识别有效的文件或文件夹路径。")
            return
 
        added_files = []
        dropped_folder = None
 
        for item_path in dropped_items:
            if os.path.isdir(item_path):
                # 如果拖入的是文件夹,则以最后一个文件夹为准(覆盖之前的拖拽项)
                dropped_folder = item_path
                added_files = [] # 清空文件列表,因为选择了文件夹
                self.update_log(f"通过拖拽选择文件夹: {dropped_folder}")
                break # 文件夹优先,停止处理其他拖拽项
            elif os.path.isfile(item_path) and item_path.lower().endswith(".epub"):
                if item_path not in self.input_files: # 避免重复添加
                    added_files.append(item_path)
            else:
                self.update_log(f"忽略拖拽项 (非文件夹或非EPUB文件): {os.path.basename(item_path)}")
 
 
        if dropped_folder:
            self.input_folder = dropped_folder
            self.input_files = []
            self.lbl_input_selection.config(text=f"已选择文件夹: {self.input_folder}")
        elif added_files:
            # 如果之前是文件夹模式,现在切换到文件模式
            if self.input_folder:
                self.input_folder = ""
                self.input_files = []
 
            self.input_files.extend(added_files) # 添加新拖入的文件
            self.lbl_input_selection.config(text=f"已选择 {len(self.input_files)} 个文件:\n" + "\n".join(os.path.basename(f) for f in self.input_files[-5:]) + ("..." if len(self.input_files)>5 else "")) # 显示最后几个
            self.update_log(f"通过拖拽添加了 {len(added_files)} 个 EPUB 文件。当前共 {len(self.input_files)} 个文件。")
        elif not added_files and not dropped_folder:
             self.update_log("拖拽的内容中未找到有效的EPUB文件或文件夹。")
 
 
        self.check_ready_to_convert()
 
 
    def select_files(self):
        """打开文件对话框选择 EPUB 文件"""
        files = filedialog.askopenfilenames(
            title="选择 EPUB 文件",
            filetypes=[("EPUB 文件", "*.epub")]
        )
        if files:
            self.input_files = list(files)
            self.input_folder = ""
            self.lbl_input_selection.config(text=f"已选择 {len(self.input_files)} 个文件:\n" + "\n".join(os.path.basename(f) for f in self.input_files[:5]) + ("..." if len(self.input_files)>5 else ""))
            self.update_log(f"已选择 {len(self.input_files)} 个文件。")
            self.check_ready_to_convert()
 
    def select_folder(self):
        """打开文件夹对话框选择包含 EPUB 文件的文件夹"""
        folder = filedialog.askdirectory(title="选择包含 EPUB 文件的文件夹")
        if folder:
            self.input_folder = folder
            self.input_files = []
            self.lbl_input_selection.config(text=f"已选择文件夹: {self.input_folder}")
            self.update_log(f"已选择文件夹: {self.input_folder}")
            self.check_ready_to_convert()
 
    def select_output_folder(self):
        """打开文件夹对话框选择输出位置"""
        # 初始目录设为当前输出目录,方便用户浏览
        initial_dir = self.output_folder if os.path.isdir(self.output_folder) else self.default_output_folder
        folder = filedialog.askdirectory(title="选择保存 TXT 文件的文件夹", initialdir=initial_dir)
        if folder:
            self.output_folder = folder
            # 更新标签以反映用户选择的路径
            self.lbl_output_folder.config(text=f"{self.output_folder}")
            self.update_log(f"输出文件夹设置为: {self.output_folder}")
            self.check_ready_to_convert()
 
    def clear_selections(self):
        """清除输入选择和日志,并将输出重置为默认值"""
        self.input_files = []
        self.input_folder = ""
        # 重置输出文件夹为其默认值
        self.output_folder = self.default_output_folder
        self.lbl_input_selection.config(text="未选择输入")
        # 更新输出标签以显示默认路径
        self.lbl_output_folder.config(text=f"默认: {self.output_folder}")
 
        self.log_area.config(state="normal")
        self.log_area.delete(1.0, tk.END)
        self.log_area.config(state="disabled")
        self.btn_convert.config(state="disabled") # 因为输入清空了
        self.update_log("选择已清除。输出已重置为默认文件夹。")
 
    def check_ready_to_convert(self):
        """检查是否可以开始转换(输入已选且输出文件夹有效)"""
        # 确保输出文件夹路径存在且是目录
        output_ok = self.output_folder and os.path.isdir(self.output_folder)
        input_ok = bool(self.input_files or self.input_folder)
 
        if input_ok and output_ok:
            self.btn_convert.config(state="normal")
        else:
            self.btn_convert.config(state="disabled")
            if input_ok and not output_ok:
                 self.update_log(f"警告: 输出文件夹 '{self.output_folder}' 无效或不存在。请选择有效的文件夹。")
 
 
    def start_conversion_thread(self):
        """在单独的线程中启动转换过程以避免 GUI 冻结"""
        if not (self.input_files or self.input_folder):
            messagebox.showerror("错误", "请先选择输入的 EPUB 文件或文件夹。")
            return
        if not self.output_folder:
            messagebox.showerror("错误", "请先设置有效的输出文件夹。")
            return
        if not os.path.isdir(self.output_folder):
             messagebox.showerror("错误", f"输出文件夹无效或不存在: {self.output_folder}")
             return
 
        # 禁用按钮
        self.btn_convert.config(state="disabled")
        self.btn_select_files.config(state="disabled")
        self.btn_select_folder.config(state="disabled")
        self.btn_select_output.config(state="disabled")
        self.btn_clear.config(state="disabled") # 转换期间也禁用清空
 
        self.update_log("开始转换...")
        conversion_thread = threading.Thread(target=self.run_conversion, daemon=True)
        conversion_thread.start()
 
    def run_conversion(self):
        """实际执行转换的函数(在线程中运行)"""
        files_to_process = []
        if self.input_files:
            files_to_process = list(self.input_files) # 使用副本,以防在转换时GUI修改了列表
        elif self.input_folder:
            try:
                self.update_log(f"正在扫描文件夹: {self.input_folder}")
                count = 0
                for filename in os.listdir(self.input_folder):
                    if filename.lower().endswith(".epub"):
                        full_path = os.path.join(self.input_folder, filename)
                        if os.path.isfile(full_path): # 确保是文件
                             files_to_process.append(full_path)
                             count += 1
                self.update_log(f"在文件夹中找到 {count} 个 EPUB 文件。")
            except FileNotFoundError:
                 self.update_log(f"错误: 输入文件夹未找到 - {self.input_folder}")
                 self.conversion_finished(success=False, message="输入文件夹未找到。")
                 return
            except Exception as e:
                 self.update_log(f"错误: 扫描输入文件夹时出错 - {e}")
                 self.conversion_finished(success=False, message="扫描输入文件夹时出错。")
                 return
 
        if not files_to_process:
            self.update_log("错误: 未找到有效的 EPUB 文件进行转换。")
            self.conversion_finished(success=False, message="未找到要转换的 EPUB 文件。")
            return
 
        total_files = len(files_to_process)
        success_count = 0
        fail_count = 0
 
        for i, epub_file in enumerate(files_to_process):
             # 检查文件是否存在(可能在扫描后被删除)
             if not os.path.exists(epub_file):
                 self.update_log(f"--- [{i+1}/{total_files}] 跳过: 文件已不存在 {os.path.basename(epub_file)} ---")
                 fail_count += 1
                 continue
 
             self.update_log(f"--- [{i+1}/{total_files}] ---")
             if convert_epub_to_txt(epub_file, self.output_folder, self.update_log):
                 success_count += 1
             else:
                 fail_count += 1
 
        summary = f"转换完成。\n总计: {total_files}, 成功: {success_count}, 失败: {fail_count}"
        self.update_log("--- " + summary.replace('\n', ' ') + " ---")
        self.conversion_finished(success=(fail_count == 0), message=summary)
 
 
    def conversion_finished(self, success=True, message="转换完成"):
        """转换完成后恢复 GUI 状态并显示消息"""
        def _update_gui():
            # 重新启用按钮
            self.btn_convert.config(state="normal")
            self.btn_select_files.config(state="normal")
            self.btn_select_folder.config(state="normal")
            self.btn_select_output.config(state="normal")
            self.btn_clear.config(state="normal")
            self.check_ready_to_convert() # 再次检查状态,以防输出文件夹在转换过程中失效
 
            if success:
                 messagebox.showinfo("完成", message)
            elif "未找到要转换" in message or "输入文件夹未找到" in message:
                 messagebox.showwarning("提示", message)
            else:
                 messagebox.showwarning("完成但有错误", message + "\n请查看日志了解失败详情。")
 
        self.root.after(0, _update_gui)
 
 
# --- Main Execution ---
if __name__ == "__main__":
    # 使用 TkinterDnD.Tk() 而不是 tk.Tk() 来启用拖拽
    root = TkinterDnD.Tk()
    app = EpubConverterApp(root)
    root.mainloop()
3#
asd123yx 发表于 2025-4-2 15:58
4#
52PJ070 发表于 2025-4-2 16:32
辛苦楼主制作并分享,下载试试
5#
tbchxq 发表于 2025-4-2 16:37
太需要了  感谢哈。。。
6#
freesaber 发表于 2025-4-2 16:51
asd123yx 发表于 2025-4-2 15:58
要是转PDF就好了

https://calibre-ebook.com/zh_CN/download

ebook-convert "文件名.epub" "文件名.pdf"
7#
jwzxwyg 发表于 2025-4-2 17:00
打包一般需要多久时间
8#
我是懒汉哟 发表于 2025-4-2 17:06
试了一下
好用
支持楼主!
9#
huangitunes 发表于 2025-4-2 17:14
感谢楼主分享
10#
czliwx 发表于 2025-4-2 17:23
谢谢分享
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2025-7-30 10:38

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表