diff --git a/Obtener-Datos-1/README.md b/Obtener-Datos-1/README.md new file mode 100644 index 0000000..0f7ba89 --- /dev/null +++ b/Obtener-Datos-1/README.md @@ -0,0 +1,58 @@ +# Fase 1 — Ingesta Automática de Datos (Adicciones ↔ Violencia) + +Esta fase inicial prepara la base de datos PostgreSQL para el análisis posterior. +El script **detecta automáticamente delimitadores, codificaciones y nombres de columnas**, +creando tablas limpias e importando los datasets CSV del directorio `datasets/`. + +--- + +## Ejecución + +1. Levanta el contenedor de PostgreSQL: + +```bash +docker compose up -d +``` + +2. Ejecuta el importador de datos: + +```bash +python3 importador.py +``` + +--- + +## Descripción Técnica + +- Detecta **codificación** (`UTF-8`, `Latin1`, `Windows-1252`, etc.) con `chardet`. +- Identifica el **delimitador CSV** (`;`, `,`, `|`, `\t`). +- Limpia nombres de columnas y crea las tablas en **PostgreSQL 15**. +- Inserta automáticamente los registros de cada dataset. +- Genera el archivo de metadatos `columnas_info.json` con las columnas detectadas. + +--- + +## Requisitos + +Archivo `requirements.txt`: + +```txt +pandas +psycopg2-binary +chardet +``` + +Instalación rápida: + +```bash +pip install -r requirements.txt +``` + +--- + +## Salida + +- Tablas generadas en la base de datos `adicciones` (puerto 5433). +- Archivo `columnas_info.json` con las estructuras detectadas. + +*Esta fase deja lista la base de datos para las siguientes fases de procesamiento y análisis estadístico.* diff --git a/Obtener-Datos-1/columnas_info.json b/Obtener-Datos-1/columnas_info.json index 0a6ef1a..1d6f247 100644 --- a/Obtener-Datos-1/columnas_info.json +++ b/Obtener-Datos-1/columnas_info.json @@ -26,7 +26,11 @@ "prov" ], "condenas_sexo_localidad": [ - "lugar_de_nacimiento_sexo_comunidades_y_ciudades_au" + "lugar_de_nacimiento", + "sexo", + "comunidades_y_ciudades_aut_nomas", + "periodo", + "total" ], "registro_prohibidos_juego": [ "a_o", diff --git a/Obtener-Datos-1/compose.yaml b/Obtener-Datos-1/compose.yaml index db64ca3..f2c33e7 100644 --- a/Obtener-Datos-1/compose.yaml +++ b/Obtener-Datos-1/compose.yaml @@ -1,19 +1,18 @@ services: - mysql: - image: mysql:8.0 - container_name: adicciones_db + postgres: + image: postgres:15 + container_name: adicciones_postgres restart: always environment: - MYSQL_ROOT_PASSWORD: root - MYSQL_DATABASE: adicciones - MYSQL_USER: user - MYSQL_PASSWORD: pass + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: adicciones ports: - - "3307:3306" + - "5433:5432" volumes: - - ./mysql_data:/var/lib/mysql + - ./pg_data:/var/lib/postgresql/data healthcheck: - test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 10s timeout: 5s retries: 5 \ No newline at end of file diff --git a/Obtener-Datos-1/importador.py b/Obtener-Datos-1/importador.py new file mode 100644 index 0000000..4e856f3 --- /dev/null +++ b/Obtener-Datos-1/importador.py @@ -0,0 +1,136 @@ +import os +import psycopg2 +import csv +import chardet +import re +import json + +# ------------------------------------------------------------ +# CONFIGURACIÓN DE CONEXIÓN +# ------------------------------------------------------------ +DB_CONFIG = { + "host": "localhost", + "port": 5433, + "user": "postgres", + "password": "postgres", + "dbname": "adicciones" +} + +DATASETS_DIR = "datasets" +OUTPUT_FILE = "columnas_info.json" + + +def sanitize_name(name: str) -> str: + """Limpia nombres de columnas para SQL.""" + name = name.strip().lower() + name = re.sub(r"[^a-z0-9_]+", "_", name) + return name[:50] + + +def detect_encoding(file_path): + """Detecta codificación del archivo con chardet.""" + with open(file_path, 'rb') as f: + raw = f.read(10000) + result = chardet.detect(raw) + return result['encoding'] or 'utf-8' + + +def normalize_encoding(enc: str) -> str: + """Convierte nombres de encoding a formato válido para PostgreSQL.""" + if not enc: + return "UTF8" + enc = enc.upper().replace("-", "").replace("_", "") + if "UTF" in enc: + return "UTF8" + if "LATIN" in enc or "ISO88591" in enc: + return "LATIN1" + if "1252" in enc or "WINDOWS" in enc: + return "WIN1252" + return "UTF8" # fallback por defecto + + +def detect_delimiter(file_path, encoding): + """Detecta el delimitador probable.""" + with open(file_path, 'r', encoding=encoding, errors='ignore') as f: + sample = f.read(4096) + try: + dialect = csv.Sniffer().sniff(sample, delimiters=";,|\t") + return dialect.delimiter + except csv.Error: + if ";" in sample: + return ";" + elif "\t" in sample: + return "\t" + elif "|" in sample: + return "|" + else: + return "," + + +def main(): + conn = psycopg2.connect(**DB_CONFIG) + cur = conn.cursor() + columnas_info = {} + + for filename in os.listdir(DATASETS_DIR): + if not filename.endswith(".csv"): + continue + + path = os.path.join(DATASETS_DIR, filename) + table_name = sanitize_name(os.path.splitext(filename)[0]) + + print(f"\n📊 Procesando: {filename} → tabla '{table_name}'") + + encoding_detected = detect_encoding(path) + pg_encoding = normalize_encoding(encoding_detected) + delimiter = detect_delimiter(path, encoding_detected) + + print(f" → detectado: delimitador '{delimiter}' | codificación '{encoding_detected}' (→ {pg_encoding})") + + # Leer encabezados manualmente + with open(path, 'r', encoding=encoding_detected, errors='ignore') as f: + reader = csv.reader(f, delimiter=delimiter) + headers = next(reader) + headers = [sanitize_name(h) for h in headers] + + columnas_info[table_name] = headers + + # Crear tabla limpia + cur.execute(f"DROP TABLE IF EXISTS {table_name} CASCADE;") + cols_sql = ", ".join([f"{col} TEXT" for col in headers]) + cur.execute(f"CREATE TABLE {table_name} ({cols_sql});") + conn.commit() + + # Intentar importar con encoding normalizado + try: + with open(path, 'r', encoding=encoding_detected, errors='ignore') as f: + next(f) # saltar encabezado + cur.copy_expert( + sql=f"COPY {table_name} ({', '.join(headers)}) FROM STDIN WITH (FORMAT CSV, DELIMITER '{delimiter}', NULL '', HEADER FALSE, ENCODING '{pg_encoding}');", + file=f + ) + conn.commit() + except Exception as e: + print(f"⚠️ Error al copiar con '{pg_encoding}': {e}") + print(" → Reintentando con ENCODING 'UTF8'") + with open(path, 'r', encoding='utf-8', errors='ignore') as f: + next(f) + cur.copy_expert( + sql=f"COPY {table_name} ({', '.join(headers)}) FROM STDIN WITH (FORMAT CSV, DELIMITER '{delimiter}', NULL '', HEADER FALSE, ENCODING 'UTF8');", + file=f + ) + conn.commit() + + print(f"✅ Tabla '{table_name}' importada correctamente ({len(headers)} columnas).") + + cur.close() + conn.close() + + with open(OUTPUT_FILE, "w", encoding="utf-8") as f: + json.dump(columnas_info, f, indent=4, ensure_ascii=False) + + print(f"\n✅ Importación completada. Estructuras guardadas en '{OUTPUT_FILE}'") + + +if __name__ == "__main__": + main() diff --git a/Obtener-Datos-1/requirements.txt b/Obtener-Datos-1/requirements.txt index d2db399..d79a627 100644 --- a/Obtener-Datos-1/requirements.txt +++ b/Obtener-Datos-1/requirements.txt @@ -1,2 +1,3 @@ pandas -mysql-connector-python \ No newline at end of file +psycopg2-binary +chardet \ No newline at end of file