181 lines
6.3 KiB
Python
181 lines
6.3 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
批量 OCR 解析工具
|
||
- 扫描输入目录下所有 PDF / DOCX / DOC / PPTX / PPT
|
||
- DOCX / DOC / PPTX / PPT 先用 LibreOffice 转换为 PDF,再上传解析
|
||
- 结果 content_list 以 JSON 格式保存到输出目录
|
||
- 已处理的文件自动跳过,支持多线程并发
|
||
"""
|
||
import json
|
||
import os
|
||
import shutil
|
||
import sys
|
||
import subprocess
|
||
import tempfile
|
||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||
from pathlib import Path
|
||
|
||
import requests
|
||
|
||
# ============ 配置(按需修改)============
|
||
API_URL = "http://192.168.0.46:59988/analyze-pdf"
|
||
INPUT_DIR = "/app/testfiles/input" # 输入文件夹
|
||
OUTPUT_DIR = "/app/testfiles/output" # 输出文件夹
|
||
MAX_WORKERS = 3 # 并发请求数(避免打垮 API 服务)
|
||
API_TIMEOUT = 300 # API 请求超时(秒)
|
||
CONV_TIMEOUT = 120 # LibreOffice 转换超时(秒)
|
||
SKIP_EXISTING = True # 跳过已存在的输出文件
|
||
|
||
# Updated SUPPORTED extensions to include ppt and pptx
|
||
SUPPORTED = {".pdf", ".docx", ".doc", ".pptx", ".ppt"}
|
||
|
||
|
||
# ============ DOCX/DOC/PPTX/PPT → PDF 转换 ============
|
||
def convert_to_pdf(input_path: str, output_dir: str) -> str:
|
||
"""使用 LibreOffice 将 DOCX/DOC/PPTX/PPT 转换为 PDF,返回生成的 PDF 路径。
|
||
|
||
每次调用使用独立的 profile 目录,避免多线程并发时 LibreOffice 互相抢锁导致静默失败。
|
||
"""
|
||
profile_dir = tempfile.mkdtemp(prefix="lo_profile_")
|
||
try:
|
||
cmd = [
|
||
"libreoffice",
|
||
f"-env:UserInstallation=file://{profile_dir}",
|
||
"--headless", "--convert-to", "pdf",
|
||
"--outdir", output_dir, input_path,
|
||
]
|
||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=CONV_TIMEOUT)
|
||
if result.returncode != 0:
|
||
detail = result.stderr.strip() or result.stdout.strip() or f"退出码 {result.returncode}"
|
||
raise RuntimeError(f"LibreOffice 转换失败: {detail}")
|
||
finally:
|
||
shutil.rmtree(profile_dir, ignore_errors=True)
|
||
|
||
stem = Path(input_path).stem
|
||
pdf_path = os.path.join(output_dir, f"{stem}.pdf")
|
||
if not os.path.exists(pdf_path):
|
||
raise RuntimeError(f"转换后找不到 PDF: {pdf_path}")
|
||
return pdf_path
|
||
|
||
|
||
# ============ API 调用 ============
|
||
def analyze_pdf(pdf_path: str) -> list:
|
||
"""上传 PDF 到 OCR API,返回 content_list。"""
|
||
with open(pdf_path, "rb") as f:
|
||
files = {"file": (os.path.basename(pdf_path), f, "application/pdf")}
|
||
resp = requests.post(API_URL, files=files, timeout=API_TIMEOUT)
|
||
|
||
if resp.status_code != 200:
|
||
try:
|
||
detail = resp.json()
|
||
except Exception:
|
||
detail = resp.text[:300]
|
||
raise RuntimeError(f"HTTP {resp.status_code}: {detail}")
|
||
|
||
try:
|
||
result = resp.json()
|
||
except Exception:
|
||
raise RuntimeError(f"响应非 JSON: {resp.text[:300]}")
|
||
|
||
return result.get("data", {}).get("content_list", [])
|
||
|
||
|
||
# ============ 单文件处理 ============
|
||
def process_file(input_path: str, output_dir: str, temp_dir: str) -> dict:
|
||
"""
|
||
处理单个文件,返回状态字典。
|
||
status: "success" | "skipped" | "empty" | "error"
|
||
"""
|
||
stem = Path(input_path).stem
|
||
ext = Path(input_path).suffix.lower()
|
||
output_path = os.path.join(output_dir, f"{stem}_content_list.json")
|
||
|
||
if SKIP_EXISTING and os.path.exists(output_path):
|
||
return {"file": stem, "status": "skipped"}
|
||
|
||
pdf_path = input_path
|
||
converted = False
|
||
try:
|
||
# DOCX / DOC / PPTX / PPT 先转 PDF
|
||
# Updated condition to include ppt and pptx
|
||
if ext in (".docx", ".doc", ".pptx", ".ppt"):
|
||
pdf_path = convert_to_pdf(input_path, temp_dir)
|
||
converted = True
|
||
|
||
content_list = analyze_pdf(pdf_path)
|
||
|
||
if not content_list:
|
||
return {"file": stem, "status": "empty"}
|
||
|
||
with open(output_path, "w", encoding="utf-8") as f:
|
||
json.dump(content_list, f, ensure_ascii=False, indent=2)
|
||
|
||
return {"file": stem, "status": "success", "items": len(content_list)}
|
||
|
||
except Exception as e:
|
||
return {"file": stem, "status": "error", "reason": str(e)}
|
||
|
||
finally:
|
||
if converted and os.path.exists(pdf_path):
|
||
os.remove(pdf_path)
|
||
|
||
|
||
# ============ 主程序 ============
|
||
def main():
|
||
input_dir = sys.argv[1] if len(sys.argv) > 1 else INPUT_DIR
|
||
output_dir = sys.argv[2] if len(sys.argv) > 2 else OUTPUT_DIR
|
||
|
||
if not os.path.isdir(input_dir):
|
||
print(f"❌ 输入目录不存在: {input_dir}")
|
||
sys.exit(1)
|
||
|
||
os.makedirs(output_dir, exist_ok=True)
|
||
|
||
# 收集并分类文件
|
||
all_files = [p for p in Path(input_dir).iterdir() if p.is_file()]
|
||
files = [str(p) for p in all_files if p.suffix.lower() in SUPPORTED]
|
||
skipped = [p.name for p in all_files if p.suffix.lower() not in SUPPORTED]
|
||
|
||
if skipped:
|
||
print(f"⏭️ 跳过 {len(skipped)} 个不支持的文件:")
|
||
for name in skipped:
|
||
print(f" · {name}")
|
||
print()
|
||
|
||
if not files:
|
||
print(f"⚠️ 目录下没有 PDF/DOCX/DOC/PPTX/PPT 文件: {input_dir}")
|
||
return
|
||
|
||
print(f"📂 输入目录: {input_dir}")
|
||
print(f"💾 输出目录: {output_dir}")
|
||
print(f"📄 共 {len(files)} 个文件,并发数 {MAX_WORKERS}\n")
|
||
|
||
stats = {"success": 0, "skipped": 0, "empty": 0, "error": 0}
|
||
|
||
with tempfile.TemporaryDirectory() as temp_dir:
|
||
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
|
||
futures = {
|
||
executor.submit(process_file, fp, output_dir, temp_dir): fp
|
||
for fp in files
|
||
}
|
||
for i, future in enumerate(as_completed(futures), 1):
|
||
res = future.result()
|
||
status = res["status"]
|
||
stats[status] += 1
|
||
|
||
icon = {"success": "✅", "skipped": "⏭️", "empty": "⚠️", "error": "❌"}[status]
|
||
detail = ""
|
||
if status == "success":
|
||
detail = f"({res['items']} items)"
|
||
elif status == "error":
|
||
detail = f"- {res['reason']}"
|
||
|
||
print(f"[{i:>3}/{len(files)}] {icon} {res['file']} {detail}")
|
||
|
||
print(f"\n🎉 完成!成功 {stats['success']} | 跳过 {stats['skipped']} "
|
||
f"| 空结果 {stats['empty']} | 失败 {stats['error']}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|