零基礎(chǔ)上手使用Python?pdfplumber提取原生PDF表格
表格神器|告別PyPDF2,用pdfplumber精準提取PDF表格
前言
在上一篇文章中,我們介紹了PyPDF2從PDF中提取文本的基本用法。但細心的讀者可能已經(jīng)發(fā)現(xiàn)了一個致命問題:PyPDF2壓根不認識表格。
當你用PyPDF2提取一份財務(wù)報表PDF時,得到的是一個亂七八糟的文本流——表格的行列結(jié)構(gòu)完全丟失,數(shù)據(jù)散落一地。這正是因為PyPDF2只按PDF內(nèi)部操作符順序拼接字符串,完全不理解“表格單元格”這種視覺結(jié)構(gòu)。
這時,我們需要一個真正的“表格神器”——pdfplumber。
pdfplumber是基于PDFMiner.six構(gòu)建的PDF解析庫,其核心能力是像素級的字符定位和表格結(jié)構(gòu)還原。它不僅能提取文本,更能像人類一樣“看懂”PDF中表格的行列結(jié)構(gòu),將數(shù)據(jù)完整、準確地提取出來。
本文將從零開始,帶你系統(tǒng)掌握pdfplumber的表格提取功能。全文包含8個模塊,附完整可運行代碼和可視化調(diào)試技巧,讀完即可上手處理真實項目。
一、為什么PyPDF2提取表格會失敗?——無坐標級解析的致命缺陷
1.1 對比實驗:同一份表格的不同命運
先用一個直觀的實驗來說明問題。
用PyPDF2提取PDF中的表格:
import PyPDF2
reader = PyPDF2.PdfReader('invoice.pdf')
text = reader.pages[0].extract_text()
print(text)
輸出結(jié)果:
商品名稱 數(shù)量 單價 金額 筆記本 2 5000 10000 鼠標 3 100 300 合計 10300
表格的結(jié)構(gòu)完全丟失,數(shù)據(jù)像一長串連續(xù)的字符串粘連在一起。這是因為PyPDF2的extract_text()方法只是簡單地將PDF中的文本對象按順序拼接,完全不考慮位置信息。
用pdfplumber提取相同的表格:
import pdfplumber
with pdfplumber.open('invoice.pdf') as pdf:
table = pdf.pages[0].extract_table()
for row in table:
print(row)
輸出結(jié)果:
['商品名稱', '數(shù)量', '單價', '金額']
['筆記本', '2', '5000', '10000']
['鼠標', '3', '100', '300']
['合計', '', '', '10300']
數(shù)據(jù)以二維列表的形式呈現(xiàn),行列結(jié)構(gòu)完整保留——這正是pdfplumber的核心優(yōu)勢。
1.2 深度原理:PyPDF2失敗的根本原因
下圖清晰解釋了為什么PyPDF2無法提取表格:

PDF本質(zhì)是一種基于圖形操作的頁面描述語言,而非天然具備語義結(jié)構(gòu)。PDF中的表格并不是一個獨立的“表格對象”,而是一堆字符和線條的集合,分散在不同位置。PyPDF2只關(guān)心“有哪些字符”,完全不關(guān)心“這些字符在哪里”;而pdfplumber會記錄每個字符的精確坐標(x0, y0, x1, y1),然后基于這些坐標重新構(gòu)建表格的行列關(guān)系。
1.3 pdfplumber的核心優(yōu)勢
| 能力維度 | PyPDF2 | pdfplumber |
|---|---|---|
| 文本提取 | 簡單拼接,段落錯亂 | 按坐標還原布局,保留段落 |
| 表格提取 | ? 不支持 | ? 自動識別邊框,還原行列 |
| 合并單元格 | ? 無法識別 | ? 自動合并 |
| 坐標定位 | ? 無 | ? 字符級精確定位 |
| 可視化調(diào)試 | ? 無 | ? to_image().draw_rects() |
| 提取指定區(qū)域 | ? 需手動裁剪 | ? crop() 區(qū)域裁剪 |
pdfplumber的優(yōu)勢總結(jié)為三點:表格提取精準(自動識別邊框和合并單元格)、文本提取智能(按布局還原順序)、支持細節(jié)控制(可指定提取區(qū)域)。
二、環(huán)境準備:一鍵安裝
2.1 安裝pdfplumber
pip install pdfplumber
驗證安裝:
python -c "import pdfplumber; print(pdfplumber.__version__)"
2.2 配套庫推薦(可選)
根據(jù)實際需求,建議同時安裝以下庫:
# 數(shù)據(jù)處理 pip install pandas # Excel導(dǎo)出 pip install openpyxl # 可視化(Jupyter環(huán)境) pip install matplotlib
2.3 虛擬環(huán)境配置(團隊協(xié)作推薦)
# 創(chuàng)建虛擬環(huán)境 python -m venv pdf_env # 激活(Windows) pdf_env\Scripts\activate # 激活(Mac/Linux) source pdf_env/bin/activate # 安裝依賴 pip install pdfplumber pandas openpyxl # 導(dǎo)出依賴清單 pip freeze > requirements.txt
三、基礎(chǔ)操作:加載PDF與提取表格
3.1 加載PDF文件
import pdfplumber
# 使用with語句自動管理資源
with pdfplumber.open("example.pdf") as pdf:
print(f"總頁數(shù): {len(pdf.pages)}")
# 獲取第一頁
first_page = pdf.pages[0]
print(f"頁面尺寸: {first_page.width} x {first_page.height}")
3.2 提取單頁表格
import pdfplumber
with pdfplumber.open("report.pdf") as pdf:
# 提取第一頁的表格
page = pdf.pages[0]
table = page.extract_table()
if table:
# 打印表頭
print("表頭:", table[0])
# 打印數(shù)據(jù)行
for row in table[1:]:
print(row)
注意:extract_table() 默認提取頁面上最大的那個表格。如果一頁有多個表格,需要用 extract_tables() 提取所有表格。
3.3 提取多頁所有表格
import pdfplumber
all_tables = []
with pdfplumber.open("multi_page_report.pdf") as pdf:
for page_num, page in enumerate(pdf.pages, 1):
tables = page.extract_tables()
if tables:
print(f"第{page_num}頁發(fā)現(xiàn) {len(tables)} 個表格")
all_tables.extend(tables)
print(f"共提取 {len(all_tables)} 個表格")
四、基礎(chǔ)輸出:導(dǎo)出為Excel/CSV/Pandas
4.1 導(dǎo)出為CSV
import csv
import pdfplumber
def table_to_csv(pdf_path, output_csv, page_num=0):
with pdfplumber.open(pdf_path) as pdf:
table = pdf.pages[page_num].extract_table()
if table:
with open(output_csv, 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerows(table)
print(f"已保存至: {output_csv}")
else:
print("未檢測到表格")
# 使用
table_to_csv("report.pdf", "output.csv")
4.2 導(dǎo)出為Excel(使用openpyxl)
import pdfplumber
from openpyxl import Workbook
def table_to_excel(pdf_path, excel_path, page_num=0):
wb = Workbook()
ws = wb.active
ws.title = "PDF表格數(shù)據(jù)"
with pdfplumber.open(pdf_path) as pdf:
tables = pdf.pages[page_num].extract_tables()
row_offset = 0
for table_idx, table in enumerate(tables):
if table_idx > 0:
# 表格之間加空行
row_offset += 1
for i, row in enumerate(table):
for j, cell in enumerate(row):
ws.cell(row=row_offset + i + 1, column=j + 1, value=cell)
row_offset += len(table)
wb.save(excel_path)
print(f"已保存至: {excel_path}")
table_to_excel("report.pdf", "output.xlsx")
4.3 轉(zhuǎn)換為Pandas DataFrame(最推薦)
import pandas as pd
import pdfplumber
def table_to_dataframe(pdf_path, page_num=0):
with pdfplumber.open(pdf_path) as pdf:
table = pdf.pages[page_num].extract_table()
if table:
# 第一行作為列名
df = pd.DataFrame(table[1:], columns=table[0])
return df
else:
return pd.DataFrame()
# 使用
df = table_to_dataframe("report.pdf")
print(df.head())
print(f"數(shù)據(jù)維度: {df.shape}")
# 導(dǎo)出為Excel
df.to_excel("output.xlsx", index=False)
4.4 批量處理文件夾內(nèi)所有PDF并導(dǎo)出Excel
import os
import pandas as pd
import pdfplumber
from pathlib import Path
def batch_extract_tables(input_folder, output_folder):
"""批量提取文件夾內(nèi)所有PDF的表格,每個PDF單獨保存為一個Excel"""
Path(output_folder).mkdir(parents=True, exist_ok=True)
pdf_files = list(Path(input_folder).glob("*.pdf")) + list(Path(input_folder).glob("*.PDF"))
for pdf_file in pdf_files:
print(f"處理: {pdf_file.name}")
all_tables = []
with pdfplumber.open(pdf_file) as pdf:
for page_num, page in enumerate(pdf.pages, 1):
tables = page.extract_tables()
for table_idx, table in enumerate(tables):
if table and len(table) > 1: # 至少包含表頭+一行數(shù)據(jù)
df = pd.DataFrame(table[1:], columns=table[0])
all_tables.append((page_num, table_idx + 1, df))
# 保存到Excel,每個表格一個sheet
if all_tables:
output_path = Path(output_folder) / f"{pdf_file.stem}_tables.xlsx"
with pd.ExcelWriter(output_path) as writer:
for page_num, table_idx, df in all_tables:
sheet_name = f"P{page_num}_T{table_idx}"
# Excel sheet名稱不能超過31個字符
df.to_excel(writer, sheet_name=sheet_name[:31], index=False)
print(f" ? 已保存 {len(all_tables)} 個表格至 {output_path.name}")
else:
print(f" ? 未檢測到有效表格")
batch_extract_tables("./pdfs", "./output")
五、進階處理:無邊框表格、跨頁表格與空行過濾
5.1 表格提取策略詳解
pdfplumber提供了4種表格邊緣檢測策略,可根據(jù)表格類型靈活選擇:
| 策略 | 說明 | 適用場景 |
|---|---|---|
"lines" | 使用頁面的圖形線條作為單元格邊界(默認) | 有明確邊框線的表格 |
"lines_strict" | 僅使用圖形線條,不使用矩形邊 | 嚴格按線條劃分的表格 |
"text" | 根據(jù)文本的左右/中心位置推斷虛擬豎線 | 無邊框表格 |
"explicit" | 僅使用用戶明確指定的線條 | 定制化表格提取 |
5.2 無邊框表格提取
無邊框表格是pdfplumber表格提取中最常見的挑戰(zhàn)。PDF中的表格如果沒有線條,pdfplumber無法自動識別單元格邊界。
解決方案:使用 "text" 策略,讓pdfplumber根據(jù)文本對齊來推斷表格結(jié)構(gòu)。
import pdfplumber
def extract_borderless_table(pdf_path, page_num=0):
"""提取無邊框表格"""
with pdfplumber.open(pdf_path) as pdf:
page = pdf.pages[page_num]
# 關(guān)鍵:將vertical_strategy設(shè)為"text"
table = page.extract_table({
"vertical_strategy": "text", # 根據(jù)文本對齊推斷豎線
"horizontal_strategy": "text", # 根據(jù)文本對齊推斷橫線
"snap_tolerance": 3, # 線條對齊容差
})
return table
# 使用
table = extract_borderless_table("borderless.pdf")
if table:
for row in table:
print(row)
進階技巧:對于布局復(fù)雜的無邊框表格,可以先裁剪出表格區(qū)域再提取。
def extract_borderless_table_advanced(pdf_path, bbox=None):
"""高級無邊框表格提?。合炔眉魠^(qū)域,再使用text策略"""
with pdfplumber.open(pdf_path) as pdf:
page = pdf.pages[0]
# 如果指定了邊界框,先裁剪
if bbox:
page = page.crop(bbox) # bbox = (x0, y0, x1, y1)
table = page.extract_table({
"vertical_strategy": "text",
"horizontal_strategy": "text",
"snap_tolerance": 5,
"intersection_tolerance": 5,
})
return table
# 手動指定表格區(qū)域
# 可以通過page.to_image()可視化找到坐標
table = extract_borderless_table_advanced("report.pdf", bbox=(50, 200, 550, 800))
5.3 跨頁表格處理
跨頁表格是另一個常見痛點:pdfplumber的extract_table()只能提取單頁內(nèi)的表格,無法自動合并跨頁數(shù)據(jù)。
解決方案:逐頁提取表格后手動合并。
import pandas as pd
import pdfplumber
def extract_multi_page_table(pdf_path, header_rows=1):
"""
提取跨頁表格,自動合并多頁數(shù)據(jù)
header_rows: 每頁開頭的表頭行數(shù)(通常為1)
"""
all_data = []
header = None
with pdfplumber.open(pdf_path) as pdf:
for page_num, page in enumerate(pdf.pages):
table = page.extract_table()
if not table:
continue
if page_num == 0:
# 第一頁:保存表頭
header = table[:header_rows]
# 數(shù)據(jù)行從表頭之后開始
data_rows = table[header_rows:]
else:
# 后續(xù)頁:跳過本頁的表頭
data_rows = table[header_rows:] if len(table) > header_rows else []
all_data.extend(data_rows)
# 構(gòu)建DataFrame
if header and all_data:
# 將表頭列表展平為列名
columns = [cell for row in header for cell in row if cell]
df = pd.DataFrame(all_data, columns=columns[:len(all_data[0])])
return df
return pd.DataFrame()
# 使用
df = extract_multi_page_table("long_report.pdf")
df.to_excel("merged_table.xlsx", index=False)
處理更復(fù)雜的跨頁場景:當表格行在頁面中間被截斷時,需要更精細的處理。
def extract_cross_page_table_advanced(pdf_path):
"""
高級跨頁表格處理:處理行在頁面中間被截斷的情況
"""
all_rows = []
partial_row = None
with pdfplumber.open(pdf_path) as pdf:
for page_num, page in enumerate(pdf.pages):
table = page.extract_table()
if not table:
continue
if page_num == 0:
# 第一頁:完整表格
all_rows.extend(table)
else:
# 后續(xù)頁:需要判斷第一行是否為上一頁的續(xù)行
# 簡化處理:直接追加,實際項目中需根據(jù)業(yè)務(wù)邏輯判斷
first_row = table[0] if table else []
if partial_row and first_row:
# 合并續(xù)行(假設(shè)續(xù)行第一列為空)
merged_row = [partial_row[i] + first_row[i] if first_row[i] else partial_row[i]
for i in range(len(partial_row))]
all_rows.append(merged_row)
all_rows.extend(table[1:])
else:
all_rows.extend(table)
# 暫存最后一行,供下一頁判斷
partial_row = table[-1] if table else None
return all_rows
5.4 空行與無效數(shù)據(jù)過濾
def clean_table_data(table):
"""
過濾空行和無效數(shù)據(jù)
"""
if not table:
return []
cleaned = []
for row in table:
# 檢查行是否全為空
if not row or all(cell is None or str(cell).strip() == '' for cell in row):
continue
# 清洗每個單元格
cleaned_row = []
for cell in row:
if cell is None:
cleaned_row.append('')
else:
# 去除首尾空白,替換換行符
cell_clean = str(cell).strip().replace('\n', ' ')
cleaned_row.append(cell_clean)
cleaned.append(cleaned_row)
return cleaned
# 使用示例
table = page.extract_table()
clean_table = clean_table_data(table)
六、綜合實戰(zhàn):同時提取PDF文本+表格并結(jié)構(gòu)化輸出
6.1 核心思路
很多PDF文檔同時包含正文文本和表格數(shù)據(jù)。我們需要:
- 提取全部文本內(nèi)容
- 提取所有表格內(nèi)容
- 按頁面順序整合輸出
import pdfplumber
import pandas as pd
from pathlib import Path
def extract_text_and_tables(pdf_path):
"""
同時提取PDF中的文本和表格,返回結(jié)構(gòu)化結(jié)果
"""
result = {
"metadata": {},
"pages": []
}
with pdfplumber.open(pdf_path) as pdf:
# 提取元信息
result["metadata"] = {
"total_pages": len(pdf.pages),
"page_width": pdf.pages[0].width if pdf.pages else 0,
"page_height": pdf.pages[0].height if pdf.pages else 0,
}
for page_num, page in enumerate(pdf.pages, 1):
page_data = {
"page_number": page_num,
"text": page.extract_text() or "",
"tables": []
}
# 提取表格
tables = page.extract_tables()
for table_idx, table in enumerate(tables):
if table and len(table) > 1:
# 轉(zhuǎn)換為DataFrame便于后續(xù)處理
df = pd.DataFrame(table[1:], columns=table[0])
page_data["tables"].append({
"table_index": table_idx + 1,
"dataframe": df,
"raw_table": table
})
result["pages"].append(page_data)
return result
# 使用示例
result = extract_text_and_tables("annual_report.pdf")
print(f"總頁數(shù): {result['metadata']['total_pages']}")
for page in result["pages"]:
print(f"\n第{page['page_number']}頁:")
print(f" 文本長度: {len(page['text'])} 字符")
print(f" 表格數(shù)量: {len(page['tables'])}")
6.2 提取表格后同時保留文本(排除表格區(qū)域)
有些場景需要提取“純文本”,即排除表格區(qū)域內(nèi)的內(nèi)容。pdfplumber提供了精確控制方案:先提取表格獲取邊界框,然后在提取文本時排除這些區(qū)域。
def extract_pure_text_excluding_tables(pdf_path, page_num=0):
"""
提取純文本:排除表格區(qū)域內(nèi)的內(nèi)容
"""
with pdfplumber.open(pdf_path) as pdf:
page = pdf.pages[page_num]
# 第一步:提取所有表格的邊界框
table_bboxes = []
tables = page.find_tables()
for table in tables:
# 每個Table對象都有.bbox屬性:(x0, y0, x1, y1)
table_bboxes.append(table.bbox)
# 第二步:獲取所有文本對象(帶坐標)
words = page.extract_words()
# 第三步:過濾掉位于表格區(qū)域內(nèi)的文本
pure_text_words = []
for word in words:
x0, top, x1, bottom = word['x0'], word['top'], word['x1'], word['bottom']
is_in_table = False
for bbox in table_bboxes:
if (x0 >= bbox[0] and x1 <= bbox[2] and
top >= bbox[1] and bottom <= bbox[3]):
is_in_table = True
break
if not is_in_table:
pure_text_words.append(word['text'])
return ' '.join(pure_text_words)
pure_text = extract_pure_text_excluding_tables("report.pdf")
print(pure_text)
七、常見問題與解決方案
7.1 表格識別失敗/錯位
問題現(xiàn)象:extract_table() 返回 None 或提取的數(shù)據(jù)行錯位。
原因:表格線條不完整、文本重疊或坐標對齊偏差。
解決方案:
# 方案1:使用可視化調(diào)試找到問題
with pdfplumber.open("problem.pdf") as pdf:
page = pdf.pages[0]
im = page.to_image()
im.draw_rects(page.extract_words()) # 繪制文本塊
im.save("debug.png") # 查看圖片,確認文本分布
# 也可以使用debug_tablefinder查看表格檢測結(jié)果
page.debug_tablefinder().show() # 在Jupyter中運行
# 方案2:調(diào)整表格檢測參數(shù)
table = page.extract_table({
"vertical_strategy": "lines",
"horizontal_strategy": "lines",
"snap_tolerance": 5, # 線條對齊容差
"join_tolerance": 3, # 線段連接容差
"edge_min_length": 3, # 最小邊緣長度
})
# 方案3:針對線條缺失的情況,使用lattice模式
table = page.extract_table({
"horizontal_strategy": "lines",
"vertical_strategy": "lines",
"min_horizontal_line_length": 50, # 最小橫線長度
"min_vertical_line_length": 20, # 最小豎線長度
})
7.2 中文亂碼問題
問題現(xiàn)象:提取的中文文本顯示為亂碼或方框。
原因:PDF中的中文采用特定編碼(如GBK或自定義字體子集),而解析庫默認以ASCII或UTF-8解碼。
解決方案:
# 方案1:檢查字體信息
with pdfplumber.open("chinese.pdf") as pdf:
page = pdf.pages[0]
# 查看頁面的字體列表
if hasattr(page, 'fonts'):
for font in page.fonts:
print(f"字體名: {font.get('name')}, 編碼: {font.get('encoding')}")
# 方案2:嘗試不同的文本提取參數(shù)
text = page.extract_text(laparams={
"detect_vertical": True, # 檢測豎排文本
})
# 方案3:如果pdfplumber亂碼嚴重,可考慮OCR方案
# 將PDF頁面轉(zhuǎn)為圖像后用PaddleOCR識別
from pdf2image import convert_from_path
from paddleocr import PaddleOCR
ocr = PaddleOCR(use_angle_cls=True, lang='ch')
images = convert_from_path('chinese.pdf')
for img in images:
result = ocr.ocr(img, cls=True)
for line in result:
print(line[1][0])
7.3 大文件內(nèi)存溢出
問題現(xiàn)象:處理超過500頁的PDF時內(nèi)存占用持續(xù)攀升。
原因:pdfplumber在打開PDF時不會立即加載所有頁面內(nèi)容,但隨著頁面處理數(shù)量增加,已處理頁面的緩存數(shù)據(jù)會逐漸累積。
解決方案:分批處理 + 及時釋放資源。
def process_large_pdf_in_batches(pdf_path, batch_size=50):
"""
分批處理大PDF文件,控制內(nèi)存占用
"""
with pdfplumber.open(pdf_path) as pdf:
total_pages = len(pdf.pages)
all_tables = []
for batch_start in range(0, total_pages, batch_size):
batch_end = min(batch_start + batch_size, total_pages)
print(f"處理批次: {batch_start + 1} - {batch_end}")
for page_num in range(batch_start, batch_end):
page = pdf.pages[page_num]
tables = page.extract_tables()
if tables:
all_tables.extend(tables)
# 批次處理后可以添加內(nèi)存清理邏輯
# 注意:pdf.pages是惰性加載的,每次訪問才加載
# 實際項目中可考慮將中間結(jié)果寫入文件而非全量保存
return all_tables
# 使用
all_tables = process_large_pdf_in_batches("large.pdf", batch_size=50)
八、完整可運行代碼:發(fā)票表格提取實戰(zhàn)案例
8.1 場景描述
財務(wù)人員需要從大量PDF電子發(fā)票中提取關(guān)鍵信息:發(fā)票代碼、發(fā)票號碼、開票日期、購買方名稱、銷售方名稱、金額、稅額、價稅合計等。
下圖展示了電子發(fā)票的典型布局:

8.2 完整代碼實現(xiàn)
import pdfplumber
import pandas as pd
import re
from pathlib import Path
from typing import Dict, List, Optional
def extract_invoice_info(pdf_path: str) -> Dict:
"""
從PDF電子發(fā)票中提取關(guān)鍵信息
支持全電發(fā)票和傳統(tǒng)增值稅電子發(fā)票
"""
result = {
"file_name": Path(pdf_path).name,
"invoice_code": "",
"invoice_number": "",
"invoice_date": "",
"buyer_name": "",
"seller_name": "",
"total_amount": "",
"total_tax": "",
"total_amount_with_tax": "",
"items": [], # 商品明細列表
"success": False
}
try:
with pdfplumber.open(pdf_path) as pdf:
page = pdf.pages[0]
text = page.extract_text() or ""
# 1. 提取發(fā)票基本信息(使用正則表達式)
# 發(fā)票代碼:通常為10-12位數(shù)字
code_match = re.search(r'發(fā)票代碼[::]\s*(\d{10,12})', text)
if code_match:
result["invoice_code"] = code_match.group(1)
# 發(fā)票號碼:通常為8位數(shù)字
number_match = re.search(r'發(fā)票號碼[::]\s*(\d{8})', text)
if number_match:
result["invoice_number"] = number_match.group(1)
# 開票日期:多種格式
date_match = re.search(r'開票日期[::]\s*(\d{4}[年/-]\d{1,2}[月/-]\d{1,2}日?)', text)
if date_match:
result["invoice_date"] = date_match.group(1)
# 購買方名稱
buyer_match = re.search(r'購買方[::]?\s*名稱[::]\s*(.+?)(?:\n|單位|納稅人)', text)
if not buyer_match:
buyer_match = re.search(r'購方名稱[::]\s*(.+?)(?:\n|納稅人)', text)
if buyer_match:
result["buyer_name"] = buyer_match.group(1).strip()
# 銷售方名稱
seller_match = re.search(r'銷售方[::]?\s*名稱[::]\s*(.+?)(?:\n|納稅人)', text)
if not seller_match:
seller_match = re.search(r'銷方名稱[::]\s*(.+?)(?:\n|納稅人)', text)
if seller_match:
result["seller_name"] = seller_match.group(1).strip()
# 金額匯總
amount_match = re.search(r'價稅合計[((]小寫[))]\s*[¥¥]?\s*(\d+\.?\d*)', text)
if amount_match:
result["total_amount_with_tax"] = amount_match.group(1)
# 2. 提取貨物清單表格
tables = page.extract_tables()
for table in tables:
if not table or len(table) < 2:
continue
# 判斷是否為貨物清單表格:檢查表頭是否包含"貨物名稱"、"金額"等關(guān)鍵詞
header = [str(cell).lower() if cell else "" for cell in table[0]]
header_str = " ".join(header)
if any(keyword in header_str for keyword in ["貨物", "名稱", "商品", "服務(wù)", "金額", "稅率", "稅額"]):
# 找到表格列索引
col_indices = {}
for idx, col in enumerate(header):
col_lower = col.lower() if col else ""
if "貨物" in col_lower or "名稱" in col_lower or "商品" in col_lower or "服務(wù)" in col_lower:
col_indices["name"] = idx
elif "金額" in col_lower and "稅額" not in col_lower:
col_indices["amount"] = idx
elif "稅率" in col_lower:
col_indices["tax_rate"] = idx
elif "稅額" in col_lower:
col_indices["tax_amount"] = idx
elif "數(shù)量" in col_lower:
col_indices["quantity"] = idx
elif "單價" in col_lower:
col_indices["unit_price"] = idx
# 提取數(shù)據(jù)行
for row in table[1:]: # 跳過表頭
if not row or all(cell is None or str(cell).strip() == "" for cell in row):
continue
item = {}
if "name" in col_indices:
item["name"] = str(row[col_indices["name"]]).strip() if row[col_indices["name"]] else ""
if "amount" in col_indices:
item["amount"] = str(row[col_indices["amount"]]).strip() if row[col_indices["amount"]] else ""
if "tax_rate" in col_indices:
item["tax_rate"] = str(row[col_indices["tax_rate"]]).strip() if row[col_indices["tax_rate"]] else ""
if "tax_amount" in col_indices:
item["tax_amount"] = str(row[col_indices["tax_amount"]]).strip() if row[col_indices["tax_amount"]] else ""
if "quantity" in col_indices:
item["quantity"] = str(row[col_indices["quantity"]]).strip() if row[col_indices["quantity"]] else ""
if "unit_price" in col_indices:
item["unit_price"] = str(row[col_indices["unit_price"]]).strip() if row[col_indices["unit_price"]] else ""
if item.get("name"):
result["items"].append(item)
result["success"] = True
except Exception as e:
result["error"] = str(e)
print(f"處理失敗: {result['file_name']}, 錯誤: {e}")
return result
def batch_extract_invoices(input_folder: str, output_excel: str):
"""
批量處理文件夾內(nèi)的所有發(fā)票PDF,匯總輸出到Excel
"""
input_path = Path(input_folder)
pdf_files = list(input_path.glob("*.pdf")) + list(input_path.glob("*.PDF"))
if not pdf_files:
print(f"未找到PDF文件: {input_folder}")
return
all_results = []
for pdf_file in pdf_files:
print(f"處理: {pdf_file.name}")
info = extract_invoice_info(str(pdf_file))
all_results.append(info)
# 轉(zhuǎn)換為DataFrame
df_main = pd.DataFrame([{
"文件名": r["file_name"],
"發(fā)票代碼": r["invoice_code"],
"發(fā)票號碼": r["invoice_number"],
"開票日期": r["invoice_date"],
"購買方": r["buyer_name"],
"銷售方": r["seller_name"],
"價稅合計": r["total_amount_with_tax"],
"是否成功": r["success"],
"商品數(shù)量": len(r.get("items", []))
} for r in all_results])
# 商品明細DataFrame
items_list = []
for r in all_results:
for item in r.get("items", []):
items_list.append({
"發(fā)票號碼": r["invoice_number"],
"商品名稱": item.get("name", ""),
"金額": item.get("amount", ""),
"稅率": item.get("tax_rate", ""),
"稅額": item.get("tax_amount", ""),
"數(shù)量": item.get("quantity", ""),
"單價": item.get("unit_price", "")
})
df_items = pd.DataFrame(items_list)
# 保存到Excel(多Sheet)
with pd.ExcelWriter(output_excel) as writer:
df_main.to_excel(writer, sheet_name="發(fā)票匯總", index=False)
if not df_items.empty:
df_items.to_excel(writer, sheet_name="商品明細", index=False)
print(f"\n處理完成!共 {len(all_results)} 張發(fā)票")
print(f"成功: {df_main['是否成功'].sum()} 張")
print(f"輸出文件: {output_excel}")
# 使用示例
if __name__ == "__main__":
# 批量處理發(fā)票
batch_extract_invoices("./invoices", "./invoice_summary.xlsx")
# 單張發(fā)票測試
# info = extract_invoice_info("./invoices/single_invoice.pdf")
# print(info)
九、總結(jié)與延伸建議
9.1 本文核心要點回顧
| 知識點 | 關(guān)鍵內(nèi)容 |
|---|---|
| 核心優(yōu)勢 | 像素級坐標定位、自動識別表格行列、支持合并單元格 |
| 安裝配置 | pip install pdfplumber,配套pandas/openpyxl |
| 基礎(chǔ)提取 | extract_table() 提取最大表格,extract_tables() 提取所有 |
| 導(dǎo)出方式 | CSV(csv模塊)、Excel(openpyxl)、DataFrame(pandas) |
| 無邊框表格 | vertical_strategy="text" 策略 |
| 跨頁表格 | 逐頁提取后手動合并,注意表頭處理 |
| 中文亂碼 | 檢查字體映射,必要時換OCR方案 |
| 大文件優(yōu)化 | 分批處理,控制內(nèi)存占用 |
9.2 工具對比:pdfplumber vs 其他PDF表格提取工具
| 工具 | 核心優(yōu)勢 | 適用場景 | 局限性 |
|---|---|---|---|
| pdfplumber | 像素級坐標定位,自動識別邊框 | 機器生成的PDF表格(最推薦) | 掃描件無效 |
| Camelot | 支持lattice和stream雙模式 | 有明確邊框的表格 | 無邊框表格需調(diào)參 |
| tabula-py | 簡單易用,基于Java Tabula | 快速原型驗證 | 需要Java環(huán)境 |
| PyMuPDF | 速度最快,功能全面 | 大型PDF批量處理 | 學(xué)習(xí)曲線較陡 |
| PyPDF2 | 輕量無依賴 | 純文本PDF | 完全不支持表格 |
各工具在不同文檔類型上的表現(xiàn)存在差異:Camelot在投標文件中表現(xiàn)最佳,而PyMuPDF在手工文檔類別中表現(xiàn)更優(yōu)。
9.3 進階學(xué)習(xí)方向
- pandas數(shù)據(jù)處理:提取后的表格DataFrame可進行數(shù)據(jù)清洗、聚合、統(tǒng)計分析
- 正則表達式:結(jié)合pdfplumber提取的非表格關(guān)鍵信息(如發(fā)票號碼、日期)
- PaddleOCR:處理掃描件PDF時,先用OCR識別再提取表格
- 可視化調(diào)試:掌握
to_image()和debug_tablefinder(),快速定位問題
9.4 一句話總結(jié)
PyPDF2只認識文字不認識表格,而pdfplumber同時認識文字和表格——這就是兩者最本質(zhì)的區(qū)別。
如果你只需要從PDF中提取純文本,PyPDF2已經(jīng)足夠;但如果你的PDF中包含表格數(shù)據(jù),pdfplumber是不可替代的首選工具。
以上就是零基礎(chǔ)上手使用Python pdfplumber提取原生PDF表格的詳細內(nèi)容,更多關(guān)于Python pdfplumber提取PDF表格的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Python pandas DataFrame操作的實現(xiàn)代碼
這篇文章主要介紹了Python pandas DataFrame操作的實現(xiàn)代碼,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2019-06-06
PyTorch中torch.utils.data.DataLoader實例詳解
torch.utils.data.DataLoader主要是對數(shù)據(jù)進行batch的劃分,下面這篇文章主要給大家介紹了關(guān)于PyTorch中torch.utils.data.DataLoader的相關(guān)資料,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下2022-09-09
Python給函數(shù)加上狀態(tài)的多種方式小結(jié)
通常,函數(shù)是無狀態(tài)的:每次調(diào)用它都會從相同的初始狀態(tài)開始執(zhí)行,而有時候,我們希望函數(shù)在多次調(diào)用之間能夠保留某些信息,這種功能可以通過給函數(shù)加上狀態(tài)來實現(xiàn),所以本文給大家介紹了Python給函數(shù)加上狀態(tài)的多種方式,需要的朋友可以參考下2025-06-06
python3連接MySQL數(shù)據(jù)庫實例詳解
這篇文章主要為大家詳細介紹了python3連接MySQL數(shù)據(jù)庫實例,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-05-05

