用python将txt转换epub
群同构
2025年05月17日 21:50

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)