我们将使用官方推荐的 vLLM + DeepSeek-OCR 架构,支持:

  • 自动将多页 PDF 转为图像
  • 并行/串行 OCR 识别
  • 输出结构化 Markdown(保留表格、标题、段落)
  • 错误重试与日志记录

🎯 目标

给定一个 input_pdfs/ 目录,自动处理所有 .pdf 文件,输出到 output_md/,每个 PDF 对应一个 .md 文件。


一、前提条件

你已完成 DeepSeek-OCR 从零部署 的基础环境搭建,包括:

  • ✅ Python 3.12.9 + CUDA 11.8
  • ✅ vLLM 0.8.5 安装成功
  • ✅ deepseek-ai/DeepSeek-OCR 模型已下载(或可自动拉取)
  • ✅ GPU 显存 ≥ 24GB(建议 A10/A100)

此外安装辅助库:

bash

编辑

1pip install pdf2image==1.17.0 pillow==10.2.0 tqdm==4.66.0

⚠️ 需要系统安装 poppler-utils(用于 PDF 转图):

bash

编辑

1# Ubuntu/Debian
2sudo apt-get install poppler-utils
3
4# macOS
5brew install poppler

二、目录结构

bash

编辑

1deepseek-ocr-batch/
2├── input_pdfs/
3│   ├── invoice_001.pdf
4│   └── report_2025.pdf
5├── output_md/
6├── logs/
7├── batch_ocr.py          # ← 主脚本
8└── config.yaml           # ← 配置文件

三、配置文件 config.yaml

yaml

编辑

1model:
2  name: "deepseek-ai/DeepSeek-OCR"
3  dtype: "bfloat16"        # 或 "float16"
4  max_model_len: 8192
5
6ocr:
7  prompt: "<image>\n<|grounding|>Convert the document to markdown."
8  image_size: 1024         # Base 模式
9  dpi: 200                 # PDF 渲染 DPI(越高越清晰,但显存压力大)
10  batch_size: 1            # 当前 vLLM 多图 batch 支持有限,建议 1
11
12paths:
13  input_dir: "./input_pdfs"
14  output_dir: "./output_md"
15  log_dir: "./logs"
16
17runtime:
18  gpu_id: 0
19  timeout_per_page: 60     # 单页超时(秒)
20  retry_times: 2

四、主脚本 batch_ocr.py

python

编辑

1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3import os
4import sys
5import yaml
6import logging
7import argparse
8from pathlib import Path
9from typing import List, Tuple
10from PIL import Image
11from pdf2image import convert_from_path
12from tqdm import tqdm
13import torch
14from vllm import LLM, SamplingParams
15
16
17def setup_logger(log_dir: str, filename: str):
18    log_dir = Path(log_dir)
19    log_dir.mkdir(parents=True, exist_ok=True)
20    logging.basicConfig(
21        level=logging.INFO,
22        format="%(asctime)s [%(levelname)s] %(message)s",
23        handlers=[
24            logging.FileHandler(log_dir / f"{filename}.log", encoding="utf-8"),
25            logging.StreamHandler(sys.stdout)
26        ]
27    )
28    return logging.getLogger()
29
30
31def pdf_to_images(pdf_path: Path, dpi: int = 200) -> List[Image.Image]:
32    """将 PDF 转为 PIL 图像列表"""
33    try:
34        images = convert_from_path(str(pdf_path), dpi=dpi, thread_count=2)
35        return images
36    except Exception as e:
37        raise RuntimeError(f"PDF to image failed: {e}")
38
39
40def init_vllm_model(config: dict):
41    """初始化 vLLM 模型"""
42    model_name = config["model"]["name"]
43    dtype = config["model"]["dtype"]
44    max_len = config["model"]["max_model_len"]
45
46    llm = LLM(
47        model=model_name,
48        trust_remote_code=True,
49        dtype=dtype,
50        max_model_len=max_len,
51        gpu_memory_utilization=0.9,
52        enforce_eager=True,  # 避免 CUDA Graph 编译问题
53        disable_custom_all_reduce=True,
54    )
55    return llm
56
57
58def ocr_single_image(llm, image: Image.Image, prompt: str) -> str:
59    """对单张图像执行 OCR"""
60    sampling_params = SamplingParams(
61        temperature=0.0,
62        max_tokens=4096,
63        skip_special_tokens=False,
64        stop_token_ids=[128001, 128009]  # <|eot_id|>, <|end_of_text|>
65    )
66
67    outputs = llm.generate(
68        {
69            "prompt": prompt,
70            "multi_modal_data": {"image": image}
71        },
72        sampling_params=sampling_params
73    )
74    return outputs[0].outputs[0].text.strip()
75
76
77def process_pdf(llm, pdf_path: Path, config: dict) -> str:
78    """处理单个 PDF,返回合并后的 Markdown"""
79    ocr_config = config["ocr"]
80    images = pdf_to_images(pdf_path, dpi=ocr_config["dpi"])
81    prompt = ocr_config["prompt"]
82    timeout = config["runtime"]["timeout_per_page"]
83
84    full_md = []
85    for i, img in enumerate(tqdm(images, desc=f"Pages in {pdf_path.name}", leave=False)):
86        try:
87            # 可选:限制图像尺寸避免 OOM
88            if max(img.size) > ocr_config["image_size"]:
89                img.thumbnail((ocr_config["image_size"], ocr_config["image_size"]), Image.Resampling.LANCZOS)
90
91            page_md = ocr_single_image(llm, img, prompt)
92            full_md.append(f"\n<!-- Page {i+1} -->\n{page_md}")
93        except Exception as e:
94            logging.error(f"Page {i+1} of {pdf_path} failed: {e}")
95            full_md.append(f"\n<!-- Page {i+1}: OCR FAILED -->\n")
96
97    return "\n".join(full_md)
98
99
100def main():
101    parser = argparse.ArgumentParser()
102    parser.add_argument("--config", default="config.yaml", help="Config file path")
103    args = parser.parse_args()
104
105    with open(args.config, "r", encoding="utf-8") as f:
106        config = yaml.safe_load(f)
107
108    # Setup paths
109    input_dir = Path(config["paths"]["input_dir"])
110    output_dir = Path(config["paths"]["output_dir"])
111    log_dir = config["paths"]["log_dir"]
112
113    output_dir.mkdir(parents=True, exist_ok=True)
114    logger = setup_logger(log_dir, "batch_ocr")
115
116    # Get all PDFs
117    pdf_files = list(input_dir.glob("*.pdf"))
118    if not pdf_files:
119        logger.warning(f"No PDF found in {input_dir}")
120        return
121
122    logger.info(f"Found {len(pdf_files)} PDF(s). Initializing model...")
123
124    # Initialize model (once)
125    llm = init_vllm_model(config)
126
127    # Process each PDF
128    for pdf_path in tqdm(pdf_files, desc="Processing PDFs"):
129        output_path = output_dir / f"{pdf_path.stem}.md"
130        if output_path.exists():
131            logger.info(f"Skip existing: {output_path}")
132            continue
133
134        try:
135            md_content = process_pdf(llm, pdf_path, config)
136            with open(output_path, "w", encoding="utf-8") as f:
137                f.write(md_content)
138            logger.info(f"✅ Saved: {output_path}")
139        except Exception as e:
140            logger.error(f"❌ Failed {pdf_path}: {e}")
141
142    logger.info("Batch processing completed.")
143
144
145if __name__ == "__main__":
146    main()

五、运行方式

bash

编辑

1# 激活环境
2conda activate deepseek-ocr
3
4# 运行批量处理
5python batch_ocr.py --config config.yaml

输出示例:

text

编辑

1Processing PDFs: 100%|██████████| 2/2 [01:23<00:00, 41.5s/it]
22026-02-05 01:15:22 [INFO] ✅ Saved: output_md/invoice_001.md
32026-02-05 01:17:05 [INFO] ✅ Saved: output_md/report_2025.md

六、高级优化建议

1. 多卡并行(按 PDF 分发)

若有多张 GPU,可写一个调度脚本,将 PDF 列表分片,每卡跑一个进程:

bash

编辑

1CUDA_VISIBLE_DEVICES=0 python batch_ocr.py --config config_gpu0.yaml &
2CUDA_VISIBLE_DEVICES=1 python batch_ocr.py --config config_gpu1.yaml &

2. 内存/显存保护

  • 在 pdf_to_images 中限制 first_page/last_page 避免超长 PDF
  • 使用 img = img.convert("RGB") 确保格式统一
  • 设置 ulimit -v 限制进程内存

3. 输出增强

  • 在 full_md 前添加元信息:markdown编辑1<!-- Source: invoice_001.pdf --> 2<!-- Processed by DeepSeek-OCR v1.0 --> 3<!-- Timestamp: 2026-02-05T01:15:22Z -->

七、常见问题

表格

问题解决方案
poppler not found安装 poppler-utils(Linux)或 brew install poppler(macOS)
OOM(显存溢出)降低 dpi(如 150),或减小 image_size(如 896)
模型加载慢首次运行会缓存到 ~/.cache/huggingface,后续加速
表格错乱确保 prompt 包含 `<

八、后续扩展

  • ✅ Web API 封装:用 FastAPI 包装 batch_ocr.py 提供 REST 接口
  • ✅ RAG 集成:将 .md 文件切片后存入向量数据库(如 Qdrant)
  • ✅ 质量评估:加入 Levenshtein 距离比对人工标注样本