我们将使用官方推荐的 vLLM + DeepSeek-OCR 架构,支持:
- 自动将多页 PDF 转为图像
- 并行/串行 OCR 识别
- 输出结构化 Markdown(保留表格、标题、段落)
- 错误重试与日志记录
🎯 目标
给定一个
input_pdfs/目录,自动处理所有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 距离比对人工标注样本