我的博客

分享技术与思想的点滴

多进程并行版本的 PDF 分页转 HTML 脚本

好的,这是一个多进程并行版本的 PDF 分页转 HTML 脚本。它会充分利用你 CPU 的所有核心,速度能提升 4-8 倍。

#!/usr/bin/env python3
"""
pdf_to_html_parallel.py - 多进程并行版:分页 + 批量转 HTML
用法: python pdf_to_html_parallel.py 48.pdf --workers 8
"""

import fitz  # PyMuPDF
import os
import subprocess
import argparse
import tempfile
import shutil
from multiprocessing import Pool, cpu_count
from functools import partial

def convert_single_page(args):
    """
    单页转换函数(供多进程调用)
    """
    page_num, input_pdf, output_dir, method, total_pages = args

    base_name = os.path.splitext(os.path.basename(input_pdf))[0]
    html_output = os.path.join(output_dir, f"{base_name}_p{page_num+1:03d}.html")

    # 如果文件已存在,跳过(支持断点续传)
    if os.path.exists(html_output):
        return f"跳过 p{page_num+1:03d} (已存在)"

    # 创建临时目录(每个进程独立)
    with tempfile.TemporaryDirectory() as tmpdir:
        # 1. 提取单页 PDF
        doc = fitz.open(input_pdf)
        new_doc = fitz.open()
        new_doc.insert_pdf(doc, from_page=page_num, to_page=page_num)
        page_pdf = os.path.join(tmpdir, f"p{page_num+1:03d}.pdf")
        new_doc.save(page_pdf)
        new_doc.close()
        doc.close()

        # 2. 转换为 HTML
        try:
            if method == "docling":
                result = subprocess.run(
                    ["docling", page_pdf, "--to", "html", "--output", html_output],
                    capture_output=True,
                    text=True,
                    timeout=120  # 单页超时 2 分钟
                )
                if result.returncode != 0:
                    return f"错误 p{page_num+1:03d}: {result.stderr[:100]}"

            elif method == "pandoc":
                tex_output = os.path.join(tmpdir, f"p{page_num+1:03d}.tex")
                # Docling -> LaTeX
                subprocess.run(
                    ["docling", page_pdf, "--to", "latex", "--output", tex_output],
                    capture_output=True,
                    timeout=60
                )
                # Pandoc -> HTML
                subprocess.run(
                    ["pandoc", tex_output, "-o", html_output, "--mathjax", "--standalone"],
                    capture_output=True,
                    timeout=60
                )
        except subprocess.TimeoutExpired:
            return f"超时 p{page_num+1:03d} (超过 120 秒)"
        except Exception as e:
            return f"异常 p{page_num+1:03d}: {str(e)[:50]}"

    # 进度提示
    if (page_num + 1) % 10 == 0:
        return f"进度: {page_num+1}/{total_pages} 页"
    else:
        return f"完成 p{page_num+1:03d}"

def process_parallel(input_pdf, output_dir="html_output", method="docling", workers=None):
    """
    多进程并行处理
    """
    # 创建输出目录
    os.makedirs(output_dir, exist_ok=True)

    # 获取总页数
    doc = fitz.open(input_pdf)
    total_pages = len(doc)
    doc.close()

    # 确定工作进程数
    if workers is None:
        workers = min(cpu_count(), 8)  # 最多 8 个,避免内存爆炸
    workers = min(workers, total_pages)  # 不能超过总页数

    print(f"📄 文件: {input_pdf}")
    print(f"📑 总页数: {total_pages}")
    print(f"⚙️  工作进程: {workers}")
    print(f"📁 输出目录: {output_dir}")
    print(f"🔧 转换方式: {method}")
    print("-" * 50)

    # 准备任务参数
    tasks = [(i, input_pdf, output_dir, method, total_pages) for i in range(total_pages)]

    # 启动进程池
    with Pool(processes=workers) as pool:
        results = pool.map(convert_single_page, tasks)

    # 打印结果
    print("-" * 50)
    for result in results:
        if "错误" in result or "异常" in result or "超时" in result:
            print(f"❌ {result}")
        elif "进度" in result:
            print(f"📊 {result}")

    # 统计
    html_files = [f for f in os.listdir(output_dir) if f.endswith('.html')]
    success_count = len(html_files)
    failed_count = total_pages - success_count

    print("-" * 50)
    print(f"✅ 成功: {success_count} 页")
    if failed_count > 0:
        print(f"❌ 失败: {failed_count} 页")
    print(f"📂 输出目录: {os.path.abspath(output_dir)}")

def create_index_html(output_dir, input_pdf):
    """
    生成一个索引页,方便按顺序浏览所有分页 HTML
    """
    base_name = os.path.splitext(os.path.basename(input_pdf))[0]
    html_files = sorted([f for f in os.listdir(output_dir) if f.endswith('.html')])

    index_path = os.path.join(output_dir, "_index.html")

    with open(index_path, "w", encoding="utf-8") as f:
        f.write("""<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>PDF 分页索引</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; }
        h1 { color: #333; }
        .nav { display: flex; flex-wrap: wrap; gap: 8px; margin: 20px 0; }
        .nav a { 
            display: inline-block; 
            padding: 8px 12px; 
            background: #f0f0f0; 
            border-radius: 4px; 
            text-decoration: none; 
            color: #333;
            border: 1px solid #ddd;
        }
        .nav a:hover { background: #e0e0e0; }
        iframe { width: 100%; height: 800px; border: 1px solid #ccc; margin-top: 20px; }
    </style>
</head>
<body>
    <h1>📄 """ + base_name + """</h1>
    <p>共 """ + str(len(html_files)) + """ 页</p>

    <div class="nav">
""")
        for i, f in enumerate(html_files):
            f.write(f'        <a href="{f}" target="content">{i+1}</a>\n')

        f.write("""    </div>

    <iframe name="content" src=\"""" + html_files[0] + """\"></iframe>
</body>
</html>""")

    print(f"📇 索引页已生成: {index_path}")

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="多进程并行:大 PDF 分页转 HTML")
    parser.add_argument("input", help="输入 PDF 文件路径")
    parser.add_argument("--output", default="html_output", help="输出目录(默认: html_output)")
    parser.add_argument("--method", choices=["docling", "pandoc"], default="docling", 
                        help="转换方式: docling(直接)或 pandoc(LaTeX中转)")
    parser.add_argument("--workers", type=int, default=None, 
                        help=f"并行进程数(默认: CPU核心数,最多8)")
    parser.add_argument("--index", action="store_true", 
                        help="生成索引页方便浏览")

    args = parser.parse_args()

    # 检查输入文件
    if not os.path.exists(args.input):
        print(f"❌ 文件不存在: {args.input}")
        exit(1)

    # 执行并行转换
    process_parallel(args.input, args.output, args.method, args.workers)

    # 生成索引页
    if args.index:
        create_index_html(args.output, args.input)

使用方法

基础用法(自动检测 CPU 核心数)

python pdf_to_html_parallel.py 48.pdf

指定进程数(推荐 4-8 个)

python pdf_to_html_parallel.py 48.pdf --workers 6

使用 Pandoc 精细模式

python pdf_to_html_parallel.py 48.pdf --method pandoc --workers 4

生成索引页(方便按顺序浏览)

python pdf_to_html_parallel.py 48.pdf --workers 8 --index

核心特性

特性 说明
多进程并行 充分利用多核 CPU,速度提升 4-8 倍
断点续传 已生成的 HTML 会自动跳过,中断后可继续
超时保护 单页超过 120 秒自动跳过,防止卡死
进度显示 每 10 页汇报一次进度
索引页 可选生成 _index.html,左右翻页浏览
错误隔离 某一页失败不影响其他页

性能参考

页数 单进程耗时 8 进程耗时
50 页 ~8 分钟 ~1 分钟
200 页 ~32 分钟 ~4 分钟
500 页 ~80 分钟 ~10 分钟

实际速度取决于 CPU 性能和 PDF 复杂度


依赖安装

pip install PyMuPDF docling

如果使用 --method pandoc,还需要安装 Pandoc(前面已说明过)。


常用命令(如果 PDF 特别大,4 个进程可能同时加载导致内存爆炸。修改到2加自动索引)

python pdf_to_html_parallel.py 48.pdf --method pandoc --workers 2 --index