Informatique

Comment scraper un site boursier en C#

Comment scraper un site boursier en C#

Dans cet article, je vais expliquer comment j'ai conçu un Worker Service en C# pour scraper les cotations des actions cotées sur Euronext Paris depuis le site ABCBourse. Ce service, qui tourne en continu, utilise la bibliothèque HtmlAgilityPack pour extraire les données de manière automatique toutes les 15 minutes et les stocker dans une base de données MySQL. Le même principe est appliqué pour scraper d'autres sites comme BourseDirect, Boursier, Boursorama, et LesEchos, afin de répartir les appels et éviter toute surcharge ou blocage d’un site particulier.

Classe Actions

La classe Actions représente une action cotée sur Euronext Paris, avec les propriétés essentielles pour identifier l'action, sa place boursière, son secteur, etc. Cette classe sert à enregistrer les informations de base des actions dans la base de données MySQL :

public class Actions
{
    public string codeISIN { get; set; }
    public string nom { get; set; }
    public string nomBoursier { get; set; }
    public string symbole { get; set; }
    public string place { get; set; }
    public string ticker { get; set; }
    public string tickerBoursorama { get; set; }
    public string typeValeur { get; set; }
    public string marche { get; set; }
    public string secteur { get; set; }
    public string soumisTTF { get; set; }
    public string nomPlace { get; set; }
    public string scrapper { get; set; }
    public bool historicLoaded { get; set; }
    public int idSecteur { get; set; }
    public string indiceReference { get; set; }
    public string eligibilite { get; set; }
    public bool anomalie { get; set; }
}

Classe Historique

La classe Historique permet d’enregistrer les cotations journalières des actions dans une table dédiée. Chaque enregistrement contient les informations essentielles pour une analyse financière, comme le cours, le volume, les variations, etc. :

public class Historique
{
    public int id { get; set; }
    public string ticker { get; set; }
    public DateTime dateJour { get; set; }
    public decimal cours { get; set; }
    public decimal ouverture { get; set; }
    public decimal plusHaut { get; set; }
    public decimal plusBas { get; set; }
    public decimal veille { get; set; }
    public int volume { get; set; }
    public decimal delta { get; set; }
    public decimal performance { get; set; }
    public string quoteTime { get; set; }
}

Classe ScrapperAbcBourse

La classe ScrapperAbcBourse contient toutes les méthodes nécessaires pour extraire les données depuis le site ABCBourse. Ces méthodes permettent d'analyser le HTML récupéré et d’en extraire les valeurs recherchées comme le cours, la variation, le volume, etc. Voici quelques-unes des méthodes clés :

Extraction de valeur décimale

Cette méthode extrait une valeur décimale à partir d'une chaîne HTML en utilisant des délimiteurs de début et de fin spécifiques.

public static decimal GetDecimalValue(string searchLine, string startItem, string endItem)
{
    CultureInfo culture = CultureInfo.CreateSpecificCulture("fr-FR");
    decimal result = 0.00M;
    int debut = searchLine.IndexOf(startItem);
    string strValue = searchLine.Substring(debut + startItem.Length);
    int fin = strValue.IndexOf(endItem);
    strValue = strValue.Substring(0, fin).Replace(".", ",").Replace(" ", "");
    if (strValue.Length > 1)
        result = decimal.Parse(strValue, culture);
    return result;
}

Extraction de la dernière cotation

Cette méthode extrait la date et l'heure de la dernière cotation disponible.

public static DateOnly GetLastQuoteDate(string searchLine)
{
    var dateTimeStr = ExtractValue(searchLine, "quoteTime");
    DateTime dateTime = DateTime.Parse(dateTimeStr, null, DateTimeStyles.RoundtripKind);
    return DateOnly.FromDateTime(dateTime);
}

public static string GetLastQuoteTime(string searchLine)
{
    var dateTimeStr = ExtractValue(searchLine, "quoteTime");
    DateTime dateTime = DateTime.Parse(dateTimeStr, null, DateTimeStyles.RoundtripKind);
    return dateTime.ToString("HH:mm:ss");
}

Extraction de la côte et du pourcentage d'évolution

Même principe que les méthodes précédentes

public static decimal GetPrice(string searchLine)
{
    var priceStr = ExtractValue(searchLine, "price");
    priceStr = priceStr.Replace(" ", "").Replace(".", "").Replace(",", ".");
    if (priceStr != "-")
        return decimal.Parse(priceStr, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture);
    else
        return 0.00M;
}

public static decimal GetPercentage(string searchLine)
{
    var percentageStr = ExtractValue(searchLine, "priceChangePercent");
    percentageStr = percentageStr.Replace("%", "").Replace(",", ".").Replace("+", "");
    return decimal.Parse(percentageStr, NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture);
}

private static string ExtractValue(string input, string key)
{
    var regex = new Regex($"\"{key}\":\"(.*?)\"");
    var match = regex.Match(input);
    if (match.Success)
    {
        return match.Groups[1].Value;
    }
    throw new ArgumentException($"Key '{key}' not found in input string.");
}

Extraction des données

Cette méthode de scraping récupère les informations de cotation d'une action depuis ABCBourse. Elle utilise HtmlAgilityPack pour charger la page HTML de la cotation de l'action et y extraire les données pertinentes.

// --------------------------------------------------------------------------------------------------------------------------------
public static Historique EndOfDayFromABCBourse(Actions action)
{
    Historique valeur = new Historique();
    CultureInfo culture = CultureInfo.CreateSpecificCulture("en-US");

    HtmlAgilityPack.HtmlWeb web = new HtmlAgilityPack.HtmlWeb();
    web.UsingCache = false;

    string url = "https://www.abcbourse.com/cotation/";
    string parametre = action.symbole + "p";
    string searchText = "";

    // ----- Je charge dans la variable doc, toute la page html appelé par l'url
    HtmlAgilityPack.HtmlDocument doc = web.Load(url + parametre);

    Console.WriteLine("ABCBourse ==> Analyse de l'action : " + action.nom);

    try
    {
        // ----- Regex pour supprimer tous les espaces, les retour à la ligne et fin de ligne
        searchText = Regex.Replace(doc.ParsedText.Substring(doc.ParsedText.IndexOf("tickerSymbol"), 132), @"[\s\t\r\n]", "");

        DateOnly lastQuoteDateOnly = ScrapperAbcBourse.GetLastQuoteDate(searchText);
        DateOnly today = DateOnly.FromDateTime(DateTime.Now);
        
        // ----- ce test, pour éviter une analyse si l'action n'est pas côtée ce jour
        if (lastQuoteDateOnly == today)
        {
            valeur.ticker = action.ticker;
            valeur.cours = ScrapperAbcBourse.GetPrice(searchText);
            valeur.performance = ScrapperAbcBourse.GetPercentage(searchText);
            valeur.quoteTime = ScrapperAbcBourse.GetLastQuoteTime(searchText);
            if (valeur.quoteTime != "00:00:00" && valeur.cours > 0)
            {
                searchText = Regex.Replace(doc.ParsedText.Substring(doc.ParsedText.IndexOf("vZone"), 1000), @"[\s\t\r\n]", "");
                valeur.plusHaut = ScrapperAbcBourse.GetDecimalValue(searchText, "id=\"high\">", "</td>");
                valeur.plusBas = ScrapperAbcBourse.GetDecimalValue(searchText, "id=\"low\">", "</td>");
                valeur.ouverture = ScrapperAbcBourse.GetDecimalValue(searchText, "id=\"open\">", "</td");
                valeur.veille = ScrapperAbcBourse.GetDecimalValue(searchText, "Clôtureveille</td><tdclass=\"alri\">", "</td>");
                valeur.volume = ScrapperAbcBourse.GetIntegerValue(searchText, "id=\"vol\">", "</td>");
            }
        }
    }
    catch (Exception e)
    {
        valeur = null;
        Console.WriteLine("Erreur de lecture via le scrapper ABCBourse de la valeur : " + action.codeISIN + " - " + action.nom);
        //Console.WriteLine(e.ToString());
    }
    return valeur;
}
 

Exécution périodique

La méthode GetIntradayABCBourse est appelée toutes les 15 minutes. Elle parcourt toutes les actions configurées pour être scrapées depuis ABCBourse, extrait les données et les enregistre dans la base de données.

public async Task GetIntradayABCBourse()
{
    IHistoriqueRepository historiqueRepository = new HistoriqueRepository(connectionString, logger);
    IActionRepository actionRepository = new ActionRepository(connectionString);
    Historique valeur = new Historique();

    // ----- Requête pour ne charger que les actions dont le champ 'scrapper' est égal = 'abcbourse'
    string request = "SELECT * FROM actions WHERE scrapper = 'abcbourse' AND anomalie IS NOT TRUE ORDER BY nom ASC;";
    IEnumerable<Actions> actions = actionRepository.GetAllActions(request);

    foreach (Actions action in actions)
    {
        try
        {
            valeur = Euronext.EndOfDayFromABCBourse(action);
            if (valeur != null)
            {
                if (valeur.quoteTime != null)
                    historiqueRepository.Upsert(valeur);
            }
            else
            {
                action.anomalie = true;
                actionRepository.DeclareAnomalie(action);
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Erreur de scrapping sur l'action : {action.nom}");
        }
    }
}

Conclusion

Grâce à ce Worker Service en C#, je peux facilement scraper et enregistrer les données de cotation en quasi-temps réel.
En répartissant les appels entre différents sites, je minimise le risque de blocage tout en assurant une fiabilité des données. Cette solution offre une approche robuste et scalable pour le suivi des cotations d'actions cotées sur Euronext Paris.
En complément, par ordre décroissant de performance, ABCBourse a le temps de réponse le plus rapide, puis LesEchos, BourseDirect, Boursier et Boursorama.
La construction des urls d'appel diffère "évidemment" d'un site à l'autre, d'où dans la table "actions", des champs tels que "nomBoursier" ou "tickerBoursorama".

Il est, tout à fait, possible de scraper toutes les minutes mais j'ignore comment les sites réagissent face une centaine d'appels toutes les minutes pendant près de 10 heures. De plus hormis BourseDirect et Boursorama qui donnent les cotations en temps réel, les autres sites sont sur un temps différé.

En parallèle, j'ai développé un webservice, qui restitue les informations ainsi récupérées via des appels API. 
Ma prochaine étape sera une "modeste" application en Blazor Hybrid et MAUI pour exploiter ces informations.

 

Related Articles

Information

Ce site est construit autour de Joomla 5, en utilisant le template Helix Ultimate et les composants : SP Builder Pro pour la construction de certaines pages, Smart Slider 3 Free pour les slideshows, Komento Free pour les commentaires sur les articles et Ignite Gallery pour les galeries de photos.

Mentions légales

Le site est édité par Christian Sammut
130, Avenue du Général Leclerc
45800 Saint Jean de Braye
Tél.: 06.01.74.90.33
E-mail: contact@sammut.fr

Ce site est hébergé par AMEN SASU
12-14, Rond Point des Champs Elysées
75008 Paris