tablas-python 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- helpers/__init__.py +27 -0
- helpers/display_helper.py +135 -0
- helpers/table_manager.py +418 -0
- tablas_python/__init__.py +68 -0
- tablas_python/cli.py +49 -0
- tablas_python-0.1.0.dist-info/METADATA +375 -0
- tablas_python-0.1.0.dist-info/RECORD +22 -0
- tablas_python-0.1.0.dist-info/WHEEL +5 -0
- tablas_python-0.1.0.dist-info/entry_points.txt +3 -0
- tablas_python-0.1.0.dist-info/licenses/LICENSE +21 -0
- tablas_python-0.1.0.dist-info/top_level.txt +3 -0
- utils/__init__.py +71 -0
- utils/batch_processor.py +111 -0
- utils/data_helpers.py +500 -0
- utils/excel_extractor.py +425 -0
- utils/excel_writer.py +188 -0
- utils/exporter.py +158 -0
- utils/file_utils.py +65 -0
- utils/pdf_extractor.py +129 -0
- utils/sqlite_extractor.py +133 -0
- utils/table_cleaner.py +246 -0
- utils/validator.py +155 -0
helpers/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Módulo de helpers para gestión, presentación, cálculo y exportación de tablas.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .table_manager import TableManager, obtener_tabla, inspeccionar_archivo, exportar_archivo_a_csv
|
|
6
|
+
from .display_helper import DisplayHelper
|
|
7
|
+
from utils.batch_processor import unir_archivos_carpeta
|
|
8
|
+
from utils.excel_writer import escribir_en_excel
|
|
9
|
+
from utils.validator import validar_dataframe, reporte_calidad, detectar_duplicados
|
|
10
|
+
from utils.data_helpers import buscar_v, conciliar_tablas, obtener_celda, modificar_celda
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"TableManager",
|
|
14
|
+
"obtener_tabla",
|
|
15
|
+
"inspeccionar_archivo",
|
|
16
|
+
"exportar_archivo_a_csv",
|
|
17
|
+
"unir_archivos_carpeta",
|
|
18
|
+
"escribir_en_excel",
|
|
19
|
+
"validar_dataframe",
|
|
20
|
+
"reporte_calidad",
|
|
21
|
+
"detectar_duplicados",
|
|
22
|
+
"buscar_v",
|
|
23
|
+
"conciliar_tablas",
|
|
24
|
+
"obtener_celda",
|
|
25
|
+
"modificar_celda",
|
|
26
|
+
"DisplayHelper",
|
|
27
|
+
]
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Módulo helper para visualización limpia en consola de tablas detectadas y DataFrames.
|
|
3
|
+
"""
|
|
4
|
+
from typing import List, Any
|
|
5
|
+
import sys
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
if sys.platform == "win32":
|
|
9
|
+
try:
|
|
10
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
11
|
+
except Exception:
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
from rich.table import Table
|
|
17
|
+
from rich.panel import Panel
|
|
18
|
+
from rich.text import Text
|
|
19
|
+
HAS_RICH = True
|
|
20
|
+
console = Console(highlight=False)
|
|
21
|
+
except ImportError:
|
|
22
|
+
HAS_RICH = False
|
|
23
|
+
console = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DisplayHelper:
|
|
27
|
+
"""Helper para presentar resúmenes visuales de tablas y datos crudos."""
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def print_header(title: str):
|
|
31
|
+
"""Imprime un título estilizado."""
|
|
32
|
+
if HAS_RICH:
|
|
33
|
+
console.print(f"\n[bold cyan]=== {title} ===[/bold cyan]")
|
|
34
|
+
else:
|
|
35
|
+
print(f"\n=== {title} ===")
|
|
36
|
+
|
|
37
|
+
@staticmethod
|
|
38
|
+
def _get_location_str(t: Any) -> str:
|
|
39
|
+
"""Obtiene la cadena descriptiva de ubicación (Página, Hoja, Celda o Tabla SQLite)."""
|
|
40
|
+
if hasattr(t, 'table_type'):
|
|
41
|
+
return f"SQLite: {t.table_name} ({t.table_type})"
|
|
42
|
+
elif hasattr(t, 'page_number'):
|
|
43
|
+
return f"PDF Pág. {t.page_number}"
|
|
44
|
+
elif hasattr(t, 'sheet_name'):
|
|
45
|
+
t_name = getattr(t, 'table_name', None)
|
|
46
|
+
range_addr = getattr(t, 'range_address', '') or getattr(t, 'start_cell', '')
|
|
47
|
+
if t_name and t_name != t.sheet_name:
|
|
48
|
+
return f"Excel Tabla '{t_name}' ({t.sheet_name} - {range_addr})"
|
|
49
|
+
elif range_addr and range_addr != "A1":
|
|
50
|
+
return f"Hoja: {t.sheet_name} ({range_addr})"
|
|
51
|
+
return f"Hoja: {t.sheet_name}"
|
|
52
|
+
return "N/A"
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
def print_tables_summary(tables_info: List[Any], file_path: str):
|
|
56
|
+
"""Muestra un resumen de todas las tablas encontradas en el archivo."""
|
|
57
|
+
if HAS_RICH:
|
|
58
|
+
table = Table(title=f"📋 Tablas detectadas en: {file_path}", show_lines=True)
|
|
59
|
+
table.add_column("ID Tabla", justify="center", style="bold green")
|
|
60
|
+
table.add_column("Ubicación / Nombre", justify="center", style="cyan")
|
|
61
|
+
table.add_column("Dimensiones (Filas x Cols)", justify="center")
|
|
62
|
+
table.add_column("Vista previa fila 1 y 2", style="dim")
|
|
63
|
+
|
|
64
|
+
for t in tables_info:
|
|
65
|
+
loc = DisplayHelper._get_location_str(t)
|
|
66
|
+
dims = f"{t.num_rows} x {t.num_cols}"
|
|
67
|
+
preview_rows = t.get_preview(2)
|
|
68
|
+
preview_str = " | ".join(str(r) for r in preview_rows)
|
|
69
|
+
if len(preview_str) > 60:
|
|
70
|
+
preview_str = preview_str[:57] + "..."
|
|
71
|
+
table.add_row(f"Tabla {t.table_id}", loc, dims, preview_str)
|
|
72
|
+
|
|
73
|
+
console.print(table)
|
|
74
|
+
else:
|
|
75
|
+
print(f"\n--- Resumen de tablas en {file_path} ---")
|
|
76
|
+
for t in tables_info:
|
|
77
|
+
loc = DisplayHelper._get_location_str(t)
|
|
78
|
+
print(f" [Tabla {t.table_id}] -> {loc} | Dimensiones: {t.num_rows} filas x {t.num_cols} columnas")
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def print_raw_preview(table_info: Any, max_rows: int = 8):
|
|
82
|
+
"""
|
|
83
|
+
Muestra una vista previa de las primeras filas con sus números de fila (1-indexed).
|
|
84
|
+
Esto permite al usuario identificar fácilmente cuál fila contiene el encabezado.
|
|
85
|
+
"""
|
|
86
|
+
raw_rows = table_info.get_preview(max_rows)
|
|
87
|
+
loc = DisplayHelper._get_location_str(table_info)
|
|
88
|
+
|
|
89
|
+
if HAS_RICH:
|
|
90
|
+
table = Table(
|
|
91
|
+
title=f"🔍 Vista Cruda: Tabla {table_info.table_id} ({loc}) - Primeras {len(raw_rows)} filas",
|
|
92
|
+
show_lines=True
|
|
93
|
+
)
|
|
94
|
+
table.add_column("Fila #", justify="center", style="bold yellow")
|
|
95
|
+
|
|
96
|
+
# Determinar máximo de columnas
|
|
97
|
+
max_cols = max(len(r) for r in raw_rows) if raw_rows else 0
|
|
98
|
+
for c in range(max_cols):
|
|
99
|
+
table.add_column(f"Col {c+1}", style="white")
|
|
100
|
+
|
|
101
|
+
for idx, row in enumerate(raw_rows, start=1):
|
|
102
|
+
cells = [str(val) if val is not None else "" for val in row]
|
|
103
|
+
# Rellenar columnas faltantes si la fila es corta
|
|
104
|
+
while len(cells) < max_cols:
|
|
105
|
+
cells.append("")
|
|
106
|
+
table.add_row(f"Fila {idx}", *cells)
|
|
107
|
+
|
|
108
|
+
console.print(table)
|
|
109
|
+
console.print("[italic dim]Consejo: Revisa los números de 'Fila #' para elegir tu 'fila_encabezado'.[/italic dim]\n")
|
|
110
|
+
else:
|
|
111
|
+
print(f"\n--- Vista Cruda: Tabla {table_info.table_id} ({loc}) ---")
|
|
112
|
+
for idx, row in enumerate(raw_rows, start=1):
|
|
113
|
+
row_str = " | ".join(str(val) if val is not None else "[vacío]" for val in row)
|
|
114
|
+
print(f" Fila {idx:2d}: {row_str}")
|
|
115
|
+
print("Consejo: Usa el número de 'Fila' para tu parámetro fila_encabezado.\n")
|
|
116
|
+
|
|
117
|
+
@staticmethod
|
|
118
|
+
def print_dataframe(df: pd.DataFrame, title: str = "DataFrame Limpio"):
|
|
119
|
+
"""Imprime un DataFrame formateado."""
|
|
120
|
+
if HAS_RICH:
|
|
121
|
+
table = Table(title=f"✨ {title} (Total: {len(df)} filas x {len(df.columns)} columnas)", show_lines=True)
|
|
122
|
+
for col in df.columns:
|
|
123
|
+
table.add_column(str(col), style="cyan", justify="left")
|
|
124
|
+
|
|
125
|
+
# Mostrar hasta 10 filas
|
|
126
|
+
for _, row in df.head(10).iterrows():
|
|
127
|
+
table.add_row(*[str(val) if pd.notna(val) else "" for val in row.values])
|
|
128
|
+
|
|
129
|
+
console.print(table)
|
|
130
|
+
if len(df) > 10:
|
|
131
|
+
console.print(f"[dim]... y {len(df) - 10} filas más.[/dim]\n")
|
|
132
|
+
else:
|
|
133
|
+
print(f"\n=== {title} ===")
|
|
134
|
+
print(df.head(10))
|
|
135
|
+
print(f"Dimensiones: {df.shape[0]} filas x {df.shape[1]} columnas\n")
|
helpers/table_manager.py
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Módulo principal de gestión de tablas para interactuar fácilmente desde main.py.
|
|
3
|
+
Permite abrir archivos PDF (pdfplumber), Excel (xlwings - abierto o cerrado) y SQLite (.db/.sqlite),
|
|
4
|
+
inspeccionar qué tablas existen y extraer DataFrames limpios en pandas indicando
|
|
5
|
+
la tabla y la fila del encabezado o ejecutando consultas.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import List, Optional, Union, Any, Dict
|
|
9
|
+
import os
|
|
10
|
+
import pandas as pd
|
|
11
|
+
|
|
12
|
+
from utils.pdf_extractor import PDFTableExtractor, RawTableInfo
|
|
13
|
+
from utils.excel_extractor import ExcelTableExtractor, RawExcelTableInfo
|
|
14
|
+
from utils.sqlite_extractor import SQLiteTableExtractor, RawSQLiteTableInfo
|
|
15
|
+
from utils.table_cleaner import TableCleaner
|
|
16
|
+
from utils.file_utils import resolve_file_path
|
|
17
|
+
from utils.exporter import TableExporter, guardar_csv, guardar_excel
|
|
18
|
+
from helpers.display_helper import DisplayHelper
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TableManager:
|
|
22
|
+
"""
|
|
23
|
+
Gestor unificado de tablas para PDFs, Excel/CSV (xlwings) y bases de datos SQLite.
|
|
24
|
+
|
|
25
|
+
Ejemplos de uso en main.py:
|
|
26
|
+
--------------------------
|
|
27
|
+
# 1. PDF
|
|
28
|
+
doc = TableManager("samples/ejemplo_facturas.pdf")
|
|
29
|
+
df = doc.get_df(tabla=3, fila_encabezado=4)
|
|
30
|
+
|
|
31
|
+
# 2. Excel (detecta si está abierto o cerrado automáticamente)
|
|
32
|
+
doc_xl = TableManager("inventario.xlsx", archivo_abierto=None)
|
|
33
|
+
df_inv = doc_xl.get_df(tabla=1, fila_encabezado=4)
|
|
34
|
+
|
|
35
|
+
# 3. SQLite
|
|
36
|
+
db = TableManager("empresa.db")
|
|
37
|
+
df_ventas = db.get_df("ventas") # o db.get_df(tabla=1)
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
file_path: str,
|
|
43
|
+
archivo_abierto: Optional[bool] = None,
|
|
44
|
+
preferir_xlwings: bool = True,
|
|
45
|
+
auto_load: bool = True
|
|
46
|
+
):
|
|
47
|
+
"""
|
|
48
|
+
Parámetros:
|
|
49
|
+
-----------
|
|
50
|
+
file_path : str
|
|
51
|
+
Ruta del archivo (relativa o absoluta).
|
|
52
|
+
archivo_abierto : bool o None (para Excel)
|
|
53
|
+
- True: se conecta al libro ya abierto en Excel.
|
|
54
|
+
- False: abre el libro cerrado en segundo plano.
|
|
55
|
+
- None: autodetecta si el archivo ya está abierto en Excel.
|
|
56
|
+
preferir_xlwings : bool (por defecto True)
|
|
57
|
+
Utiliza xlwings para Excel con fallback a openpyxl.
|
|
58
|
+
auto_load : bool (por defecto True)
|
|
59
|
+
Carga las tablas automáticamente al instanciar.
|
|
60
|
+
"""
|
|
61
|
+
self.file_path = resolve_file_path(file_path)
|
|
62
|
+
if not os.path.exists(self.file_path):
|
|
63
|
+
raise FileNotFoundError(f"No se encontró el archivo: {self.file_path}")
|
|
64
|
+
|
|
65
|
+
self.ext = os.path.splitext(self.file_path)[1].lower()
|
|
66
|
+
self.archivo_abierto = archivo_abierto
|
|
67
|
+
self.preferir_xlwings = preferir_xlwings
|
|
68
|
+
self.tables: List[Union[RawTableInfo, RawExcelTableInfo, RawSQLiteTableInfo]] = []
|
|
69
|
+
|
|
70
|
+
valid_extensions = ['.pdf', '.xlsx', '.xls', '.xlsm', '.csv', '.db', '.sqlite', '.sqlite3', '.db3']
|
|
71
|
+
if self.ext not in valid_extensions:
|
|
72
|
+
raise ValueError(f"Extensión no soportada: {self.ext}. Formatos soportados: {valid_extensions}")
|
|
73
|
+
|
|
74
|
+
if auto_load:
|
|
75
|
+
self.cargar_tablas()
|
|
76
|
+
|
|
77
|
+
def cargar_tablas(
|
|
78
|
+
self,
|
|
79
|
+
paginas: Optional[List[int]] = None,
|
|
80
|
+
hojas: Optional[List[Union[str, int]]] = None,
|
|
81
|
+
pdf_settings: Optional[Dict[str, Any]] = None,
|
|
82
|
+
dividir_bloques_excel: bool = False
|
|
83
|
+
) -> List[Any]:
|
|
84
|
+
"""
|
|
85
|
+
Carga o recarga las tablas del archivo aplicando filtros de páginas u hojas.
|
|
86
|
+
"""
|
|
87
|
+
if self.ext == '.pdf':
|
|
88
|
+
extractor = PDFTableExtractor(self.file_path)
|
|
89
|
+
self.tables = extractor.extract_all_tables(pages=paginas, table_settings=pdf_settings)
|
|
90
|
+
elif self.ext in ['.db', '.sqlite', '.sqlite3', '.db3']:
|
|
91
|
+
extractor = SQLiteTableExtractor(self.file_path)
|
|
92
|
+
self.tables = extractor.extract_all_tables()
|
|
93
|
+
else:
|
|
94
|
+
extractor = ExcelTableExtractor(
|
|
95
|
+
self.file_path,
|
|
96
|
+
archivo_abierto=self.archivo_abierto,
|
|
97
|
+
preferir_xlwings=self.preferir_xlwings
|
|
98
|
+
)
|
|
99
|
+
self.tables = extractor.extract_all_tables(sheets=hojas, split_blank_blocks=dividir_bloques_excel)
|
|
100
|
+
|
|
101
|
+
return self.tables
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def total_tablas(self) -> int:
|
|
105
|
+
"""Cantidad total de tablas detectadas en el documento o base de datos."""
|
|
106
|
+
return len(self.tables)
|
|
107
|
+
|
|
108
|
+
def resumen(self):
|
|
109
|
+
"""Muestra en consola un resumen claro de todas las tablas encontradas."""
|
|
110
|
+
if not self.tables:
|
|
111
|
+
print(f"⚠️ No se detectaron tablas en {self.file_path}.")
|
|
112
|
+
return
|
|
113
|
+
DisplayHelper.print_tables_summary(self.tables, self.file_path)
|
|
114
|
+
|
|
115
|
+
def ver_crudo(self, tabla: Union[int, str] = 1, max_filas: int = 8):
|
|
116
|
+
"""
|
|
117
|
+
Muestra la vista previa cruda de una tabla con sus números de fila (1-indexed).
|
|
118
|
+
Muy útil para identificar exactamente en qué fila están los encabezados reales.
|
|
119
|
+
"""
|
|
120
|
+
table_info = self._get_raw_table_info(tabla)
|
|
121
|
+
DisplayHelper.print_raw_preview(table_info, max_rows=max_filas)
|
|
122
|
+
|
|
123
|
+
def _get_raw_table_info(self, tabla: Union[int, str]) -> Any:
|
|
124
|
+
"""Obtiene el objeto de información cruda de una tabla por su ID (1-indexed) o nombre."""
|
|
125
|
+
if isinstance(tabla, int):
|
|
126
|
+
if tabla < 1 or tabla > len(self.tables):
|
|
127
|
+
raise IndexError(
|
|
128
|
+
f"Tabla {tabla} no válida. El archivo tiene {len(self.tables)} tabla(s) detectada(s)."
|
|
129
|
+
)
|
|
130
|
+
return self.tables[tabla - 1]
|
|
131
|
+
|
|
132
|
+
# Buscar por nombre (para SQLite o Excel)
|
|
133
|
+
target_name = str(tabla).strip().lower()
|
|
134
|
+
for t in self.tables:
|
|
135
|
+
if hasattr(t, 'table_name') and t.table_name.lower() == target_name:
|
|
136
|
+
return t
|
|
137
|
+
if hasattr(t, 'sheet_name') and t.sheet_name.lower() == target_name:
|
|
138
|
+
return t
|
|
139
|
+
|
|
140
|
+
raise ValueError(f"No se encontró la tabla o pestaña '{tabla}' en {self.file_path}")
|
|
141
|
+
|
|
142
|
+
def get_df(
|
|
143
|
+
self,
|
|
144
|
+
tabla: Union[int, str] = 1,
|
|
145
|
+
fila_encabezado: Optional[Union[int, List[int], str]] = 1,
|
|
146
|
+
celda_inicio: Optional[str] = None,
|
|
147
|
+
celda: Optional[str] = None,
|
|
148
|
+
rango: Optional[str] = None,
|
|
149
|
+
hoja: Optional[Union[str, int]] = None,
|
|
150
|
+
skip_footer: int = 0,
|
|
151
|
+
eliminar_filas_vacias: bool = True,
|
|
152
|
+
eliminar_columnas_vacias: bool = True,
|
|
153
|
+
auto_inferir_tipos: bool = True,
|
|
154
|
+
) -> pd.DataFrame:
|
|
155
|
+
"""
|
|
156
|
+
Extrae y limpia la tabla deseada devolviendo un pandas DataFrame limpio.
|
|
157
|
+
|
|
158
|
+
Parámetros:
|
|
159
|
+
-----------
|
|
160
|
+
tabla : int o str (por defecto 1)
|
|
161
|
+
- int: Número de tabla (1-indexed). Ej: tabla=3.
|
|
162
|
+
- str: Nombre oficial de tabla en Excel (ej: 'TablaProductos'),
|
|
163
|
+
nombre de tabla en SQLite (ej: 'ventas'), o nombre de hoja.
|
|
164
|
+
fila_encabezado : int, list[int], 'auto' o None (por defecto 1)
|
|
165
|
+
Número de fila que contiene los nombres de columnas (1-indexed).
|
|
166
|
+
Ejemplo: fila_encabezado=4 descarta filas 1, 2, 3 como basura.
|
|
167
|
+
(En SQLite o cuando se usa celda_inicio/rango exacto, se suele usar 1).
|
|
168
|
+
celda_inicio / celda : str, opcional (ej: 'C4', 'B3')
|
|
169
|
+
En Excel, coordenada de la celda donde inicia la tabla.
|
|
170
|
+
rango : str, opcional (ej: 'C4:F20')
|
|
171
|
+
En Excel, rango exacto a extraer.
|
|
172
|
+
hoja : str o int, opcional
|
|
173
|
+
Nombre o índice de la hoja en Excel donde se ubica la celda o rango.
|
|
174
|
+
skip_footer : int (por defecto 0)
|
|
175
|
+
Cantidad de filas finales a descartar (por ejemplo notas al pie o totales).
|
|
176
|
+
eliminar_filas_vacias : bool (por defecto True)
|
|
177
|
+
Descarta filas completamente vacías.
|
|
178
|
+
eliminar_columnas_vacias : bool (por defecto True)
|
|
179
|
+
Descarta columnas completamente vacías.
|
|
180
|
+
auto_inferir_tipos : bool (por defecto True)
|
|
181
|
+
Limpia signos monetarios y convierte valores numéricos cuando sea posible.
|
|
182
|
+
|
|
183
|
+
Retorna:
|
|
184
|
+
--------
|
|
185
|
+
pd.DataFrame
|
|
186
|
+
DataFrame de pandas listo para ser usado.
|
|
187
|
+
"""
|
|
188
|
+
# Caso especial para SQLite
|
|
189
|
+
if self.ext in ['.db', '.sqlite', '.sqlite3', '.db3']:
|
|
190
|
+
extractor = SQLiteTableExtractor(self.file_path)
|
|
191
|
+
return extractor.get_table_df(tabla)
|
|
192
|
+
|
|
193
|
+
# Caso extracción directa por celda_inicio o rango en Excel
|
|
194
|
+
target_cell = celda_inicio or celda
|
|
195
|
+
if target_cell or rango:
|
|
196
|
+
extractor = ExcelTableExtractor(
|
|
197
|
+
self.file_path,
|
|
198
|
+
archivo_abierto=self.archivo_abierto,
|
|
199
|
+
preferir_xlwings=self.preferir_xlwings
|
|
200
|
+
)
|
|
201
|
+
raw_matrix = extractor.extract_by_cell_or_range(
|
|
202
|
+
celda_inicio=target_cell,
|
|
203
|
+
rango=rango,
|
|
204
|
+
hoja=hoja
|
|
205
|
+
)
|
|
206
|
+
return TableCleaner.clean(
|
|
207
|
+
raw_data=raw_matrix,
|
|
208
|
+
header_row=fila_encabezado,
|
|
209
|
+
skip_footer=skip_footer,
|
|
210
|
+
drop_empty_rows=eliminar_filas_vacias,
|
|
211
|
+
drop_empty_cols=eliminar_columnas_vacias,
|
|
212
|
+
auto_clean_types=auto_inferir_tipos,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# Caso estándar por ID o por nombre de tabla/hoja
|
|
216
|
+
table_info = self._get_raw_table_info(tabla)
|
|
217
|
+
|
|
218
|
+
# Si la tabla es una Named Table de Excel que ya parte exactamente en su rango,
|
|
219
|
+
# los encabezados están en la fila 1 de dicha tabla
|
|
220
|
+
header_to_use = fila_encabezado
|
|
221
|
+
if hasattr(table_info, 'start_cell') and table_info.start_cell and getattr(table_info, 'table_name', None) != table_info.sheet_name:
|
|
222
|
+
# Es una tabla con nombre oficial que inicia en start_cell
|
|
223
|
+
if fila_encabezado == 1 or fila_encabezado == 4: # si el usuario no especificó otra cosa
|
|
224
|
+
header_to_use = 1
|
|
225
|
+
|
|
226
|
+
df = TableCleaner.clean(
|
|
227
|
+
raw_data=table_info.raw_data,
|
|
228
|
+
header_row=header_to_use,
|
|
229
|
+
skip_footer=skip_footer,
|
|
230
|
+
drop_empty_rows=eliminar_filas_vacias,
|
|
231
|
+
drop_empty_cols=eliminar_columnas_vacias,
|
|
232
|
+
auto_clean_types=auto_inferir_tipos,
|
|
233
|
+
)
|
|
234
|
+
return df
|
|
235
|
+
|
|
236
|
+
# Alias para máxima flexibilidad
|
|
237
|
+
get_tabla = get_df
|
|
238
|
+
get_table = get_df
|
|
239
|
+
|
|
240
|
+
def query(self, sql_query: str, params: Optional[Union[tuple, dict]] = None) -> pd.DataFrame:
|
|
241
|
+
"""
|
|
242
|
+
Ejecuta una consulta SQL si el archivo cargado es una base de datos SQLite.
|
|
243
|
+
"""
|
|
244
|
+
if self.ext not in ['.db', '.sqlite', '.sqlite3', '.db3']:
|
|
245
|
+
raise ValueError("El método query() solo está disponible para bases de datos SQLite.")
|
|
246
|
+
extractor = SQLiteTableExtractor(self.file_path)
|
|
247
|
+
return extractor.query_df(sql_query, params=params)
|
|
248
|
+
|
|
249
|
+
def exportar(
|
|
250
|
+
self,
|
|
251
|
+
df: pd.DataFrame,
|
|
252
|
+
ruta_salida: str,
|
|
253
|
+
formato: Optional[str] = None,
|
|
254
|
+
sep: str = ";",
|
|
255
|
+
encoding: str = "utf-8-sig",
|
|
256
|
+
index: bool = False
|
|
257
|
+
) -> str:
|
|
258
|
+
"""
|
|
259
|
+
Exporta un DataFrame a Excel o CSV.
|
|
260
|
+
"""
|
|
261
|
+
if formato is None:
|
|
262
|
+
_, ext = os.path.splitext(ruta_salida)
|
|
263
|
+
formato = ext.replace(".", "").lower()
|
|
264
|
+
if not formato:
|
|
265
|
+
formato = "csv" if ruta_salida.lower().endswith(".csv") else "xlsx"
|
|
266
|
+
|
|
267
|
+
if formato in ['csv']:
|
|
268
|
+
return TableExporter.to_csv(df, output_path=ruta_salida, sep=sep, encoding=encoding, index=index)
|
|
269
|
+
elif formato in ['xlsx', 'excel']:
|
|
270
|
+
return TableExporter.to_excel(df, output_path=ruta_salida, index=index)
|
|
271
|
+
else:
|
|
272
|
+
raise ValueError(f"Formato no soportado: {formato}. Use 'csv' o 'xlsx'.")
|
|
273
|
+
|
|
274
|
+
def exportar_csv(
|
|
275
|
+
self,
|
|
276
|
+
df: pd.DataFrame,
|
|
277
|
+
ruta_salida: str,
|
|
278
|
+
sep: str = ";",
|
|
279
|
+
encoding: str = "utf-8-sig",
|
|
280
|
+
index: bool = False
|
|
281
|
+
) -> str:
|
|
282
|
+
"""Exporta un DataFrame a CSV con separador y codificación optimizados para Excel."""
|
|
283
|
+
return TableExporter.to_csv(df, output_path=ruta_salida, sep=sep, encoding=encoding, index=index)
|
|
284
|
+
|
|
285
|
+
def exportar_todas_a_csv(
|
|
286
|
+
self,
|
|
287
|
+
directorio_salida: str,
|
|
288
|
+
fila_encabezado: Optional[Union[int, List[int], str]] = 1,
|
|
289
|
+
sep: str = ";",
|
|
290
|
+
encoding: str = "utf-8-sig",
|
|
291
|
+
index: bool = False,
|
|
292
|
+
**clean_kwargs
|
|
293
|
+
) -> List[str]:
|
|
294
|
+
"""
|
|
295
|
+
Extrae y exporta TODAS las tablas encontradas en el archivo a archivos CSV individuales.
|
|
296
|
+
|
|
297
|
+
Parámetros:
|
|
298
|
+
-----------
|
|
299
|
+
directorio_salida : str
|
|
300
|
+
Directorio donde se guardarán los archivos CSV.
|
|
301
|
+
fila_encabezado : int, 'auto', etc.
|
|
302
|
+
Fila de encabezado a aplicar para tablas de PDF/Excel.
|
|
303
|
+
|
|
304
|
+
Retorna:
|
|
305
|
+
--------
|
|
306
|
+
List[str]
|
|
307
|
+
Lista con las rutas de todos los archivos CSV generados.
|
|
308
|
+
"""
|
|
309
|
+
tables_dict: Dict[str, pd.DataFrame] = {}
|
|
310
|
+
base_name = os.path.splitext(os.path.basename(self.file_path))[0]
|
|
311
|
+
|
|
312
|
+
for idx, t_info in enumerate(self.tables, start=1):
|
|
313
|
+
if self.ext in ['.db', '.sqlite', '.sqlite3', '.db3']:
|
|
314
|
+
t_name = getattr(t_info, 'table_name', f"tabla_{idx}")
|
|
315
|
+
df = self.get_df(tabla=t_name)
|
|
316
|
+
key = f"{base_name}_{t_name}"
|
|
317
|
+
else:
|
|
318
|
+
t_name = getattr(t_info, 'table_name', None) or getattr(t_info, 'sheet_name', None)
|
|
319
|
+
if t_name and t_name != "CSV":
|
|
320
|
+
key = f"{base_name}_{t_name}"
|
|
321
|
+
elif hasattr(t_info, 'page_number'):
|
|
322
|
+
key = f"{base_name}_pag_{t_info.page_number}_tab_{t_info.table_in_page}"
|
|
323
|
+
else:
|
|
324
|
+
key = f"{base_name}_tabla_{idx}"
|
|
325
|
+
|
|
326
|
+
df = self.get_df(tabla=idx, fila_encabezado=fila_encabezado, **clean_kwargs)
|
|
327
|
+
|
|
328
|
+
tables_dict[key] = df
|
|
329
|
+
|
|
330
|
+
saved_files = TableExporter.export_batch_to_csv(
|
|
331
|
+
tables_dict=tables_dict,
|
|
332
|
+
output_dir=directorio_salida,
|
|
333
|
+
sep=sep,
|
|
334
|
+
encoding=encoding,
|
|
335
|
+
index=index
|
|
336
|
+
)
|
|
337
|
+
return saved_files
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def obtener_tabla(
|
|
341
|
+
archivo: str,
|
|
342
|
+
tabla: Union[int, str] = 1,
|
|
343
|
+
fila_encabezado: Optional[Union[int, List[int], str]] = 1,
|
|
344
|
+
celda_inicio: Optional[str] = None,
|
|
345
|
+
celda: Optional[str] = None,
|
|
346
|
+
rango: Optional[str] = None,
|
|
347
|
+
hoja: Optional[Union[str, int]] = None,
|
|
348
|
+
skip_footer: int = 0,
|
|
349
|
+
archivo_abierto: Optional[bool] = None,
|
|
350
|
+
**kwargs
|
|
351
|
+
) -> pd.DataFrame:
|
|
352
|
+
"""
|
|
353
|
+
Función de una sola línea para extraer directamente un DataFrame desde cualquier PDF, Excel o SQLite.
|
|
354
|
+
|
|
355
|
+
Ejemplos:
|
|
356
|
+
---------
|
|
357
|
+
# PDF (Tabla 3, encabezado en fila 4):
|
|
358
|
+
df = obtener_tabla("facturas.pdf", tabla=3, fila_encabezado=4)
|
|
359
|
+
|
|
360
|
+
# Excel por Celda de Inicio (ej: tabla parte en la celda C4):
|
|
361
|
+
df = obtener_tabla("inventario.xlsx", celda_inicio="C4", hoja="Inventario")
|
|
362
|
+
|
|
363
|
+
# Excel por Nombre Oficial de Tabla:
|
|
364
|
+
df = obtener_tabla("inventario.xlsx", tabla="TablaProductos")
|
|
365
|
+
|
|
366
|
+
# Excel por Rango exacto:
|
|
367
|
+
df = obtener_tabla("inventario.xlsx", rango="B3:F15")
|
|
368
|
+
|
|
369
|
+
# SQLite por Nombre de Tabla:
|
|
370
|
+
df = obtener_tabla("empresa.db", tabla="ventas")
|
|
371
|
+
"""
|
|
372
|
+
manager = TableManager(archivo, archivo_abierto=archivo_abierto)
|
|
373
|
+
return manager.get_df(
|
|
374
|
+
tabla=tabla,
|
|
375
|
+
fila_encabezado=fila_encabezado,
|
|
376
|
+
celda_inicio=celda_inicio,
|
|
377
|
+
celda=celda,
|
|
378
|
+
rango=rango,
|
|
379
|
+
hoja=hoja,
|
|
380
|
+
skip_footer=skip_footer,
|
|
381
|
+
**kwargs
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def inspeccionar_archivo(archivo: str, max_filas_preview: int = 8, archivo_abierto: Optional[bool] = None):
|
|
386
|
+
"""
|
|
387
|
+
Función de ayuda rápida para ver todas las tablas y sus primeras filas con números de fila.
|
|
388
|
+
"""
|
|
389
|
+
manager = TableManager(archivo, archivo_abierto=archivo_abierto)
|
|
390
|
+
manager.resumen()
|
|
391
|
+
for i in range(1, manager.total_tablas + 1):
|
|
392
|
+
manager.ver_crudo(tabla=i, max_filas=max_filas_preview)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def exportar_archivo_a_csv(
|
|
396
|
+
archivo: str,
|
|
397
|
+
carpeta_salida: str,
|
|
398
|
+
fila_encabezado: Optional[Union[int, List[int], str]] = 1,
|
|
399
|
+
sep: str = ";",
|
|
400
|
+
encoding: str = "utf-8-sig",
|
|
401
|
+
archivo_abierto: Optional[bool] = None,
|
|
402
|
+
**kwargs
|
|
403
|
+
) -> List[str]:
|
|
404
|
+
"""
|
|
405
|
+
Función de una sola línea para extraer y guardar TODAS las tablas de un archivo en archivos CSV individuales.
|
|
406
|
+
|
|
407
|
+
Ejemplo:
|
|
408
|
+
--------
|
|
409
|
+
archivos_guardados = exportar_archivo_a_csv("facturas.pdf", carpeta_salida="exports/facturas", fila_encabezado=4)
|
|
410
|
+
"""
|
|
411
|
+
manager = TableManager(archivo, archivo_abierto=archivo_abierto)
|
|
412
|
+
return manager.exportar_todas_a_csv(
|
|
413
|
+
directorio_salida=carpeta_salida,
|
|
414
|
+
fila_encabezado=fila_encabezado,
|
|
415
|
+
sep=sep,
|
|
416
|
+
encoding=encoding,
|
|
417
|
+
**kwargs
|
|
418
|
+
)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tablas-python: Suite integral para extracción, transformación, conciliación y exportación de tablas en pandas.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from helpers.table_manager import (
|
|
6
|
+
TableManager,
|
|
7
|
+
obtener_tabla,
|
|
8
|
+
inspeccionar_archivo,
|
|
9
|
+
exportar_archivo_a_csv,
|
|
10
|
+
)
|
|
11
|
+
from helpers.display_helper import DisplayHelper
|
|
12
|
+
from utils.exporter import TableExporter, guardar_csv, guardar_excel
|
|
13
|
+
from utils.excel_writer import escribir_en_excel
|
|
14
|
+
from utils.batch_processor import unir_archivos_carpeta
|
|
15
|
+
from utils.validator import DataValidator, validar_dataframe, reporte_calidad, detectar_duplicados
|
|
16
|
+
from utils.data_helpers import (
|
|
17
|
+
limpiar_numero,
|
|
18
|
+
limpiar_columnas_numericas,
|
|
19
|
+
normalizar_fechas,
|
|
20
|
+
formato_moneda,
|
|
21
|
+
formato_porcentaje,
|
|
22
|
+
formato_miles,
|
|
23
|
+
formatear_dataframe,
|
|
24
|
+
agregar_fila_totales,
|
|
25
|
+
calcular_participacion,
|
|
26
|
+
calcular_variacion,
|
|
27
|
+
aplicar_impuesto,
|
|
28
|
+
agrupar_y_resumir,
|
|
29
|
+
obtener_celda,
|
|
30
|
+
modificar_celda,
|
|
31
|
+
buscar_v,
|
|
32
|
+
conciliar_tablas,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
__version__ = "0.1.0"
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"TableManager",
|
|
39
|
+
"obtener_tabla",
|
|
40
|
+
"inspeccionar_archivo",
|
|
41
|
+
"exportar_archivo_a_csv",
|
|
42
|
+
"DisplayHelper",
|
|
43
|
+
"TableExporter",
|
|
44
|
+
"guardar_csv",
|
|
45
|
+
"guardar_excel",
|
|
46
|
+
"escribir_en_excel",
|
|
47
|
+
"unir_archivos_carpeta",
|
|
48
|
+
"DataValidator",
|
|
49
|
+
"validar_dataframe",
|
|
50
|
+
"reporte_calidad",
|
|
51
|
+
"detectar_duplicados",
|
|
52
|
+
"limpiar_numero",
|
|
53
|
+
"limpiar_columnas_numericas",
|
|
54
|
+
"normalizar_fechas",
|
|
55
|
+
"formato_moneda",
|
|
56
|
+
"formato_porcentaje",
|
|
57
|
+
"formato_miles",
|
|
58
|
+
"formatear_dataframe",
|
|
59
|
+
"agregar_fila_totales",
|
|
60
|
+
"calcular_participacion",
|
|
61
|
+
"calcular_variacion",
|
|
62
|
+
"aplicar_impuesto",
|
|
63
|
+
"agrupar_y_resumir",
|
|
64
|
+
"obtener_celda",
|
|
65
|
+
"modificar_celda",
|
|
66
|
+
"buscar_v",
|
|
67
|
+
"conciliar_tablas",
|
|
68
|
+
]
|