ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

使用 python 给 PDF 添加目录书签

2022-02-02 19:00:28  阅读:251  来源: 互联网

标签:txt outline python self 书签 path PDF pdf out



有时下载到扫描版的 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. 参考

标签:txt,outline,python,self,书签,path,PDF,pdf,out
来源: https://www.cnblogs.com/wreng/p/15860619.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有