Quant0 commited on
Commit
68e0797
·
verified ·
1 Parent(s): a840e82

Create scraping.py

Browse files
Files changed (1) hide show
  1. scraping.py +276 -0
scraping.py ADDED
@@ -0,0 +1,276 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================================
2
+ # scraping.py — Descarga de tablas web (ScraperAPI + BeautifulSoup)
3
+ # y utilidades de limpieza para su procesamiento
4
+ # ------------------------------------------------------------
5
+ # NOTA PARA AUDITORÍA:
6
+ # - Este módulo encapsula:
7
+ # (1) Limpieza básica de texto y conversión de cantidades monetarias.
8
+ # (2) Descarga robusta de tablas HTML mediante ScraperAPI + BeautifulSoup.
9
+ # (3) Helpers para obtener las tablas de bitcointreasuries y generar
10
+ # una lista de tickers (para usar en el dropdown de la UI).
11
+ # - No tiene efectos secundarios al importarlo.
12
+ # - API key de ScraperAPI:
13
+ # * Se lee de la variable de entorno SCRAPERAPI_KEY si no se pasa
14
+ # explícitamente por parámetro.
15
+ # ============================================================
16
+
17
+ from __future__ import annotations
18
+
19
+ import os
20
+ import re
21
+ from typing import Optional, Tuple, List
22
+
23
+ import numpy as np
24
+ import pandas as pd
25
+ import requests
26
+ from bs4 import BeautifulSoup
27
+
28
+
29
+ # ------------------------------------------------------------
30
+ # Constantes de origen (bitcointreasuries)
31
+ # ------------------------------------------------------------
32
+ BITCOINTREASURIES_STOCKS_URL = "https://bitcointreasuries.net/"
33
+ BITCOINTREASURIES_ETFS_URL = "https://bitcointreasuries.net/etfs-and-exchanges"
34
+
35
+
36
+ # ============================================================
37
+ # Utilidades de limpieza
38
+ # ============================================================
39
+ def limpiar_texto(texto: str) -> str:
40
+ """
41
+ Elimina caracteres especiales y espacios extra de una cadena.
42
+
43
+ Parámetros
44
+ ----------
45
+ texto : str
46
+ Texto de entrada.
47
+
48
+ Devuelve
49
+ --------
50
+ str
51
+ Texto limpio (solo letras, dígitos, espacios, puntos y comas).
52
+ """
53
+ # Coincide con tu lógica original: eliminar todo lo que no sea
54
+ # letras/números/espacios/punto/coma con una pequeña whitelist
55
+ # y luego hacer strip.
56
+ texto_limpio_parcial = re.sub(r"[^a-zA-Z0-9.,\s-]", "", texto)
57
+ texto_final = texto_limpio_parcial.strip()
58
+ return texto_final
59
+
60
+
61
+ def expand_money(val) -> float:
62
+ """
63
+ Convierte representaciones monetarias abreviadas a valor numérico (float).
64
+
65
+ Soporta sufijos:
66
+ - 'B' (billions) → *1_000_000_000
67
+ - 'M' (millions) → *1_000_000
68
+ - 'K' (thousands) → *1_000
69
+
70
+ Parámetros
71
+ ----------
72
+ val : any
73
+ Cadena como '$1.2B', '500M', '40K', o valor numérico/NaN.
74
+
75
+ Devuelve
76
+ --------
77
+ float
78
+ Valor expandido en unidades básicas (p. ej. USD). Devuelve NaN si falla.
79
+ """
80
+ if pd.isna(val):
81
+ return np.nan
82
+
83
+ s = str(val).replace("$", "").replace(",", "").upper()
84
+ try:
85
+ if "B" in s:
86
+ return float(s.replace("B", "")) * 1_000_000_000
87
+ if "M" in s:
88
+ return float(s.replace("M", "")) * 1_000_000
89
+ if "K" in s:
90
+ return float(s.replace("K", "")) * 1_000
91
+ return float(s)
92
+ except Exception:
93
+ return np.nan
94
+
95
+
96
+ # ============================================================
97
+ # Descarga de tablas con ScraperAPI + BeautifulSoup
98
+ # ============================================================
99
+ def descargar_tabla_web(
100
+ url: str,
101
+ indice_tabla: int = 0,
102
+ api_key: Optional[str] = None,
103
+ timeout: float = 30.0,
104
+ ) -> Optional[pandas.core.frame.DataFrame]:
105
+ """
106
+ Descarga una tabla HTML desde `url` usando ScraperAPI para evitar bloqueos.
107
+
108
+ La lógica respeta tu implementación original:
109
+ - Construye la URL de ScraperAPI con ?api_key=...&url=...
110
+ - Parseo con BeautifulSoup
111
+ - Selección de la tabla por índice (0 por defecto)
112
+ - Construcción del DataFrame (con headers si están presentes)
113
+
114
+ Parámetros
115
+ ----------
116
+ url : str
117
+ URL objetivo que contiene la/las tabla(s).
118
+ indice_tabla : int
119
+ Índice de la tabla a extraer en el DOM (0 = primera).
120
+ api_key : str, opcional
121
+ Clave de ScraperAPI. Si no se provee, se usa la variable de entorno
122
+ SCRAPERAPI_KEY. Si tampoco está, la petición se realizará directamente
123
+ a `url` sin pasar por ScraperAPI (puede fallar por bloqueos anti-bot).
124
+ timeout : float
125
+ Timeout de la petición HTTP (segundos).
126
+
127
+ Devuelve
128
+ --------
129
+ pandas.DataFrame | None
130
+ DataFrame con los datos de la tabla, o None si no se encuentra/ocurre error.
131
+ """
132
+ try:
133
+ # Determinar endpoint efectivo: con o sin ScraperAPI
134
+ api_key = api_key or os.getenv("SCRAPERAPI_KEY", "").strip()
135
+ if api_key:
136
+ api_url = f"http://api.scraperapi.com?api_key={api_key}&url={url}"
137
+ target = api_url
138
+ else:
139
+ # Fallback directo a la URL si no hay API key
140
+ target = url
141
+
142
+ response = requests.get(target, timeout=timeout)
143
+ response.raise_for_status()
144
+
145
+ soup = BeautifulSoup(response.text, "html.parser")
146
+ tables = soup.find_all("table")
147
+
148
+ if len(tables) <= indice_tabla:
149
+ print(f"[scraping] No se encontró la tabla con índice {indice_tabla} en {url}.")
150
+ return None
151
+
152
+ table = tables[indice_tabla]
153
+
154
+ # Extraer cabeceras (si existen)
155
+ headers = [th.text.strip() for th in table.find_all("th")]
156
+ data = []
157
+ for row in table.find_all("tr"):
158
+ cols = row.find_all("td")
159
+ cols = [ele.text.strip() for ele in cols]
160
+ if cols:
161
+ data.append(cols)
162
+
163
+ # Construir DataFrame con o sin headers (si la longitud coincide)
164
+ if headers and data and len(headers) == len(data[0]):
165
+ df = pd.DataFrame(data, columns=headers)
166
+ else:
167
+ df = pd.DataFrame(data)
168
+
169
+ return df
170
+
171
+ except requests.exceptions.RequestException as e:
172
+ print(f"[scraping] Error al acceder a la URL: {e}")
173
+ return None
174
+ except Exception as e:
175
+ print(f"[scraping] Ocurrió un error inesperado: {e}")
176
+ return None
177
+
178
+
179
+ # ============================================================
180
+ # Helpers específicos para bitcointreasuries
181
+ # ============================================================
182
+ def get_bitcointreasuries_tables(
183
+ stocks_table_index: int = 3,
184
+ etfs_table_index: int = 3,
185
+ api_key: Optional[str] = None,
186
+ ) -> Tuple[Optional[pd.DataFrame], Optional[pd.DataFrame]]:
187
+ """
188
+ Descarga las tablas de *stocks* y *ETFs* desde bitcointreasuries.
189
+
190
+ Parámetros
191
+ ----------
192
+ stocks_table_index : int
193
+ Índice de la tabla en la página principal (por defecto: 3).
194
+ etfs_table_index : int
195
+ Índice de la tabla en la página de ETFs (por defecto: 3).
196
+ api_key : str, opcional
197
+ Clave de ScraperAPI para el scraping.
198
+
199
+ Devuelve
200
+ --------
201
+ (DataFrame | None, DataFrame | None)
202
+ Tupla (df_stocks, df_etfs). Cada uno puede ser None si falla.
203
+ """
204
+ df_stocks = descargar_tabla_web(
205
+ BITCOINTREASURIES_STOCKS_URL, indice_tabla=stocks_table_index, api_key=api_key
206
+ )
207
+ df_etfs = descargar_tabla_web(
208
+ BITCOINTREASURIES_ETFS_URL, indice_tabla=etfs_table_index, api_key=api_key
209
+ )
210
+ return df_stocks, df_etfs
211
+
212
+
213
+ def generate_tickers_from_bitcointreasuries(
214
+ stocks_table_index: int = 3,
215
+ etfs_table_index: int = 3,
216
+ api_key: Optional[str] = None,
217
+ ) -> List[str]:
218
+ """
219
+ Genera una lista de nombres (y típicamente tickers al final del nombre)
220
+ a partir de las tablas de bitcointreasuries.
221
+
222
+ NOTA: En tu `app1 6.py` original, se reutiliza la columna 'Name' de la tabla
223
+ (índice 2) y luego, en otros pasos, se separa 'Name' y 'Ticker' por el
224
+ último espacio. Aquí devolvemos simplemente la lista de 'Name' limpia.
225
+
226
+ Parámetros
227
+ ----------
228
+ stocks_table_index : int
229
+ Índice de la tabla en la página principal de stocks.
230
+ etfs_table_index : int
231
+ Índice de la tabla en la página de ETFs.
232
+ api_key : str, opcional
233
+ Clave de ScraperAPI.
234
+
235
+ Devuelve
236
+ --------
237
+ list[str]
238
+ Lista concatenada de nombres (stocks + etfs) limpios.
239
+ """
240
+ tickers: List[str] = []
241
+
242
+ df_stocks, df_etfs = get_bitcointreasuries_tables(
243
+ stocks_table_index=stocks_table_index,
244
+ etfs_table_index=etfs_table_index,
245
+ api_key=api_key,
246
+ )
247
+
248
+ # Procesar stocks (si existen). En tu código original se usa la columna 2.
249
+ if df_stocks is not None and not df_stocks.empty:
250
+ try:
251
+ df_stocks = df_stocks.copy()
252
+ df_stocks["Name"] = df_stocks[2].apply(limpiar_texto)
253
+ tickers.extend(df_stocks["Name"].dropna().tolist())
254
+ except Exception:
255
+ # En caso de estructura inesperada de columnas
256
+ pass
257
+
258
+ # Procesar ETFs (si existen). En tu código original se usa la columna 2.
259
+ if df_etfs is not None and not df_etfs.empty:
260
+ try:
261
+ df_etfs = df_etfs.copy()
262
+ df_etfs["Name"] = df_etfs[2].apply(limpiar_texto)
263
+ tickers.extend(df_etfs["Name"].dropna().tolist())
264
+ except Exception:
265
+ pass
266
+
267
+ return tickers
268
+
269
+
270
+ __all__ = [
271
+ "descargar_tabla_web",
272
+ "limpiar_texto",
273
+ "expand_money",
274
+ "get_bitcointreasuries_tables",
275
+ "generate_tickers_from_bitcointreasuries",
276
+ ]