You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于PDFMiner从试卷PDF提取题目及子题截图的技术难题

从PDF考试试卷提取题目及子题独立截图的解决方案

问题概述

需从PDF格式的A-Level物理试卷中提取题干与子题的独立截图,但使用PDFMiner时无法精准识别题号:

  • 误将页面中的其他数字判定为题号
  • 无法覆盖两种题号排版场景:主题号单独存在于LTTextBoxHorizontal、主题号与子题(如(a))同属一个LTTextBoxHorizontal

现有代码问题分析

  1. 题号匹配逻辑过于简单:仅通过text.startswith(str(q_num))判断,未考虑试卷题号的标准格式(如1.、(a)),导致误判其他数字
  2. 未结合排版特征:忽略了题号通常具备的左侧定位、更大字号/加粗等视觉特征
  3. 未处理题号与子题同框的场景:现有逻辑仅针对单独成框的题号,无法识别同框内的题号与子题分界

改进方案(无AI免费实现)

核心思路

结合PDFMiner的布局分析能力,通过正则匹配格式+排版特征筛选定位题号区域,再用pdf2image将PDF转为图片后,基于识别到的边界框(bbox)截取独立截图。

具体步骤

  • 正则匹配题号格式:针对目标试卷的题号规则,用正则匹配主题号(如^\d+\.\s)和子题号(如^\([a-z]\)\s)
  • 排版特征辅助筛选:提取文本框中字符的字号、粗细,题号通常使用更大字号或加粗字体;同时题号多位于页面左侧固定区域
  • 区分两种题号场景:
    • 单独成框的主题号:直接定位该文本框的bbox作为题干起始
    • 与子题同框的主题号:拆分文本框内的行,找到主题号对应的行bbox
  • 生成独立截图:将PDF转为图片后,根据每个题号/子题的bbox区域裁剪并保存

完整实现代码

import re
from pdfminer.high_level import extract_pages
from pdfminer.layout import LTTextBoxHorizontal, LTTextLineHorizontal, LTChar
from pdf2image import convert_from_path
from PIL import Image

# 配置参数
PDF_PATH = "./PhysicsPdfs/9702_m23_qp_22.pdf"
OUTPUT_DIR = "./extracted_questions"
MIN_FONT_SIZE = 12  # 题号通常的最小字号
LEFT_X_THRESHOLD = 100  # 题号左侧x坐标阈值

# 创建输出目录
import os
os.makedirs(OUTPUT_DIR, exist_ok=True)

# 正则表达式:匹配主题号(如1. 2.)和子题号(如(a) (b))
MAIN_Q_PATTERN = re.compile(r"^\d+\.\s")
SUB_Q_PATTERN = re.compile(r"^\([a-z]\)\s")

def is_bold(char: LTChar) -> bool:
    """判断字符是否为加粗"""
    return "Bold" in char.fontname or "bold" in char.fontname

def get_textline_font_info(textline: LTTextLineHorizontal) -> tuple[float, bool]:
    """获取文本行的最大字号和是否加粗"""
    max_size = 0
    has_bold = False
    for char in textline:
        if isinstance(char, LTChar):
            if char.size > max_size:
                max_size = char.size
            if is_bold(char):
                has_bold = True
    return max_size, has_bold

def extract_question_bboxes(pdf_path: str) -> list[dict]:
    """提取所有主题干和子题的边界框信息"""
    question_bboxes = []
    current_page = 0

    for page_layout in extract_pages(pdf_path):
        current_page += 1
        for element in page_layout:
            if isinstance(element, LTTextBoxHorizontal):
                # 遍历文本框内的每一行
                for textline in element:
                    if isinstance(textline, LTTextLineHorizontal):
                        text = textline.get_text().strip()
                        if not text:
                            continue

                        # 获取文本行的字体信息和位置
                        x0, y0, x1, y1 = textline.bbox
                        font_size, is_bold_text = get_textline_font_info(textline)

                        # 匹配主题号:左侧位置、字号达标、格式匹配、加粗
                        if (x0 < LEFT_X_THRESHOLD 
                            and font_size >= MIN_FONT_SIZE 
                            and is_bold_text
                            and MAIN_Q_PATTERN.match(text)):
                            question_num = MAIN_Q_PATTERN.match(text).group().strip()
                            question_bboxes.append({
                                "type": "main",
                                "num": question_num,
                                "page": current_page - 1,  # pdf2image的页码从0开始
                                "bbox": (x0, y0, x1, y1)
                            })
                        # 匹配子题号:格式匹配
                        elif SUB_Q_PATTERN.match(text):
                            sub_num = SUB_Q_PATTERN.match(text).group().strip()
                            question_bboxes.append({
                                "type": "sub",
                                "num": sub_num,
                                "page": current_page - 1,
                                "bbox": (x0, y0, x1, y1)
                            })
    return question_bboxes

def crop_questions(pdf_path: str, bboxes: list[dict], output_dir: str):
    """根据边界框裁剪图片并保存"""
    # 将PDF转为图片(dpi设置为300保证清晰度)
    pages = convert_from_path(pdf_path, dpi=300)

    for idx, bbox_info in enumerate(bboxes):
        page_idx = bbox_info["page"]
        img = pages[page_idx]
        img_width, img_height = img.size

        # PDF的bbox坐标是左下角为原点,PIL是左上角为原点,需要转换
        x0, y0, x1, y1 = bbox_info["bbox"]
        # 转换y坐标:PDF的y0是底部,PIL的y0是顶部
        pil_y0 = img_height - y1
        pil_y1 = img_height - y0

        # 裁剪区域:(left, upper, right, lower)
        crop_area = (x0*3, pil_y0*3, x1*3, pil_y1*3)  # dpi=300,坐标放大3倍
        cropped_img = img.crop(crop_area)

        # 保存图片
        filename = f"{bbox_info['type']}_q_{bbox_info['num'].replace('.', '').replace('(', '').replace(')', '')}_{idx+1}.png"
        cropped_img.save(os.path.join(output_dir, filename))

# 执行流程
if __name__ == "__main__":
    bboxes = extract_question_bboxes(PDF_PATH)
    crop_questions(PDF_PATH, bboxes, OUTPUT_DIR)
    print(f"已提取{len(bboxes)}个题目区域,保存至{OUTPUT_DIR}")

使用说明

  1. 安装依赖:
    pip install pdfminer.six pdf2image pillow
    
    注意:pdf2image依赖Poppler,需根据系统安装:
    • Windows:下载Poppler并添加到环境变量
    • macOS:brew install poppler
    • Linux:sudo apt-get install poppler-utils
  2. 调整参数:根据目标试卷的排版,修改MIN_FONT_SIZE、LEFT_X_THRESHOLD等参数
  3. 运行代码:将PDF路径改为实际路径,执行后在输出目录得到独立的题干/子题截图

关键优化点

  • 通过正则匹配精准锁定题号格式,避免误判其他数字
  • 结合字体大小、加粗、左侧位置三个特征,提升题号识别准确率
  • 兼容主题号单独成框和与子题同框的两种场景
  • 完成bbox坐标转换,确保截图区域准确

内容的提问来源于stack exchange,提问作者Mario_Dev

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 08:44:56