data-analysis-ai / utils /utils.py
qsocialnow's picture
-
7248503 verified
Raw
History Blame Contribute Delete
11 kB
import os
import pandas as pd
import re
import unicodedata
from docx import Document
from bs4 import BeautifulSoup
import markdown
from docx.shared import Pt
from docx.oxml.ns import qn
from openai import OpenAI
openai = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
def preparar_bloques_json(nombre_archivo, max_chars=100000):
"""
Prepara bloques de datos desde un archivo Excel para convertirlos a formato JSON.
Cada bloque contendrá un máximo de `max_chars` caracteres en total.
Args:
nombre_archivo (str): Ruta al archivo .xlsx.
max_chars (int): Número máximo de caracteres por bloque.
Returns:
List[Dict]: Lista de bloques en formato JSON {pregunta: [respuestas]}.
"""
df = pd.read_excel(nombre_archivo)
df['count_char'] = df.apply(lambda fila: sum(len(str(val)) for val in fila if pd.notnull(val)), axis=1)
bloques, bloque_actual, char_count = [], [], 0
for _, fila in df.iterrows():
if char_count + fila['count_char'] > max_chars:
bloques.append(pd.DataFrame(bloque_actual))
bloque_actual, char_count = [], 0
bloque_actual.append(fila)
char_count += fila['count_char']
if bloque_actual:
bloques.append(pd.DataFrame(bloque_actual))
bloques_json = []
for bloque_df in bloques:
bloque_dict = {}
for col in bloque_df.columns:
if col != 'count_char':
respuestas = bloque_df[col].dropna().astype(str).tolist()
if respuestas:
bloque_dict[col] = respuestas
bloques_json.append(bloque_dict)
return bloques_json
def preparar_bloques_texto_redes(textos, max_chars=120000):
"""
Recibe una lista de textos (strings) y la divide en bloques de longitud máxima.
"""
# df = pd.read_excel(nombre_archivo)
# if "texto" not in df.columns:
# raise ValueError("❌ La columna 'texto' no está presente en el archivo.")
# textos = df["texto"].dropna().astype(str).tolist()
texto_plano = "\n".join(textos)
bloques = []
while texto_plano:
bloque = texto_plano[:max_chars]
ultimo_salto = bloque.rfind("\n")
if ultimo_salto == -1:
ultimo_salto = len(bloque)
bloques.append(bloque[:ultimo_salto])
texto_plano = texto_plano[ultimo_salto:].lstrip()
return bloques
def markdown_a_word_docx(archivo_md, archivo_docx):
"""
Convierte un archivo Markdown a un documento Word (.docx).
- Elimina espacios excesivos.
- Aplica estilos a títulos.
- Reduce el espaciado entre párrafos.
Parámetros:
- archivo_md (str): Ruta al archivo .md.
- archivo_docx (str): Ruta de salida para el archivo .docx.
"""
with open(archivo_md, "r", encoding="utf-8") as f:
markdown_text = f.read()
html_content = markdown.markdown(markdown_text)
soup = BeautifulSoup(html_content, "html.parser")
doc = Document()
# Ajustar el estilo de los párrafos normales
style = doc.styles['Normal']
font = style.font
font.name = 'Arial'
font.size = Pt(11)
# for element in soup.children:
# if element.name == "h1":
# doc.add_heading(element.get_text(), level=1)
# elif element.name == "h2":
# doc.add_heading(element.get_text(), level=2)
# elif element.name == "h3":
# doc.add_heading(element.get_text(), level=3)
# elif element.name in ["ul", "ol"]:
# for li in element.find_all("li"):
# style = 'List Number' if element.name == "ol" else 'List Bullet'
# doc.add_paragraph(li.get_text(), style=style)
# elif element.name == "p":
# doc.add_paragraph(element.get_text())
# elif element.name == "blockquote":
# doc.add_paragraph(element.get_text(), style='Intense Quote')
# else:
# if hasattr(element, "get_text"):
# doc.add_paragraph(element.get_text())
for element in soup.children:
if not hasattr(element, "name"):
continue # Evita texto suelto fuera de etiquetas
if element.name == "h1":
p = doc.add_heading(element.get_text(), level=1)
p.paragraph_format.space_after = Pt(4)
elif element.name == "h2":
p = doc.add_heading(element.get_text(), level=2)
p.paragraph_format.space_after = Pt(3)
elif element.name == "h3":
p = doc.add_heading(element.get_text(), level=3)
p.paragraph_format.space_after = Pt(2)
elif element.name in ["ul", "ol"]:
style = 'List Number' if element.name == "ol" else 'List Bullet'
for li in element.find_all("li"):
p = doc.add_paragraph(li.get_text(strip=True), style=style)
p.paragraph_format.space_after = Pt(1)
elif element.name == "blockquote":
p = doc.add_paragraph(element.get_text(strip=True), style='Intense Quote')
p.paragraph_format.space_after = Pt(2)
elif element.name == "p":
text = element.get_text(strip=True)
if text:
p = doc.add_paragraph(text)
p.paragraph_format.space_after = Pt(2)
doc.save(archivo_docx)
def validar_y_leer_excel_encuesta(archivo):
"""
Valida que el archivo sea un Excel correcto y que tenga una estructura mínima válida.
Devuelve el DataFrame si es válido o una tupla (None, mensaje de error) si no lo es.
"""
nombre = archivo.name
if not nombre.endswith(".xlsx"):
return None, "❌ El archivo debe ser un Excel (.xlsx). Por favor sube un archivo válido."
try:
df = pd.read_excel(nombre)
except Exception as e:
return None, f"❌ No se pudo leer el archivo Excel. Verifica que esté correctamente guardado. Detalle: {str(e)}"
if df.shape[1] < 2:
return None, "❌ El archivo debe tener al menos 2 columnas (cada una correspondiente a una pregunta distinta)."
if not all(isinstance(col, str) and col.strip() != "" for col in df.columns):
return None, "❌ El archivo debe tener encabezados válidos en las columnas (nombres de las preguntas)."
if df.dropna(how="all").empty:
return None, "❌ El archivo está vacío o no contiene respuestas válidas."
return df, None
def validar_y_leer_excel_redes(archivo):
"""
Valida que el archivo de redes sociales sea un Excel correcto con las columnas obligatorias:
'red_social', 'fecha', 'autor', 'texto'.
"""
nombre = archivo.name
if not nombre.endswith(".xlsx"):
return None, "❌ El archivo debe ser un Excel (.xlsx). Por favor sube un archivo válido."
try:
df = pd.read_excel(nombre)
except Exception as e:
return None, f"❌ No se pudo leer el archivo Excel. Verifica que esté correctamente guardado. Detalle: {str(e)}"
if df.shape[1] < 4:
return None, "❌ El archivo debe tener al menos 4 columnas."
if not all(isinstance(col, str) and col.strip() != "" for col in df.columns):
return None, "❌ El archivo debe tener encabezados válidos en las columnas."
if df.dropna(how="all").empty:
return None, "❌ El archivo está vacío o no contiene datos válidos."
columnas_requeridas = {"red_social", "fecha", "autor", "texto"}
columnas_presentes = set(col.lower().strip() for col in df.columns)
if not columnas_requeridas.issubset(columnas_presentes):
faltantes = columnas_requeridas - columnas_presentes
return None, f"❌ Faltan columnas obligatorias: {', '.join(faltantes)}"
return df, None
from docx import Document
def combinar_archivos_docx(lista_archivos, salida):
documento_final = Document()
# Eliminar contenido inicial de forma segura
body = documento_final.element.body
for _ in range(len(body)):
body.remove(body[0])
for i, archivo in enumerate(lista_archivos):
doc_temp = Document(archivo)
# Verificar si el documento no está vacío
tiene_contenido = any(
p.text.strip() for p in doc_temp.paragraphs if p.text
)
if i != 0 and tiene_contenido:
documento_final.add_page_break()
for elemento in doc_temp.element.body:
# Evitar agregar elementos vacíos
if elemento.tag.endswith('p'):
texto = ''.join([n.text for n in elemento.iter() if n.text])
if not texto.strip():
continue
documento_final.element.body.append(elemento)
documento_final.save(salida)
def clean_text(text):
if not isinstance(text, str):
return ""
# Quitar 'RT'
text = re.sub(r'^RT[\s]+', '', text)
# Quitar URLs
text = re.sub(r'https?:\/\/\S+', '', text)
# Quitar menciones
text = re.sub(r'@\w+', '', text)
# Quitar hashtags (opcional)
# text = re.sub(r'#\w+', '', text)
# Eliminar emojis y símbolos no ASCII
text = text.encode('ascii', 'ignore').decode('ascii')
# Normalizar acentos y caracteres especiales
text = unicodedata.normalize("NFKD", text).encode("ascii", "ignore").decode("utf-8")
# Quitar puntuación
text = re.sub(r'[^\w\s]', '', text)
# Convertir a minúsculas
text = text.lower()
# Eliminar espacios extra
text = re.sub(r'\s+', ' ', text).strip()
return text
def generar_resumenes_por_red_social(red_social, textos, prompts, modelo="gpt-4o-mini"):
bloques = preparar_bloques_texto_redes(textos)
resumenes_por_prompt = {}
for clave, prompt_info in prompts.items():
resumenes_bloques = []
for i, bloque in enumerate(bloques, start=1):
try:
response = openai.chat.completions.create(
model=modelo,
messages=[
{"role": "system", "content": prompt_info["system"]},
{"role": "user", "content": prompt_info["user"] + "\n\n```json\n" + str(bloque) + "\n```"}
],
temperature=0.4
)
resumen = response.choices[0].message.content
except Exception as e:
resumen = f"⚠️ Error en bloque {i}: {str(e)}"
resumenes_bloques.append(f"\n\n🧩 **Resumen bloque {i}/{len(bloques)} ({prompt_info['nombre']})**\n{'-'*80}\n{resumen}")
# Concatenar todos los bloques como resumen global de este prompt
resumenes_por_prompt[clave] = "\n".join(resumenes_bloques)
return resumenes_por_prompt