Files
contractor/parser.cfm
T

438 lines
22 KiB
Plaintext
Raw 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.
<cfsetting showdebugoutput="no" enablecfoutputonly="true">
<cfheader name="Content-Type" value="application/json; charset=utf-8">
<cfparam name="url.doc_id" default="">
<!--- ═══════════════════════════════════════════════════════════
Helper: parsePDF — PDFBox → {elements, errors}
═══════════════════════════════════════════════════════════ --->
<cffunction name="parsePDF" 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 bis = createObject("java", "java.io.BufferedInputStream").init(bais)>
<cfset var pdfDoc = 0>
<cftry>
<cfset pdfDoc = createObject("java", "org.apache.pdfbox.Loader").loadPDF(bis)>
<cfcatch>
<cftry>
<cfset pdfDoc = createObject("java", "org.apache.pdfbox.pdmodel.PDDocument").load(bis)>
<cfcatch>
<cfset arrayAppend(out.errors, "PDFBox: cannot open — " & cfcatch.message)>
</cfcatch>
</cftry>
</cfcatch>
</cftry>
<cfif NOT isNumeric(pdfDoc)>
<cfset var stripper = createObject("java", "org.apache.pdfbox.text.PDFTextStripper")>
<cfset var pageCount = pdfDoc.getNumberOfPages()>
<cfloop from="1" to="#pageCount#" index="pn">
<cfset stripper.setStartPage(pn)>
<cfset stripper.setEndPage(pn)>
<cfset var pageText = stripper.getText(pdfDoc)>
<cfif len(trim(pageText))>
<cfloop list="#pageText#" index="line" delimiters="#chr(10)#">
<cfset line = trim(line)>
<cfif len(line)>
<cfset arrayAppend(out.elements, {type: "paragraph", style: "", text: line, page: pn})>
</cfif>
</cfloop>
</cfif>
</cfloop>
<cfset pdfDoc.close()>
</cfif>
<cfset bis.close()>
<cfset bais.close()>
<cfcatch>
<cfset arrayAppend(out.errors, "PDFBox: " & cfcatch.message)>
</cfcatch>
</cftry>
<cfreturn out>
</cffunction>
<!--- ═══════════════════════════════════════════════════════════
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 var 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">
<cfset var out = {elements: [], errors: []}>
<cftry>
<cfset var bais = createObject("java", "java.io.ByteArrayInputStream").init(fileBytes)>
<cfset var zis = createObject("java", "java.util.zip.ZipInputStream").init(bais)>
<cfset var xmlStr = "">
<cfloop condition="true">
<cfset var entry = zis.getNextEntry()>
<cfif isNull(entry)><cfbreak></cfif>
<cfif entry.getName() EQ "word/document.xml">
<cftry>
<cfset var xmlBytes = zis.readAllBytes()>
<cfset xmlStr = createObject("java", "java.lang.String").init(xmlBytes, "UTF-8")>
<cfcatch>
<cfset var baos = createObject("java", "java.io.ByteArrayOutputStream").init()>
<cfloop condition="true">
<cfset var b = zis.read()>
<cfif b LT 0><cfbreak></cfif>
<cfset baos.write(b)>
</cfloop>
<cfset xmlStr = createObject("java", "java.lang.String").init(baos.toByteArray(), "UTF-8")>
</cfcatch>
</cftry>
<cfbreak>
</cfif>
<cfset zis.closeEntry()>
</cfloop>
<cfset zis.close()>
<cfset bais.close()>
<cfif NOT len(xmlStr)>
<cfset arrayAppend(out.errors, "word/document.xml not found")>
<cfelse>
<cfset var docXml = xmlParse(xmlStr)>
<cfset var body = "">
<cftry>
<cfset body = xmlSearch(docXml, "//:body")>
<cfcatch>
<cfset body = xmlSearch(docXml, "//*[local-name()='body']")>
</cfcatch>
</cftry>
<cfif arrayLen(body) GT 0>
<cfset body = body[1]>
<cfloop array="#body.XmlChildren#" index="child">
<cfset var localName = child.XmlName>
<cfif localName EQ "p" OR right(localName, 2) EQ ":p">
<cfset var paraText = "">
<cftry>
<cfset var textNodes = xmlSearch(child, "//:t")>
<cfcatch>
<cfset textNodes = xmlSearch(child, "//*[local-name()='t']")>
</cfcatch>
</cftry>
<cfloop array="#textNodes#" index="tn">
<cfset paraText &= tn.XmlText>
</cfloop>
<cfset paraText = trim(paraText)>
<cfif len(paraText)>
<cfset arrayAppend(out.elements, {type: "paragraph", style: "", text: paraText, page: 0})>
</cfif>
<cfelseif localName EQ "tbl" OR right(localName, 4) EQ ":tbl">
<cfset var rows = []>
<cftry>
<cfset var trNodes = xmlSearch(child, "//:tr")>
<cfcatch>
<cfset trNodes = xmlSearch(child, "//*[local-name()='tr']")>
</cfcatch>
</cftry>
<cfloop array="#trNodes#" index="trNode">
<cfset var row = []>
<cftry>
<cfset var tcNodes = xmlSearch(trNode, ".//:tc")>
<cfcatch>
<cfset tcNodes = xmlSearch(trNode, ".//*[local-name()='tc']")>
</cfcatch>
</cftry>
<cfloop array="#tcNodes#" index="tcNode">
<cfset var cellText = "">
<cftry>
<cfset var tNodes = xmlSearch(tcNode, ".//:t")>
<cfcatch>
<cfset tNodes = xmlSearch(tcNode, ".//*[local-name()='t']")>
</cfcatch>
</cftry>
<cfloop array="#tNodes#" index="tNode">
<cfset cellText &= tNode.XmlText>
</cfloop>
<cfset arrayAppend(row, trim(cellText))>
</cfloop>
<cfif arrayLen(row) GT 0>
<cfset arrayAppend(rows, row)>
</cfif>
</cfloop>
<cfif arrayLen(rows) GT 0>
<cfset arrayAppend(out.elements, {type: "table", rows: rows, page: 0})>
</cfif>
</cfif>
</cfloop>
<cfelse>
<cfset arrayAppend(out.errors, "body not found")>
</cfif>
</cfif>
<cfcatch>
<cfset arrayAppend(out.errors, "DOCX: " & cfcatch.message)>
</cfcatch>
</cftry>
<cfreturn out>
</cffunction>
<!--- ═══════════════════════════════════════════════════════════
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>
<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">
<cfset var out = {elements: [], errors: []}>
<cftry>
<cfset var raw = createObject("java", "java.lang.String").init(fileBytes, "windows-1251")>
<cfset var lines = raw.split("\n")>
<cfloop array="#lines#" index="var line">
<cfset line = trim(line)>
<cfif len(line) GT 3 AND (reFind("[а-яА-ЯёЁ]", line) OR reFind("\d{2,}", line))>
<cfset arrayAppend(out.elements, {type: "paragraph", style: "", text: line, page: 0})>
</cfif>
</cfloop>
<cfcatch>
<cfset arrayAppend(out.errors, ".doc: " & cfcatch.message)>
</cfcatch>
</cftry>
<cfreturn out>
</cffunction>
<!--- ═══════════════════════════════════════════════ MAIN --->
<cfset result = {ok: false, error: ""}>
<cftry>
<cfif NOT len(url.doc_id)>
<cfset result.error = "doc_id required">
<cfelse>
<cfquery name="doc" datasource="baza">
SELECT id, filename, mime_type, original_bytes, status
FROM documents WHERE id = <cfqueryparam value="#url.doc_id#" cfsqltype="cf_sql_varchar">
</cfquery>
<cfif doc.recordCount EQ 0>
<cfset result.error = "document not found">
<cfelse>
<cfset elements = []>
<cfset errors = []>
<cfset files = []>
<!--- ZIP: поиск PDF/DOCX во всех подпапках --->
<cfif doc.mime_type EQ "application/zip">
<cftry>
<cfset zipBytes = binaryDecode(doc.original_bytes, "base64")>
<cfset zbais = createObject("java", "java.io.ByteArrayInputStream").init(zipBytes)>
<cfset zzis = createObject("java", "java.util.zip.ZipInputStream").init(zbais)>
<cfloop condition="true">
<cfset zentry = zzis.getNextEntry()>
<cfif isNull(zentry)><cfbreak></cfif>
<cfset zname = zentry.getName()>
<cfif right(zname, 1) EQ "/">
<cfset zzis.closeEntry()><cfcontinue>
</cfif>
<cfset entryBytes = "">
<cftry>
<cfset entryBytes = zzis.readAllBytes()>
<cfcatch>
<cfset zbaos = createObject("java", "java.io.ByteArrayOutputStream").init()>
<cfloop condition="true">
<cfset zb = zzis.read()>
<cfif zb LT 0><cfbreak></cfif>
<cfset zbaos.write(zb)>
</cfloop>
<cfset entryBytes = zbaos.toByteArray()>
</cfcatch>
</cftry>
<cfset zext = listLast(zname, ".")>
<cfset parsed = {elements: [], errors: []}>
<cfif zext EQ "pdf">
<cfset parsed = parsePDF(entryBytes)>
<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>
<cfset prefix = doc.filename & "/">
<cfloop array="#parsed.elements#" index="el">
<cfset el.archive_path = prefix & zname>
<cfset arrayAppend(elements, el)>
</cfloop>
<cfset arrayAppend(files, {
filename: zname, archive_path: prefix & zname,
element_count: arrayLen(parsed.elements),
error_count: arrayLen(parsed.errors)
})>
<cfloop array="#parsed.errors#" index="e">
<cfset arrayAppend(errors, zname & ": " & e)>
</cfloop>
<cfset zzis.closeEntry()>
</cfloop>
<cfset zzis.close()><cfset zbais.close()>
<cfcatch>
<cfset arrayAppend(errors, "ZIP: " & cfcatch.message)>
</cfcatch>
</cftry>
<cfelseif doc.mime_type EQ "application/pdf">
<cfset fileBytes = binaryDecode(doc.original_bytes, "base64")>
<cfset parsed = parsePDF(fileBytes)>
<cfset elements = parsed.elements>
<cfset errors = parsed.errors>
<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>
<cfelseif doc.mime_type EQ "text/plain" OR doc.mime_type EQ "application/octet-stream">
<cftry>
<cfset fileBytes = binaryDecode(doc.original_bytes, "base64")>
<cfset text = createObject("java", "java.lang.String").init(fileBytes, "UTF-8")>
<cfloop list="#text#" index="line" delimiters="#chr(10)#">
<cfset line = trim(line)>
<cfif len(line)>
<cfset arrayAppend(elements, {type: "paragraph", style: "", text: line, page: 1})>
</cfif>
</cfloop>
<cfcatch>
<cfset arrayAppend(errors, "TXT: " & cfcatch.message)>
</cfcatch>
</cftry>
<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>
<cfset arrayAppend(errors, "unsupported: " & doc.mime_type)>
</cfif>
<cfset newStatus = arrayLen(elements) GT 0 ? "parsed" : "error">
<cfif arrayLen(errors) GT 0 AND arrayLen(elements) EQ 0>
<cfset newStatus = "error">
</cfif>
<cfquery datasource="baza">
UPDATE documents
SET elements_json = <cfqueryparam value="#serializeJSON(elements)#" cfsqltype="cf_sql_varchar">::jsonb,
status = <cfqueryparam value="#newStatus#" cfsqltype="cf_sql_varchar">,
error_message = <cfqueryparam value="#arrayLen(errors) ? serializeJSON(errors) : ''#" cfsqltype="cf_sql_varchar">
WHERE id = <cfqueryparam value="#url.doc_id#" cfsqltype="cf_sql_varchar">
</cfquery>
<!--- Статистика --->
<cfset paraCount = 0>
<cfset tblCount = 0>
<cfset tblRows = 0>
<cfset textPreview = "">
<cfset tblHeaders = []>
<cfloop array="#elements#" index="el">
<cfif el.type EQ "paragraph">
<cfset paraCount++>
<cfif len(textPreview) LT 200>
<cfset textPreview &= el.text & chr(10)>
</cfif>
<cfelseif el.type EQ "table">
<cfset tblCount++>
<cfset tblRows += arrayLen(el.rows)>
<cfif arrayLen(el.rows) GT 0>
<cfset arrayAppend(tblHeaders, el.rows[1])>
</cfif>
</cfif>
</cfloop>
<cfset textPreview = left(textPreview, 200)>
<cfset result = {ok: true, doc_id: url.doc_id, filename: doc.filename,
mime_type: doc.mime_type, status: newStatus,
element_count: arrayLen(elements), error_count: arrayLen(errors),
paragraphs: paraCount, tables: tblCount, table_rows: tblRows,
text_preview: textPreview, table_headers: tblHeaders,
errors: errors, files: files}>
</cfif>
</cfif>
<cfcatch>
<cfset result = {ok: false, error: cfcatch.message, detail: cfcatch.detail}>
</cfcatch>
</cftry>
<cfoutput>#serializeJSON(result)#</cfoutput>