使用 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. 參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM