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