epub通常是用Kindle等黑白阅读器阅读,所以往往不需要图片质量太高,彩色高质量图片往往还体积庞大,有必要瘦身。

因为epub本身就是zip封装,所以最开始的想法也就是将其完整解压出来,替换掉image文件夹中的图片,再封装回去。

但是……

好,首先来解决图片瘦身的问题,采用的方案是灰阶化加缩小:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import os
from PIL import Image
from concurrent.futures import ThreadPoolExecutor
import shutil

def compress_image(img_path, output_dir, quality=30, max_size=800, grayscale=True):
"""压缩单张图片到极致"""
try:
img = Image.open(img_path)

# 转为灰度(节省约30%空间)
if grayscale and img.mode != 'L':
img = img.convert('L')

# 缩小尺寸
if max(img.size) > max_size:
ratio = max_size / max(img.size)
new_size = (int(img.width*ratio), int(img.height*ratio))
img = img.resize(new_size, Image.LANCZOS)

# 获取原始文件名(保持编码不变)
original_filename = os.path.basename(img_path)
output_path = os.path.join(output_dir, original_filename)

# 如果是PNG文件,需要修改扩展名但保持文件名主体不变
if img_path.lower().endswith('.png'):
# 只替换扩展名,保持文件名主体编码不变
name_without_ext = os.path.splitext(original_filename)[0]
output_path = os.path.join(output_dir, name_without_ext + '.jpg')

# 保存图片(关键修改:保持原始文件名编码。这里很重要,因为有些文件名中的特殊字符会被某些程序更改,比如此例中的全角斜杠/会被自动替换为ú»)
img.save(output_path,
quality=quality,
optimize=True,
progressive=True)

print(f"压缩成功: {original_filename}")
except Exception as e:
print(f"压缩失败 {img_path}: {str(e)}")

# 配置参数
input_dir = r"D:\epub_books" # 输入文件夹(解压出来的image文件夹内容)
output_dir = r"D:\epub_compressed" # 输出文件夹(运行该程序后自动生成相同数量的灰阶缩小图片)
quality = 30 # 质量参数(1-100,值越小体积越小)
max_size = 800 # 最大边长(像素)
grayscale = True # 是否转为灰度

# 创建输出目录
os.makedirs(output_dir, exist_ok=True)

# 获取所有图片文件
image_files = []
for root, _, files in os.walk(input_dir):
for file in files:
if file.lower().endswith(('.png', '.jpg', '.jpeg', 'gif', 'webp', 'bmp')):
image_files.append(os.path.join(root, file))

# 多线程批量处理(加速)
with ThreadPoolExecutor(max_workers=4) as executor:
for img_path in image_files:
executor.submit(compress_image, img_path, output_dir, quality, max_size, grayscale)

print("所有图片处理完成!")

上面有一段保持原文件名编码的部分很重要,因为有些文件名中的特殊字符会被某些程序更改,比如此例中的全角斜杠 会被自动替换为 ú»,在压缩文件包中有时就会如此显示(win文件夹中显示正常,因为win会“猜测”修正)。之后的再更名为epub也会如此操作,epub应该不支持某些字符,比如全角斜杠 ,会将它自动替换为 ú»,而文章的html文件中的<img src=仍保留并继续寻找全角斜杠 的地址,所以就会显示不出图片。

如上完成图片的批量瘦身后,用其替换掉解压来的image文件夹中的内容。

然后再将解压出来的epub文件重新封装回去,这里有需要注意的地方。

本来以为只是个zip压缩文件,就丢进惯常所用的winrar中算了,结果在更名epub后直接打不开,提示并非可用的zip文件。

上网查了一下,发现是因为epub中的mimetype文件不可压缩,且必须放在第一个(这个似乎倒无所谓),它大概相当于一个导航式的文件,不能压缩。那么干脆,所有的文件都不压缩了,使用7zip的存储模式,将替换过image内容的epub文件都放入,然后改名epub,可以打开了。

image-20251023073648545

可以打开重新打包的epub进行阅读了。

但然后你会发现,一切都没问题,只是文章中所有图片都显示红叉叉。这种一般都是图片未能加载的问题了,也就是说,<img src=中的图片地址与真实图片地址未能映射一致的问题。

经过仔细查对(在Calibre的Editor中对照html和左侧文件视图中的图片,发现前者是全角斜杠/,后者是ú»),就是前面说的那种文件名编码在转换过程中自动更改的情况。

image-20251023074416232

image-20251023074641613

问题很清楚了,就是在改为epub的过程中,可能是因为epub不支持全角斜杠 的文件名编码,所以全部自动替换成了 ú»。所以,解决方案也有两种,一是改图片文件名,二是改html中的调用地址,于是分别写了两版程序:

  1. 改图片文件名

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    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
    import os
    import zipfile
    import tempfile
    import shutil
    import re
    from pathlib import Path
    import xml.etree.ElementTree as ET
    from bs4 import BeautifulSoup
    import fnmatch

    class EpubSlashFixer:
    """EPUB全角斜杠修复工具 - 统一将 ú» 修复为正确的全角斜杠 /"""

    def __init__(self, epub_path):
    self.epub_path = os.path.abspath(epub_path)
    self.temp_dir = None
    self.fixed_files = 0
    self.fixed_references = 0
    self.processed_files = []

    def extract_epub(self):
    """解压EPUB文件到临时目录"""
    self.temp_dir = tempfile.mkdtemp(prefix="epub_fix_")
    print(f"📦 解压EPUB到临时目录: {self.temp_dir}")

    try:
    with zipfile.ZipFile(self.epub_path, 'r') as zf:
    zf.extractall(self.temp_dir)
    print("✅ EPUb解压完成")
    return True
    except Exception as e:
    print(f"❌ EPUb解压失败: {e}")
    return False

    def find_files_with_mojibake(self):
    """查找所有包含 ú» 的文件和目录"""
    mojibake_files = []
    mojibake_dirs = []

    for root, dirs, files in os.walk(self.temp_dir):
    # 检查目录名
    for dir_name in dirs:
    if 'ú»' in dir_name:
    full_path = os.path.join(root, dir_name)
    mojibake_dirs.append(full_path)

    # 检查文件名
    for file_name in files:
    if 'ú»' in file_name:
    full_path = os.path.join(root, file_name)
    mojibake_files.append(full_path)

    return mojibake_files, mojibake_dirs

    def rename_files_and_dirs(self):
    """重命名所有包含 ú» 的文件和目录"""
    print("🔧 开始重命名文件和目录...")

    files, dirs = self.find_files_with_mojibake()
    total_items = len(files) + len(dirs)

    if total_items == 0:
    print("✅ 未发现需要重命名的文件或目录")
    return True

    print(f"📊 发现 {total_items} 个需要重命名的项目")

    # 先重命名目录(从深到浅)
    dirs.sort(key=lambda x: x.count(os.sep), reverse=True)
    for dir_path in dirs:
    self.rename_item(dir_path, is_dir=True)

    # 重命名文件
    for file_path in files:
    self.rename_item(file_path, is_dir=False)

    print(f"✅ 重命名完成: {self.fixed_files} 个项目已修复")
    return True

    def rename_item(self, old_path, is_dir=False):
    """重命名单个文件或目录"""
    try:
    old_name = os.path.basename(old_path)
    parent_dir = os.path.dirname(old_path)

    # 替换 ú» 为 /
    new_name = old_name.replace('ú»', '/')
    new_path = os.path.join(parent_dir, new_name)

    # 确保新路径不存在
    if not os.path.exists(new_path):
    os.rename(old_path, new_path)
    item_type = "目录" if is_dir else "文件"
    print(f"✅ {item_type}重命名: {old_name}{new_name}")
    self.fixed_files += 1
    self.processed_files.append((old_path, new_path))
    else:
    print(f"⚠️ 跳过: {new_name} 已存在")

    except Exception as e:
    print(f"❌ 重命名失败 {old_path}: {e}")

    def update_content_references(self):
    """更新所有内容文件中的引用"""
    print("🔧 开始更新文件引用...")

    content_files = self.find_content_files()

    for file_path in content_files:
    try:
    with open(file_path, 'r', encoding='utf-8') as f:
    content = f.read()

    # 检查是否包含 ú»
    if 'ú»' in content:
    # 替换所有 ú» 为 /
    new_content = content.replace('ú»', '/')

    with open(file_path, 'w', encoding='utf-8') as f:
    f.write(new_content)

    print(f"📄 更新引用: {os.path.basename(file_path)}")
    self.fixed_references += 1

    except UnicodeDecodeError:
    # 跳过二进制文件
    continue
    except Exception as e:
    print(f"⚠️ 处理文件失败 {file_path}: {e}")

    def find_content_files(self):
    """查找所有需要检查的内容文件"""
    content_extensions = [
    '.html', '.htm', '.xhtml', # HTML文件
    '.css', # 样式表
    '.opf', '.ncx', # EPUB元数据文件
    '.xml', # 其他XML文件
    '.txt', '.js' # 文本和脚本文件
    ]

    content_files = []

    for root, dirs, files in os.walk(self.temp_dir):
    for file in files:
    if any(file.endswith(ext) for ext in content_extensions):
    content_files.append(os.path.join(root, file))

    return content_files

    def update_opf_manifest(self):
    """专门处理OPF文件中的manifest条目"""
    print("🔧 检查OPF文件清单...")

    opf_files = []
    for root, dirs, files in os.walk(self.temp_dir):
    for file in files:
    if file.endswith('.opf'):
    opf_files.append(os.path.join(root, file))

    for opf_file in opf_files:
    try:
    # 读取文件内容
    with open(opf_file, 'r', encoding='utf-8') as f:
    content = f.read()

    # 检查是否包含 ú»
    if 'ú»' in content:
    # 替换所有 ú» 为 /
    new_content = content.replace('ú»', '/')

    with open(opf_file, 'w', encoding='utf-8') as f:
    f.write(new_content)

    print(f"📋 更新OPF清单: {os.path.basename(opf_file)}")

    except Exception as e:
    print(f"❌ 处理OPF文件失败 {opf_file}: {e}")

    def repack_epub(self, output_path):
    """重新打包EPUB文件"""
    print(f"📦 重新打包EPUB: {output_path}")

    try:
    # 确保输出目录存在
    os.makedirs(os.path.dirname(output_path), exist_ok=True)

    with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as epub:
    # 首先添加mimetype(必须无压缩)
    mimetype_path = os.path.join(self.temp_dir, 'mimetype')
    if os.path.exists(mimetype_path):
    epub.write(mimetype_path, 'mimetype', zipfile.ZIP_STORED)
    print("📄 添加mimetype文件")

    # 添加其他所有文件
    for root, dirs, files in os.walk(self.temp_dir):
    for file in files:
    if file != 'mimetype': # mimetype已经处理过了
    file_path = os.path.join(root, file)
    # 计算相对路径
    arcname = os.path.relpath(file_path, self.temp_dir)
    epub.write(file_path, arcname)

    print("✅ EPUb重新打包完成")
    return True

    except Exception as e:
    print(f"❌ EPUb打包失败: {e}")
    return False

    def cleanup(self):
    """清理临时文件"""
    if self.temp_dir and os.path.exists(self.temp_dir):
    shutil.rmtree(self.temp_dir)
    print("🧹 清理临时文件")

    def run(self, output_path=None):
    """运行完整的修复流程"""
    if not os.path.exists(self.epub_path):
    print(f"❌ 文件不存在: {self.epub_path}")
    return False

    if not output_path:
    # 默认输出路径:原文件名_fixed.epub
    base_dir = os.path.dirname(self.epub_path)
    base_name = os.path.splitext(os.path.basename(self.epub_path))[0]
    output_path = os.path.join(base_dir, f"{base_name}_fixed.epub")

    print(f"🔄 开始修复EPUB: {self.epub_path}")
    print("=" * 60)

    # 解压EPUB
    if not self.extract_epub():
    return False

    try:
    # 执行修复步骤
    self.rename_files_and_dirs() # 重命名文件和目录
    self.update_content_references() # 更新内容文件引用
    self.update_opf_manifest() # 更新OPF清单

    # 重新打包
    success = self.repack_epub(output_path)

    if success:
    print("\n🎯 修复完成!")
    print("=" * 40)
    print(f"📊 修复统计:")
    print(f" 重命名项目: {self.fixed_files}")
    print(f" 更新引用: {self.fixed_references}")
    print(f" 输出文件: {output_path}")

    return success

    except Exception as e:
    print(f"❌ 修复过程中出错: {e}")
    return False
    finally:
    self.cleanup()


    class BatchEpubFixer:
    """批量EPUB修复工具"""

    def __init__(self, directory):
    self.directory = os.path.abspath(directory)
    self.processed_count = 0
    self.failed_files = []

    def find_epub_files(self):
    """查找目录中的所有EPUB文件"""
    epub_files = []
    for root, dirs, files in os.walk(self.directory):
    for file in files:
    if file.lower().endswith('.epub'):
    epub_files.append(os.path.join(root, file))
    return epub_files

    def process_all(self):
    """批量处理所有EPUB文件"""
    epub_files = self.find_epub_files()

    if not epub_files:
    print("❌ 未找到EPUB文件")
    return

    print(f"🔍 找到 {len(epub_files)} 个EPUB文件")

    for epub_file in epub_files:
    print(f"\n{'='*60}")
    print(f"🔄 处理: {os.path.basename(epub_file)}")

    try:
    fixer = EpubSlashFixer(epub_file)
    if fixer.run():
    self.processed_count += 1
    print(f"✅ 完成: {os.path.basename(epub_file)}")
    else:
    self.failed_files.append(epub_file)
    print(f"❌ 失败: {os.path.basename(epub_file)}")

    except Exception as e:
    error_msg = f"处理失败 {epub_file}: {e}"
    print(f"❌ {error_msg}")
    self.failed_files.append((epub_file, error_msg))

    # 输出统计
    print(f"\n{'='*60}")
    print("📊 批量处理完成!")
    print(f"✅ 成功处理: {self.processed_count}")
    print(f"❌ 失败: {len(self.failed_files)}")

    if self.failed_files:
    print("\n失败文件:")
    for item in self.failed_files:
    if isinstance(item, tuple):
    print(f" - {os.path.basename(item[0])}: {item[1]}")
    else:
    print(f" - {os.path.basename(item)}")


    def main():
    """主函数"""
    print("📚 EPUB全角斜杠修复工具")
    print("=" * 60)
    print("功能: 将EPUB中的 ú» 统一修复为正确的全角斜杠 /")
    print("=" * 60)

    print("请选择操作模式:")
    print("1. 修复单个EPUB文件")
    print("2. 批量修复目录中的所有EPUB文件")
    print("3. 退出")

    while True:
    choice = input("\n请输入选择 (1-3): ").strip()

    if choice == '1':
    epub_path = input("请输入EPUB文件路径: ").strip()

    if not os.path.exists(epub_path):
    print("❌ 文件不存在")
    continue

    if not epub_path.lower().endswith('.epub'):
    print("❌ 文件不是EPUB格式")
    continue

    output_path = input("请输入输出文件路径(直接回车使用默认): ").strip()
    if not output_path:
    output_path = None

    fixer = EpubSlashFixer(epub_path)
    success = fixer.run(output_path)

    if success:
    print("🎉 修复完成!")
    else:
    print("❌ 修复失败")

    elif choice == '2':
    directory = input("请输入包含EPUB文件的目录路径: ").strip()

    if not os.path.exists(directory):
    print("❌ 目录不存在")
    continue

    # 确认操作
    confirm = input("确定要批量处理所有EPUB文件吗? (y/n): ").strip().lower()
    if confirm != 'y':
    print("操作已取消")
    continue

    batch_fixer = BatchEpubFixer(directory)
    batch_fixer.process_all()

    elif choice == '3':
    print("👋 再见!")
    break

    else:
    print("❌ 无效选择,请重新输入")


    if __name__ == "__main__":
    try:
    main()
    except KeyboardInterrupt:
    print("\n👋 用户中断操作")
    except Exception as e:
    print(f"❌ 程序执行出错: {e}")
  2. 改html中的调用地址

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    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
    import os
    import zipfile
    import tempfile
    import shutil
    import re
    from bs4 import BeautifulSoup
    import xml.etree.ElementTree as ET

    class EpubReferenceFixer:
    """EPUB引用修复工具 - 只修复HTML中的图片引用(/ → ú»)"""

    def __init__(self, epub_path):
    self.epub_path = os.path.abspath(epub_path)
    self.temp_dir = None
    self.fixed_references = 0
    self.processed_files = []

    def extract_epub(self):
    """解压EPUB文件到临时目录"""
    self.temp_dir = tempfile.mkdtemp(prefix="epub_ref_fix_")
    print(f"📦 解压EPUB到临时目录: {self.temp_dir}")

    try:
    with zipfile.ZipFile(self.epub_path, 'r') as zf:
    zf.extractall(self.temp_dir)
    print("✅ EPUb解压完成")
    return True
    except Exception as e:
    print(f"❌ EPUb解压失败: {e}")
    return False

    def find_html_files(self):
    """查找所有HTML文件"""
    html_files = []
    for root, dirs, files in os.walk(self.temp_dir):
    for file in files:
    if file.lower().endswith(('.html', '.htm', '.xhtml')):
    html_files.append(os.path.join(root, file))
    return html_files

    def fix_html_references(self):
    """修复HTML文件中的图片引用(/ → ú»)"""
    print("🔧 开始修复HTML文件引用...")

    html_files = self.find_html_files()

    for html_file in html_files:
    try:
    # 读取HTML文件
    with open(html_file, 'r', encoding='utf-8') as f:
    content = f.read()

    # 使用BeautifulSoup解析
    soup = BeautifulSoup(content, 'html.parser')
    updated = False

    # 修复所有img标签的src属性
    for img in soup.find_all('img'):
    src = img.get('src', '')
    if src and '/' in src:
    new_src = src.replace('/', 'ú»')
    img['src'] = new_src
    print(f"🖼️ 修复图片引用: {src}{new_src}")
    updated = True
    self.fixed_references += 1

    # 修复其他可能包含图片引用的标签
    for tag in soup.find_all(['a', 'link']):
    href = tag.get('href', '')
    if href and '/' in href and any(href.lower().endswith(ext) for ext in ['.jpg', '.jpeg', '.png', '.gif', '.webp']):
    new_href = href.replace('/', 'ú»')
    tag['href'] = new_href
    print(f"🔗 修复链接引用: {href}{new_href}")
    updated = True
    self.fixed_references += 1

    # 修复CSS背景图片
    for tag in soup.find_all(style=True):
    style = tag['style']
    if '/' in style:
    new_style = style.replace('/', 'ú»')
    tag['style'] = new_style
    updated = True
    self.fixed_references += 1

    if updated:
    # 保存更新后的内容
    with open(html_file, 'w', encoding='utf-8') as f:
    f.write(str(soup))

    self.processed_files.append(os.path.basename(html_file))
    print(f"✅ 更新HTML文件: {os.path.basename(html_file)}")

    except Exception as e:
    print(f"⚠️ 处理HTML文件失败 {html_file}: {e}")

    def fix_css_references(self):
    """修复CSS文件中的图片引用"""
    print("🔧 检查CSS文件引用...")

    for root, dirs, files in os.walk(self.temp_dir):
    for file in files:
    if file.lower().endswith('.css'):
    css_file = os.path.join(root, file)
    try:
    with open(css_file, 'r', encoding='utf-8') as f:
    content = f.read()

    if '/' in content:
    new_content = content.replace('/', 'ú»')
    with open(css_file, 'w', encoding='utf-8') as f:
    f.write(new_content)

    print(f"🎨 修复CSS文件: {file}")
    self.fixed_references += content.count('/')

    except Exception as e:
    print(f"⚠️ 跳过CSS文件 {css_file}: {e}")

    def repack_epub(self, output_path):
    """重新打包EPUB文件"""
    print(f"📦 重新打包EPUB: {output_path}")

    try:
    # 确保输出目录存在
    os.makedirs(os.path.dirname(output_path), exist_ok=True)

    with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as epub:
    # 首先添加mimetype(必须无压缩)
    mimetype_path = os.path.join(self.temp_dir, 'mimetype')
    if os.path.exists(mimetype_path):
    epub.write(mimetype_path, 'mimetype', zipfile.ZIP_STORED)

    # 添加其他所有文件(保持原样)
    for root, dirs, files in os.walk(self.temp_dir):
    for file in files:
    if file != 'mimetype':
    file_path = os.path.join(root, file)
    arcname = os.path.relpath(file_path, self.temp_dir)
    epub.write(file_path, arcname)

    print("✅ EPUb重新打包完成")
    return True

    except Exception as e:
    print(f"❌ EPUb打包失败: {e}")
    return False

    def cleanup(self):
    """清理临时文件"""
    if self.temp_dir and os.path.exists(self.temp_dir):
    shutil.rmtree(self.temp_dir)
    print("🧹 清理临时文件")

    def run(self, output_path=None):
    """运行完整的修复流程"""
    if not os.path.exists(self.epub_path):
    print(f"❌ 文件不存在: {self.epub_path}")
    return False

    if not output_path:
    # 默认输出路径:原文件名_fixed.epub
    base_dir = os.path.dirname(self.epub_path)
    base_name = os.path.splitext(os.path.basename(self.epub_path))[0]
    output_path = os.path.join(base_dir, f"{base_name}_fixed.epub")

    print(f"🔄 开始修复EPUB引用: {self.epub_path}")
    print("=" * 60)
    print("修复策略: 只修改HTML/CSS中的引用(/ → ú»),保持文件名不变")
    print("=" * 60)

    # 解压EPUB
    if not self.extract_epub():
    return False

    try:
    # 执行修复步骤
    self.fix_html_references() # 修复HTML引用
    self.fix_css_references() # 修复CSS引用

    # 重新打包
    success = self.repack_epub(output_path)

    if success:
    print("\n🎯 修复完成!")
    print("=" * 40)
    print(f"📊 修复统计:")
    print(f" 修复引用数量: {self.fixed_references}")
    print(f" 处理的HTML文件: {len(self.processed_files)}")
    if self.processed_files:
    print(f" 文件列表: {', '.join(self.processed_files)}")
    print(f" 输出文件: {output_path}")
    print("\n💡 提示: 文件名保持原样,只修改了引用路径")

    return success

    except Exception as e:
    print(f"❌ 修复过程中出错: {e}")
    return False
    finally:
    self.cleanup()


    class BatchEpubReferenceFixer:
    """批量EPUB引用修复工具"""

    def __init__(self, directory):
    self.directory = os.path.abspath(directory)
    self.processed_count = 0
    self.failed_files = []
    self.total_fixed = 0

    def find_epub_files(self):
    """查找目录中的所有EPUB文件"""
    epub_files = []
    for root, dirs, files in os.walk(self.directory):
    for file in files:
    if file.lower().endswith('.epub'):
    epub_files.append(os.path.join(root, file))
    return epub_files

    def process_all(self):
    """批量处理所有EPUB文件"""
    epub_files = self.find_epub_files()

    if not epub_files:
    print("❌ 未找到EPUB文件")
    return

    print(f"🔍 找到 {len(epub_files)} 个EPUB文件")

    for epub_file in epub_files:
    print(f"\n{'='*60}")
    print(f"🔄 处理: {os.path.basename(epub_file)}")

    try:
    fixer = EpubReferenceFixer(epub_file)
    if fixer.run():
    self.processed_count += 1
    self.total_fixed += fixer.fixed_references
    print(f"✅ 完成: {os.path.basename(epub_file)} (修复 {fixer.fixed_references} 处引用)")
    else:
    self.failed_files.append(epub_file)
    print(f"❌ 失败: {os.path.basename(epub_file)}")

    except Exception as e:
    error_msg = f"处理失败 {epub_file}: {e}"
    print(f"❌ {error_msg}")
    self.failed_files.append((epub_file, error_msg))

    # 输出统计
    print(f"\n{'='*60}")
    print("📊 批量处理完成!")
    print(f"✅ 成功处理: {self.processed_count} 个文件")
    print(f"🔧 总共修复: {self.total_fixed} 处引用")
    print(f"❌ 失败: {len(self.failed_files)} 个文件")

    if self.failed_files:
    print("\n失败文件:")
    for item in self.failed_files:
    if isinstance(item, tuple):
    print(f" - {os.path.basename(item[0])}: {item[1]}")
    else:
    print(f" - {os.path.basename(item)}")


    def quick_fix_single(epub_path, output_path=None):
    """快速修复单个EPUB文件"""
    fixer = EpubReferenceFixer(epub_path)
    return fixer.run(output_path)


    def main():
    """主函数"""
    print("📚 EPUB引用修复工具")
    print("=" * 60)
    print("功能: 只修复HTML/CSS中的图片引用(/ → ú»),保持文件名不变")
    print("适用: EPUB标准不支持全角斜杠,只能使用URL编码")
    print("=" * 60)

    print("请选择操作模式:")
    print("1. 修复单个EPUB文件")
    print("2. 批量修复目录中的所有EPUB文件")
    print("3. 退出")

    while True:
    choice = input("\n请输入选择 (1-3): ").strip()

    if choice == '1':
    epub_path = input("请输入EPUB文件路径: ").strip()

    if not os.path.exists(epub_path):
    print("❌ 文件不存在")
    continue

    if not epub_path.lower().endswith('.epub'):
    print("❌ 文件不是EPUB格式")
    continue

    output_path = input("请输入输出文件路径(直接回车使用默认): ").strip()
    if not output_path:
    output_path = None

    success = quick_fix_single(epub_path, output_path)

    if success:
    print("🎉 修复完成!")
    else:
    print("❌ 修复失败")

    elif choice == '2':
    directory = input("请输入包含EPUB文件的目录路径: ").strip()

    if not os.path.exists(directory):
    print("❌ 目录不存在")
    continue

    # 确认操作
    confirm = input("确定要批量处理所有EPUB文件吗? (y/n): ").strip().lower()
    if confirm != 'y':
    print("操作已取消")
    continue

    batch_fixer = BatchEpubReferenceFixer(directory)
    batch_fixer.process_all()

    elif choice == '3':
    print("👋 再见!")
    break

    else:
    print("❌ 无效选择,请重新输入")


    if __name__ == "__main__":
    try:
    main()
    except KeyboardInterrupt:
    print("\n👋 用户中断操作")
    except Exception as e:
    print(f"❌ 程序执行出错: {e}")

**注意:**以上两种方法,只选其一即可,两个都用就又回到原点了——地址仍不一致!


上面是对原epub文件不做更改的方法。因为原epub文件已经做好了图片与html文章的对应,不能贸然更改图片名称。这种方法对原epub的各种内容和顺序几乎没有更改。下面还有一种可重新将html文章与图片进行组织对应的小程序,它会对原epub的内容顺序进行重构,重新生成一个epub:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
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
import os
import zipfile
import uuid
import shutil
from datetime import datetime

class InteractiveEPUBCreator:
def __init__(self):
print("=" * 50)
print(" EPUB 文件生成工具")
print("=" * 50)

def get_user_input(self):
"""获取用户输入"""
print("\n请提供以下信息:")

# 获取书籍信息
self.title = input("1. 书籍标题: ").strip() or "未命名书籍"
self.author = input("2. 作者姓名: ").strip() or "未知作者"

# 获取文件夹路径
print("\n请指定文件夹路径:")
while True:
self.html_dir = input("3. HTML文件所在文件夹路径: ").strip()
if os.path.exists(self.html_dir):
break
print("❌ 文件夹不存在,请重新输入!")

while True:
self.image_dir = input("4. 图片文件所在文件夹路径: ").strip()
if os.path.exists(self.image_dir):
break
print("❌ 文件夹不存在,请重新输入!")

# 获取输出路径
self.output_epub = input("5. 输出EPUB文件路径(如: my_book.epub): ").strip()
if not self.output_epub:
self.output_epub = "output_book.epub"

# 创建临时工作目录
self.work_dir = "temp_epub_build"
os.makedirs(self.work_dir, exist_ok=True)

def scan_files(self):
"""扫描HTML和图片文件"""
print("\n🔍 扫描文件...")

# 扫描HTML文件
self.html_files = []
for file in os.listdir(self.html_dir):
if file.lower().endswith(('.html', '.xhtml', '.htm')):
self.html_files.append(file)
print(f" 发现HTML: {file}")

if not self.html_files:
print("❌ 在HTML文件夹中未找到任何HTML文件!")
return False

# 扫描图片文件
self.image_files = []
image_exts = ('.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg')
for file in os.listdir(self.image_dir):
if file.lower().endswith(image_exts):
self.image_files.append(file)
print(f" 发现图片: {file}")

print(f"✅ 找到 {len(self.html_files)} 个HTML文件和 {len(self.image_files)} 个图片文件")
return True

def create_epub_structure(self):
"""创建EPUB文件结构"""
print("\n📁 创建EPUB结构...")

# 创建必要目录
epub_dirs = [
os.path.join(self.work_dir, 'META-INF'),
os.path.join(self.work_dir, 'OEBPS', 'Images'),
os.path.join(self.work_dir, 'OEBPS', 'Text'),
os.path.join(self.work_dir, 'OEBPS', 'Styles')
]

for dir_path in epub_dirs:
os.makedirs(dir_path, exist_ok=True)

# 创建mimetype文件
mimetype_path = os.path.join(self.work_dir, 'mimetype')
with open(mimetype_path, 'w', encoding='utf-8') as f:
f.write('application/epub+zip')
print("✅ 创建 mimetype 文件")

def copy_content_files(self):
"""复制内容文件到EPUB结构"""
print("\n📄 复制内容文件...")

# 复制HTML文件
html_count = 0
for html_file in self.html_files:
src = os.path.join(self.html_dir, html_file)
dst = os.path.join(self.work_dir, 'OEBPS', 'Text', html_file)
shutil.copy2(src, dst)
html_count += 1

# 复制图片文件
image_count = 0
for image_file in self.image_files:
src = os.path.join(self.image_dir, image_file)
dst = os.path.join(self.work_dir, 'OEBPS', 'Images', image_file)
shutil.copy2(src, dst)
image_count += 1

print(f"✅ 复制了 {html_count} 个HTML文件和 {image_count} 个图片文件")

def create_container_xml(self):
"""创建container.xml"""
content = '''<?xml version="1.0" encoding="UTF-8"?>
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
<rootfiles>
<rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/>
</rootfiles>
</container>'''

container_path = os.path.join(self.work_dir, 'META-INF', 'container.xml')
with open(container_path, 'w', encoding='utf-8') as f:
f.write(content)
print("✅ 创建 container.xml")

def create_content_opf(self):
"""创建content.opf文件"""
book_id = f"urn:uuid:{uuid.uuid4()}"
current_time = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")

manifest_items = []
spine_items = []

# 添加导航
manifest_items.append('<item id="nav" href="toc.ncx" media-type="application/x-dtbncx+xml"/>')

# 添加图片到manifest
for i, img_file in enumerate(self.image_files):
ext = os.path.splitext(img_file)[1].lower()
media_type = 'image/jpeg' if ext in ['.jpg', '.jpeg'] else 'image/png'
if ext == '.gif':
media_type = 'image/gif'
elif ext == '.svg':
media_type = 'image/svg+xml'

manifest_items.append(f'<item id="image{i+1}" href="Images/{img_file}" media-type="{media_type}"/>')

# 添加HTML章节到manifest和spine
for i, html_file in enumerate(self.html_files):
manifest_items.append(f'<item id="chapter{i+1}" href="Text/{html_file}" media-type="application/xhtml+xml"/>')
spine_items.append(f'<itemref idref="chapter{i+1}" linear="yes"/>')

content = f'''<?xml version="1.0" encoding="UTF-8"?>
<package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="BookId">
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
<dc:identifier id="BookId">{book_id}</dc:identifier>
<dc:title>{self.title}</dc:title>
<dc:creator>{self.author}</dc:creator>
<dc:language>zh-CN</dc:language>
<meta property="dcterms:modified">{current_time}</meta>
</metadata>
<manifest>
{"".join(manifest_items)}
</manifest>
<spine>
{"".join(spine_items)}
</spine>
</package>'''

opf_path = os.path.join(self.work_dir, 'OEBPS', 'content.opf')
with open(opf_path, 'w', encoding='utf-8') as f:
f.write(content)
print("✅ 创建 content.opf")

def create_toc_ncx(self):
"""创建toc.ncx文件"""
content = f'''<?xml version="1.0" encoding="UTF-8"?>
<ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1">
<head>
<meta name="dtb:uid" content="urn:uuid:{uuid.uuid4()}"/>
</head>
<docTitle><text>{self.title}</text></docTitle>
<navMap>'''

# 为每个HTML文件创建导航点
for i, html_file in enumerate(self.html_files):
chapter_name = os.path.splitext(html_file)[0]
content += f'''
<navPoint id="chapter{i+1}" playOrder="{i+1}">
<navLabel><text>{chapter_name}</text></navLabel>
<content src="Text/{html_file}"/>
</navPoint>'''

content += '''
</navMap>
</ncx>'''

toc_path = os.path.join(self.work_dir, 'OEBPS', 'toc.ncx')
with open(toc_path, 'w', encoding='utf-8') as f:
f.write(content)
print("✅ 创建 toc.ncx")

def build_epub(self):
"""构建最终的EPUB文件"""
print(f"\n🏗️ 正在构建EPUB文件: {self.output_epub}")

with zipfile.ZipFile(self.output_epub, 'w', compression=zipfile.ZIP_DEFLATED) as epub:
# 首先添加mimetype(无压缩)
mimetype_path = os.path.join(self.work_dir, 'mimetype')
epub.write(mimetype_path, 'mimetype', compress_type=zipfile.ZIP_STORED)

# 添加其他所有文件
for root, dirs, files in os.walk(self.work_dir):
for file in files:
if file == 'mimetype':
continue
file_path = os.path.join(root, file)
arcname = os.path.relpath(file_path, self.work_dir)
epub.write(file_path, arcname)

print(f"✅ EPUB文件已创建: {self.output_epub}")

# 显示文件信息
file_size = os.path.getsize(self.output_epub) / 1024 / 1024
print(f"📊 文件大小: {file_size:.2f} MB")

def cleanup(self):
"""清理临时文件"""
if os.path.exists(self.work_dir):
shutil.rmtree(self.work_dir)
print("🧹 清理临时文件")

def run(self):
"""运行整个流程"""
try:
# 步骤1: 获取用户输入
self.get_user_input()

# 步骤2: 扫描文件
if not self.scan_files():
return

# 确认继续
confirm = input("\n是否继续创建EPUB?(y/n): ").strip().lower()
if confirm != 'y':
print("操作已取消")
return

# 步骤3: 创建EPUB结构
self.create_epub_structure()
self.copy_content_files()
self.create_container_xml()
self.create_content_opf()
self.create_toc_ncx()

# 步骤4: 构建EPUB
self.build_epub()

# 步骤5: 清理
self.cleanup()

print("\n🎉 EPUB生成完成!")
print("您可以使用Calibre或其他电子书阅读器打开验证。")

except Exception as e:
print(f"❌ 发生错误: {str(e)}")
self.cleanup()

# 运行程序
if __name__ == "__main__":
creator = InteractiveEPUBCreator()
creator.run()