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
utils/excel_extractor.py
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Módulo para extracción de tablas desde archivos Excel (.xlsx, .xls, .xlsm) y CSV usando xlwings.
|
|
3
|
+
Soporta:
|
|
4
|
+
1. Reconocimiento por nombres de tablas oficiales de Excel (ListObjects / Named Tables).
|
|
5
|
+
2. Extracción por celda de inicio donde parte la tabla (ej: celda_inicio="C4" o "B3").
|
|
6
|
+
3. Extracción por rango exacto (ej: rango="C4:F20").
|
|
7
|
+
4. Archivos ABIERTOS o CERRADOS en Microsoft Excel.
|
|
8
|
+
5. Rutas relativas o absolutas.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import List, Any, Optional, Union, Dict, Tuple
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
import pandas as pd
|
|
16
|
+
|
|
17
|
+
# Intentar importar xlwings
|
|
18
|
+
try:
|
|
19
|
+
import xlwings as xw
|
|
20
|
+
HAS_XLWINGS = True
|
|
21
|
+
except ImportError:
|
|
22
|
+
HAS_XLWINGS = False
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
import openpyxl
|
|
26
|
+
from openpyxl.utils.cell import coordinate_to_tuple, get_column_letter
|
|
27
|
+
HAS_OPENPYXL = True
|
|
28
|
+
except ImportError:
|
|
29
|
+
HAS_OPENPYXL = False
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class RawExcelTableInfo:
|
|
34
|
+
"""Información y contenido crudo de una tabla u hoja detectada en Excel/CSV."""
|
|
35
|
+
table_id: int
|
|
36
|
+
source_file: str
|
|
37
|
+
sheet_name: str
|
|
38
|
+
raw_data: List[List[Any]]
|
|
39
|
+
num_rows: int
|
|
40
|
+
num_cols: int
|
|
41
|
+
table_name: Optional[str] = None
|
|
42
|
+
start_cell: Optional[str] = None
|
|
43
|
+
range_address: Optional[str] = None
|
|
44
|
+
|
|
45
|
+
def get_preview(self, max_rows: int = 5) -> List[List[Any]]:
|
|
46
|
+
"""Retorna las primeras filas crudas para inspección visual."""
|
|
47
|
+
return self.raw_data[:max_rows]
|
|
48
|
+
|
|
49
|
+
from .file_utils import resolve_file_path
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ExcelTableExtractor:
|
|
53
|
+
"""
|
|
54
|
+
Extractor de tablas para Excel basado en xlwings con soporte para:
|
|
55
|
+
- Tablas con nombre oficial de Excel (ListObjects).
|
|
56
|
+
- Celdas de inicio (ej: C4) y rangos (ej: B3:F15).
|
|
57
|
+
- Archivos abiertos o cerrados.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
file_path: str,
|
|
63
|
+
archivo_abierto: Optional[bool] = None,
|
|
64
|
+
preferir_xlwings: bool = True
|
|
65
|
+
):
|
|
66
|
+
self.file_path = resolve_file_path(file_path)
|
|
67
|
+
if not os.path.exists(self.file_path):
|
|
68
|
+
raise FileNotFoundError(f"No se encontró el archivo Excel en: {self.file_path}")
|
|
69
|
+
|
|
70
|
+
self.ext = os.path.splitext(self.file_path)[1].lower()
|
|
71
|
+
self.archivo_abierto = archivo_abierto
|
|
72
|
+
self.preferir_xlwings = preferir_xlwings and HAS_XLWINGS
|
|
73
|
+
|
|
74
|
+
def _is_workbook_open_in_excel(self, file_name: str) -> bool:
|
|
75
|
+
"""Comprueba si el libro ya está abierto en alguna sesión activa de Excel."""
|
|
76
|
+
if not HAS_XLWINGS:
|
|
77
|
+
return False
|
|
78
|
+
try:
|
|
79
|
+
for book in xw.books:
|
|
80
|
+
if book.name.lower() == file_name.lower() or book.fullname.lower() == self.file_path.lower():
|
|
81
|
+
return True
|
|
82
|
+
except Exception:
|
|
83
|
+
return False
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
def extract_by_cell_or_range(
|
|
87
|
+
self,
|
|
88
|
+
celda_inicio: Optional[str] = None,
|
|
89
|
+
rango: Optional[str] = None,
|
|
90
|
+
hoja: Optional[Union[str, int]] = None
|
|
91
|
+
) -> List[List[Any]]:
|
|
92
|
+
"""
|
|
93
|
+
Extrae datos a partir de una celda de inicio (ej: 'C4') o un rango específico (ej: 'C4:G20').
|
|
94
|
+
"""
|
|
95
|
+
if self.preferir_xlwings:
|
|
96
|
+
try:
|
|
97
|
+
return self._extract_range_xlwings(celda_inicio=celda_inicio, rango=rango, hoja=hoja)
|
|
98
|
+
except Exception:
|
|
99
|
+
return self._extract_range_openpyxl(celda_inicio=celda_inicio, rango=rango, hoja=hoja)
|
|
100
|
+
else:
|
|
101
|
+
return self._extract_range_openpyxl(celda_inicio=celda_inicio, rango=rango, hoja=hoja)
|
|
102
|
+
|
|
103
|
+
def _extract_range_xlwings(
|
|
104
|
+
self,
|
|
105
|
+
celda_inicio: Optional[str] = None,
|
|
106
|
+
rango: Optional[str] = None,
|
|
107
|
+
hoja: Optional[Union[str, int]] = None
|
|
108
|
+
) -> List[List[Any]]:
|
|
109
|
+
file_name = os.path.basename(self.file_path)
|
|
110
|
+
is_already_open = self._is_workbook_open_in_excel(file_name)
|
|
111
|
+
should_connect = self.archivo_abierto is True or (self.archivo_abierto is None and is_already_open)
|
|
112
|
+
|
|
113
|
+
app = None
|
|
114
|
+
book = None
|
|
115
|
+
needs_close = False
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
if should_connect:
|
|
119
|
+
try:
|
|
120
|
+
book = xw.books[file_name]
|
|
121
|
+
except Exception:
|
|
122
|
+
book = xw.Book(self.file_path)
|
|
123
|
+
else:
|
|
124
|
+
app = xw.App(visible=False, add_book=False)
|
|
125
|
+
app.display_alerts = False
|
|
126
|
+
app.screen_updating = False
|
|
127
|
+
book = app.books.open(self.file_path, read_only=True)
|
|
128
|
+
needs_close = True
|
|
129
|
+
|
|
130
|
+
# Seleccionar hoja
|
|
131
|
+
if hoja is None:
|
|
132
|
+
sht = book.sheets[0]
|
|
133
|
+
elif isinstance(hoja, int):
|
|
134
|
+
sht = book.sheets[hoja - 1]
|
|
135
|
+
else:
|
|
136
|
+
sht = book.sheets[hoja]
|
|
137
|
+
|
|
138
|
+
# Leer por rango o por celda_inicio
|
|
139
|
+
if rango:
|
|
140
|
+
val = sht.range(rango).value
|
|
141
|
+
elif celda_inicio:
|
|
142
|
+
# Expandir tabla a partir de la celda de inicio
|
|
143
|
+
clean_cell = celda_inicio.replace("$", "").upper()
|
|
144
|
+
val = sht.range(clean_cell).expand('table').value
|
|
145
|
+
else:
|
|
146
|
+
val = sht.used_range.value
|
|
147
|
+
|
|
148
|
+
if val is None:
|
|
149
|
+
return []
|
|
150
|
+
if not isinstance(val, list):
|
|
151
|
+
return [[val]]
|
|
152
|
+
if val and not isinstance(val[0], list):
|
|
153
|
+
return [val]
|
|
154
|
+
return val
|
|
155
|
+
|
|
156
|
+
finally:
|
|
157
|
+
if needs_close:
|
|
158
|
+
if book:
|
|
159
|
+
try: book.close()
|
|
160
|
+
except Exception: pass
|
|
161
|
+
if app:
|
|
162
|
+
try: app.quit()
|
|
163
|
+
except Exception: pass
|
|
164
|
+
|
|
165
|
+
def _extract_range_openpyxl(
|
|
166
|
+
self,
|
|
167
|
+
celda_inicio: Optional[str] = None,
|
|
168
|
+
rango: Optional[str] = None,
|
|
169
|
+
hoja: Optional[Union[str, int]] = None
|
|
170
|
+
) -> List[List[Any]]:
|
|
171
|
+
"""Fallback con openpyxl para extraer por celda de inicio o rango."""
|
|
172
|
+
wb = openpyxl.load_workbook(self.file_path, data_only=True)
|
|
173
|
+
if hoja is None:
|
|
174
|
+
ws = wb.active
|
|
175
|
+
elif isinstance(hoja, int):
|
|
176
|
+
ws = wb.worksheets[hoja - 1]
|
|
177
|
+
else:
|
|
178
|
+
ws = wb[hoja]
|
|
179
|
+
|
|
180
|
+
if rango:
|
|
181
|
+
cells = ws[rango]
|
|
182
|
+
if isinstance(cells, tuple):
|
|
183
|
+
if isinstance(cells[0], tuple):
|
|
184
|
+
return [[c.value for c in row] for row in cells]
|
|
185
|
+
return [[c.value for c in cells]]
|
|
186
|
+
return [[cells.value]]
|
|
187
|
+
|
|
188
|
+
if celda_inicio:
|
|
189
|
+
row_idx, col_idx = coordinate_to_tuple(celda_inicio.replace("$", "").upper())
|
|
190
|
+
# Leer desde row_idx, col_idx hasta el final de la región de datos
|
|
191
|
+
rows = []
|
|
192
|
+
for r in range(row_idx, ws.max_row + 1):
|
|
193
|
+
row_vals = [ws.cell(row=r, column=c).value for c in range(col_idx, ws.max_column + 1)]
|
|
194
|
+
# Detener si la fila está completamente vacía
|
|
195
|
+
if all(v is None for v in row_vals):
|
|
196
|
+
break
|
|
197
|
+
rows.append(row_vals)
|
|
198
|
+
|
|
199
|
+
# Recortar columnas vacías al final
|
|
200
|
+
if rows:
|
|
201
|
+
max_valid_col = 0
|
|
202
|
+
for r in rows:
|
|
203
|
+
for i in reversed(range(len(r))):
|
|
204
|
+
if r[i] is not None:
|
|
205
|
+
max_valid_col = max(max_valid_col, i + 1)
|
|
206
|
+
break
|
|
207
|
+
if max_valid_col > 0:
|
|
208
|
+
rows = [r[:max_valid_col] for r in rows]
|
|
209
|
+
return rows
|
|
210
|
+
|
|
211
|
+
# Toda la hoja
|
|
212
|
+
return [[c.value for c in row] for row in ws.iter_rows()]
|
|
213
|
+
|
|
214
|
+
def extract_all_tables(
|
|
215
|
+
self,
|
|
216
|
+
sheets: Optional[List[Union[str, int]]] = None,
|
|
217
|
+
split_blank_blocks: bool = False
|
|
218
|
+
) -> List[RawExcelTableInfo]:
|
|
219
|
+
"""
|
|
220
|
+
Extrae todas las tablas u hojas del archivo Excel.
|
|
221
|
+
Detecta automáticamente si existen tablas con nombre oficial (ListObjects) en las hojas.
|
|
222
|
+
"""
|
|
223
|
+
if self.ext == '.csv':
|
|
224
|
+
df_raw = pd.read_csv(self.file_path, header=None, dtype=object)
|
|
225
|
+
raw_list = df_raw.values.tolist()
|
|
226
|
+
info = RawExcelTableInfo(
|
|
227
|
+
table_id=1,
|
|
228
|
+
source_file=self.file_path,
|
|
229
|
+
sheet_name="CSV",
|
|
230
|
+
table_name="CSV",
|
|
231
|
+
raw_data=raw_list,
|
|
232
|
+
num_rows=len(raw_list),
|
|
233
|
+
num_cols=len(raw_list[0]) if raw_list else 0
|
|
234
|
+
)
|
|
235
|
+
return [info]
|
|
236
|
+
|
|
237
|
+
if self.preferir_xlwings:
|
|
238
|
+
try:
|
|
239
|
+
return self._extract_all_xlwings(sheets=sheets)
|
|
240
|
+
except Exception:
|
|
241
|
+
return self._extract_all_openpyxl(sheets=sheets, split_blank_blocks=split_blank_blocks)
|
|
242
|
+
else:
|
|
243
|
+
return self._extract_all_openpyxl(sheets=sheets, split_blank_blocks=split_blank_blocks)
|
|
244
|
+
|
|
245
|
+
def _extract_all_xlwings(
|
|
246
|
+
self,
|
|
247
|
+
sheets: Optional[List[Union[str, int]]] = None
|
|
248
|
+
) -> List[RawExcelTableInfo]:
|
|
249
|
+
file_name = os.path.basename(self.file_path)
|
|
250
|
+
is_already_open = self._is_workbook_open_in_excel(file_name)
|
|
251
|
+
should_connect = self.archivo_abierto is True or (self.archivo_abierto is None and is_already_open)
|
|
252
|
+
|
|
253
|
+
tables_found: List[RawExcelTableInfo] = []
|
|
254
|
+
app = None
|
|
255
|
+
book = None
|
|
256
|
+
needs_close = False
|
|
257
|
+
|
|
258
|
+
try:
|
|
259
|
+
if should_connect:
|
|
260
|
+
try:
|
|
261
|
+
book = xw.books[file_name]
|
|
262
|
+
except Exception:
|
|
263
|
+
book = xw.Book(self.file_path)
|
|
264
|
+
else:
|
|
265
|
+
app = xw.App(visible=False, add_book=False)
|
|
266
|
+
app.display_alerts = False
|
|
267
|
+
app.screen_updating = False
|
|
268
|
+
book = app.books.open(self.file_path, read_only=True)
|
|
269
|
+
needs_close = True
|
|
270
|
+
|
|
271
|
+
all_sheet_names = [s.name for s in book.sheets]
|
|
272
|
+
target_sheets = []
|
|
273
|
+
if sheets is None:
|
|
274
|
+
target_sheets = all_sheet_names
|
|
275
|
+
else:
|
|
276
|
+
for s in sheets:
|
|
277
|
+
if isinstance(s, int) and 1 <= s <= len(all_sheet_names):
|
|
278
|
+
target_sheets.append(all_sheet_names[s - 1])
|
|
279
|
+
elif str(s) in all_sheet_names:
|
|
280
|
+
target_sheets.append(str(s))
|
|
281
|
+
|
|
282
|
+
global_id = 1
|
|
283
|
+
for sheet_name in target_sheets:
|
|
284
|
+
sht = book.sheets[sheet_name]
|
|
285
|
+
|
|
286
|
+
# 1. Comprobar si la hoja tiene tablas oficiales de Excel (ListObjects)
|
|
287
|
+
named_tables = list(sht.tables)
|
|
288
|
+
if named_tables:
|
|
289
|
+
for nt in named_tables:
|
|
290
|
+
t_vals = nt.range.value
|
|
291
|
+
if not isinstance(t_vals, list):
|
|
292
|
+
t_vals = [[t_vals]]
|
|
293
|
+
elif t_vals and not isinstance(t_vals[0], list):
|
|
294
|
+
t_vals = [t_vals]
|
|
295
|
+
|
|
296
|
+
addr = nt.range.address
|
|
297
|
+
start_c = addr.split(":")[0].replace("$", "") if ":" in addr else addr.replace("$", "")
|
|
298
|
+
|
|
299
|
+
info = RawExcelTableInfo(
|
|
300
|
+
table_id=global_id,
|
|
301
|
+
source_file=self.file_path,
|
|
302
|
+
sheet_name=sheet_name,
|
|
303
|
+
table_name=nt.name,
|
|
304
|
+
start_cell=start_c,
|
|
305
|
+
range_address=addr,
|
|
306
|
+
raw_data=t_vals,
|
|
307
|
+
num_rows=len(t_vals),
|
|
308
|
+
num_cols=max((len(r) for r in t_vals if isinstance(r, list)), default=0)
|
|
309
|
+
)
|
|
310
|
+
tables_found.append(info)
|
|
311
|
+
global_id += 1
|
|
312
|
+
else:
|
|
313
|
+
# Hoja estándar
|
|
314
|
+
used_range = sht.used_range
|
|
315
|
+
raw_values = used_range.value
|
|
316
|
+
if raw_values is None:
|
|
317
|
+
continue
|
|
318
|
+
if not isinstance(raw_values, list):
|
|
319
|
+
raw_matrix = [[raw_values]]
|
|
320
|
+
elif raw_values and not isinstance(raw_values[0], list):
|
|
321
|
+
raw_matrix = [raw_values]
|
|
322
|
+
else:
|
|
323
|
+
raw_matrix = raw_values
|
|
324
|
+
|
|
325
|
+
addr = used_range.address
|
|
326
|
+
start_c = addr.split(":")[0].replace("$", "") if ":" in addr else "A1"
|
|
327
|
+
|
|
328
|
+
info = RawExcelTableInfo(
|
|
329
|
+
table_id=global_id,
|
|
330
|
+
source_file=self.file_path,
|
|
331
|
+
sheet_name=sheet_name,
|
|
332
|
+
table_name=sheet_name,
|
|
333
|
+
start_cell=start_c,
|
|
334
|
+
range_address=addr,
|
|
335
|
+
raw_data=raw_matrix,
|
|
336
|
+
num_rows=len(raw_matrix),
|
|
337
|
+
num_cols=max((len(r) for r in raw_matrix if isinstance(r, list)), default=0)
|
|
338
|
+
)
|
|
339
|
+
tables_found.append(info)
|
|
340
|
+
global_id += 1
|
|
341
|
+
|
|
342
|
+
return tables_found
|
|
343
|
+
|
|
344
|
+
finally:
|
|
345
|
+
if needs_close:
|
|
346
|
+
if book:
|
|
347
|
+
try: book.close()
|
|
348
|
+
except Exception: pass
|
|
349
|
+
if app:
|
|
350
|
+
try: app.quit()
|
|
351
|
+
except Exception: pass
|
|
352
|
+
|
|
353
|
+
def _extract_all_openpyxl(
|
|
354
|
+
self,
|
|
355
|
+
sheets: Optional[List[Union[str, int]]] = None,
|
|
356
|
+
split_blank_blocks: bool = False
|
|
357
|
+
) -> List[RawExcelTableInfo]:
|
|
358
|
+
wb = openpyxl.load_workbook(self.file_path, data_only=True)
|
|
359
|
+
all_sheet_names = wb.sheetnames
|
|
360
|
+
|
|
361
|
+
target_sheets = []
|
|
362
|
+
if sheets is None:
|
|
363
|
+
target_sheets = all_sheet_names
|
|
364
|
+
else:
|
|
365
|
+
for s in sheets:
|
|
366
|
+
if isinstance(s, int) and 1 <= s <= len(all_sheet_names):
|
|
367
|
+
target_sheets.append(all_sheet_names[s - 1])
|
|
368
|
+
elif str(s) in all_sheet_names:
|
|
369
|
+
target_sheets.append(str(s))
|
|
370
|
+
|
|
371
|
+
tables_found: List[RawExcelTableInfo] = []
|
|
372
|
+
global_id = 1
|
|
373
|
+
|
|
374
|
+
for sheet_name in target_sheets:
|
|
375
|
+
ws = wb[sheet_name]
|
|
376
|
+
|
|
377
|
+
# Tablas oficiales openpyxl
|
|
378
|
+
if hasattr(ws, 'tables') and ws.tables:
|
|
379
|
+
for t_name, t_obj in ws.tables.items():
|
|
380
|
+
# t_obj puede ser el objeto Table o tener ref
|
|
381
|
+
ref = getattr(t_obj, 'ref', str(t_obj))
|
|
382
|
+
cells = ws[ref]
|
|
383
|
+
if isinstance(cells, tuple):
|
|
384
|
+
if isinstance(cells[0], tuple):
|
|
385
|
+
raw_matrix = [[c.value for c in row] for row in cells]
|
|
386
|
+
else:
|
|
387
|
+
raw_matrix = [[c.value for c in cells]]
|
|
388
|
+
else:
|
|
389
|
+
raw_matrix = [[cells.value]]
|
|
390
|
+
|
|
391
|
+
start_c = ref.split(":")[0] if ":" in ref else ref
|
|
392
|
+
|
|
393
|
+
info = RawExcelTableInfo(
|
|
394
|
+
table_id=global_id,
|
|
395
|
+
source_file=self.file_path,
|
|
396
|
+
sheet_name=sheet_name,
|
|
397
|
+
table_name=t_name,
|
|
398
|
+
start_cell=start_c,
|
|
399
|
+
range_address=ref,
|
|
400
|
+
raw_data=raw_matrix,
|
|
401
|
+
num_rows=len(raw_matrix),
|
|
402
|
+
num_cols=max((len(r) for r in raw_matrix if isinstance(r, list)), default=0)
|
|
403
|
+
)
|
|
404
|
+
tables_found.append(info)
|
|
405
|
+
global_id += 1
|
|
406
|
+
else:
|
|
407
|
+
# Leer hoja completa
|
|
408
|
+
raw_matrix = [[c.value for c in row] for row in ws.iter_rows()]
|
|
409
|
+
if not raw_matrix:
|
|
410
|
+
continue
|
|
411
|
+
info = RawExcelTableInfo(
|
|
412
|
+
table_id=global_id,
|
|
413
|
+
source_file=self.file_path,
|
|
414
|
+
sheet_name=sheet_name,
|
|
415
|
+
table_name=sheet_name,
|
|
416
|
+
start_cell="A1",
|
|
417
|
+
range_address=f"A1:{get_column_letter(ws.max_column)}{ws.max_row}",
|
|
418
|
+
raw_data=raw_matrix,
|
|
419
|
+
num_rows=len(raw_matrix),
|
|
420
|
+
num_cols=max((len(r) for r in raw_matrix if isinstance(r, list)), default=0)
|
|
421
|
+
)
|
|
422
|
+
tables_found.append(info)
|
|
423
|
+
global_id += 1
|
|
424
|
+
|
|
425
|
+
return tables_found
|
utils/excel_writer.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Módulo para escribir y actualizar datos en archivos Excel usando xlwings.
|
|
3
|
+
Permite pegar DataFrames directamente en celdas específicas (ej: 'B5') de libros
|
|
4
|
+
abiertos en pantalla (en vivo) o cerrados en segundo plano, sin dañar formatos ni fórmulas existentes.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional, Union, Any
|
|
8
|
+
import os
|
|
9
|
+
import pandas as pd
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
import xlwings as xw
|
|
13
|
+
HAS_XLWINGS = True
|
|
14
|
+
except ImportError:
|
|
15
|
+
HAS_XLWINGS = False
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
import openpyxl
|
|
19
|
+
from openpyxl.utils.cell import coordinate_to_tuple
|
|
20
|
+
HAS_OPENPYXL = True
|
|
21
|
+
except ImportError:
|
|
22
|
+
HAS_OPENPYXL = False
|
|
23
|
+
|
|
24
|
+
from .file_utils import resolve_file_path
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _is_workbook_open_in_excel(file_path: str, file_name: str) -> bool:
|
|
28
|
+
"""Comprueba si el libro ya está abierto en Excel activo."""
|
|
29
|
+
if not HAS_XLWINGS:
|
|
30
|
+
return False
|
|
31
|
+
try:
|
|
32
|
+
for book in xw.books:
|
|
33
|
+
if book.name.lower() == file_name.lower() or book.fullname.lower() == file_path.lower():
|
|
34
|
+
return True
|
|
35
|
+
except Exception:
|
|
36
|
+
return False
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def escribir_en_excel(
|
|
41
|
+
df: pd.DataFrame,
|
|
42
|
+
archivo_excel: str,
|
|
43
|
+
hoja: Union[str, int] = 1,
|
|
44
|
+
celda_inicio: str = "A1",
|
|
45
|
+
incluir_encabezados: bool = True,
|
|
46
|
+
incluir_indice: bool = False,
|
|
47
|
+
guardar: bool = True,
|
|
48
|
+
archivo_abierto: Optional[bool] = None,
|
|
49
|
+
crear_si_no_existe: bool = True
|
|
50
|
+
) -> str:
|
|
51
|
+
"""
|
|
52
|
+
Escribe un DataFrame directamente en una celda específica de un libro de Excel.
|
|
53
|
+
|
|
54
|
+
Parámetros:
|
|
55
|
+
-----------
|
|
56
|
+
df : pd.DataFrame
|
|
57
|
+
El DataFrame con los datos a escribir.
|
|
58
|
+
archivo_excel : str
|
|
59
|
+
Ruta o nombre del archivo Excel.
|
|
60
|
+
hoja : str o int (por defecto 1)
|
|
61
|
+
Nombre o número de la hoja donde se escribirán los datos.
|
|
62
|
+
celda_inicio : str (por defecto 'A1')
|
|
63
|
+
Coordenada de la celda superior izquierda donde comenzará a escribirse (ej: 'B5', 'C4').
|
|
64
|
+
incluir_encabezados : bool (por defecto True)
|
|
65
|
+
Si escribe los nombres de las columnas en la primera fila.
|
|
66
|
+
incluir_indice : bool (por defecto False)
|
|
67
|
+
Si incluye la columna de índice de pandas.
|
|
68
|
+
guardar : bool (por defecto True)
|
|
69
|
+
Si guarda el archivo tras escribir.
|
|
70
|
+
archivo_abierto : bool o None
|
|
71
|
+
- True: Se conecta a la ventana activa de Excel.
|
|
72
|
+
- False: Trabaja en segundo plano cerrado.
|
|
73
|
+
- None: Detecta automáticamente si está abierto o cerrado.
|
|
74
|
+
crear_si_no_existe : bool (por defecto True)
|
|
75
|
+
Crea un nuevo archivo Excel si no existe en la ruta dada.
|
|
76
|
+
|
|
77
|
+
Retorna:
|
|
78
|
+
--------
|
|
79
|
+
str
|
|
80
|
+
Ruta absoluta del archivo modificado.
|
|
81
|
+
"""
|
|
82
|
+
try:
|
|
83
|
+
resolved_path = resolve_file_path(archivo_excel)
|
|
84
|
+
except Exception:
|
|
85
|
+
resolved_path = os.path.abspath(archivo_excel)
|
|
86
|
+
|
|
87
|
+
if not os.path.exists(resolved_path):
|
|
88
|
+
if crear_si_no_existe:
|
|
89
|
+
os.makedirs(os.path.dirname(resolved_path), exist_ok=True)
|
|
90
|
+
# Crear libro vacío con openpyxl o pandas
|
|
91
|
+
df_init = pd.DataFrame()
|
|
92
|
+
with pd.ExcelWriter(resolved_path, engine='openpyxl') as writer:
|
|
93
|
+
sheet_title = hoja if isinstance(hoja, str) else "Hoja1"
|
|
94
|
+
df_init.to_excel(writer, sheet_name=sheet_title)
|
|
95
|
+
else:
|
|
96
|
+
raise FileNotFoundError(f"No existe el archivo Excel: {resolved_path}")
|
|
97
|
+
|
|
98
|
+
file_name = os.path.basename(resolved_path)
|
|
99
|
+
|
|
100
|
+
# Intentar con xlwings si está disponible
|
|
101
|
+
if HAS_XLWINGS:
|
|
102
|
+
try:
|
|
103
|
+
is_open = _is_workbook_open_in_excel(resolved_path, file_name)
|
|
104
|
+
should_connect = archivo_abierto is True or (archivo_abierto is None and is_open)
|
|
105
|
+
|
|
106
|
+
app = None
|
|
107
|
+
book = None
|
|
108
|
+
needs_close = False
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
if should_connect:
|
|
112
|
+
try:
|
|
113
|
+
book = xw.books[file_name]
|
|
114
|
+
except Exception:
|
|
115
|
+
book = xw.Book(resolved_path)
|
|
116
|
+
else:
|
|
117
|
+
app = xw.App(visible=False, add_book=False)
|
|
118
|
+
app.display_alerts = False
|
|
119
|
+
app.screen_updating = False
|
|
120
|
+
book = app.books.open(resolved_path)
|
|
121
|
+
needs_close = True
|
|
122
|
+
|
|
123
|
+
# Seleccionar o crear hoja
|
|
124
|
+
sheet_names = [s.name for s in book.sheets]
|
|
125
|
+
if isinstance(hoja, int):
|
|
126
|
+
if 1 <= hoja <= len(book.sheets):
|
|
127
|
+
sht = book.sheets[hoja - 1]
|
|
128
|
+
else:
|
|
129
|
+
sht = book.sheets.add(f"Hoja{hoja}")
|
|
130
|
+
else:
|
|
131
|
+
if str(hoja) in sheet_names:
|
|
132
|
+
sht = book.sheets[str(hoja)]
|
|
133
|
+
else:
|
|
134
|
+
sht = book.sheets.add(str(hoja))
|
|
135
|
+
|
|
136
|
+
# Escribir DataFrame en la celda indicada
|
|
137
|
+
clean_cell = celda_inicio.replace("$", "").upper()
|
|
138
|
+
sht.range(clean_cell).options(
|
|
139
|
+
index=incluir_indice,
|
|
140
|
+
header=incluir_encabezados
|
|
141
|
+
).value = df
|
|
142
|
+
|
|
143
|
+
if guardar:
|
|
144
|
+
book.save()
|
|
145
|
+
|
|
146
|
+
return resolved_path
|
|
147
|
+
|
|
148
|
+
finally:
|
|
149
|
+
if needs_close:
|
|
150
|
+
if book:
|
|
151
|
+
try: book.close()
|
|
152
|
+
except Exception: pass
|
|
153
|
+
if app:
|
|
154
|
+
try: app.quit()
|
|
155
|
+
except Exception: pass
|
|
156
|
+
|
|
157
|
+
except Exception as e:
|
|
158
|
+
# Fallback a openpyxl si ocurre algún error COM
|
|
159
|
+
pass
|
|
160
|
+
|
|
161
|
+
# Fallback con openpyxl si xlwings no pudo ejecutarse
|
|
162
|
+
if HAS_OPENPYXL:
|
|
163
|
+
wb = openpyxl.load_workbook(resolved_path)
|
|
164
|
+
sheet_title = hoja if isinstance(hoja, str) else (f"Hoja{hoja}" if isinstance(hoja, int) and hoja > len(wb.sheetnames) else wb.sheetnames[hoja-1])
|
|
165
|
+
if sheet_title in wb.sheetnames:
|
|
166
|
+
ws = wb[sheet_title]
|
|
167
|
+
else:
|
|
168
|
+
ws = wb.create_sheet(title=sheet_title)
|
|
169
|
+
|
|
170
|
+
start_row, start_col = coordinate_to_tuple(celda_inicio.replace("$", "").upper())
|
|
171
|
+
|
|
172
|
+
current_r = start_row
|
|
173
|
+
if incluir_encabezados:
|
|
174
|
+
for c_idx, col_name in enumerate(df.columns, start=start_col):
|
|
175
|
+
ws.cell(row=current_r, column=c_idx, value=str(col_name))
|
|
176
|
+
current_r += 1
|
|
177
|
+
|
|
178
|
+
for _, row in df.iterrows():
|
|
179
|
+
for c_idx, val in enumerate(row.values, start=start_col):
|
|
180
|
+
cell_val = None if pd.isna(val) else val
|
|
181
|
+
ws.cell(row=current_r, column=c_idx, value=cell_val)
|
|
182
|
+
current_r += 1
|
|
183
|
+
|
|
184
|
+
if guardar:
|
|
185
|
+
wb.save(resolved_path)
|
|
186
|
+
return resolved_path
|
|
187
|
+
|
|
188
|
+
raise RuntimeError("Se requiere xlwings u openpyxl para escribir en Excel.")
|