txt小说没有目录结构,小说阅读器的目录通过正则语句判断,会有遗漏和误判,所以我习惯将txt小说转换成md文件来获得准确的目录。但是md文件存在问题,主要在于阅读器的适配上,大部分的小说阅读器不会渲染md格式的文件,md的编辑器对于小说的阅读体验不算好,所以再将md转换成epub格式。这样在各种小说阅读器都获得良好的体验。
一开始使用pandoc,还有一些其他人写的python脚本,效果都很难让我满意。最后决定自己写一份符合自己要求的脚本,使用markdown,ebooklib着两个python库。
逃不掉的步骤是将txt文件的后缀名改为md,然后对目录进行手动标注,我一般是将目录设置成md的二级标题。也可以在小说中添加图片,在yaml中设置书名,作者和封面图地址。然后使用python脚本将md转换成epub
import os
import re from ebooklib import epub from markdown import markdown css_content = ''' /* ========================== */ /* 段落通用设置 */ /* ========================== */ p { font-size: 1rem; text-indent: 2em; /* 保留首行缩进 */ margin-bottom: 1.25rem; /* 用下边距分隔段落 */ text-align: justify; } /* ========================== */ /* 标题层级系统 */ /* ========================== */ /* 基于黄金比例的字号系统 */ h1 { font-size: 3.289rem; /* 约50px */ font-weight: 800; line-height: 1.2; margin: 3rem 0 1.5rem; } h2 { font-size: 2.074rem; /* 约33px */ font-weight: 700; line-height: 1.3; margin: 2rem 0 1rem; } h3 { font-size: 1.728rem; /* 约28px */ font-weight: 600; margin: 1.5rem 0 0.75rem; } h4 { font-size: 1.44rem; /* 约23px */ font-weight: 500; margin: 1.25rem 0 0.5rem; } h5 { font-size: 1.2rem; /* 约19px */ font-weight: 500; margin: 1rem 0 0.25rem; } h6 { font-size: 1rem; /* 16px */ font-weight: 500; margin: 0.75rem 0; } ''' class MarkdownToEpub: """ 将Markdown文件转换为EPUB电子书的类。 参数: - md_file_path: Markdown文件的路径。 - epub_file_path: 生成的EPUB文件的保存路径。 - title: 电子书的标题,默认为'未知'。 - author: 电子书的作者,默认为'佚名'。 - cover_path: 电子书的封面图片路径,默认为None。 (入参不指定时,下面参数可以从文件的yaml读取: title: 电子书的标题 author: 电子书的作者 cover: 电子书的封面图片路径) - css_content: 电子书的CSS样式内容。 """ def __init__(self, md_file_path, epub_file_path, title='未知', author='佚名', cover_path=None, css_content=css_content): # Markdown文件路径 self.md_file_path = md_file_path # EPUB文件路径 self.epub_file_path = epub_file_path # 电子书标题 self.title = title # 电子书作者 self.author = author # 封面图片路径 self.cover_path = cover_path # Markdown内容初始化为空字符串 self.md_content = '' # Markdown子内容列表初始化为空列表 self.md_sub_content = [] # 创建EpubBook实例 self.book = epub.EpubBook() # CSS样式内容 self.css_content = css_content # 图片映射字典,用于记录图片的原始路径和在EPUB中的相对路径 self.image_map = {} # 读取md文件 def read_md(self): with open(self.md_file_path, 'r', encoding='utf-8') as f: self.md_content = f.read() # 获取标题和作者 def get_yaml(self): matchObj = re.match(r'^---(.*?)---\n(.*)$', self.md_content, re.DOTALL) if matchObj is not None: yaml = matchObj.group(1) self.md_content = matchObj.group(2) else: yaml = '' if self.title == '未知' and (re.search(r'书名 *: *(.+)', yaml) is not None): if (title := re.search(r'书名 *: *(.+)', yaml).group(1)) is not None: self.title = title.strip() else: self.title = os.path.splitext( os.path.basename(self.md_file_path))[0] if self.author == '佚名' and (re.search(r'作者 *: *(.+)', yaml) is not None): if (author := re.search(r'作者 *: *(.+)', yaml).group(1)) is not None: self.author = author if (not self.cover_path) and (re.search(r'封面 *: *(.+)', yaml) is not None): cover_path = re.search(r'封面 *: *(.+)', yaml).group(1) # 检查封面路径是否为相对路径,并转换为绝对路径 if not os.path.isabs(cover_path): cover_path = os.path.join( os.path.dirname(self.md_file_path), cover_path) # 判断cover_path是否为图片格式的文件,如果不是图片格式的文件,不设置封面 if os.path.isfile(cover_path): if cover_path.endswith(('.jpg', '.jpeg', '.png', '.gif')): self.cover_path = cover_path def split_md(self): md_content = self.md_content # 添加默认标题 if not re.match(r'^#+ .*', md_content): md_content = "## 正文\n" + md_content # 获取标题列表 title_list = re.findall(r'^#+ .*', md_content, re.MULTILINE) sub_chapter = [] first_flag = True for sub_title in title_list: _, md_content = md_content.split(sub_title, 1) if first_flag: first_flag = False continue sub_chapter.append(_) sub_chapter.append(md_content) if len(sub_chapter) != len(title_list): raise ValueError('标题数量与内容数量不一致') for i in range(len(sub_chapter)): self.md_sub_content.append( { 'title': re.match(r'^#+ (.*)', title_list[i]).group(1).strip(), 'content': title_list[i]+sub_chapter[i], 'level': title_list[i].find(' '), } ) if min([chapter['level'] for chapter in self.md_sub_content]) <= 1: raise ValueError('不支持一级标题') def _add_image(self): # 添加图片到EPUB书籍 image_pattern = re.compile(r'!\[.*?\]\((.*?)\)') image_files = set() for chapter in self.md_sub_content: for match in image_pattern.finditer(chapter['content']): image_path = match.group(1) # 检查图片路径是否为相对路径,并转换为绝对路径 old_image_path = image_path if not os.path.isabs(image_path): image_path = os.path.join( os.path.dirname(self.md_file_path), image_path) if os.path.isfile(image_path): if image_path.endswith(('.jpg', '.jpeg', '.png', '.gif')): image_files.add(image_path) image_name = os.path.basename(image_path) # 记录原始路径和EPUB中的相对路径 self.image_map[old_image_path] = f'images/{image_name}' for image_path in image_files: image_name = os.path.basename(image_path) image_item = epub.EpubImage( uid=image_name, file_name=f'images/{image_name}', media_type='image/png' if image_name.endswith( '.png') else 'image/jpeg' ) with open(image_path, 'rb') as f: image_item.content = f.read() self.book.add_item(image_item) def _build_toc(self): sections = [] for idx, sub_content in enumerate(self.md_sub_content, 1): if sub_content["level"] == 2: parent_node = None else: if sub_content["level"] == sections[-1]["level"]: parent_node = sections[-1]["parent_node"] elif sub_content["level"] > sections[-1]["level"]: parent_node = sections[-1]["node"] else: for j in range(len(sections)-1, -1, -1): if sub_content["level"] > sections[j]["level"]: parent_node = sections[j]["node"] break sections.append( { "node": epub.Link(href=f'chap_{idx}.xhtml', title=sub_content['title'], uid=f'chapter_{idx}'), "level": sub_content["level"], "parent_node": parent_node } ) def add_to_toc(sections, parent_node=None): toc = [] sub_sections = [ sub_section for sub_section in sections if sub_section["parent_node"] is parent_node] for section in sub_sections: sub2_sections = [ sub2_section for sub2_section in sections if sub2_section["parent_node"] is section["node"]] if sub2_sections == []: toc.append(section['node']) else: sub_toc = [] sub_toc.append(section['node']) sub_toc.append(add_to_toc(sections, section['node'])) toc.append(sub_toc) return tuple(toc) self.book.toc = add_to_toc(sections) def write_epub(self): # 设置书籍信息 self.book.set_identifier('id_' + str(os.urandom(16))) self.book.set_language('zh') self.book.set_title(self.title) self.book.add_author(self.author) if self.cover_path: self.book.set_cover('cover.jpg', open( self.cover_path, 'rb').read()) if self.css_content: self.css_item = epub.EpubItem( uid="style_css", file_name="style.css", media_type="text/css", content=self.css_content ) self.book.add_item(self.css_item) # 添加图片 self._add_image() # 添加标题页面 epub_chapter = epub.EpubHtml( title=f"Chapter title", file_name=f'chap_title.xhtml', lang='zh', content="<p style=\"font-size: 2rem;font-weight: 800;line-height: 1.2;" + f"text-align: center;margin: 2rem auto 0;\">{self.title}</p>" + "<br />" + "<p style=\"font-size: 1.5rem;font-weight: 600;""line-height: 1.2;" + f"text-align: center;\">{self.author}</p>" ) self.book.add_item(epub_chapter) self.book.spine.append(epub_chapter) # 转换并添加所有章节 for idx, chapter in enumerate(self.md_sub_content, 1): html_content = markdown(chapter['content']) # 替换图片路径为EPUB中的相对路径 for original_path, epub_path in self.image_map.items(): html_content = html_content.replace(original_path, epub_path) epub_chapter = epub.EpubHtml( title=f"Chapter {idx}", file_name=f'chap_{idx}.xhtml', lang='zh', content=html_content ) if self.css_item: epub_chapter.add_item(self.css_item) self.book.add_item(epub_chapter) self.book.spine.append(epub_chapter) # 创建目录 self._build_toc() # 添加导航文件 self.book.add_item(epub.EpubNcx()) self.book.add_item(epub.EpubNav()) # 生成EPUB文件,显式设置ignore_ncx选项 epub.write_epub(self.epub_file_path, self.book, {'ignore_ncx': False}) print("-" * 40) print(f"EPUB文件已生成:{self.epub_file_path}") def build_epub(self): self.read_md() self.get_yaml() self.split_md() self.write_epub() def main(input_path): # 检查输入路径是文件夹还是文件 if os.path.isdir(input_path): for root, dirs, files in os.walk(input_path): for file in files: if file.endswith(('.md','.txt','.MD','.TXT')): md_file_path = os.path.join(root, file) epub_file_path = os.path.splitext(md_file_path)[0] + ".epub" MarkdownToEpub(md_file_path, epub_file_path).write_epub() elif os.path.isfile(input_path) and input_path.endswith(('.md','.txt','.MD','.TXT')): # 如果是单个Markdown文件,直接处理 epub_file_path = os.path.splitext(input_path)[0] + ".epub" MarkdownToEpub(input_path, epub_file_path).write_epub() else: print("输入路径无效,请输入有效的Markdown文件路径或文件夹路径。") if __name__ == '__main__': input_path = input("请输入Markdown文件路径或文件夹路径: ").strip('"') main(input_path)