Python 如何高效實現 PDF 內容差異對比

Python 如何高效實現 PDF 內容差異對比

  • 1. 安裝 PyMuPDF 庫
  • 2. 獲取 PDF 內容
    • 通過文件路徑獲取
    • 通過 URL 獲取
  • 3. 提取 PDF 每頁信息
  • 4. 內容對比
    • metadata 差異
    • 文本對比
    • 可視化對比
  • 5. 提升對比效率
    • 通過哈希值快速判斷頁面是否相同
    • 早停機制
    • 多進程機制
  • 6. 其他

最近有接觸到 PDF 內容對比,所以分享一下如何用 Python 實現 PDF 內容對比。

1. 安裝 PyMuPDF 庫

PyMuPDF 提供了豐富的文檔操作功能,包括文本/圖像提取、頁面渲染、文檔合并拆分、注釋添加等。支持格式包括 PDF、EPUB、XPS 等。它是基于 C 語言庫 MuPDF 的 Python 綁定,MuPDF 由 Artifex 公司開發,以高性能和小巧著稱。通過 pip install PyMuPDF 安裝,但在代碼中需通過 import fitz 調用其功能。fitz 是該庫的核心模塊,fitz 名稱源自 MuPDF 的原始渲染引擎 “Fitz”。為保持一致性,PyMuPDF 的 Python 接口沿用了此名稱。

pip install pymupdf
import fitz

2. 獲取 PDF 內容

fitz.open 是 PyMuPDF(fitz 模塊)中用于打開 PDF 或其他支持的文檔格式的函數。它返回一個 fitz.Document 對象。
通過 fitz.Document 對象,可以:

  • 訪問頁面:
    使用索引訪問文檔中的頁面,例如 doc[0] 表示第一頁。
    每個頁面是一個 fitz.Page 對象。
  • 獲取文檔信息:
    獲取文檔的元數據(如標題、作者、創建時間等)。
    獲取文檔的頁數。

獲取 PDF 內容有兩種方式:

通過文件路徑獲取

def get_pdf_content_from_path(pdf_path):"""Get PDF content from a local file path"""pdf = fitz.open(pdf_path)return pdf 

通過 URL 獲取

注意通過接口調用獲取 repsonce.content 字節類型 content,而不是 response.text 字符串類型 content

屬性response.contentresponse.text
返回類型bytes(字節)str(字符串)
解碼不進行解碼,返回原始二進制數據自動根據 response.encoding 解碼
適用場景處理二進制文件(如圖片、PDF 等)處理文本數據(如 HTML、JSON 等)
手動解碼需要手動解碼(如 content.decode(‘utf-8’))自動解碼,無需額外操作
def get_pdf_content_from_datalake(content_url):"""Get PDF content using content_url"""content = get_content_by_content_url(content_url)try:pdf = fitz.open(filetype="pdf", stream=content)except Exception as e:raise ValueError(f"Failed to open PDF from DataLake for URL: {content_url}. Error: {str(e)}")return pdf

3. 提取 PDF 每頁信息

PDF 通常有很多頁 content,需要比較每頁的 content,前面獲取到 fitz.Document,使用索引訪問文檔中的頁面 doc[index] 返回 fitz.Page 對象。
下面是 fitz.Page 的常用屬性,我們對比內容只需要用到 get_text() 和 get_pixmap(),通過比較每頁的 text 和像素就能找出 PDF 任何細微的差異,包括內容格式,e.g 字體,加粗,高亮,table 布局,圖片大小等。

屬性/方法描述
number當前頁面的頁碼(從 0 開始)。
rect頁面尺寸(矩形區域)。
rotation頁面旋轉角度(0、90、180 或 270)。
mediabox頁面媒體框的尺寸。
cropbox頁面裁剪框的尺寸。
get_text()提取頁面文本(支持多種格式,如 “text”、“html”、“json”)。
get_pixmap()將頁面渲染為圖像。
search_for()搜索頁面中的文本。
get_images()獲取頁面中的嵌入圖像信息。
add_annot()在頁面上添加注釋。
write()將頁面內容導出為字節流。

其中 get_pixmap() 用于將 PDF 頁面渲染為像素圖(圖像)。它是將 PDF 頁面轉換為圖像格式的核心方法,常用于生成頁面的可視化表示或進行圖像比較。返回的 fitz.Pixmap 對象包含圖像的像素數據和相關信息,常用屬性如下:

屬性名描述
samples圖像的原始像素數據(字節流)。
width圖像的寬度(像素)。
height圖像的高度(像素)。
stride每行像素的字節數。
colorspace圖像的顏色空間(如 RGB、灰度等)。
  # Determine the maximum number of pagesmax_pages = max(len(pdf_base), len(pdf_target))
def extract_page_data(pdf, page_num):"""Extract text and pixel data from a PDF page."""page = pdf[page_num]text = page.get_text()pix = page.get_pixmap()return {"text": text,"pix_samples": pix.samples,"pix_width": pix.width,"pix_height": pix.height,}
def generate_page_data(pdf_base, pdf_target, max_pages, doc_folder):"""Generator to yield page data for multiprocessing."""for page_num in range(max_pages):page_data_base = extract_page_data(pdf_base, page_num)page_data_target = extract_page_data(pdf_target, page_num)yield (page_data_base, page_data_target, page_num, doc_folder)

4. 內容對比

metadata 差異

fitz.Document 對象元數據 metadata 屬性,通常包括文檔的基本信息,例如標題、作者、創建時間等。如果忽略 metadata 差異,可以忽略此項對比。

以下是 metadata 字典中常見的鍵及其含義:

鍵名描述
title文檔的標題(Title)。
author文檔的作者(Author)。
subject文檔的主題(Subject)。
keywords文檔的關鍵字(Keywords)。
creator創建文檔的應用程序(Creator)。
producer生成文檔的工具或軟件(Producer)。
creationDate文檔的創建日期(Creation Date)。
modDate文檔的最后修改日期(Modification Date)。
trapped文檔是否被標記為“Trapped”(通常為 True 或 False,可能為空)。
compare_metadata(pdf_base.metadata, pdf_target.metadata, result)
def compare_metadata(metadata_base, metadata_target, result):"""Compare PDF metadata"""for key in set(metadata_base.keys()) | set(metadata_target.keys()):if metadata_base.get(key) != metadata_target.get(key):result["metadata_differences"].append(f"Metadata '{key}' differs: pdf_base='{metadata_base.get(key)}', pdf_target='{metadata_target.get(key)}'")

文本對比

ndiff 是 Python 標準庫 difflib 中的一個方法,用于逐行比較兩個字符串序列,并生成一個可讀的差異列表。它特別適合用于文本比較,能夠清晰地標記出新增、刪除和修改的部分。

difflib.ndiff 的功能

  • 輸入: 兩個字符串序列(通常是通過 splitlines() 分割的多行文本)。
  • 輸出: 一個迭代器,生成每一行的差異標記。
  • 差異標記:
    -:表示在第一個序列中存在,但在第二個序列中不存在的行。
    +:表示在第二個序列中存在,但在第一個序列中不存在的行。
    (空格):表示兩個序列中都存在的行(沒有變化)。
    ?:表示上一行的具體差異(通常用于標記字符級別的變化)。
def compare_text_content(page_data_base, page_data_target, page_num, result):"""Compare text content of two pages."""text_base = page_data_base["text"]text_target = page_data_target["text"]if text_base != text_target:result["text_differences"].append(f"Text differs on page {page_num + 1}")diff = list(difflib.ndiff(text_base.splitlines(), text_target.splitlines()))differences = [d for d in diff if d.startswith('+ ') or d.startswith('- ')]if differences:result["text_differences"].append(f"Page {page_num + 1} specific differences: {differences[:5]}...")

可視化對比

比較兩個 PDF 頁面視覺內容,通過比較頁面的像素數據來檢測頁面之間的視覺差異。

  • 頁面尺寸比較:
    首先比較兩個頁面的寬度和高度,如果頁面尺寸不同,記錄差異并退出函數。
  • 像素數據比較:
    將頁面的像素數據轉換為圖像對象。使用 PIL.Image.frombytes 將頁面的像素數據轉換為 RGB 圖像對象。
    使用 ImageChops.difference 計算兩個圖像的差異,返回一個差異圖像,其中每個像素的值表示兩個圖像對應像素的差異程度。
  • 保存差異圖像:
    如果發現差異,保存基準頁面、目標頁面和差異圖像到指定的文件夾。
    記錄差異信息到 result 字典中。
def compare_visual_content(page_data_base, page_data_target, page_num, doc_folder, result):"""Compare visual content of two pages."""if (page_data_base["pix_width"] != page_data_target["pix_width"] orpage_data_base["pix_height"] != page_data_target["pix_height"]):result["format_differences"].append(f"Page {page_num + 1} size differs: PDF_base={page_data_base['pix_width']}x{page_data_base['pix_height']}, "f"PDF_target={page_data_target['pix_width']}x{page_data_target['pix_height']}")returnimg_base = Image.frombytes("RGB", [page_data_base["pix_width"], page_data_base["pix_height"]], page_data_base["pix_samples"])img_target = Image.frombytes("RGB", [page_data_target["pix_width"], page_data_target["pix_height"]], page_data_target["pix_samples"])diff_img = ImageChops.difference(img_base, img_target)if np.any(np.array(diff_img)):img_base_path = os.path.join(doc_folder, f"page_{page_num + 1}_pdf_base.png")img_target_path = os.path.join(doc_folder, f"page_{page_num + 1}_pdf_target.png")diff_path = os.path.join(doc_folder, f"page_{page_num + 1}_diff.png")img_base.save(img_base_path)img_target.save(img_target_path)diff_img.save(diff_path)result["format_differences"].append(f"differs on page {page_num + 1}: difference image saved at {diff_path}")

5. 提升對比效率

通過哈希值快速判斷頁面是否相同

通過比較頁面內容的哈希值(包括文本和像素數據),如果哈希值相同,則跳過進一步比較。
如果哈希值不同,調用 compare_text_content 和 compare_visual_content 方法分別比較文本和視覺內容。

def hash_page_content(page_data):"""Generate a hash for the page content."""text_hash = hashlib.md5(page_data["text"].encode()).hexdigest()pix_hash = hashlib.md5(page_data["pix_samples"]).hexdigest()return text_hash, pix_hashdef compare_page(page_data_base, page_data_target, page_num, doc_folder):"""Compare a single page for text and visual differences."""result = {"text_differences": [],"format_differences": []}try:# Compare hashes firstbase_hash = hash_page_content(page_data_base)target_hash = hash_page_content(page_data_target)if base_hash == target_hash:return result  # Skip comparison if hashes are identical# Compare text and visual contentcompare_text_content(page_data_base, page_data_target, page_num, result)compare_visual_content(page_data_base, page_data_target, page_num, doc_folder, result)except Exception as e:result["format_differences"].append(f"Failed to compare page {page_num + 1}: {str(e)}")return result

早停機制

如果 PDF 差異頁面非常很多,后續的頁面差異其實是無意義的,我們可以設定一個差異頁面數量的最大值,比如 3 或 5,當發現的差異頁面數量達到指定的最大值時,函數會停止進一步的比較。

def compare_page_with_limit(args, diff_page_count, max_diff_pages, lock):"""Compare a single page with early termination."""page_data_base, page_data_target, page_num, doc_folder = argswith lock:if diff_page_count.value >= max_diff_pages:return None  # Skip further processing if limit is reachedpage_result = compare_page(page_data_base, page_data_target, page_num, doc_folder)if page_result["text_differences"] or page_result["format_differences"]:with lock:diff_page_count.value += 1return page_result

多進程機制

如果需要比較的 PDF 文件比較多,我們也可以采用多進程并發比較,提升腳本執行時間。這里可以根據實際情況,是基于 PDF 之間并行,還是基于單個 PDF 頁面之間并行。我這邊是基于 PDF 頁面之間并發執行的,考慮到大多數 PDF 頁面達上百頁,頁面之間并發效率更高。

pool.starmap 是 Python 中 multiprocessing.Pool 提供的一種方法,用于在多進程環境下并行執行函數。它類似于 map 方法,但支持將多個參數傳遞給目標函數。

這里定義了一個 diff_page_count 共享變量(通過 manager.Value 創建),因為是 int 型,所以在多進程環境下需要使用 lock 來保護它。這是因為 manager.Value 本身并不能保證對其值的操作是原子的(atomic)。
共享變量的非原子操作,對共享變量的操作(如 diff_page_count.value += 1)實際上是由多個步驟組成的:

  • 讀取當前值。
  • 增加值。
  • 寫回新值。

在多進程環境下,如果多個進程同時執行這些步驟,就可能導致數據競爭(race condition),從而導致共享變量的值不正確。假設兩個進程同時讀取 diff_page_count.value 的值為 5,然后分別將其加 1 并寫回。最終的結果可能是 6 而不是預期的 7,因為兩個進程的操作互相覆蓋了。使用 lock 可以確保在一個進程修改共享變量時,其他進程必須等待,直到當前進程完成操作并釋放鎖。這就避免了數據競爭,確保共享變量的值始終正確。

當然如果換成 diff_page_count = manager.list(),它的操作(如添加或刪除元素)是線程安全的,底層已經實現了同步機制。因此,多個進程可以安全地向列表中添加元素,而無需顯式使用 lock。但是 manager.list() 的操作比直接操作 manager.Value 稍慢,因為它需要處理線程安全。如果性能是關鍵問題,仍然可以考慮使用 manager.Value 和 lock。

def prepare_output_folder(output_folder, pdf_object_id):"""Prepare the output folder for storing comparison results."""output_folder = os.path.join(constants.OUTPUT_DIR, output_folder)os.makedirs(output_folder, exist_ok=True)doc_folder = os.path.join(output_folder, pdf_object_id.replace(":", "_"))clear_and_create_content_dir(doc_folder)return doc_folderdef compare_pdf(pdf_base_path, pdf_target_path, pdf_object_id, pdf_base_object_url, pdf_target_object_url,is_from_datalake=True, output_folder="pdf_diff_results", max_diff_pages=3):"""Compare two PDF files for content and format differences"""# Prepare output folderdoc_folder = prepare_output_folder(output_folder, pdf_object_id)# Initialize resultresult = {"text_differences": [],"format_differences": [],"metadata_differences": [],"page_count": {"pdf_base": 0, "pdf_target": 0}}# Open PDF filespdf_base = get_pdf_content_from_datalake(pdf_base_object_url) if is_from_datalake else get_pdf_content_from_path(pdf_base_path)pdf_target = get_pdf_content_from_datalake(pdf_target_object_url) if is_from_datalake else get_pdf_content_from_path(pdf_target_path)# Compare page countresult["page_count"]["pdf_base"] = len(pdf_base)result["page_count"]["pdf_target"] = len(pdf_target)# Compare metadata, ignore differences in creation/modification dates# compare_metadata(pdf_base.metadata, pdf_target.metadata, result)# Determine the maximum number of pagesmax_pages = max(len(pdf_base), len(pdf_target))# Compare pages in parallel using a generatorwith Manager() as manager:# Shared counter for tracking pages with differencesdiff_page_count = manager.Value('i', 0)lock = manager.Lock()# Create a pool of worker processeswith Pool() as pool:page_results = pool.starmap(compare_page_with_limit,[(args, diff_page_count, max_diff_pages, lock) for args in generate_page_data(pdf_base, pdf_target, max_pages, doc_folder)])if diff_page_count.value >= max_diff_pages:print(f"Early termination: {diff_page_count.value} pages with differences found, stopping further processing.")pool.terminate()pool.join()# Aggregate resultsfor page_result in page_results:if page_result is None:continue  # Skip if terminated earlyresult["text_differences"].extend(page_result["text_differences"])result["format_differences"].extend(page_result["format_differences"])return result

6. 其他

還有一些其他細節問題,這里就不細說了,一個完整的腳本執行是需要考慮很多因素的,目的就是為了全自動化,減少人工干預成本,提高整體效率。
這里羅列一些:

  • 測試數據收集和配置,方便后期定制化執行不同的測試數據集
  • 腳本執行過程中的 log,方便 troubleshooting
  • 生成測試報告,包括細節信息,匯總信息(total,fail,pass),及其他統計信息,方便 triage
  • 部署到 Jenkins 上日常執行,并發送測試報告,方便 CICD

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/76865.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/76865.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/76865.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

OpenGL學習筆記(簡介、三角形、著色器、紋理、坐標系統、攝像機)

目錄 簡介核心模式與立即渲染模式狀態機對象GLFW和GLAD Hello OpenGLTriangle 三角形頂點緩沖對象 VBO頂點數組對象 VAO元素緩沖對象 EBO/ 索引緩沖對象 IEO 著色器GLSL數據類型輸入輸出Uniform 紋理紋理過濾Mipmap 多級漸遠紋理實際使用方式紋理單元 坐標系統裁剪空間 攝像機自…

MIPI與DVP接口攝像頭:深度解析與應用指南

1、MIPI 1.1 MIPI簡介 MIPI是什么?MIPI:mobile industry processor interface移動行業處理器接口。它是一個由Intel、Motorola、Nokia、NXP、Samsung、ST(意法半導體)和TI(德州儀器)等公司發起的開放標準…

35信號和槽_信號槽小結

Qt 信號槽 1.信號槽是啥~~ 尤其是和 Linux 中的信號進行了對比(三要素) 1) 信號源 2) 信號的類型 3)信號的處理方式 2.信號槽 使用 connect 3.如何查閱文檔. 一個控件,內置了哪些信號,信號都是何時觸發 一…

6547網:藍橋STEMA考試 Scratch 試卷(2025年3月)

『STEMA考試是藍橋青少教育理念的一部分,旨在培養學生的知識廣度和獨立思考能力。考試內容主要考察學生的未來STEM素養、計算思維能力和創意編程實踐能力。』 一、選擇題 第一題 運行下列哪個程序后,飛機會向左移動? ( ) A. …

使用 Python 爬取并打印雙色球近期 5 場開獎數據

使用 Python 爬取并打印雙色球近期 5 場開獎數據 前期準備安裝所需庫 完整代碼代碼解析 1. 導入必要的庫2. 定義函數 get_recent_five_ssq 3. 設置請求的 URL 和 Headers 4. 發送請求并處理響應5. 解析 HTML 內容6. 提取并打印數據7. 錯誤處理 首先看下運行的效果圖&#xff1a…

前端快速入門學習3——CSS介紹與選擇器

1.概述 CSS全名是cascading style sheets,中文名層疊樣式表。 用于定義網頁樣式和布局的樣式表語言。 通過 CSS,你可以指定頁面中各個元素的顏色、字體、大小、間距、邊框、背景等樣式,從而實現更精確的頁面設計。 HTML與CSS的關系:HTML相當…

JVM 內存區域詳解

JVM 內存區域詳解 Java 虛擬機(JVM)的內存區域劃分為多個部分,每個部分有特定的用途和管理機制。以下是 JVM 內存區域的核心組成及其功能: 一、運行時數據區(Runtime Data Areas) 1. 線程共享區域 內存…

基于SpringBoot的水產養殖系統【附源碼】

基于SpringBoot的水產養殖系統(源碼L文說明文檔) 目錄 4 系統設計 4.1 總體功能 4.2 系統模塊設計 4.3 數據庫設計 4.3.1 數據庫設計 4.3.2 數據庫E-R 圖 4.3.3 數據庫表設計 5 系統實現 5.1 管理員功能模塊的實…

從零構建大語言模型全棧開發指南:第五部分:行業應用與前沿探索-5.2.2超級對齊與AGI路徑探討

?? 點擊關注不迷路 ?? 點擊關注不迷路 ?? 點擊關注不迷路 文章大綱 大語言模型全棧開發指南:倫理與未來趨勢 - 第五部分:行業應用與前沿探索5.2.2 超級對齊與AGI路徑探討超級對齊:定義與核心挑戰1. 技術挑戰2. 倫理挑戰AGI發展路徑:從專用到通用智能階段1:`專用智能…

基于大模型的重癥肌無力的全周期手術管理技術方案

目錄 技術方案文檔1. 數據預處理模塊2. 多任務預測模型架構3. 動態風險預測引擎4. 手術方案優化系統5. 技術驗證模塊6. 系統集成架構7. 核心算法清單8. 關鍵流程圖詳述實施路線圖技術方案文檔 1. 數據預處理模塊 流程圖 [輸入原始數據] → [聯邦學習節點數據對齊] → [多模態特…

盲盒小程序開發平臺搭建:打造個性化、高互動性的娛樂消費新體驗

在數字化浪潮席卷消費市場的今天,盲盒小程序以其獨特的趣味性和互動性,迅速成為了年輕人追捧的娛樂消費新寵。盲盒小程序不僅為用戶帶來了拆盒的驚喜和刺激,更為商家提供了創新的營銷手段。為了滿足市場對盲盒小程序日益增長的需求&#xff0…

前端對接下載文件接口、對接dart app

嵌套在dart app里面的前端項目 1.前端調下載接口 ->后端返回 application/pdf格式的文件 ->前端將pdf處理為blob ->blob轉base64 ->調用dart app的 sdk saveFile ->保存成功 async download() {try {// 調用封裝的 downloadEContract 方法獲取 Blob 數據const …

Spring常見問題復習

############Spring############# Bean的生命周期是什么? BeanFactory和FactoryBean的區別? ApplicationContext和BeanFactory的區別? BeanFactoryAware注解,還有什么其它的Aware注解 BeanFactoryAware方法和Bean注解的方法執行順…

C++_類和對象(下)

【本節目標】 再談構造函數Static成員友元內部類匿名對象拷貝對象時的一些編譯器優化再次理解封裝 1. 再談構造函數 1.1 構造函數體賦值 在創建對象時,編譯器通過調用構造函數,給對象中各個成員變量一個合適的初始值。 class Date { public:Date(in…

連續數據離散化與逆離散化策略

數學語言描述: 在區間[a,b]中有一組符合某分布的數據: 1.求相同區間中另一組符合同樣分布的數據與這組數據的均方誤差 2.求區間中點與數據的均方誤差 3.求在區間中均勻分布的一組數據與這組數據的均方誤差 一:同分布數據隨機映射 假設在…

Redash:一個開源的數據查詢與可視化工具

Redash 是一款免費開源的數據可視化與協作工具,可以幫助用戶快速連接數據源、編寫查詢、生成圖表并構建交互式儀表盤。它簡化了數據探索和共享的過程,尤其適合需要團隊協作的數據分析場景。 數據源 Redash 支持各種 SQL、NoSQL、大數據和 API 數據源&am…

FreeRTOS的空閑任務

在 FreeRTOS 中,空閑任務(Idle Task) 是操作系統自動創建的一個特殊任務,其作用和管理方式如下: 1. 空閑任務創建 FreeRTOS 內核自動創建:當調用 vTaskStartScheduler() 啟動調度器時,內核會自…

Java進階之旅-day05:網絡編程

引言 在當今數字化的時代,網絡編程在軟件開發中扮演著至關重要的角色。Java 作為一門廣泛應用的編程語言,提供了強大的網絡編程能力。今天,我們深入學習了 Java 網絡編程的基礎知識,包括基本的通信架構、網絡編程三要素、IP 地址、…

大數據(4.3)Hive基礎查詢完全指南:從SELECT到復雜查詢的10大核心技巧

目錄 背景一、Hive基礎查詢核心語法1. 基礎查詢(SELECT & FROM)2. 條件過濾(WHERE)3. 聚合與分組(GROUP BY & HAVING)4. 排序與限制(ORDER BY & LIMIT) 二、復雜查詢實戰…

Synopsys:設計對象

相關閱讀 Synopsyshttps://blog.csdn.net/weixin_45791458/category_12812219.html?spm1001.2014.3001.5482 對于Synopsys的EDA工具(如Design Compiler、PrimeTime、IC Compiler)等,設計對象(Design Objects)是組成整個設計的抽象表示&…