• 使用 python 给 PDF 添加目录书签



    有时下载到扫描版的 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.pygetOutlineRoot() 函数(pdf.py 的路径为 ${PYTHON_PATH}/site-packages/PyPDF2/pdf.py

    def getOutlineRoot(self):
        if '/Outlines' in self._root_object:
            outline = self._root_object['/Outlines']
            try:
                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. 代码下载

    1.6. 参考

  • 相关阅读:
    嵌套类型返回错误解决办法(如迭代器的设计)
    UITableView的多选删除模式
    UITableView的编辑模式
    指针和引用初理解
    strstr()函数实现
    一句话的单词倒置
    字符串过滤程
    strcpy函数
    二叉排序树(Binary Sort Tree)
    二叉树插入操作
  • 原文地址:https://www.cnblogs.com/wreng/p/15860619.html
Copyright © 2020-2023  润新知