PDF → libreoffice → DOCX таблицы, fallback pdfplumber, v1.29

This commit is contained in:
2026-06-17 21:15:11 +04:00
parent f7458da9ac
commit aae6ea2a88
2 changed files with 51 additions and 48 deletions
+50 -47
View File
@@ -123,21 +123,22 @@ def _parse_doc(file_bytes: bytes) -> dict:
def _parse_pdf(file_bytes: bytes) -> dict: def _parse_pdf(file_bytes: bytes) -> dict:
result = {"elements": [], "errors": []} result = {"elements": [], "errors": []}
import tempfile, os as _os
# Сохранить PDF во временный файл (camelot требует путь) # Попробовать libreoffice → docx (точные таблицы)
tmp_path = None
try: try:
with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp: docx_result = _pdf_via_libreoffice(file_bytes)
tmp.write(file_bytes) if docx_result["elements"]:
tmp_path = tmp.name return docx_result
result["errors"].extend(docx_result.get("errors", []))
except Exception as e:
result["errors"].append(f"libreoffice pdf: {e}")
# Fallback: pdfplumber
try:
import pdfplumber import pdfplumber
with pdfplumber.open(io.BytesIO(file_bytes)) as pdf: with pdfplumber.open(io.BytesIO(file_bytes)) as pdf:
for page in pdf.pages: for page in pdf.pages:
pn = page.page_number pn = page.page_number
# Текст страницы
text = page.extract_text() text = page.extract_text()
if text: if text:
for line in text.split("\n"): for line in text.split("\n"):
@@ -149,53 +150,55 @@ def _parse_pdf(file_bytes: bytes) -> dict:
"text": line, "text": line,
"page": pn, "page": pn,
}) })
tables = page.extract_tables()
# Таблицы — camelot (точнее), иначе pdfplumber for tbl in tables:
tables_found = False if tbl:
try:
import camelot
# Сначала lattice (таблицы с рамками)
for flavor in ("lattice", "stream"):
try:
ctables = camelot.read_pdf(tmp_path, pages="all", flavor=flavor)
for ct in ctables:
rows = [list(ct.df.columns)] + ct.df.values.tolist()
result["elements"].append({ result["elements"].append({
"type": "table", "type": "table",
"rows": rows, "rows": tbl,
"page": ct.page, "page": pn,
}) })
tables_found = True
except Exception:
pass
except ImportError:
result["errors"].append("camelot not installed, using pdfplumber")
except Exception as e:
result["errors"].append(f"camelot error: {e}")
# Fallback: pdfplumber таблицы если camelot ничего не нашёл
if not tables_found:
with pdfplumber.open(io.BytesIO(file_bytes)) as pdf:
for page in pdf.pages:
pn = page.page_number
tables = page.extract_tables()
for tbl in tables:
if tbl:
result["elements"].append({
"type": "table",
"rows": tbl,
"page": pn,
})
except Exception as e: except Exception as e:
result["errors"].append(f"pdf parse error: {e}") result["errors"].append(f"pdfplumber: {e}")
finally:
if tmp_path and _os.path.exists(tmp_path):
_os.unlink(tmp_path)
return result return result
def _pdf_via_libreoffice(file_bytes: bytes) -> dict:
"""Конвертировать PDF → DOCX через libreoffice, затем python-docx."""
result = {"elements": [], "errors": []}
tmp_pdf = None
tmp_dir = None
try:
with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp:
tmp.write(file_bytes)
tmp_pdf = tmp.name
tmp_dir = tempfile.mkdtemp()
subprocess.run(
["libreoffice", "--headless", "--convert-to", "docx", "--outdir", tmp_dir, tmp_pdf],
timeout=60, capture_output=True,
)
docx_files = [f for f in os.listdir(tmp_dir) if f.endswith(".docx")]
if docx_files:
with open(os.path.join(tmp_dir, docx_files[0]), "rb") as f:
result = _parse_docx(f.read())
else:
result["errors"].append("libreoffice PDF→DOCX: no output")
except FileNotFoundError:
result["errors"].append("libreoffice not installed")
except Exception as e:
result["errors"].append(f"libreoffice PDF→DOCX: {e}")
finally:
if tmp_pdf and os.path.exists(tmp_pdf):
os.unlink(tmp_pdf)
if tmp_dir and os.path.exists(tmp_dir):
for f in os.listdir(tmp_dir):
os.unlink(os.path.join(tmp_dir, f))
os.rmdir(tmp_dir)
return result
# ── ZIP ── # ── ZIP ──
def _parse_zip(file_bytes: bytes) -> dict: def _parse_zip(file_bytes: bytes) -> dict:
+1 -1
View File
@@ -59,7 +59,7 @@
<body> <body>
<div class="topbar"> <div class="topbar">
<img src="{{ url_for('static', filename='nubes-logo.svg') }}" alt="Nubes"> <img src="{{ url_for('static', filename='nubes-logo.svg') }}" alt="Nubes">
<span class="title">Сверка договоров <span style="font-weight:400;color:var(--muted);font-size:12px;">v1.28</span></span> <span class="title">Сверка договоров <span style="font-weight:400;color:var(--muted);font-size:12px;">v1.29</span></span>
</div> </div>
<div class="content"> <div class="content">