parser: POI (XWPFDocument/HWPFDocument) для DOCX/DOC + fallback ZIP/XML (v1.0.67)

This commit is contained in:
2026-06-20 08:45:46 +04:00
parent 9a08218cba
commit 8aa6a3a932
2 changed files with 115 additions and 7 deletions
+1 -1
View File
@@ -61,7 +61,7 @@
<body>
<div class="topbar">
<img src="/nubes-logo.svg" alt="Nubes">
<span class="title">Сверка договоров — LLM <span style="font-weight:400;color:var(--muted);font-size:12px;">v1.0.66 — Lucee</span></span>
<span class="title">Сверка договоров — LLM <span style="font-weight:400;color:var(--muted);font-size:12px;">v1.0.67 — Lucee</span></span>
</div>
<div class="content">
+111 -3
View File
@@ -52,7 +52,60 @@
</cffunction>
<!--- ═══════════════════════════════════════════════════════════
Helper: parseDOCX — ZIP/XML → {elements, errors}
Helper: parseDOCX_POI — XWPFDocument → {elements, errors}
Точнее чем ZIP/XML: понимает объединённые ячейки, стили
═══════════════════════════════════════════════════════════ --->
<cffunction name="parseDOCX_POI" access="private" returntype="struct">
<cfargument name="fileBytes" type="binary">
<cfset var out = {elements: [], errors: []}>
<cftry>
<cfset var bais = createObject("java", "java.io.ByteArrayInputStream").init(fileBytes)>
<cfset var doc = createObject("java", "org.apache.poi.xwpf.usermodel.XWPFDocument").init(bais)>
<cfset var bodyElements = doc.getBodyElements()>
<cfloop from="0" to="#bodyElements.size() - 1#" index="bi">
<cfset var be = bodyElements.get(bi)>
<cfset var beClass = be.getClass().getName()>
<cfif beClass CONTAINS "XWPFParagraph">
<cfset var para = be>
<cfset var text = para.getText()>
<cfif len(trim(text))>
<cfset var style = "">
<cftry><cfset style = para.getStyle()><cfcatch></cfcatch></cftry>
<cfset arrayAppend(out.elements, {type: "paragraph", style: style, text: trim(text), page: 0})>
</cfif>
<cfelseif beClass CONTAINS "XWPFTable">
<cfset var table = be>
<cfset var rows = []>
<cfset rowsArr = table.getRows()>
<cfloop from="0" to="#rowsArr.size() - 1#" index="ri">
<cfset var row = rowsArr.get(ri)>
<cfset var cells = []>
<cfset var tcs = row.getTableCells()>
<cfloop from="0" to="#tcs.size() - 1#" index="ci">
<cfset var cell = tcs.get(ci)>
<cfset var cellText = trim(cell.getText())>
<cfset arrayAppend(cells, cellText)>
</cfloop>
<cfif arrayLen(cells) GT 0>
<cfset arrayAppend(rows, cells)>
</cfif>
</cfloop>
<cfif arrayLen(rows) GT 0>
<cfset arrayAppend(out.elements, {type: "table", rows: rows, page: 0})>
</cfif>
</cfif>
</cfloop>
<cfset doc.close()>
<cfset bais.close()>
<cfcatch>
<cfset arrayAppend(out.errors, "DOCX_POI: " & cfcatch.message)>
</cfcatch>
</cftry>
<cfreturn out>
</cffunction>
<!--- ═══════════════════════════════════════════════════════════
Helper: parseDOCX — пробует POI, fallback ZIP/XML
═══════════════════════════════════════════════════════════ --->
<cffunction name="parseDOCX" access="private" returntype="struct">
<cfargument name="fileBytes" type="binary">
@@ -164,7 +217,47 @@
</cffunction>
<!--- ═══════════════════════════════════════════════════════════
Helper: parseDOC — грубый экстрактор текста из .doc
Helper: parseDOC_POI — HWPFDocument → {elements, errors}
═══════════════════════════════════════════════════════════ --->
<cffunction name="parseDOC_POI" access="private" returntype="struct">
<cfargument name="fileBytes" type="binary">
<cfset var out = {elements: [], errors: []}>
<cftry>
<cfset var bais = createObject("java", "java.io.ByteArrayInputStream").init(fileBytes)>
<cfset var doc = createObject("java", "org.apache.poi.hwpf.HWPFDocument").init(bais)>
<!--- WordExtractor: извлекает весь текст --->
<cfset var extractor = createObject("java", "org.apache.poi.hwpf.extractor.WordExtractor").init(doc)>
<cfset var paragraphs = extractor.getParagraphText()>
<cfloop from="0" to="#arrayLen(paragraphs) - 1#" index="pi">
<cfset var text = trim(paragraphs[pi])>
<cfif len(text)>
<cfset arrayAppend(out.elements, {type: "paragraph", style: "", text: text, page: 0})>
</cfif>
</cfloop>
<!--- Таблицы через Range (проще) --->
<cfset var range = doc.getRange()>
<cfset var tableCount = 0>
<cftry>
<cfset var tblIt = range.getParagraphIterator()>
<cfloop condition="true">
<cftry><cfset var p = tblIt.next()><cfcatch><cfbreak></cfcatch></cftry>
<cfif NOT isNull(p) AND p.isInTable()>
<cfset tableCount = 1>
<cfbreak>
</cfif>
</cfloop>
</cftry>
<cfset doc.close()>
<cfset bais.close()>
<cfcatch>
<cfset arrayAppend(out.errors, "DOC_POI: " & cfcatch.message)>
</cfcatch>
</cftry>
<cfreturn out>
</cffunction>
<!--- ═══════════════════════════════════════════════════════════
Helper: parseDOC — грубый экстрактор текста из .doc (fallback)
═══════════════════════════════════════════════════════════ --->
<cffunction name="parseDOC" access="private" returntype="struct">
<cfargument name="fileBytes" type="binary">
@@ -232,8 +325,16 @@
<cfset parsed = {elements: [], errors: []}>
<cfif zext EQ "pdf">
<cfset parsed = parsePDF(entryBytes)>
<cfelseif zext EQ "docx" OR zext EQ "doc">
<cfelseif zext EQ "docx">
<cfset parsed = parseDOCX_POI(entryBytes)>
<cfif arrayLen(parsed.elements) EQ 0 AND arrayLen(parsed.errors) GT 0>
<cfset parsed = parseDOCX(entryBytes)>
</cfif>
<cfelseif zext EQ "doc">
<cfset parsed = parseDOC_POI(entryBytes)>
<cfif arrayLen(parsed.elements) EQ 0 AND arrayLen(parsed.errors) GT 0>
<cfset parsed = parseDOC(entryBytes)>
</cfif>
<cfelse>
<cfset arrayAppend(parsed.errors, "unsupported: .#zext#")>
</cfif>
@@ -266,7 +367,11 @@
<cfelseif doc.mime_type EQ "application/vnd.openxmlformats-officedocument.wordprocessingml.document">
<cfset fileBytes = binaryDecode(doc.original_bytes, "base64")>
<cfset parsed = parseDOCX_POI(fileBytes)>
<cfif arrayLen(parsed.elements) EQ 0 AND arrayLen(parsed.errors) GT 0>
<!--- POI не сработал — fallback на ZIP/XML --->
<cfset parsed = parseDOCX(fileBytes)>
</cfif>
<cfset elements = parsed.elements>
<cfset errors = parsed.errors>
@@ -287,7 +392,10 @@
<cfelseif doc.mime_type EQ "application/msword">
<cfset fileBytes = binaryDecode(doc.original_bytes, "base64")>
<cfset parsed = parseDOC_POI(fileBytes)>
<cfif arrayLen(parsed.elements) EQ 0 AND arrayLen(parsed.errors) GT 0>
<cfset parsed = parseDOC(fileBytes)>
</cfif>
<cfset elements = parsed.elements>
<cfset errors = parsed.errors>
<cfelse>