kgrag/mulfilesparse.py
2026-06-30 13:35:52 +08:00

181 lines
6.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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()