Files
descarga_adjuntos_correos_cpe/pdf_utilities.py
T

169 lines
6.0 KiB
Python

import pdfplumber
import re
import os
import llm_processor
# ========================================================================
def extraer_texto_pdf(ruta_pdf):
"""Extrae todo el texto de un PDF usando pdfplumber."""
texto_completo = ""
try:
with pdfplumber.open(ruta_pdf) as pdf:
for page in pdf.pages:
texto_completo += page.extract_text() + "\n"
return texto_completo
except Exception as e:
print(f" ⚠️ Error leyendo PDF {ruta_pdf}: {e}")
return ""
# ========================================================================
def extraer_ruc(texto):
"""Busca un RUC (11 dígitos, empieza con 10 o 20) en el texto."""
# Intentamos buscar patrones de RUC: RUC 2060... o R.U.C. 2060...
patrones = [
r'R\.?U\.?C\.?\s*[:\-\s]\s*((?:10|20)\d{9})', # R.U.C: 20...
r'\b((?:10|20)\d{9})\b' # 11 dígitos sueltos que empiecen con 10 o 20
]
for patron in patrones:
match = re.search(patron, texto, re.IGNORECASE)
if match:
return match.group(1)
return "N/A"
# ========================================================================
def _flexible(palabra):
"""Convierte una palabra en un patrón regex que tolera espacios entre caracteres.
Ej: FACTURA -> F\\s*A\\s*C\\s*T\\s*U\\s*R\\s*A
"""
return '\\s*'.join(palabra)
def extraer_tipo_documento(texto):
"""Identifica el tipo de documento SUNAT basándose en el texto usando regex multilínea.
Soporta texto con espacios entre caracteres (ej: 'F A C T U R A')."""
t = texto.upper()
patrones = {
"01": [
rf'{_flexible("FACTURA")}\s+({_flexible("ELECTRÓNICA")}|{_flexible("ELECTRONICA")}|DE\s+VENTA)',
rf'{_flexible("FACTURA")}',
],
"03": [
rf'{_flexible("BOLETA")}\s+DE\s+{_flexible("VENTA")}',
rf'{_flexible("BOLETA")}\s+{_flexible("ELECTRÓNICA")}',
rf'{_flexible("BOLETA")}\s+{_flexible("ELECTRONICA")}',
],
"07": [
rf'{_flexible("NOTA")}\s+DE\s+{_flexible("CRÉDITO")}',
rf'{_flexible("NOTA")}\s+DE\s+{_flexible("CREDITO")}',
],
"08": [
rf'{_flexible("NOTA")}\s+DE\s+{_flexible("DÉBITO")}',
rf'{_flexible("NOTA")}\s+DE\s+{_flexible("DEBITO")}',
],
"14": [
rf'{_flexible("RECIBO")}\s+POR\s+{_flexible("HONORARIOS")}',
],
"09": [
rf'{_flexible("GUÍA")}\s+DE\s+{_flexible("REMISIÓN")}',
rf'{_flexible("GUIA")}\s+DE\s+{_flexible("REMISION")}',
],
}
for tipo, regex_list in patrones.items():
for p in regex_list:
if re.search(p, t, re.IGNORECASE | re.MULTILINE):
return tipo
return "99"
# ========================================================================
def extraer_serie_numero(texto):
"""Busca la serie y número (ej. F001-123, E001-0000123, EG07-2367)."""
serie_patrones = [
# Formato con guión: letras+dígitos - número
r'\b([A-Z]{1,4}\d{2,4})\s*-\s*(\d+)\b',
# Fallback sin guión
r'([A-Z]{1,4}\d{2,4})\s+(\d{1,10})',
# Serie numérica corta sin letras
r'\b(\d{3})\s*-\s*(\d+)\b'
]
for patron in serie_patrones:
match = re.search(patron, texto)
if match:
return f"{match.group(1)}-{match.group(2)}"
return "No encontrado"
# ========================================================================
def _necesita_llm(resultado):
"""True si algún campo clave no se encontró con regex."""
return (
resultado['ruc'] in ("N/A", "Desconocido") or
resultado['tipo_doc'] == "99" or
resultado['id'] in ("No encontrado", "N/A")
)
def _campos_rellenados(regex_original, regex_result):
"""Cuenta cuántos campos cambió el LLM respecto al regex original."""
count = 0
if regex_original['ruc'] in ("N/A", "Desconocido") and regex_result['ruc'] not in ("N/A", "Desconocido"):
count += 1
if regex_original['tipo_doc'] == "99" and regex_result['tipo_doc'] != "99":
count += 1
if regex_original['id'] in ("No encontrado", "N/A") and regex_result['id'] not in ("No encontrado", "N/A"):
count += 1
return count
def _merge_llm(regex_result, llm_data):
"""Rellena campos vacíos del regex con datos del LLM sin pisar lo ya encontrado."""
if regex_result['ruc'] in ("N/A", "Desconocido"):
ruc = llm_data.get('ruc', '')
if re.match(r'^(10|20)\d{9}$', str(ruc)):
regex_result['ruc'] = ruc
if regex_result['tipo_doc'] == "99":
tipo = str(llm_data.get('tipo', '99'))
tipo_codes = {"01", "02", "03", "07", "08", "09", "14"}
if tipo in tipo_codes:
regex_result['tipo_doc'] = tipo
if regex_result['id'] in ("No encontrado", "N/A"):
serie = str(llm_data.get('serie', ''))
numero = str(llm_data.get('numero', ''))
if serie and numero:
regex_result['id'] = f"{serie}-{numero}"
return regex_result
def obtener_datos_pdf(ruta_pdf):
"""Orquestador para extraer metadata de un PDF.
Primero intenta con regex; si faltan campos, usa LLM como fallback."""
texto = extraer_texto_pdf(ruta_pdf)
if not texto:
return {"valido": False, "error": "No se pudo extraer texto"}
resultado = {
"valido": True,
"tipo_doc": extraer_tipo_documento(texto),
"ruc": extraer_ruc(texto),
"id": extraer_serie_numero(texto),
"llm_usado": False,
"llm_respuesta": None
}
if _necesita_llm(resultado):
resultado["llm_usado"] = True
llm_data = llm_processor.extraer_con_llm(texto)
resultado["llm_respuesta"] = dict(llm_data) if llm_data else None
if llm_data:
antes = dict(resultado)
resultado = _merge_llm(resultado, llm_data)
return resultado
# ========================================================================