本文目录导读:

我来为您提供一个批量转换文档为电子书的Python脚本,支持常见的文档格式转换。
基础版:支持TXT、PDF、DOCX转EPUB
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
批量文档转电子书脚本
支持格式: TXT, PDF, DOCX → EPUB, MOBI
"""
import os
import sys
import argparse
from pathlib import Path
from typing import List, Optional
import logging
from concurrent.futures import ThreadPoolExecutor, as_completed
# 第三方库导入
try:
from ebooklib import epub
from bs4 import BeautifulSoup
import fitz # PyMuPDF
from docx import Document
from tqdm import tqdm
except ImportError as e:
print(f"缺少必要库: {e}")
print("请安装: pip install ebooklib beautifulsoup4 pymupdf python-docx tqdm")
sys.exit(1)
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
class DocumentConverter:
"""文档转换器类"""
SUPPORTED_INPUT = {'.txt', '.pdf', '.docx'}
SUPPORTED_OUTPUT = {'.epub', '.mobi'}
def __init__(self, input_dir: str, output_dir: str, output_format: str = 'epub'):
"""
初始化转换器
Args:
input_dir: 输入目录
output_dir: 输出目录
output_format: 输出格式 (epub/mobi)
"""
self.input_dir = Path(input_dir)
self.output_dir = Path(output_dir)
self.output_format = output_format.lower()
if self.output_format not in self.SUPPORTED_OUTPUT:
raise ValueError(f"不支持的输出格式: {output_format}")
# 创建输出目录
self.output_dir.mkdir(parents=True, exist_ok=True)
def convert_document(self, file_path: Path) -> Optional[Path]:
"""
转换单个文档
Args:
file_path: 输入文件路径
Returns:
转换后的文件路径,失败返回None
"""
try:
# 根据文件类型选择转换方法
suffix = file_path.suffix.lower()
if suffix == '.txt':
content = self._read_txt(file_path)
elif suffix == '.pdf':
content = self._read_pdf(file_path)
elif suffix == '.docx':
content = self._read_docx(file_path)
else:
logger.warning(f"不支持的文件格式: {file_path}")
return None
# 生成电子书
output_file = self.output_dir / f"{file_path.stem}.{self.output_format}"
if self.output_format == 'epub':
self._create_epub(file_path.stem, content, output_file)
else: # mobi格式可能需要额外转换,这里简单处理
self._create_epub(file_path.stem, content, output_file.with_suffix('.epub'))
# 注意: 真正的MOBI转换需要Calibre的ebook-convert工具
logger.info(f"转换成功: {file_path.name} → {output_file.name}")
return output_file
except Exception as e:
logger.error(f"转换失败 {file_path.name}: {str(e)}")
return None
def _read_txt(self, file_path: Path) -> str:
"""读取TXT文件"""
try:
# 尝试多种编码
encodings = ['utf-8', 'gbk', 'gb2312', 'big5', 'latin-1']
for encoding in encodings:
try:
with open(file_path, 'r', encoding=encoding) as f:
return f.read()
except UnicodeDecodeError:
continue
raise ValueError(f"无法识别文件编码: {file_path}")
except Exception as e:
raise Exception(f"读取TXT失败: {str(e)}")
def _read_pdf(self, file_path: Path) -> str:
"""读取PDF文件"""
try:
doc = fitz.open(str(file_path))
text = ""
for page in doc:
text += page.get_text()
doc.close()
return text
except Exception as e:
raise Exception(f"读取PDF失败: {str(e)}")
def _read_docx(self, file_path: Path) -> str:
"""读取DOCX文件"""
try:
doc = Document(file_path)
paragraphs = [p.text for p in doc.paragraphs]
return '\n\n'.join(paragraphs)
except Exception as e:
raise Exception(f"读取DOCX失败: {str(e)}")
def _create_epub(self, title: str, content: str, output_file: Path):
"""创建EPUB文件"""
try:
# 创建EPUB对象
book = epub.EpubBook()
book.set_identifier(f'id_{hash(title)}')
book.set_title(title)
book.set_language('zh')
# 添加作者元数据
book.add_author('Converter')
# 创建章节
# 简单分章:每2000字分一章
chapter_size = 2000
chapters = [content[i:i+chapter_size]
for i in range(0, len(content), chapter_size)]
if not chapters:
chapters = [''] # 空内容时添加空章节
for i, chapter_content in enumerate(chapters):
chapter = epub.EpubHtml(
title=f'第{i+1}章',
file_name=f'chapter_{i+1}.xhtml',
lang='zh'
)
# 将纯文本转换为HTML
html_content = f'<h2>第{i+1}章</h2><p>{chapter_content}</p>'
chapter.content = html_content
book.add_item(chapter)
book.toc.append(chapter)
# 添加样式
style = '''
body { font-family: "Noto Serif CJK SC", serif; }
h2 { text-align: center; margin-bottom: 1em; }
p { text-indent: 2em; line-height: 1.8; }
'''
nav_css = epub.EpubItem(
uid='style',
file_name='style.css',
media_type='text/css',
content=style
)
book.add_item(nav_css)
# 设置spine
book.spine = ['nav'] + book.toc
# 添加默认导航
book.add_item(epub.EpubNcx())
book.add_item(epub.EpubNav())
# 保存EPUB文件
epub.write_epub(str(output_file), book)
except Exception as e:
raise Exception(f"创建EPUB失败: {str(e)}")
def batch_convert(self, max_workers: int = 4) -> List[Path]:
"""
批量转换所有支持的文件
Args:
max_workers: 最大线程数
Returns:
成功转换的文件列表
"""
# 收集所有支持的文件
files = []
for ext in self.SUPPORTED_INPUT:
files.extend(self.input_dir.glob(f"*{ext}"))
files.extend(self.input_dir.glob(f"*{ext.upper()}"))
if not files:
logger.warning(f"在 {self.input_dir} 中未找到支持的文档")
return []
logger.info(f"找到 {len(files)} 个文档,开始转换...")
# 多线程转换
converted_files = []
with ThreadPoolExecutor(max_workers=max_workers) as executor:
# 使用tqdm显示进度
futures = {executor.submit(self.convert_document, f): f for f in files}
with tqdm(total=len(files), desc="转换进度", unit="个") as pbar:
for future in as_completed(futures):
result = future.result()
if result:
converted_files.append(result)
pbar.update(1)
pbar.set_postfix(success=len(converted_files))
logger.info(f"转换完成!成功: {len(converted_files)}/{len(files)} 个文件")
return converted_files
def main():
"""主函数"""
parser = argparse.ArgumentParser(
description='批量转换文档为电子书',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
示例:
python convert_ebook.py -i ./documents -o ./ebooks
python convert_ebook.py -i ./docs -o ./epubs --format epub --threads 8
python convert_ebook.py -i ./docs -o ./mobis --format mobi
'''
)
parser.add_argument('-i', '--input', required=True, help='输入文档目录')
parser.add_argument('-o', '--output', default='./output', help='输出电子书目录 (默认: ./output)')
parser.add_argument('-f', '--format', choices=['epub', 'mobi'], default='epub',
help='输出格式 (默认: epub)')
parser.add_argument('-t', '--threads', type=int, default=4,
help='并行线程数 (默认: 4)')
parser.add_argument('--log-level', choices=['DEBUG', 'INFO', 'WARNING', 'ERROR'],
default='INFO', help='日志级别')
args = parser.parse_args()
# 设置日志级别
logger.setLevel(getattr(logging, args.log_level))
try:
# 检查输入目录
input_dir = Path(args.input)
if not input_dir.exists():
raise ValueError(f"输入目录不存在: {args.input}")
if not input_dir.is_dir():
raise ValueError(f"输入路径不是目录: {args.input}")
# 创建转换器并执行转换
converter = DocumentConverter(
input_dir=input_dir,
output_dir=Path(args.output),
output_format=args.format
)
converted_files = converter.batch_convert(max_workers=args.threads)
# 输出总结
print("\n" + "="*50)
print(f"转换完成!成功处理 {len(converted_files)} 个文件")
for i, file in enumerate(converted_files, 1):
print(f"{i}. {file}")
print("="*50)
except Exception as e:
logger.error(f"转换过程出错: {str(e)}")
sys.exit(1)
if __name__ == "__main__":
main()
高级版:加图形界面(可选)
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
带GUI的批量转换器(需要安装 tkinter)
"""
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import threading
from pathlib import Path
import sys
import os
# 导入转换器
from convert_ebook import DocumentConverter # 假设上面的代码保存为 convert_ebook.py
class ConverterGUI:
"""图形界面转换器"""
def __init__(self):
self.root = tk.Tk()
self.root.title("批量文档转电子书工具")
self.root.geometry("600x400")
# 变量
self.input_dir = tk.StringVar()
self.output_dir = tk.StringVar(value="./output")
self.output_format = tk.StringVar(value="epub")
self.threads = tk.IntVar(value=4)
self.progress_var = tk.DoubleVar(value=0)
self.converter = None
self.setup_ui()
def setup_ui(self):
"""设置UI"""
# 输入目录
ttk.Label(self.root, text="输入目录:").grid(row=0, column=0, padx=5, pady=5, sticky='w')
ttk.Entry(self.root, textvariable=self.input_dir, width=40).grid(row=0, column=1, padx=5, pady=5)
ttk.Button(self.root, text="浏览...", command=self.select_input).grid(row=0, column=2, padx=5)
# 输出目录
ttk.Label(self.root, text="输出目录:").grid(row=1, column=0, padx=5, pady=5, sticky='w')
ttk.Entry(self.root, textvariable=self.output_dir, width=40).grid(row=1, column=1, padx=5, pady=5)
ttk.Button(self.root, text="浏览...", command=self.select_output).grid(row=1, column=2, padx=5)
# 输出格式
ttk.Label(self.root, text="输出格式:").grid(row=2, column=0, padx=5, pady=5, sticky='w')
format_frame = ttk.Frame(self.root)
format_frame.grid(row=2, column=1, padx=5, pady=5, sticky='w')
ttk.Radiobutton(format_frame, text="EPUB", variable=self.output_format, value="epub").pack(side='left')
ttk.Radiobutton(format_frame, text="MOBI", variable=self.output_format, value="mobi").pack(side='left')
# 线程数
ttk.Label(self.root, text="并行线程:").grid(row=3, column=0, padx=5, pady=5, sticky='w')
ttk.Spinbox(self.root, from_=1, to=16, textvariable=self.threads, width=5).grid(row=3, column=1, padx=5, pady=5, sticky='w')
# 进度条
self.progress = ttk.Progressbar(self.root, variable=self.progress_var,
maximum=100, length=400)
self.progress.grid(row=4, column=0, columnspan=3, padx=20, pady=20)
# 日志文本框
self.log_text = tk.Text(self.root, height=8, width=70)
self.log_text.grid(row=5, column=0, columnspan=3, padx=20, pady=10)
# 滚动条
scrollbar = ttk.Scrollbar(self.root, command=self.log_text.yview)
scrollbar.grid(row=5, column=3, sticky='ns')
self.log_text.config(yscrollcommand=scrollbar.set)
# 按钮
button_frame = ttk.Frame(self.root)
button_frame.grid(row=6, column=0, columnspan=3, pady=10)
ttk.Button(button_frame, text="开始转换", command=self.start_convert).pack(side='left', padx=10)
ttk.Button(button_frame, text="清空日志", command=self.clear_log).pack(side='left', padx=10)
ttk.Button(button_frame, text="退出", command=self.root.quit).pack(side='left', padx=10)
def select_input(self):
"""选择输入目录"""
directory = filedialog.askdirectory(title="选择文档目录")
if directory:
self.input_dir.set(directory)
def select_output(self):
"""选择输出目录"""
directory = filedialog.askdirectory(title="选择输出目录")
if directory:
self.output_dir.set(directory)
def start_convert(self):
"""开始转换"""
if not self.input_dir.get():
messagebox.showerror("错误", "请选择输入目录")
return
# 禁用转换按钮
self.log("开始转换...")
# 在新线程中执行
thread = threading.Thread(target=self.do_convert)
thread.daemon = True
thread.start()
def do_convert(self):
"""执行转换"""
try:
converter = DocumentConverter(
input_dir=self.input_dir.get(),
output_dir=self.output_dir.get(),
output_format=self.output_format.get()
)
# 覆盖日志方法
def log_callback(msg):
self.root.after(0, self.log, msg)
# 转换文件
files = converter.batch_convert(max_workers=self.threads.get())
self.root.after(0, lambda: self.log(f"转换完成!共处理 {len(files)} 个文件"))
self.root.after(0, lambda: messagebox.showinfo("完成", "转换完成!"))
except Exception as e:
self.root.after(0, lambda: messagebox.showerror("错误", str(e)))
self.root.after(0, lambda: self.log(f"转换失败: {str(e)}"))
def log(self, message):
"""输出日志"""
self.log_text.insert(tk.END, message + "\n")
self.log_text.see(tk.END)
# 更新进度(简单模拟)
self.progress_var.set(min(self.progress_var.get() + 10, 100))
def clear_log(self):
"""清空日志"""
self.log_text.delete(1.0, tk.END)
self.progress_var.set(0)
def run(self):
"""运行主循环"""
self.root.mainloop()
if __name__ == "__main__":
app = ConverterGUI()
app.run()
使用说明
安装依赖
pip install ebooklib beautifulsoup4 pymupdf python-docx tqdm # 如果下载慢,可以使用国内镜像 pip install -i https://pypi.tuna.tsinghua.edu.cn/simple ebooklib beautifulsoup4 pymupdf python-docx tqdm
命令行使用
# 基本用法 python convert_ebook.py -i ./documents -o ./ebooks # 指定输出格式 python convert_ebook.py -i ./docs -o ./epubs --format epub # 增加并行线程 python convert_ebook.py -i ./docs -o ./ebooks --threads 8 # 查看帮助 python convert_ebook.py --help
目录结构示例
documents/ ├── 小说1.txt ├── 说明书.pdf ├── 报告.docx └── 文章.txt # 转换后 output/ ├── 小说1.epub ├── 说明书.epub ├── 报告.epub └── 文章.epub
这个脚本提供了以下功能:
- 多格式支持:TXT、PDF、DOCX
- 多线程处理:并行转换提高效率
- 进度显示:转换过程清晰可见
- 错误处理:单个文件失败不影响其他文件
- 编码识别:自动识别TXT文件编码
- 图形界面(可选)**:方便不熟悉命令行的用户
如果需要支持更多格式或转换到Kindle格式(MOBI),建议安装Calibre并使用其命令行工具配合此脚本使用。