分享

PDF文档转换成mobi格式(for kindle),并解决排版问题

 阮朝阳的图书馆 2022-03-04

0. 前言

正式介绍之前,先回答下面几个问题:
在这里插入图片描述

1. 为什么要将PDF转换成mobi?
想要将PDF转换成mobi格式,初衷在于想在kindle上面看一些从网上获取到的PDF文档。直接将PDF导入kindle本来也可以,但是效果不是很好——要么竖着看,但是字体很小;要么横着看,字体会大一些,但是总感觉比较别扭,而且PDF的一页需要在kindle上翻3页。
kindle支持azw3、mobi等格式,但是不支持直接将azw3格式的文档直接导入到kindle,所以需要将PDF文档转换成mobi格式

2. 为什么不直接用在线转换工具?
其实网上有很多工具支持将PDF转换成mobi格式,但是效果都很差:

  1. 章节标题和正文内容没有区别;
  2. 正文内容格式混乱,在kindle上看是以PDF的一行进行的分段,行首也没有空格
    在这里插入图片描述

3. 将PDF转换成mobi格式,我大概怎么做?
将PDF转换成mobi格式,我主要是借助于calibre工具:
在这里插入图片描述

4. 转换效果如何?
转换之后效果如下图,其中对章节标题和段落划分进行了处理:
在这里插入图片描述

1. 下载和安装calibre

calibre下载地址:
https:///download

根据自己的系统下载安装即可

2. PDF导入calibre,并转换为azw3格式

打开calibre,点击菜单栏的“添加数据”,选择PDF格式文件,点击“Open”
在这里插入图片描述
在calibre的主窗口中选中刚导入的图书,点击菜单栏的“转换书籍”,在弹出的转换窗口,将输出格式选择为“AZW3”,然后点击“确定”。

转换过程可能会持续一小段时间,看calibre主窗口右下角的任务执行结束即转换完成
在这里插入图片描述
【注】这里有一个坑,图书转换的设置,目录针对章节有一个默认的最大值,需要根据图书的实际情况进行调整。我一般将两个值分别设置为1000,50
在这里插入图片描述

3. 编辑电子书,获取HTML内容和图片

在calibre主窗口选中目标电子书,点击菜单栏的“编辑书籍”打开编辑书籍窗口

在编辑书籍窗口,选中文本下的“part0000.html”,点击右键,选择“导出part0000.html”

同样的方式,全选图片下的所有图片,然后导出
在这里插入图片描述

4. 程序处理HTML文档

由上一步可以看到,直接由PDF转换得到的HTML文档,PDF里面的每一行转换之后在HTML文档中都独立成了一段,而且段首也没有空格,章节标题也没有突出展示。
在这里插入图片描述
针对这个问题,于是自己写了一段Java代码,主要是根据语义对文档内容进行分段,同时将标题突出,然后将每一张的起始位置独立成页。

具体的代码见文末,里面也有比较详细的注释,有一些特殊情况的处理可以根据自己的要求适当修改代码。
在这里插入图片描述

5. 将HTML文档导入calibre,并转换成azw3格式

首先,将calibre中已有的同名电子书删掉,操作方式是:在calibre主窗口,选中要删除的电子书,右键,选择“移除书籍” > “移除选定书籍”,之后在弹出窗口做二次确认
在这里插入图片描述
然后,将处理之后的HTML文档导入到calibre,操作方式同第2章。

最后,由于直接导入的HTML文档是ZIP格式,不能直接进行编辑,所以需要再次将文档转换成azw3格式,操作方式同第3章。可以看到,导入HTML文档之后,图书的封面丢失了,可以在转换的时候,直接修改封面图(可以用第3章导出保存的图片),如下图:
在这里插入图片描述

6. 编辑azw3文档

这一步是非必须的,但是如果电子书中有插图,那么就需要在这一步进行补充。另外也可以在文件预览中检查各个HTML文件,如有必要,也可以进行简单编辑。

进入编辑操作同第3章所介绍,这里主要介绍一下补充图片。

在编辑书籍窗口,点击菜单栏的“新增文件”按钮。

然后在弹出框选择“导入文件”,选择需要导入的图片。注意,文件的路径最终要是“images/xx”格式。

最后点击“确定”。

如果有多张图片,则重复上面的操作。目前calibre不支持批量导入。

编辑完成之后,点击编辑书籍窗口菜单栏的“保存”按钮进行保存。
在这里插入图片描述

7. 将azw3文档转换成mobi格式

在calibre主窗口,选中电子书,点击“转换书籍”,在弹出窗口中,输入格式选择“AZW3”
,输出格式选择“MOBI”。点击“确定”。然后等转换任务结束。
在这里插入图片描述
转换结束之后,在calibre主窗口,选中电子书,在右侧边栏,点击“点击打开”即可获取到mobi格式的电子书。然后将其共享至kindle就可以了。
在这里插入图片描述

8. 附录

推荐的PDF电子书网址:
http://www./
(大家有其他好的电子书链接也欢迎评论区共享。)

HTML格式文档处理代码:

package com.amwalle.walle.util;

import org.springframework.util.StringUtils;

import java.io.*;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class PDFConverter {

    public static void main(String[] args) throws IOException {
        Scanner scanner = new Scanner(System.in);
        System.out.println('请输入要转换的文件路径: ');
        String filePath = scanner.nextLine();
        System.out.println(filePath);
        convert(filePath);
    }

    public enum HeaderLevel {
        Part('H1', '.*第.*部分.*'),
        Chapter('H2', '(.*第.*章.*)|(.*[1-9].*)');

        private String headerLevel;
        private String expression;

        HeaderLevel(String headerLevel, String expression) {
            this.headerLevel = headerLevel;
            this.expression = expression;
        }

        public static String getHeaderLevel(String input) {
            if (StringUtils.isEmpty(input) || (!input.contains('</a>') && !input.contains('<p class=\'calibre1\'>'))) {
                return null;
            }

            String dummy = preHandleHeader(input);

            for (HeaderLevel level : HeaderLevel.values()) {
                Pattern pattern = Pattern.compile(level.expression);
                Matcher matcher = pattern.matcher(dummy);

                if (matcher.find()) {
                    return level.headerLevel;
                }
            }

            return null;
        }

        private static String preHandleHeader(String input) {
            String dummy = input;
            if (input.contains('</a>') && input.contains('</p>')) {
                dummy = dummy.substring(dummy.indexOf('</a>') + 4, dummy.indexOf('</p>'));
            } else if (input.contains('<p class=\'calibre1\'>') && input.contains('</p>')) {
                dummy = dummy.substring(dummy.indexOf('<p class=\'calibre1\'>') + 20, dummy.indexOf('</p>'));
            }

            dummy = dummy.replaceAll('<b class=\'calibre3\'> </b>', '');
            dummy = dummy.replaceAll('<b class=\'calibre3\'>', '');
            dummy = dummy.replaceAll(' </b>', '');
            dummy = dummy.replaceAll('</b>', '');
            dummy = removeSpace(dummy);
            return dummy;
        }

        public String getHeaderLevel() {
            return headerLevel;
        }

        public void setHeaderLevel(String headerLevel) {
            this.headerLevel = headerLevel;
        }

        public String getExpression() {
            return expression;
        }

        public void setExpression(String expression) {
            this.expression = expression;
        }
    }

    public static void convert(String filePath) throws IOException {
        File file = new File(filePath);
        if (!file.isFile() || !file.canRead() || !file.getName().endsWith('.html')) {
            System.out.println('请输入正确的html文件路径,并保证文件可读!');
        }

        String title = file.getName();
        title = title.replace('.html', '');

        File outFile = new File(filePath.replace('.html', '1.html'));
        if (outFile.exists()) {
            outFile.delete();
            outFile.createNewFile();
        }

        try (Scanner scanner = new Scanner(file, 'UTF-8'); BufferedWriter outWriter = new BufferedWriter(new FileWriter(outFile))) {
            Set<String> catalog = new HashSet<>();

            StringBuffer output = new StringBuffer();
            boolean isIndentationNeeded = false;
            int count = 0;
            while (scanner.hasNextLine()) {
                count++;
                // 有些文件太大,需要在中间存一下文件
                if (count >= 10000) {
                    outWriter.append(output);
                    count = 0;
                    output.delete(0, output.length());
                }

                String line = scanner.nextLine();

                // 目录前面的内容(封面、版权等)
                if (catalog.isEmpty() && !line.startsWith('<p class=\'calibre1\'><a href=\'')) {
                    output.append(line).append('\n');
                    continue;
                }

                // 处理目录:目录数据存set用于后续增加标题格式
                if (line.endsWith('</a></p>')) {
                    if (catalog.isEmpty()) {
                        output.delete(output.length() - 1, output.length());
                        String header = output.toString();
                        String catalogTitle = header.substring(header.lastIndexOf('\n') + 1);
                        // 格式化目录标题,增加目录前的分页
                        if (catalogTitle.contains('目录') || catalogTitle.contains('Contents')) {
                            catalogTitle = '<h1>' + catalogTitle + '</h1>' + '\n';
                            output.delete(header.lastIndexOf('\n') + 1, output.length());
                            output.append(addPagination(title));
                            output.append(catalogTitle);
                        } else {
                            output.append('\n');
                            output.append(addPagination(title));
                        }
                    }

                    line = line.replaceAll('part0000\\.html', '');
                    output.append(line).append('\n');

                    String temp = line.substring(line.lastIndexOf('\'>') + 2, line.indexOf('</a></p>'));
                    temp = removeSpace(temp);
                    catalog.add(temp);

                    continue;
                }

                if (StringUtils.isEmpty(line)) {
                    output.append('\n');
                    continue;
                }

                // 处理章节标题
                if (isLineATitle(line, catalog)) {
                    isIndentationNeeded = true;

                    String titleLevel = HeaderLevel.getHeaderLevel(line);
                    if (HeaderLevel.Part.headerLevel.equals(titleLevel)) {
                        output.append(addPagination(title));
                        output.append('<h1>').append(line).append('</h1>').append('\n');
                        continue;
                    }

                    if (HeaderLevel.Chapter.headerLevel.equals(titleLevel)) {
                        output.append(addPagination(title));
                        output.append('<h2>').append(line).append('</h2>').append('\n');
                        continue;
                    }

                    output.append('<h3>').append(line).append('</h3>').append('\n');
                    continue;
                }

                // 首行缩进
                if (isIndentationNeeded) {
                    line = line.replace('<p class=\'calibre1\'>', '<p class=\'calibre1\' style=\'text-indent:2em;\'>');
                    line = line.replace('</p>', '');
                    output.append(line).append('\n');
                    isIndentationNeeded = false;
                    continue;
                }

                // 文本内容分段
                if (isParagraphEnd(line)) {
                    isIndentationNeeded = true;
                    line = line.replace('<p class=\'calibre1\'>', '');
                    line = line.replace('</p>', '');
                    output.append(line).append('\n');
                    continue;
                }

                // 普通行处理:去掉换行
                if (line.startsWith('<p class=\'calibre1\'>') && line.endsWith('</p>')) {
                    line = line.replace('<p class=\'calibre1\'>', '');
                    line = line.replace('</p>', '');
                    output.append(line);
                    continue;
                }

                output.append(line).append('\n');
            }
            outWriter.append(output);

            System.out.println('********************************************');
            System.out.println('Done! 生成的新文件路径: ');
            System.out.println(outFile.getPath());
        } catch (FileNotFoundException e) {
            System.out.println('File convert failed!');
            e.printStackTrace();
        }
    }

    private static String removeSpace(String input) {
        String result = input.trim().replaceAll('\\.', '').replaceAll('·', '');
        result = result.replaceAll('\\s*', '').replaceAll('\\u00a0', '').replaceAll((char) 12288 + '', '');

        return result;
    }

    private static String addPagination(String title) {
        return '\n' +
                '</body>\n' +
                '</html>\n' +
                '<html xml:lang=\'zh-cn\' xmlns=\'http://www./1999/xhtml\'>\n' +
                '<head>\n' +
                '<title>' + title + '</title>\n' +
                '  <link rel=\'stylesheet\' type=\'text/css\' href=\'../styles/0002.css\'/>\n' +
                '  <link rel=\'stylesheet\' type=\'text/css\' href=\'../styles/0001.css\'/>\n' +
                '</head>\n' +
                '<body style=\'font-family:songti;\'>\n' +
                '\n';
    }

    private static boolean isLineATitle(String input, Set<String> catalog) {
        if (StringUtils.isEmpty(input) || (!input.contains('</a>') && !input.contains('<p class=\'calibre1\'>'))) {
            return false;
        }

        String dummy = HeaderLevel.preHandleHeader(input);

        for (String catalogEntry : catalog) {
            if (StringUtils.isEmpty(catalogEntry)) {
                continue;
            }

            if (catalogEntry.equals(dummy)) {
                return true;
            }
        }

        return false;
    }

    private static boolean isParagraphEnd(String input) {
        Pattern pattern = Pattern.compile('.*(\\w</p>)$');
        Matcher matcher = pattern.matcher(input);
        if (matcher.matches()) {
            return false;
        }

        // 句子结尾的标点符号:./!/?/……/'」 (英文+中文)
        Pattern pattern1 = Pattern.compile('.*(([\\.|!|\\?|\'|\\u3002|\\uff01|\\uff1f|\\u2026|\\u201d|\\u300d])|(\\. ))</p>$');
        Matcher matcher1 = pattern1.matcher(input);
        return matcher1.matches();
    }
}

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多