pdfplumber、pandas根据指定字段提取PDF跨页表格数据

import pdfplumber
import pandas as pd
from typing import List, Dict, Tuple, Optional

def extract_table_by_title(
    pdf_path: str,
    target_title: str,
    page_range: Tuple[int, int] = (1, None),  # (起始页, 结束页),None 表示到最后一页
    title_padding: float = 5,  # 标题下方搜索表格的垂直间距(像素)
    table_merge_threshold: float = 30,  # 跨页表格合并的垂直阈值(像素)
    header_match_tolerance: float = 0.8,  # 表头匹配的相似度阈值(0-1)
    debug: bool = False
) -> List[pd.DataFrame]:
    """
    精准提取 PDF 中指定标题下方的表格,支持跨多页、复杂排版场景,保留空白行
    :param pdf_path: PDF 文件路径
    :param target_title: 要匹配的标题文本(支持模糊匹配)
    :param page_range: 搜索的页码范围,(1, 5) 表示 1-5 页,第二值为 None 表示到最后一页
    :param title_padding: 标题下方开始搜索表格的垂直距离(避免误判标题内的表格)
    :param table_merge_threshold: 跨页表格合并的垂直阈值(判断是否为同一表格)
    :param header_match_tolerance: 表头匹配的相似度阈值,用于判断跨页表格是否为同一表格的延续
    :param debug: 是否输出调试信息(坐标、页码等)
    :return: 提取到的表格列表(每个元素是 DataFrame)
    """
    all_tables = []
    current_table_rows: List[List[str]] = []  # 存储跨页收集的表格行(原始列表形式)
    current_header: Optional[List[str]] = None  # 存储当前表格的表头
    title_found = False  # 标记是否找到目标标题
    title_bottom = 0.0  # 记录标题的底部 y 坐标(用于定位表格范围)
    page_width = 0.0  # 记录页面宽度,用于辅助判断表格是否跨页

    def _is_header_match(header1: List[str], header2: List[str]) -> bool:
        """判断两个表头是否匹配,基于内容相似度"""
        if len(header1) != len(header2):
            return False
        match_count = 0
        for h1, h2 in zip(header1, header2):
            if h1.strip().lower() == h2.strip().lower():
                match_count += 1
        return match_count / len(header1) >= header_match_tolerance

    with pdfplumber.open(pdf_path) as pdf:
        # 处理页码范围
        start_page = page_range[0]
        end_page = page_range[1] if page_range[1] is not None else len(pdf.pages)
        end_page = min(end_page, len(pdf.pages))  # 确保不超过实际页数
        if start_page > end_page:
            return all_tables

        # 记录页面宽度(默认取第一页宽度,假设所有页宽度一致)
        first_page = pdf.pages[0]
        page_width = first_page.bbox[2] - first_page.bbox[0]

        for page_num in range(start_page, end_page + 1):
            page = pdf.pages[page_num - 1]  # pdfplumber 内部页码从 0 开始
            page_height = page.bbox[3]  # 页面底部 y 坐标
            page_text = page.extract_text()
            if debug:
                print(f"\n=== 处理第 {page_num} 页 ===")

            # ------------- 1. 查找标题(未找到时执行)-------------
            if not title_found:
                # 遍历页面文本块,匹配标题(支持模糊包含)
                words = page.extract_words()
                for word_group in words:
                    if target_title in word_group["text"]:
                        title_found = True
                        title_bottom = word_group["bottom"]  # 标题的底部坐标
                        if debug:
                            print(f"找到标题「{target_title}」,坐标: {word_group}")

                        # 计算当前页表格搜索范围:标题底部 + 间距 ~ 页面底部
                        table_search_bbox = (
                            page.bbox[0],  # 左边界(页面最左)
                            title_bottom + title_padding,  # 上边界(标题下方)
                            page.bbox[2],  # 右边界(页面最右)
                            page_height  # 下边界(页面最底)
                        )
                        if debug:
                            print(f"表格搜索范围: {table_search_bbox}")

                        # 提取标题下方区域的表格
                        cropped_page = page.within_bbox(table_search_bbox)
                        tables_in_area = cropped_page.extract_tables()

                        # 处理提取到的表格(优先选第一个有效表格)
                        for table in tables_in_area:
                            if len(table) == 0:
                                continue
                            # 第一行作为表头
                            current_header = [col.strip() for col in table[0]]
                            # 内容行(从第二行开始,保留空白行)
                            content_rows = table[1:]  # 不再过滤空白行
                            current_table_rows.extend(content_rows)

                            # 检查表格是否跨页(表格底部接近页面底部 或 接近页面宽度)
                            table_bottom = cropped_page.bbox[3] if cropped_page.bbox else 0.0
                            table_width = cropped_page.bbox[2] - cropped_page.bbox[0] if cropped_page.bbox else 0.0
                            if (page_height - table_bottom) < table_merge_threshold or \
                                    abs(table_width - page_width) < table_merge_threshold:
                                if debug:
                                    print(f"第 {page_num} 页表格可能跨页,继续收集...")
                            else:
                                # 表格未跨页,直接生成 DataFrame
                                if current_table_rows:
                                    df = pd.DataFrame(current_table_rows, columns=current_header)
                                    all_tables.append(df)
                                    current_table_rows = []
                                    current_header = None
                            break  # 找到标题下方第一个表格后跳出循环
                continue  # 找到标题后,当前页后续逻辑处理跨页表格

            # ------------- 2. 处理跨页表格(标题已找到时执行)-------------
            if title_found and current_header is not None:
                # 提取当前页所有表格(判断是否是延续表格)
                all_page_tables = page.extract_tables()
                for table in all_page_tables:
                    if len(table) == 0:
                        continue
                    cleaned_table = [row for row in table]  # 保留空白行,仅去除单元格空白(可选)
                    # 如需保留单元格空白,可直接使用 table:
                    # cleaned_table = table

                    if not cleaned_table:
                        continue

                    # 判断是否是当前表格的延续(通过表头匹配 / 无表头延续)
                    new_header = cleaned_table[0] if len(cleaned_table) > 0 else []
                    if _is_header_match(current_header, new_header):
                        # 遇到相同表头,说明是新表格,先保存当前表格
                        if current_table_rows:
                            df = pd.DataFrame(current_table_rows, columns=current_header)
                            all_tables.append(df)
                            current_table_rows = []
                        # 新表格的表头和内容
                        current_header = new_header
                        current_table_rows.extend(cleaned_table[1:])
                    else:
                        # 无相同表头,视为当前表格的延续(检查列数是否匹配)
                        if len(cleaned_table[0]) == len(current_header):
                            current_table_rows.extend(cleaned_table)
                        else:
                            if debug:
                                print(f"第 {page_num} 页表格列数不匹配,跳过:{cleaned_table}")

                # 检查当前页是否是最后一页,或表格是否结束
                table_bottom = page.bbox[3] if current_table_rows else 0.0
                if page_num == end_page or (page_height - table_bottom) > table_merge_threshold:
                    if current_table_rows:
                        df = pd.DataFrame(current_table_rows, columns=current_header)
                        all_tables.append(df)
                        current_table_rows = []
                        current_header = None

    return all_tables

# 演示
if __name__ == "__main__":
    pdf_path = "test.pdf"  # 替换为你的 PDF 文件路径
    target_title = "员工数据"  # 替换为要提取的标题

    # 配置参数:根据 PDF 实际排版调整
    tables = extract_table_by_title(
        pdf_path,
        target_title,
        page_range=(1, None),  # 从第 1 页搜索到最后一页
        title_padding=2,  # 标题下方较小间距,适配紧密排版
        table_merge_threshold=20,  # 跨页合并阈值,可根据实际调整
        header_match_tolerance=0.6,  # 表头匹配容忍度,允许部分差异
        debug=True  # 开启调试,输出详细信息
    )

    if tables:
        for i, df in enumerate(tables):
            print(f"\n=== 提取到第 {i+1} 个表格 ===")
            print(df)
            # 保存为 CSV(按需启用)
            df.to_csv(f"extracted_table_{i+1}.csv", index=False, encoding="utf-8")
    else:
        print(f"未找到标题为「{target_title}」的表格")

你可能感兴趣的:(pdfplumber、pandas根据指定字段提取PDF跨页表格数据)