PDF → libreoffice → DOCX таблицы, fallback pdfplumber, v1.29
This commit is contained in:
+45
-42
@@ -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,35 +150,6 @@ def _parse_pdf(file_bytes: bytes) -> dict:
|
|||||||
"text": line,
|
"text": line,
|
||||||
"page": pn,
|
"page": pn,
|
||||||
})
|
})
|
||||||
|
|
||||||
# Таблицы — camelot (точнее), иначе pdfplumber
|
|
||||||
tables_found = False
|
|
||||||
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({
|
|
||||||
"type": "table",
|
|
||||||
"rows": rows,
|
|
||||||
"page": ct.page,
|
|
||||||
})
|
|
||||||
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()
|
tables = page.extract_tables()
|
||||||
for tbl in tables:
|
for tbl in tables:
|
||||||
if tbl:
|
if tbl:
|
||||||
@@ -186,16 +158,47 @@ def _parse_pdf(file_bytes: bytes) -> dict:
|
|||||||
"rows": tbl,
|
"rows": tbl,
|
||||||
"page": pn,
|
"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:
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user