import io import os import re import csv import tempfile import subprocess import logging from typing import List from bs4 import BeautifulSoup from pypdf import PdfReader from docx import Document import openpyxl import pdfplumber from PIL import Image import pytesseract from striprtf.striprtf import rtf_to_text logger = logging.getLogger(__name__) class DocumentProcessor: # ========================================================= # HELPERS # ========================================================= @staticmethod def _cleanup_text(text: str) -> str: if not text: return "" text = text.replace("\xa0", " ") text = text.replace("\t", " ") text = re.sub(r"[ \t]+", " ", text) text = re.sub(r"\n{3,}", "\n\n", text) return text.strip() @staticmethod def _cell_to_text(v) -> str: if v is None: return "" s = str(v).strip() return s if s else "" # ========================================================= # HTML # ========================================================= @staticmethod def normalize_email_html(html_content: str) -> str: """ Очищает HTML письма и нормализует таблицы """ soup = BeautifulSoup( html_content, "html.parser" ) for tag in soup(["script", "style"]): tag.decompose() for table in soup.find_all("table"): table["style"] = ( "border-collapse: collapse; width: 100%;" ) for cell in table.find_all(["td", "th"]): cell["style"] = ( "border:1px solid #ccc;padding:6px;" ) return str(soup) # ========================================================= # MAIN # ========================================================= def extract_text( self, content: bytes, filename: str ) -> str: ext = filename.lower().split(".")[-1] try: # ================================================= # PDF # ================================================= if ext == "pdf": return self._extract_pdf(content) # ================================================= # WORD # ================================================= elif ext in ["docx", "docm"]: return self._extract_docx(content) elif ext == "doc": return self._extract_doc(content) elif ext == "rtf": return self._extract_rtf(content) # ================================================= # EXCEL # ================================================= elif ext in ["xlsx", "xlsm", "xlsxm"]: return self._extract_excel_openpyxl(content) elif ext == "xls": return self._extract_excel_xls(content) elif ext == "csv": return self._extract_csv(content) # ================================================= # IMAGES / OCR # ================================================= elif ext in [ "png", "jpg", "jpeg", "gif", "bmp", "webp" ]: return self._extract_image(content) elif ext in ["tif", "tiff"]: return self._extract_tiff(content) # ================================================= # TEXT # ================================================= elif ext == "txt": try: return content.decode("utf-8") except Exception: return content.decode( "cp1251", errors="ignore" ) # ================================================= # UNKNOWN # ================================================= logger.warning( f"Unsupported attachment format: {ext}" ) return "" except Exception: logger.exception( f"Attachment processing failed: {filename}" ) return "" # ========================================================= # PDF # ========================================================= def _extract_pdf( self, content: bytes ) -> str: try: parts = [] with pdfplumber.open( io.BytesIO(content) ) as pdf: for page_num, page in enumerate( pdf.pages, start=1 ): parts.append( f"=== PAGE {page_num} ===" ) # ===================================== # TEXT # ===================================== try: text = page.extract_text() if text: parts.append(text) except Exception as e: logger.warning( f"PDF text extraction failed: {e}" ) # ===================================== # TABLES # ===================================== try: tables = page.extract_tables() for table_idx, table in enumerate( tables, start=1 ): parts.append( f"=== TABLE {table_idx} ===" ) for row in table: if not row: continue cells = [] for cell in row: if cell is None: continue txt = str(cell).strip() txt = " ".join( txt.split() ) if txt: cells.append(txt) if cells: parts.append( " | ".join(cells) ) except Exception as e: logger.warning( f"PDF table extraction failed: {e}" ) text = "\n".join(parts) return self._cleanup_text(text) except Exception as e: logger.error(f"PDF parse failed: {e}") return "" # ========================================================= # DOCX / DOCM # ========================================================= def _extract_docx( self, content: bytes ) -> str: try: doc = Document(io.BytesIO(content)) parts = [] # ============================================= # PARAGRAPHS # ============================================= for p in doc.paragraphs: txt = p.text.strip() if txt: parts.append(txt) # ============================================= # TABLES # ============================================= for table_idx, table in enumerate( doc.tables ): parts.append( f"=== TABLE {table_idx + 1} ===" ) for row in table.rows: cells = [] for cell in row.cells: cell_parts = [] # ================================= # DIRECT CELL TEXT # ================================= txt = "\n".join( p.text.strip() for p in cell.paragraphs if p.text.strip() ) txt = " ".join(txt.split()) if txt: cell_parts.append(txt) # ================================= # NESTED TABLES # ================================= for nested_table in cell.tables: for nested_row in nested_table.rows: nested_vals = [] for nested_cell in nested_row.cells: nested_txt = "\n".join( p.text.strip() for p in nested_cell.paragraphs if p.text.strip() ) nested_txt = " ".join( nested_txt.split() ) if nested_txt: nested_vals.append( nested_txt ) if nested_vals: cell_parts.append( " || ".join( nested_vals ) ) # ================================= # FINAL CELL # ================================= if cell_parts: cells.append( " <<