Files
Mercados-Web/src/Mercados.Infrastructure/DataFetchers/MercadoAgroFetcher.cs

161 lines
6.8 KiB
C#
Raw Normal View History

using AngleSharp;
using Mercados.Core.Entities;
using Mercados.Infrastructure.Persistence.Repositories;
using Microsoft.Extensions.Logging;
using System.Globalization;
namespace Mercados.Infrastructure.DataFetchers
{
public class MercadoAgroFetcher : IDataFetcher
{
public string SourceName => "MercadoAgroganadero";
private const string DataUrl = "https://www.mercadoagroganadero.com.ar/dll/hacienda6.dll/haciinfo000225";
private readonly IHttpClientFactory _httpClientFactory;
private readonly ICotizacionGanadoRepository _cotizacionRepository;
private readonly IFuenteDatoRepository _fuenteDatoRepository;
private readonly ILogger<MercadoAgroFetcher> _logger;
public MercadoAgroFetcher(
IHttpClientFactory httpClientFactory,
ICotizacionGanadoRepository cotizacionRepository,
IFuenteDatoRepository fuenteDatoRepository,
ILogger<MercadoAgroFetcher> logger)
{
_httpClientFactory = httpClientFactory;
_cotizacionRepository = cotizacionRepository;
_fuenteDatoRepository = fuenteDatoRepository;
_logger = logger;
}
public async Task<(bool Success, string Message)> FetchDataAsync()
{
_logger.LogInformation("Iniciando fetch para {SourceName}.", SourceName);
try
{
var htmlContent = await GetHtmlContentAsync();
if (string.IsNullOrEmpty(htmlContent))
{
return (false, "No se pudo obtener el contenido HTML.");
}
var cotizaciones = ParseHtmlToEntities(htmlContent);
if (!cotizaciones.Any())
{
return (false, "No se encontraron cotizaciones válidas en el HTML.");
}
await _cotizacionRepository.GuardarMuchosAsync(cotizaciones);
await UpdateSourceInfoAsync();
_logger.LogInformation("Fetch para {SourceName} completado exitosamente. Se guardaron {Count} registros.", SourceName, cotizaciones.Count);
return (true, $"Proceso completado. Se guardaron {cotizaciones.Count} registros.");
}
catch (Exception ex)
{
_logger.LogError(ex, "Ocurrió un error durante el fetch para {SourceName}.", SourceName);
return (false, $"Error: {ex.Message}");
}
}
private async Task<string> GetHtmlContentAsync()
{
// Pedimos el cliente HTTP con el nombre específico que tiene la política de Polly
var client = _httpClientFactory.CreateClient("MercadoAgroFetcher");
// Es importante simular un navegador para evitar bloqueos.
client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36");
var response = await client.GetAsync(DataUrl);
response.EnsureSuccessStatusCode();
// El sitio usa una codificación específica, hay que decodificarla correctamente.
var stream = await response.Content.ReadAsStreamAsync();
using var reader = new StreamReader(stream, System.Text.Encoding.GetEncoding("windows-1252"));
return await reader.ReadToEndAsync();
}
private List<CotizacionGanado> ParseHtmlToEntities(string html)
{
var config = Configuration.Default;
var context = BrowsingContext.New(config);
var document = context.OpenAsync(req => req.Content(html)).Result;
var tabla = document.QuerySelector("table.table.table-striped");
if (tabla == null)
{
_logger.LogWarning("No se encontró la tabla de cotizaciones en el HTML.");
return new List<CotizacionGanado>();
}
var cotizaciones = new List<CotizacionGanado>();
// Omitimos las primeras 2 filas (cabeceras) y las últimas 2 (totales/pies).
var filas = tabla.QuerySelectorAll("tr").Skip(2).SkipLast(2);
foreach (var fila in filas)
{
var celdas = fila.QuerySelectorAll("td").Select(c => c.TextContent.Trim()).ToList();
if (celdas.Count < 12) continue; // Fila inválida
try
{
var cotizacion = new CotizacionGanado
{
Categoria = celdas[1],
Especificaciones = $"{celdas[2]} - {celdas[3]}",
Maximo = ParseDecimal(celdas[4]),
Minimo = ParseDecimal(celdas[5]),
Promedio = ParseDecimal(celdas[6]),
Mediano = ParseDecimal(celdas[7]),
Cabezas = ParseInt(celdas[8]),
KilosTotales = ParseInt(celdas[9]),
KilosPorCabeza = ParseInt(celdas[10]),
ImporteTotal = ParseDecimal(celdas[11]),
FechaRegistro = DateTime.UtcNow
};
cotizaciones.Add(cotizacion);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "No se pudo parsear una fila de la tabla. Contenido: {RowContent}", string.Join(" | ", celdas));
}
}
return cotizaciones;
}
private async Task UpdateSourceInfoAsync()
{
var fuente = await _fuenteDatoRepository.ObtenerPorNombreAsync(SourceName);
if (fuente == null)
{
await _fuenteDatoRepository.CrearAsync(new FuenteDato
{
Nombre = SourceName,
Url = DataUrl,
UltimaEjecucionExitosa = DateTime.UtcNow
});
}
else
{
fuente.Url = DataUrl;
fuente.UltimaEjecucionExitosa = DateTime.UtcNow;
await _fuenteDatoRepository.ActualizarAsync(fuente);
}
}
// --- Funciones de Ayuda para Parseo ---
private readonly CultureInfo _cultureInfo = new CultureInfo("es-AR");
private decimal ParseDecimal(string value)
{
// El sitio usa '.' como separador de miles y ',' como decimal.
// Primero quitamos el de miles, luego reemplazamos la coma decimal por un punto.
var cleanValue = value.Replace("$", "").Replace(".", "").Trim();
return decimal.Parse(cleanValue, NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign, _cultureInfo);
}
private int ParseInt(string value)
{
return int.Parse(value.Replace(".", ""), CultureInfo.InvariantCulture);
}
}
}