1.4.1. 问题一:ValueError: {’/Type’: ‘/Outlines’, ‘/Count’: 0} is not in list
1.4.2. 问题二:RuntimeError: generator raised StopIteration
1.5. 代码下载
1.6. 参考
有时下载到扫描版的 PDF 是不带书签目录的,这样阅读起来很不方便。下面通过 python 实现一个半自动化添加书签目录的脚本。
1.1. 安装
PyPDF2
pip install pypdf2
未避免后续运行程序报错,python
版本必须是 3.7 之前的(3.6)。
1.2. 提取 PDF 的目录信息并保存在 txt
这一步是比较麻烦的,需要手动实现。一般可以使用一些 OCR 文字识别工具,或者将目录页转化为 word 来操作。然后整理为如下的 txt 格式:
每一行包含三项:级别 level
、 标题 title
、 页数 page
,用空格隔开
使用“.
”来判断书签的级别,例如:
“第1章” 包含 0 个 “.
” 是一级标题
“1.1” 包含 1 个 “.
” 是二级标题
“1.1.1” 包含 2 个 “.
” 是三级标题
……(以此类推)
请不要有多余的空行
这里是我整理后的 txt:
第1章 绪论 1
1.1 本书的目的 1
1.2 信息融合的主要挑战 5
1.3 为什么需要随机集或FISST 5
1.3.1 多目标滤波的复杂性 6
1.3.2 超越启发式 7
1.3.3 单目标与多目标统计学的区别 7
1.3.4 常规数据与模糊数据的区别 7
1.3.5 形式化贝叶斯建模 8
1.3.6 模糊信息建模 8
1.3.7 多源多目标形式化建模 9
1.3. 编程实现
点击查看代码
import PyPDF2
import sys
class PdfDirGenerator:
def __init__(self, pdf_path:str, txt_path:str, offset:int, out_path:str=None, levelmark:str='.'):
self.pdf_path = pdf_path # pdf路径
self.txt_path = txt_path # 包含pdf目录信息的txt
self.offset = offset # 目录页数偏移量
self.out_path = out_path # 输出路径
self.levelmark = levelmark # 用于判断书签级别的标志符
self.dir_parent = [None]
def getLevelId(self, level):
"""计算书签的级数(级数的标志符号为“.”)
一级目录: 0 个“.”,例如: 第1章、附录A等
二级目录: 1个“.”,例如: 1.1、A.1
三级目录: 2个“.”,例如: 2.1.3
mark_num = 0
for c in level:
if c == self.levelmark:
mark_num += 1
return mark_num + 1
def run(self):
print("--------------------------- Adding the bookmark ---------------------------")
print(" * PDF Source: %s" % self.pdf_path)
print(" * TXT Source: %s" % self.txt_path)
print(" * Offset: %d" % self.offset)
print("---------------------------------------------------------------------------")
with open(self.txt_path, 'r', encoding='utf-8') as txt:
pdf_reader = PyPDF2.PdfFileReader(self.pdf_path)
pdf_writer = PyPDF2.PdfFileWriter()
pdf_writer.cloneDocumentFromReader(pdf_reader)
# BUG: ValueError: {’/Type’: ‘/Outlines’, ‘/Count’: 0} is not in list
# 修改代码 ${PYTHON_PATH}/site-packages/PyPDF2/pdf.py): getOutlineRoot 函数
# 参考:https://www.codetd.com/en/article/11823498
lines = txt.readlines()
num_all_lines = len(lines)
for i, line in enumerate(lines):
pline = line.split(' ')
level = pline[0]; title = pline[1]; page = int(pline[2]) + self.offset
# 1. 计算当前的 level 的级数 id
# 2. 当前书签的父结点存放在 dir_parent[id-1] 上
# 3. 更新/插入 dir_parent[id]
id = self.getLevelId(level)
if id >= len(self.dir_parent):
self.dir_parent.append(None)
self.dir_parent[id] = pdf_writer.addBookmark(level+' '+title, page-1, self.dir_parent[id-1])
print(" * [%d/%d finished] level: %s(%d), title: %s, page: %d" % (i+1, num_all_lines, level, id, title, page))
if self.out_path is None:
self.out_path = self.pdf_path[:-4] + '(书签).pdf'
with open(self.out_path, 'wb') as out_pdf:
pdf_writer.write(out_pdf)
print("---------------------------------------------------------------------------")
print(" * Save: %s" % self.out_path)
print("---------------------------------- Done! ----------------------------------")
if __name__ == '__main__':
input_num = len(sys.argv)
assert(input_num > 3)
opath = None
if input_num > 4:
opath = sys.argv[4]
mark='.'
if input_num > 5:
mark = sys.argv[5]
pdg = PdfDirGenerator(
pdf_path=sys.argv[1],
txt_path=sys.argv[2],
offset=int(sys.argv[3]), # 一般是目录结束页的页数
out_path=opath,
levelmark=mark
pdg.run()
上述代码保存在 PdfDirGenerator.py
中,其中有3个参数和2个可选参数:
第1个参数:待插入书签的 PDF 的路径
第2个参数:包含目录信息的 txt 的路径
第3个参数:正文内容的偏移页数(一般填目录结束页的页数)
第4个参数(可选):输出路径
第5个参数(可选):级数标志,默认为“.”
例如,在命令行中输入:
python .\PdfDirGenerator.py .\多源多目标统计信息融合Mahler.pdf .\dir.txt 27
运行效果:
1.4. 可能遇到的错误
这里主要参考 https://www.codetd.com/en/article/11823498
1.4.1. 问题一:ValueError: {’/Type’: ‘/Outlines’, ‘/Count’: 0} is not in list
如果 PDF 之前被其他软件修改过,可能会有如下错误:
Traceback (most recent call last):
File ".\PDFbookmark.py", line 70, in <module>
print(addBookmark(args[1], args[2], int(args[3])))
File ".\PDFbookmark.py", line 55, in addBookmark
new_bookmark = writer.addBookmark(title, page + page_offset, parent=parent)
File "C:\Anaconda3\lib\site-packages\PyPDF2\pdf.py", line 732, in addBookmark
outlineRef = self.getOutlineRoot()
File "C:\Anaconda3\lib\site-packages\PyPDF2\pdf.py", line 607, in getOutlineRoot
idnum = self._objects.index(outline) + 1
ValueError: {
'/Type': '/Outlines', '/Count': 0} is not in list
解决方法:修改 pdf.py
的 getOutlineRoot()
函数(pdf.py
的路径为 ${PYTHON_PATH}/site-packages/PyPDF2/pdf.py)
def getOutlineRoot(self):
if '/Outlines' in self._root_object:
outline = self._root_object['/Outlines']
idnum = self._objects.index(outline) + 1
except ValueError:
if not isinstance(outline, TreeObject):
def _walk(node):
node.__class__ = TreeObject
for child in node.children():
_walk(child)
_walk(outline)
outlineRef = self._addObject(outline)
self._addObject(outlineRef.getObject())
self._root_object[NameObject('/Outlines')] = outlineRef
idnum = self._objects.index(outline) + 1
outlineRef = IndirectObject(idnum, 0, self)
assert outlineRef.getObject() == outline
else:
outline = TreeObject()
outline.update({ })
outlineRef = self._addObject(outline)
self._root_object[NameObject('/Outlines')] = outlineRef
return outline
1.4.2. 问题二:RuntimeError: generator raised StopIteration
如果在你做了上面的修改后,在运行脚本时报错:untimeError: generator raised StopIteration
,请检查使用 Python 版本是不是 3.7或者更高版本(从版本v3.7之后,Python终止迭代过程发生了变化,细节可以参考PEP 479)。为避免报错,请使用低于3.7版本的 python,例如 3.6 版本。
1.5. 代码下载
https://gitee.com/iam002/add_pdf_bookmarker
这里用的 PDF 是 多源多目标统计信息融合 by Mahler (z-lib.org).pdf ,有需要的同学可点击 阿里云盘 下载。
1.6. 参考
https://www.codetd.com/en/article/11823498
https://www.cnblogs.com/1blog/p/15186521.html
https://www.jianshu.com/p/1aac3ae4d620?tdsourcetag=s_pcqq_aiomsg