0. 前言
正式介绍之前,先回答下面几个问题:
1. 为什么要将PDF转换成mobi? 想要将PDF转换成mobi格式,初衷在于想在kindle上面看一些从网上获取到的PDF文档。直接将PDF导入kindle本来也可以,但是效果不是很好——要么竖着看,但是字体很小;要么横着看,字体会大一些,但是总感觉比较别扭,而且PDF的一页需要在kindle上翻3页。 kindle支持azw3、mobi等格式,但是不支持直接将azw3格式的文档直接导入到kindle,所以需要将PDF文档转换成mobi格式
2. 为什么不直接用在线转换工具? 其实网上有很多工具支持将PDF转换成mobi格式,但是效果都很差:
- 章节标题和正文内容没有区别;
- 正文内容格式混乱,在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();
}
}
|