跳到主要内容

PDF识别提取专家

收藏0

PDF/图片/扫描件 OCR 提取文字转 Markdown。支持 PP-OCRv6 三档模型,适合合同、发票、中文扫描件等。灵栈搬运自 SkillHub pdf-ocr-md v3.4.0。

让 AI 安装

Minsaroun最新版本 0.1.0更新于 2026-11-04下载 0 次

scripts/pdf_parse_structured.py

10.5 KB
1#!/usr/bin/env python3
2"""
3pdf_parse_structured.py — 结构化文档解析工具
4
5利用 docling 进行版面分析, 输出结构化的 Markdown 和 JSON,
6保留文档的标题层级、表格、列表等逻辑结构。
7
8用法:
9 pip install docling
10
11 # 结构化解析为 Markdown + JSON
12 python pdf_parse_structured.py input.pdf -o ./output
13
14 # 输出包含: output/input.md + output/input.json + output/input_images/
15"""
16
17import argparse
18import json
19import os
20import sys
21import time
22from pathlib import Path
23from typing import Any
24
25
26# ── 文档解析引擎 ──────────────────────────────────────────────
27
28def parse_document(input_path: str, do_ocr: bool = True,
29 do_table: bool = True, pages: str | None = None) -> dict[str, Any]:
30 """
31 使用 docling 解析文档, 返回结构化结果。
32
33 参数:
34 input_path: PDF 或图片路径
35 do_ocr: 是否启用 OCR (扫描件需要)
36 do_table: 是否检测表格结构
37 pages: 页码范围, 如 "1-5,10" (None = 全部)
38 """
39 from docling.document_converter import DocumentConverter
40 from docling.datamodel.pipeline_options import PdfPipelineOptions
41
42 pipeline_opts = PdfPipelineOptions()
43 pipeline_opts.do_ocr = do_ocr
44 pipeline_opts.do_table_structure = do_table
45
46 # 启用图片生成 (用于可视化)
47 pipeline_opts.generate_page_images = True
48 pipeline_opts.generate_table_images = True
49
50 # 高级版面分析
51 pipeline_opts.do_picture_classification = True
52 pipeline_opts.do_formula_enrichment = True
53
54 converter = DocumentConverter()
55 result = converter.convert(input_path, pipeline_options=pipeline_opts)
56
57 doc = result.document
58
59 # ── 提取元数据 ──
60 metadata = {
61 "file": os.path.basename(input_path),
62 "pages": len(doc.pages),
63 "has_ocr": do_ocr,
64 }
65
66 # ── 提取各页内容 ──
67 pages_data = []
68 for page_num, page in doc.pages.items():
69 page_info = {
70 "page": page_num,
71 "width": page.size.width if page.size else 0,
72 "height": page.size.height if page.size else 0,
73 }
74 pages_data.append(page_info)
75
76 # ── 提取文本内容(保留版面顺序) ──
77 items = []
78 # 遍历 docling 的 item 层级
79 for item in doc.iterate_items():
80 item_data = {
81 "type": item.label.value if hasattr(item.label, 'value') else str(item.label),
82 "text": item.text.strip() if item.text else "",
83 "bbox": {
84 "x": item.bbox.x if item.bbox else 0,
85 "y": item.bbox.y if item.bbox else 0,
86 "w": item.bbox.width if item.bbox else 0,
87 "h": item.bbox.height if item.bbox else 0,
88 } if item.bbox else None,
89 "page": item.page_num if hasattr(item, 'page_num') else 0,
90 }
91 items.append(item_data)
92
93 # ── 提取表格 ──
94 tables = []
95 for table in doc.tables:
96 table_data = {
97 "page": table.prov[0].page_no if table.prov else 0,
98 "bbox": {
99 "x": table.bbox.x if table.bbox else 0,
100 "y": table.bbox.y if table.bbox else 0,
101 "w": table.bbox.width if table.bbox else 0,
102 "h": table.bbox.height if table.bbox else 0,
103 } if table.bbox else None,
104 }
105
106 # 提取表格数据为二维数组
107 grid = []
108 for row in table.data:
109 grid.append([cell.text if cell.text else "" for cell in row])
110 table_data["grid"] = grid
111
112 # 生成 Markdown 表格
113 md_lines = []
114 if grid:
115 # 表头
116 header = "| " + " | ".join(grid[0]) + " |"
117 md_lines.append(header)
118 # 分隔线
119 sep = "| " + " | ".join(["---"] * len(grid[0])) + " |"
120 md_lines.append(sep)
121 # 数据行
122 for row in grid[1:]:
123 md_lines.append("| " + " | ".join(row) + " |")
124
125 table_data["markdown"] = "\n".join(md_lines)
126 tables.append(table_data)
127
128 return {
129 "metadata": metadata,
130 "pages": pages_data,
131 "items": items,
132 "tables": tables,
133 "text": doc.text if hasattr(doc, 'text') else "",
134 }
135
136
137# ── Markdown 生成 ──────────────────────────────────────────────
138
139def generate_markdown(result: dict[str, Any], include_tables: bool = True) -> str:
140 """将 docling 解析结果转为带结构的 Markdown"""
141 lines = []
142
143 # 文档信息
144 meta = result["metadata"]
145 lines.append(f"# 文档解析结果 — {meta['file']}")
146 lines.append(f"> 页数: {meta['pages']} | OCR: {'是' if meta['has_ocr'] else '否'}")
147 lines.append("")
148
149 # 按页组织
150 current_page = 0
151 page_counter = 0
152
153 for item in result["items"]:
154 item_page = item.get("page", 0)
155
156 if item_page != current_page:
157 if current_page > 0:
158 lines.append("---\n")
159 page_counter += 1
160 lines.append(f"\n## 第 {page_counter} 页\n")
161 current_page = item_page
162
163 item_type = item["type"]
164 text = item["text"]
165
166 if not text:
167 continue
168
169 # 根据类型输出
170 if item_type in ("heading", "title", "section-heading"):
171 # 检测标题层级 (基于字体大小/位置)
172 level = 3 # 默认 h3
173 lines.append(f"{'#' * level} {text}\n")
174 elif item_type == "paragraph":
175 lines.append(f"{text}\n")
176 elif item_type == "list":
177 lines.append(f"- {text}")
178 elif item_type == "caption":
179 lines.append(f"*{text}*\n")
180 elif item_type == "page-header":
181 lines.append(f"*{text}* \n")
182 elif item_type == "page-footer":
183 lines.append(f"--- *{text}*\n")
184 else:
185 lines.append(f"{text}\n")
186
187 # 表格
188 if include_tables and result["tables"]:
189 lines.append("\n## 表格\n")
190 for i, table in enumerate(result["tables"]):
191 if table["markdown"]:
192 lines.append(f"### 表格 {i+1} (第 {table['page']} 页)\n")
193 lines.append(table["markdown"])
194 lines.append("")
195
196 return "\n".join(lines)
197
198
199# ── 工具函数 ───────────────────────────────────────────────────
200
201def save_json(data: dict, output_path: str):
202 """保存 JSON 结果"""
203 os.makedirs(os.path.dirname(output_path) or '.', exist_ok=True)
204 with open(output_path, 'w', encoding='utf-8') as f:
205 json.dump(data, f, ensure_ascii=False, indent=2)
206
207
208def save_markdown(md: str, output_path: str):
209 """保存 Markdown 结果"""
210 os.makedirs(os.path.dirname(output_path) or '.', exist_ok=True)
211 with open(output_path, 'w', encoding='utf-8') as f:
212 f.write(md)
213
214
215def format_duration(seconds: float) -> str:
216 if seconds < 1:
217 return f"{seconds*1000:.0f}ms"
218 elif seconds < 60:
219 return f"{seconds:.1f}s"
220 else:
221 m, s = divmod(seconds, 60)
222 return f"{int(m)}m{int(s)}s"
223
224
225# ── CLI ────────────────────────────────────────────────────────
226
227def main():
228 parser = argparse.ArgumentParser(
229 description="结构化文档解析 — 版面分析 + 表格提取 + 结构化输出",
230 formatter_class=argparse.RawDescriptionHelpFormatter,
231 epilog="""
232示例:
233 %(prog)s input.pdf -o ./output # 默认: OCR + 表格
234 %(prog)s input.pdf -o ./output --no-ocr # 文本层 PDF (更快)
235 %(prog)s input.pdf -o ./output --no-table # 跳过表格提取
236 %(prog)s input.pdf -o ./output --pages "1-3" # 仅前3页
237 %(prog)s input.pdf -o ./output --json-only # 仅输出 JSON
238 %(prog)s input.pdf -o ./output --md-only # 仅输出 Markdown
239
240工作流集成:
241 # 预处理 → 结构化解析
242 python scripts/pdf_preprocess.py input.pdf -o ./preprocessed --all
243 python scripts/pdf_parse_structured.py ./preprocessed/input_p001.jpg -o ./output
244
245 # 直接解析 (平整扫描件)
246 python scripts/pdf_parse_structured.py input.pdf -o ./output
247 """
248 )
249 parser.add_argument('input', help='输入文件 (PDF / 图片)')
250 parser.add_argument('-o', '--output', default='./output',
251 help='输出目录 (默认: ./output)')
252
253 parser.add_argument('--no-ocr', action='store_true',
254 help='禁用 OCR (适用于文本层 PDF)')
255 parser.add_argument('--no-table', action='store_true',
256 help='跳过表格结构检测')
257 parser.add_argument('--pages', type=str, default=None,
258 help='页码范围, 如 "1-5,10" (默认: 全部)')
259 parser.add_argument('--json-only', action='store_true',
260 help='仅输出 JSON')
261 parser.add_argument('--md-only', action='store_true',
262 help='仅输出 Markdown')
263
264 args = parser.parse_args()
265
266 input_path = args.input
267 if not os.path.exists(input_path):
268 print(f"❌ 文件不存在: {input_path}")
269 sys.exit(1)
270
271 output_dir = args.output
272 basename = os.path.splitext(os.path.basename(input_path))[0]
273
274 print(f"📄 输入: {input_path}")
275 print(f" OCR: {'禁用' if args.no_ocr else '启用'}")
276 print(f" 表格: {'跳过' if args.no_table else '检测'}")
277
278 t0 = time.time()
279
280 result = parse_document(
281 input_path,
282 do_ocr=not args.no_ocr,
283 do_table=not args.no_table,
284 pages=args.pages,
285 )
286
287 elapsed = time.time() - t0
288
289 # 输出统计
290 print(f"\n📊 解析统计:")
291 print(f" 页数: {result['metadata']['pages']}")
292 print(f" 元素: {len(result['items'])} 个")
293 print(f" 表格: {len(result['tables'])} 个")
294 print(f" 耗时: {format_duration(elapsed)}")
295
296 # 保存结果
297 os.makedirs(output_dir, exist_ok=True)
298
299 if not args.md_only:
300 json_path = os.path.join(output_dir, f"{basename}.json")
301 save_json(result, json_path)
302 print(f"\n ✅ JSON: {json_path}")
303
304 if not args.json_only:
305 md = generate_markdown(result, include_tables=not args.no_table)
306 md_path = os.path.join(output_dir, f"{basename}.md")
307 save_markdown(md, md_path)
308 print(f" ✅ Markdown: {md_path}")
309
310 print(f"\n✅ 结构化解析完成!")
311
312
313if __name__ == '__main__':
314 main()

文件内容是平台检查时读出的文本,只供查看。平台不会运行包里的任何脚本或程序。