From 9351347a11f2b24731d7cdd5884099d6fb24e1ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Tue, 23 Jun 2026 12:22:05 +0400 Subject: [PATCH] v1.0.149: VM parses PDF + stores elements_json in Lucee --- deploy/convert_server.py | 52 +++++++++++++++++++++++++++++++++++++++- index.cfm | 47 +++++++++++++----------------------- 2 files changed, 68 insertions(+), 31 deletions(-) diff --git a/deploy/convert_server.py b/deploy/convert_server.py index 7d20404..0c43ea5 100755 --- a/deploy/convert_server.py +++ b/deploy/convert_server.py @@ -346,14 +346,64 @@ class Handler(BaseHTTPRequestHandler): json={"upload_id": upload_id, "contract_id": contract_id}, ) if resp.status_code == 200: + result = resp.json() + doc_id = result.get("DOC_ID", "") + + # Парсинг на VM + сохранить в Lucee + parsed = self._parse_file(filename, file_data) + if parsed and parsed.get("status") == "parsed" and doc_id: + try: + escaped = json.dumps(parsed["elements"], ensure_ascii=False).replace("'", "''") + with httpx.Client(http2=True, timeout=15, verify=False) as c: + c.post( + f"{LUCEE_URL}/api.cfm", + data={ + "action": "execute", + "sql": f"UPDATE documents SET elements_json='{escaped}'::jsonb, status='parsed' WHERE id='{doc_id}'" + } + ) + except Exception: + pass + + result["parsed"] = parsed self.send_response(200) self._send_cors() self.send_header("Content-Type", "application/json; charset=utf-8") self.end_headers() - self.wfile.write(resp.content) + self.wfile.write(json.dumps(result, ensure_ascii=False).encode()) else: self._send_error_cors(502, f"assemble failed: HTTP {resp.status_code}") + def _parse_file(self, filename, data): + """Распарсить файл на VM: PDF→PyPDF2, DOCX/DOC→python-docx, иначе plain text.""" + import base64 + ext = filename.rsplit('.', 1)[-1].lower() if '.' in filename else '' + try: + if ext == 'pdf': + from PyPDF2 import PdfReader + import io as io_mod + reader = PdfReader(io_mod.BytesIO(data)) + elements = [] + for page in reader.pages: + text = page.extract_text() + if text: + for line in text.split('\n'): + line = line.strip() + if line: + elements.append({"type": "paragraph", "text": line, "style": ""}) + return {"status": "parsed", "element_count": len(elements), "elements": elements} + elif ext in ('docx', 'doc'): + # Для DOCX/DOC — оставляем Lucee (Apache POI работает) + return None + else: + # Plain text + text = data.decode('utf-8', errors='ignore')[:5000] + lines = [l.strip() for l in text.split('\n') if l.strip()] + return {"status": "parsed", "element_count": len(lines), + "elements": [{"type": "paragraph", "text": l, "style": ""} for l in lines]} + except Exception as e: + return {"status": "error", "error": str(e), "element_count": 0} + def _handle_parse_pdf(self): """Распарсить PDF через PyPDF2 (получает base64 тело от Lucee).""" import base64 diff --git a/index.cfm b/index.cfm index 1f8210b..d9eeea2 100644 --- a/index.cfm +++ b/index.cfm @@ -339,20 +339,14 @@ fileInput.addEventListener('change', async function() { if (!contractId && zf.contract_id) contractId = zf.contract_id; renderTable(); - // Авто-парсинг + // Авто-парсинг — VM уже распарсила var zt0 = Date.now(); - try { - var zpr = await fetch('/parser.cfm?doc_id=' + zf.doc_id); - var zpd = await zpr.json(); - var zelapsed = ((Date.now() - zt0) / 1000).toFixed(1); - if (zpd.OK && zpd.STATUS === 'parsed') { - fileQueue[zIdx].parsed = true; - fileQueue[zIdx].status = '✓ ' + zpd.ELEMENT_COUNT + ' эл. (' + zelapsed + 'с)'; - } else { - fileQueue[zIdx].status = '✗ ' + (zpd.ERROR_COUNT > 0 ? (zpd.ERRORS && zpd.ERRORS[0] || 'ошибка') : 'ошибка') + ''; - } - } catch(zpe) { - fileQueue[zIdx].status = '✗ парсинг: сеть'; + var zelapsed = ((Date.now() - zt0) / 1000).toFixed(1); + if (zf.PARSED && zf.PARSED.STATUS === 'parsed') { + fileQueue[zIdx].parsed = true; + fileQueue[zIdx].status = '✓ ' + zf.PARSED.ELEMENT_COUNT + ' эл. (' + zelapsed + 'с)'; + } else { + fileQueue[zIdx].status = '✗ ' + ((zf.PARSED && zf.PARSED.ERROR) || 'ошибка') + ''; } renderTable(); } @@ -389,24 +383,17 @@ fileInput.addEventListener('change', async function() { fileQueue[rowIdx].status = ''; fileQueue[rowIdx].uploaded = true; - // Авто-парсинг сразу после загрузки + // Авто-парсинг — VM уже распарсила, берём из ответа var t0 = Date.now(); - fileQueue[rowIdx].status = '⏳ парсинг...'; - renderTable(); - try { - var pr = await fetch('/parser.cfm?doc_id=' + resp.DOC_ID); - var pd = await pr.json(); - var elapsed = ((Date.now() - t0) / 1000).toFixed(1); - if (pd.OK && pd.STATUS === 'parsed') { - fileQueue[rowIdx].parsed = true; - fileQueue[rowIdx].parseInfo = pd; - fileQueue[rowIdx].status = '✓ ' + pd.ELEMENT_COUNT + ' эл. (' + elapsed + 'с)'; - } else { - fileQueue[rowIdx].status = '✗ ' + (pd.ERROR_COUNT > 0 ? (pd.ERRORS && pd.ERRORS[0] || 'ошибка') : 'ошибка') + ''; - fileQueue[rowIdx].parseInfo = pd; - } - } catch(pe) { - fileQueue[rowIdx].status = '✗ парсинг: сеть'; + var elapsed = ((Date.now() - t0) / 1000).toFixed(1); + if (resp.PARSED && resp.PARSED.STATUS === 'parsed') { + fileQueue[rowIdx].parsed = true; + fileQueue[rowIdx].parseInfo = resp.PARSED; + fileQueue[rowIdx].status = '✓ ' + resp.PARSED.ELEMENT_COUNT + ' эл. (' + elapsed + 'с)'; + } else { + var errMsg = (resp.PARSED && resp.PARSED.ERROR) || 'ошибка'; + fileQueue[rowIdx].status = '✗ ' + errMsg + ''; + fileQueue[rowIdx].parseInfo = resp.PARSED; } } catch(err) { fileQueue[rowIdx].status = '✗ ' + err.message + '';