最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

零基礎(chǔ)上手使用Python?pdfplumber提取原生PDF表格

 更新時間:2026年04月09日 09:03:59   作者:無心水  
pdfplumber是基于PDFMiner.six構(gòu)建的PDF解析庫,它不僅能提取文本,還可以將數(shù)據(jù)完整準確地提取出來,本文將從零開始,帶你系統(tǒng)掌握pdfplumber的表格提取功能并附完整可運行代碼和可視化調(diào)試技巧

表格神器|告別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)勢

能力維度PyPDF2pdfplumber
文本提取簡單拼接,段落錯亂按坐標還原布局,保留段落
表格提取? 不支持? 自動識別邊框,還原行列
合并單元格? 無法識別? 自動合并
坐標定位? 無? 字符級精確定位
可視化調(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ù)。我們需要:

  1. 提取全部文本內(nèi)容
  2. 提取所有表格內(nèi)容
  3. 按頁面順序整合輸出
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)代碼

    這篇文章主要介紹了Python pandas DataFrame操作的實現(xiàn)代碼,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2019-06-06
  • python爬蟲獲取京東手機圖片的圖文教程

    python爬蟲獲取京東手機圖片的圖文教程

    下面小編就為大家分享一篇python爬蟲獲取京東手機圖片的圖文教程,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2017-12-12
  • Python的線程之線程同步

    Python的線程之線程同步

    這篇文章主要為大家介紹了Python線程同步,具有一定的參考價值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來幫助
    2021-12-12
  • PyTorch中torch.utils.data.DataLoader實例詳解

    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中的__repr__()方法小結(jié)

    Python中的__repr__()方法小結(jié)

    在 Python 中,__repr__()?是一個特殊方法,用于定義對象的字符串表示形式,本文主要介紹了Python中的__repr__()方法小結(jié),具有一定的參考價值,感興趣的可以了解一下
    2024-01-01
  • Python中的迭代器和生成器詳解

    Python中的迭代器和生成器詳解

    這篇文章主要介紹了Python中的迭代器和生成器詳解,生成器表達式是用來生成函數(shù)調(diào)用時序列參數(shù)的一種迭代器寫法,生成器對象可以遍歷或轉(zhuǎn)化為列表或元組等數(shù)據(jù)結(jié)構(gòu),但不能切片,需要的朋友可以參考下
    2023-07-07
  • Python給函數(shù)加上狀態(tài)的多種方式小結(jié)

    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ù)庫實例詳解

    這篇文章主要為大家詳細介紹了python3連接MySQL數(shù)據(jù)庫實例,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2018-05-05
  • 基于python實現(xiàn)井字棋小游戲

    基于python實現(xiàn)井字棋小游戲

    這篇文章主要為大家詳細介紹了python實現(xiàn)井字棋小游戲,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2020-03-03
  • Python如何建立多個值和單個鍵的映射

    Python如何建立多個值和單個鍵的映射

    在Python中,常見的字典只能映射單個鍵到單個值,若需映射單個鍵到多值,可以通過將值存儲于列表或集合中實現(xiàn),使用列表可以保持元素插入順序,而使用集合則可以去重,collections模塊的defaultdict類簡化了此類多值字典的創(chuàng)建過程
    2024-09-09

最新評論

肃宁县| 喀喇| 浮梁县| 布尔津县| 徐州市| 青州市| 蕲春县| 正宁县| 武胜县| 高要市| 防城港市| 始兴县| 西昌市| 高雄县| 观塘区| 来宾市| 宿松县| 长子县| 和硕县| 开远市| 苍梧县| 讷河市| 延长县| 上林县| 庄河市| 都匀市| 辉南县| 西昌市| 永寿县| 兖州市| 疏勒县| 维西| 宜阳县| 邢台县| 洛阳市| 名山县| 盈江县| 临汾市| 竹山县| 贵定县| 自贡市|