pdf-extraction

pdf-extraction

熱門

使用 pdfplumber 從 PDF 中提取文字、表格與元數據

295星標
68分支
更新於 2026/1/31
SKILL.md
readonlyread-only
name
pdf-extraction
description

使用 pdfplumber 從 PDF 中提取文字、表格與元數據

version
1.0

PDF 提取技能

概述

本技能可使用 pdfplumber 精確提取 PDF 文件中的文字、表格與元數據。pdfplumber 是 PDF 資料提取的首選函式庫,能提供詳細的字元級定位、準確的表格偵測以及視覺化除錯功能,遠勝於基本 PDF 閱讀器。

使用方式

  1. 提供您要提取的 PDF 檔案
  2. 指定您需要的內容:文字、表格、圖片或元數據
  3. 我將產生 pdfplumber 程式碼並執行

範例提示:

  • "從這份財務報告中提取所有表格"
  • "取得這份文件第 5 到 10 頁的文字"
  • "找出並提取此 PDF 中的發票總金額"
  • "將此 PDF 表格轉換為 CSV/Excel"

領域知識

pdfplumber 基礎

import pdfplumber

# 開啟 PDF
with pdfplumber.open('document.pdf') as pdf:
    # 存取頁面
    first_page = pdf.pages[0]
    
    # 文件元數據
    print(pdf.metadata)
    
    # 頁數
    print(len(pdf.pages))

PDF 結構

PDF 文件
├── metadata(標題、作者、建立日期)
├── pages[]
│   ├── chars(含位置資訊的個別字元)
│   ├── words(分組後的字詞)
│   ├── lines(水平/垂直線條)
│   ├── rects(矩形)
│   ├── curves(貝茲曲線)
│   └── images(內嵌圖片)
└── outline(書籤/目錄)

文字提取

基本文字
with pdfplumber.open('document.pdf') as pdf:
    # 單頁
    text = pdf.pages[0].extract_text()
    
    # 所有頁面
    full_text = ''
    for page in pdf.pages:
        full_text += page.extract_text() or ''
進階文字選項
# 保留版面配置
text = page.extract_text(
    x_tolerance=3,      # 水平分組容差
    y_tolerance=3,      # 垂直容差
    layout=True,        # 保留版面
    x_density=7.25,     # 每單位寬度字元數
    y_density=13        # 每單位高度字元數
)

# 提取含位置資訊的字詞
words = page.extract_words(
    x_tolerance=3,
    y_tolerance=3,
    keep_blank_chars=False,
    use_text_flow=False
)

# 每個字詞包含:text, x0, top, x1, bottom 等
for word in words:
    print(f"{word['text']} 位於 ({word['x0']}, {word['top']})")
字元層級存取
# 取得所有字元
chars = page.chars

for char in chars:
    print(f"'{char['text']}' 位於 ({char['x0']}, {char['top']})")
    print(f"  字型:{char['fontname']},大小:{char['size']}")

表格提取

基本表格提取
with pdfplumber.open('report.pdf') as pdf:
    page = pdf.pages[0]
    
    # 提取所有表格
    tables = page.extract_tables()
    
    for i, table in enumerate(tables):
        print(f"表格 {i+1}:")
        for row in table:
            print(row)
進階表格設定
# 自訂表格偵測
table_settings = {
    "vertical_strategy": "lines",      # 或 "text", "explicit"
    "horizontal_strategy": "lines",
    "explicit_vertical_lines": [],     # 自訂垂直線位置
    "explicit_horizontal_lines": [],
    "snap_tolerance": 3,
    "snap_x_tolerance": 3,
    "snap_y_tolerance": 3,
    "join_tolerance": 3,
    "edge_min_length": 3,
    "min_words_vertical": 3,
    "min_words_horizontal": 1,
    "intersection_tolerance": 3,
    "text_tolerance": 3,
    "text_x_tolerance": 3,
    "text_y_tolerance": 3,
}

tables = page.extract_tables(table_settings)
表格尋找
# 尋找表格(不提取)
table_finder = page.find_tables()

for table in table_finder:
    print(f"表格位於:{table.bbox}")  # (x0, top, x1, bottom)
    
    # 提取特定表格
    data = table.extract()

視覺化除錯

# 建立視覺化除錯圖片
im = page.to_image(resolution=150)

# 繪製偵測到的物件
im.draw_rects(page.chars)        # 字元邊界框
im.draw_rects(page.words)        # 字詞邊界框
im.draw_lines(page.lines)        # 線條
im.draw_rects(page.rects)        # 矩形

# 儲存除錯圖片
im.save('debug.png')

# 除錯表格
im.reset()
im.debug_tablefinder()
im.save('table_debug.png')

裁切與篩選

裁切至特定區域
# 定義邊界框 (x0, top, x1, bottom)
bbox = (0, 0, 300, 200)

# 裁切頁面
cropped = page.crop(bbox)

# 從裁切區域提取
text = cropped.extract_text()
tables = cropped.extract_tables()
依位置篩選
# 依區域篩選字元
def within_bbox(obj, bbox):
    x0, top, x1, bottom = bbox
    return (obj['x0'] >= x0 and obj['x1'] <= x1 and
            obj['top'] >= top and obj['bottom'] <= bottom)

bbox = (100, 100, 400, 300)
filtered_chars = [c for c in page.chars if within_bbox(c, bbox)]
依字型篩選
# 依字型取得文字
def extract_by_font(page, font_name):
    chars = [c for c in page.chars if font_name in c['fontname']]
    return ''.join(c['text'] for c in chars)

# 提取粗體文字(字型名稱常含 "Bold")
bold_text = extract_by_font(page, 'Bold')

# 依大小提取
large_chars = [c for c in page.chars if c['size'] > 14]

元數據與結構

with pdfplumber.open('document.pdf') as pdf:
    # 文件元數據
    meta = pdf.metadata
    print(f"標題:{meta.get('Title')}")
    print(f"作者:{meta.get('Author')}")
    print(f"建立日期:{meta.get('CreationDate')}")
    
    # 頁面資訊
    for i, page in enumerate(pdf.pages):
        print(f"第 {i+1} 頁:{page.width} x {page.height}")
        print(f"  旋轉角度:{page.rotation}")

最佳實務

  1. 視覺化除錯:使用 to_image() 了解 PDF 結構
  2. 調整表格設定:針對特定 PDF 調整容差值
  3. 處理掃描 PDF:先使用 OCR(本技能適用於原生文字)
  4. 逐頁處理:大型 PDF 避免一次載入所有頁面
  5. 檢查文字是否存在:部分 PDF 為圖片格式,需確認文字存在

常見模式

提取所有表格至 DataFrame

import pandas as pd

def pdf_tables_to_dataframes(pdf_path):
    """從 PDF 提取所有表格並轉為 pandas DataFrame。"""
    dfs = []
    
    with pdfplumber.open(pdf_path) as pdf:
        for i, page in enumerate(pdf.pages):
            tables = page.extract_tables()
            
            for j, table in enumerate(tables):
                if table and len(table) > 1:
                    # 第一列作為標題
                    df = pd.DataFrame(table[1:], columns=table[0])
                    df['_page'] = i + 1
                    df['_table'] = j + 1
                    dfs.append(df)
    
    return dfs

提取特定區域

def extract_invoice_amount(pdf_path):
    """從典型發票版面提取金額。"""
    with pdfplumber.open(pdf_path) as pdf:
        page = pdf.pages[0]
        
        # 搜尋 "Total" 並取得附近數字
        words = page.extract_words()
        
        for i, word in enumerate(words):
            if 'total' in word['text'].lower():
                # 查看後續幾個字詞
                for next_word in words[i+1:i+5]:
                    text = next_word['text'].replace(',', '').replace('$', '')
                    try:
                        return float(text)
                    except ValueError:
                        continue
    
    return None

多欄版面

def extract_columns(page, num_columns=2):
    """從多欄版面提取文字。"""
    width = page.width
    col_width = width / num_columns
    
    columns = []
    for i in range(num_columns):
        x0 = i * col_width
        x1 = (i + 1) * col_width
        
        cropped = page.crop((x0, 0, x1, page.height))
        columns.append(cropped.extract_text())
    
    return columns

範例

範例 1:財務報表表格提取

import pdfplumber
import pandas as pd

def extract_financial_tables(pdf_path):
    """從財務報告提取表格並儲存至 Excel。"""
    
    with pdfplumber.open(pdf_path) as pdf:
        all_tables = []
        
        for page_num, page in enumerate(pdf.pages):
            # 除錯:儲存表格視覺化
            im = page.to_image()
            im.debug_tablefinder()
            im.save(f'debug_page_{page_num+1}.png')
            
            # 提取表格
            tables = page.extract_tables({
                "vertical_strategy": "lines",
                "horizontal_strategy": "lines",
                "snap_tolerance": 5,
            })
            
            for table in tables:
                if table and len(table) > 1:
                    # 清理資料
                    clean_table = []
                    for row in table:
                        clean_row = [cell.strip() if cell else '' for cell in row]
                        clean_table.append(clean_row)
                    
                    df = pd.DataFrame(clean_table[1:], columns=clean_table[0])
                    df['來源頁面'] = page_num + 1
                    all_tables.append(df)
        
        # 儲存至 Excel 多個工作表
        with pd.ExcelWriter('extracted_tables.xlsx') as writer:
            for i, df in enumerate(all_tables):
                df.to_excel(writer, sheet_name=f'Table_{i+1}', index=False)
        
        return all_tables

tables = extract_financial_tables('annual_report.pdf')
print(f"已提取 {len(tables)} 個表格")

範例 2:發票資料提取

import pdfplumber
import re
from datetime import datetime

def extract_invoice_data(pdf_path):
    """從發票 PDF 提取結構化資料。"""
    
    data = {
        'invoice_number': None,
        'date': None,
        'total': None,
        'line_items': []
    }
    
    with pdfplumber.open(pdf_path) as pdf:
        page = pdf.pages[0]
        text = page.extract_text()
        
        # 提取發票號碼
        inv_match = re.search(r'Invoice\s*#?\s*:?\s*(\w+)', text, re.IGNORECASE)
        if inv_match:
            data['invoice_number'] = inv_match.group(1)
        
        # 提取日期
        date_match = re.search(r'Date\s*:?\s*(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})', text)
        if date_match:
            data['date'] = date_match.group(1)
        
        # 提取總金額
        total_match = re.search(r'Total\s*:?\s*\$?([\d,]+\.?\d*)', text, re.IGNORECASE)
        if total_match:
            data['total'] = float(total_match.group(1).replace(',', ''))
        
        # 從表格提取明細項目
        tables = page.extract_tables()
        for table in tables:
            if table and any('description' in str(row).lower() for row in table[:2]):
                # 找到明細表格
                for row in table[1:]:  # 跳過標題列
                    if row and len(row) >= 3:
                        data['line_items'].append({
                            'description': row[0],
                            'quantity': row[1] if len(row) > 1 else None,
                            'amount': row[-1]
                        })
    
    return data

invoice = extract_invoice_data('invoice.pdf')
print(f"發票 #{invoice['invoice_number']}")
print(f"總金額:${invoice['total']}")

範例 3:履歷解析器

import pdfplumber

def parse_resume(pdf_path):
    """從履歷提取結構化章節。"""
    
    with pdfplumber.open(pdf_path) as pdf:
        full_text = ''
        for page in pdf.pages:
            full_text += (page.extract_text() or '') + '\n'
        
        # 常見履歷章節
        sections = {
            'contact': '',
            'summary': '',
            'experience': '',
            'education': '',
            'skills': ''
        }
        
        # 依常見標題分割
        import re
        section_patterns = {
            'summary': r'(summary|objective|profile)',
            'experience': r'(experience|employment|work history)',
            'education': r'(education|academic)',
            'skills': r'(skills|competencies|technical)'
        }
        
        lines = full_text.split('\n')
        current_section = 'contact'
        
        for line in lines:
            line_lower = line.lower().strip()
            
            # 檢查是否為章節標題
            for section, pattern in section_patterns.items():
                if re.match(pattern, line_lower):
                    current_section = section
                    break
            
            sections[current_section] += line + '\n'
        
        return sections

resume = parse_resume('resume.pdf')
print("技能:", resume['skills'])

限制

  • 無法從掃描/圖片 PDF 提取(請先使用 OCR)
  • 複雜版面可能需要手動調整
  • 部分 PDF 加密類型不受支援
  • 內嵌字型可能影響文字提取
  • 不具備直接編輯 PDF 的能力

安裝

pip install pdfplumber

# 用於圖片除錯(選用)
pip install Pillow

資源