天天看點

pdf各種處理 PDF 的實用代碼:PyPDF2、PDFMiner、pdfplumber

你不懂得安排自己的人生,會有很多人幫你安排,他們需要你做的事。

PDF檔案我們經常用,尤其是這兩個場景:

  • 下載下傳參考資料,如各類報告、文檔

    分享隻讀資料,友善傳播同時保留源檔案

場景和子產品

是以,對于PDF檔案,常見的需求也就是兩類:

  1. 處理檔案本身,屬于檔案頁面級操作,如合并/分拆PDF頁面、加/解密、加/去水印;

    處理檔案内容,屬于内容級操作,如提取文字、表格資料、圖表等。

目前Python用于處理PDF的子產品,主要有3個:

  • PyPDF2:子產品成熟,最後一次更新在2年前,适合頁面級操作,文字提取效果較差。

    PDFMiner:擅長文字抽取,目前主分支已停止維護,取而代之的是pdfminer.six

    pdfplumber:基于pdfminer.six的文本内容抽取工具,使用門檻更低,如支援表格提取。

實戰中,可以根據需求的類型選擇子產品。如果是頁面級的操作,就用PyPDF2,如果需要内容抽取,優先使用pdfplumber。

對應的子產品安裝:

pip install pypdf2

  pip install pdfminer.six

  pip install pdfplumber


           

下面按使用場景示範3個子產品的使用。

PyPDF2

PyPDF2的主要能力在頁面級操作,比如:

  • 擷取PDF文檔基本資訊

    PDF分割及合并

    PDF的旋轉及排序

    PDF加水印及去水印

    PDF加密及解密

PyPDF2的核心兩個類是PdfFileReader和PdfFileWriter,完成PDF檔案的讀寫操作。

擷取PDF文檔基本資訊

import pathlib
from PyPDF2 import PdfFileReader
path = list(pathlib.Path.cwd().parents)[1].joinpath('data/automate/002pdf')
f_path = path.joinpath('2020-新冠肺炎疫情對中國連鎖餐飲行業的影響調研報告-中國連鎖經營協會.pdf')
with open(f_path, 'rb') as f:
    pdf = PdfFileReader(f)
    info = pdf.getDocumentInfo()
    cnt_page = pdf.getNumPages()
    is_encrypt = pdf.getIsEncrypted()
print(f'''
作者: {info.author}
建立者: {info.creator}
制作者: {info.producer}
主題: {info.subject}
标題: {info.title}
總頁數: {cnt_page}
是否加密: {is_encrypt}
''')

           

PDF分割及合并

import pathlib
from PyPDF2 import PdfFileReader, PdfFileWriter
path = list(pathlib.Path.cwd().parents)[1].joinpath('data/automate/002pdf')
f_path = path.joinpath('2020-新冠肺炎疫情對中國連鎖餐飲行業的影響調研報告-中國連鎖經營協會.pdf')
out_path = path.joinpath('002pdf_split_merge.pdf')
out_path_1 = path.joinpath('002pdf_split_half_front.pdf')
out_path_2 = path.joinpath('002pdf_split_half_back.pdf')
# 把檔案分為兩半
with open(f_path, 'rb') as f, open(out_path_1, 'wb') as f_out1, open(out_path_2, 'wb') as f_out2:
    pdf = PdfFileReader(f)
    pdf_out1 = PdfFileWriter()
    pdf_out2 = PdfFileWriter()
    cnt_pages = pdf.getNumPages()
    print(f'共 {cnt_pages} 頁')
    for i in range(cnt_pages):
        if i <= cnt_pages //2:
            pdf_out1.addPage(pdf.getPage(i))
        else:
            pdf_out2.addPage(pdf.getPage(i))
    pdf_out1.write(f_out1)
    pdf_out2.write(f_out2)
# 再把後半個檔案與前半個檔案合并,後半個檔案在前
with open(out_path, 'wb') as f_out:
    cnt_f, cnt_b = pdf_out1.getNumPages(), pdf_out2.getNumPages()
    pdf_out = PdfFileWriter()
    for i in range(cnt_b):
        pdf_out.addPage(pdf_out2.getPage(i))
    for i in range(cnt_f):
        pdf_out.addPage(pdf_out1.getPage(i))
    pdf_out.write(f_out)

           

PDF的旋轉及排序

import pathlib
from PyPDF2 import PdfFileReader, PdfFileWriter
path = list(pathlib.Path.cwd().parents)[1].joinpath('data/automate/002pdf')
f_path = path.joinpath('2020-新冠肺炎疫情對中國連鎖餐飲行業的影響調研報告-中國連鎖經營協會.pdf')
out_path = path.joinpath('002pdf_rotate.pdf')
with open(f_path, 'rb') as f, open(out_path, 'wb') as f_out:
    pdf = PdfFileReader(f)
    pdf_out = PdfFileWriter()
    page = pdf.getPage(0).rotateClockwise(90)
    pdf_out.addPage(page)
    # 把第二頁放到前面
    pdf_out.addPage(pdf.getPage(2))
    page = pdf.getPage(1).rotateCounterClockwise(90)
    pdf_out.addPage(page)
    pdf_out.write(f_out)

           

PDF加水印及去水印

加圖檔水印,其實就是在頁面中增加一個透明背景的圖檔,通過頁面的mergePage方法即可完成。

import pathlib
from PyPDF2 import PdfFileReader, PdfFileWriter
path = list(pathlib.Path.cwd().parents)[1].joinpath('data/automate/002pdf')
f_path = path.joinpath('2020-新冠肺炎疫情對中國連鎖餐飲行業的影響調研報告-中國連鎖經營協會.pdf')
wm_path = path.joinpath('watermark.pdf')
en_path = path.joinpath('002pdf_with_watermark_en.pdf')
out_path = path.joinpath('002pdf_with_watermark.pdf')
with open(f_path, 'rb') as f, open(wm_path, 'rb') as f_wm, open(out_path, 'wb') as f_out:
    pdf = PdfFileReader(f)
    pdf_wm = PdfFileReader(f_wm)
    pdf_out = PdfFileWriter()
    wm_cn_page = pdf_wm.getPage(0)
    wm_en_page = pdf_wm.getPage(1)
    cnt_pages = pdf.getNumPages()
    for i in range(cnt_pages):
        page = pdf.getPage(i)
        page.mergePage(wm_cn_page)
        pdf_out.addPage(page)
    pdf_out.write(f_out)

           

去水印,就比較複雜,需要根據不同情況具體分析。因為水印可能是文字、圖檔或者各種組合,關鍵是識别出特征。

去水印的3個常見思路參考:

  • 找到特征詞後替換,适合英文文檔,但不适用于中文等CJK字元。

    把PDF頁轉成圖檔後,用圖像算法去水印,但這樣會破壞檔案原資訊結構。

    根據水印大小位置特征,找到所有元素後删除。這是更推薦的方式。

第3種方式效果最好,但如果碰到一些複雜的文檔水印,就非常考驗耐心。

你得一個個識别操作指令,一邊替換一邊檢查效果,直到水印成功去除。

但,未必剩下的所有頁都可以用同樣特征模式來消除,因為這份PDF可能經過多人加水印,已經包含多種加水印方式。

是以,去水印并沒有一種100%安全有效(不錯删資訊)且通用的方法。

加水印、去水印本質上是一種攻防政策。

比如一些工具推出去水印功能,一旦公開,加水印方就能識别并避開它的去除方法。

最後,尊重版權,是每個人應有的态度。

除了學習外,正式使用時,應該遵守内容創作方的規則。

PDF加密解密

PDF裡的密碼,分為使用者密碼和所有者密碼。

PyPDF2裡提供了基本的加密功能,“防君子不防小人”。

如果打開PDF檔案後,複制了新檔案,那新檔案就不受所有者密碼的限制,可被修改。

import pathlib
from PyPDF2 import PdfFileReader, PdfFileWriter
path = list(pathlib.Path.cwd().parents)[1].joinpath('data/automate/002pdf')
f_path = path.joinpath('2020-新冠肺炎疫情對中國連鎖餐飲行業的影響調研報告-中國連鎖經營協會.pdf')
out_path_encrypt = path.joinpath('002pdf_encrypt.pdf')
out_path_decrypt = path.joinpath('002pdf_decrypt.pdf')
with open(f_path, 'rb') as f, open(out_path_encrypt, 'wb') as f_out:
    pdf = PdfFileReader(f)
    pdf_out = PdfFileWriter()
    cnt_pages = pdf.getNumPages()
    for i in range(cnt_pages):
        page = pdf.getPage(i)
        pdf_out.addPage(page)
    pdf_out.encrypt('123456', owner_pwd='654321')
    pdf_out.write(f_out)
# 重新讀取加密檔案并生成解密檔案
with open(out_path_encrypt, 'rb') as f, open(out_path_decrypt, 'wb') as f_out:
    pdf = PdfFileReader(f)
    if not pdf.isEncrypted:
        print('檔案未被加密')
    else:
        success = pdf.decrypt('123456')
        # if not success:
        pdf_out = PdfFileWriter()
        pdf_out.appendPagesFromReader(pdf)
        pdf_out.write(f_out)

           

pdfminer.six

PDFMiner的操作門檻比較高,需要部分了解PDF的文檔結構模型,适合定制開發複雜的内容處理工具。

平時直接用PDFMiner比較少,這裡隻示範基本的文檔内容操作:

import pathlib
from pdfminer.pdfparser import PDFParser
from pdfminer.pdfdocument import PDFDocument
from pdfminer.pdfpage import PDFPage
from pdfminer.pdfinterp import PDFResourceManager
from pdfminer.pdfinterp import PDFPageInterpreter
from pdfminer.pdfdevice import PDFDevice
from pdfminer.layout import LAParams, LTTextBox, LTFigure, LTImage
from pdfminer.converter import PDFPageAggregator
path = list(pathlib.Path.cwd().parents)[1].joinpath('data/automate/002pdf')
f_path = path.joinpath('2020-新冠肺炎疫情對中國連鎖餐飲行業的影響調研報告-中國連鎖經營協會.pdf')
with open(f_path, 'rb') as f:
    parser = PDFParser(f)
    doc = PDFDocument(parser)
    rsrcmgr = PDFResourceManager()
    laparams = LAParams()
    device = PDFPageAggregator(rsrcmgr, laparams=laparams)
    interpreter = PDFPageInterpreter(rsrcmgr, device)
    for page in PDFPage.create_pages(doc):
        interpreter.process_page(page)
        layout = device.get_result()
        for x in layout:
            # 擷取文本對象
            if isinstance(x, LTTextBox):
                print(x.get_text().strip())
            # 擷取圖檔對象
            if isinstance(x,LTImage):
                print('這裡擷取到一張圖檔')
            # 擷取 figure 對象
            if isinstance(x,LTFigure):
                print('這裡擷取到一個 figure 對象')

           

雖然pdfminer使用門檻較高,但遇到複雜情況,最後還得用它。目前開源子產品中,它對PDF的支援應該是最全的了。

下面這個pdfplumber就是基于pdfminer.six開發的子產品,降低了使用門檻。

pdfplumber

相比pdfminer.six,pdfplumber提供了更便捷的PDF内容抽取接口。

日常工作中常用的操作,比如:

  • 提取PDF内容,儲存到txt檔案

    提取PDF中的表格到Excel

    提取PDF中的圖檔

    提取PDF中的圖表

提取PDF内容,儲存到txt檔案

import pathlib
import pdfplumber
path = list(pathlib.Path.cwd().parents)[1].joinpath('data/automate/002pdf')
f_path = path.joinpath('2020-新冠肺炎疫情對中國連鎖餐飲行業的影響調研報告-中國連鎖經營協會.pdf')
out_path = path.joinpath('002pdf_out.txt')
with pdfplumber.open(f_path) as pdf, open(out_path ,'a') as txt:
    for page in pdf.pages:
        textdata = page.extract_text()
        txt.write(textdata)

           

提取PDF中的表格到Excel

import pathlib
import pdfplumber
from openpyxl import Workbook
path = list(pathlib.Path.cwd().parents)[1].joinpath('data/automate/002pdf')
f_path = path.joinpath('2020-新冠肺炎疫情對中國連鎖餐飲行業的影響調研報告-中國連鎖經營協會.pdf')
out_path = path.joinpath('002pdf_excel.xlsx')
wb = Workbook()
sheet = wb.active
with pdfplumber.open(f_path) as pdf:
    for i in range(19, 22):
        page = pdf.pages[i]
        table = page.extract_table()
        for row in table:
            sheet.append(row)
wb.save(out_path)
           

上面用到了openpyxl的功能建立了一個Excel檔案,後面會有單獨文章介紹它。

提取PDF中的圖檔

import pathlib
import pdfplumber
from PIL import Image
path = list(pathlib.Path.cwd().parents)[1].joinpath('data/automate/002pdf')
f_path = path.joinpath('2020-疫情影響下的中國社群趨勢研究-艾瑞.pdf')
out_path = path.joinpath('002pdf_images.png')
with pdfplumber.open(f_path) as pdf, open(out_path, 'wb') as fout:
    page = pdf.pages[10]
    # for img in page.images:
    im = page.to_image()
    im.save(out_path, format='PNG')
    imgs = page.images
    for i, img in enumerate(imgs):
        size = img['width'], img['height']
        data = img['stream'].get_data()
        out_path = path.joinpath(f'002pdf_images_{i}.png')
        with open(out_path, 'wb') as fimg_out:
            fimg_out.write(data)
           

上面用到了PIL(Pillow)的功能處理圖檔。

提取PDF中的圖表

圖表與圖像不同,指的是類似直方圖、餅圖之類的資料生成圖。

import pathlib
import pdfplumber
from PIL import Image
path = list(pathlib.Path.cwd().parents)[1].joinpath('data/automate/002pdf')
f_path = path.joinpath('2020-新冠肺炎疫情對中國連鎖餐飲行業的影響調研報告-中國連鎖經營協會.pdf')
out_path = path.joinpath('002pdf_figures.png')
with pdfplumber.open(f_path) as pdf, open(out_path, 'wb') as fout:
    page = pdf.pages[7]
    im = page.to_image()
    im.save(out_path, format='PNG')
    figures = page.figures
    for i, fig in enumerate(figures):
        size = fig['width'], fig['height']
        crop = page.crop((fig['x0'], fig['top'], fig['x1'], fig['bottom']))
        img_crop = crop.to_image()
        out_path = path.joinpath(f'002pdf_figures_{i}.png')
        img_crop.save(out_path, format='png')
    im.draw_rects(page.extract_words(), stroke='yellow')
    im.draw_rects(page.images, stroke='blue')
    im.draw_rects(page.figures)
im # show in notebook