MySQL GF * * Source : * https://www.investing.com/indices/france-40-components * * Référentiel : * indice * * Données : * cours_actions * ============================================================ */ $dbHost = 'localhost'; $dbName = 'GF'; $dbUser = 'root'; $dbPass = 'sysadm-1963'; $url = 'https://www.investing.com/indices/france-40-components'; $logPrefix = date('Y-m-d H:i:s'); echo "$logPrefix - Début import CAC40\n"; /* * ============================================================ * MYSQL * ============================================================ */ try { $pdo = new PDO( "mysql:host=$dbHost;dbname=$dbName;charset=utf8mb4", $dbUser, $dbPass, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false ] ); } catch (PDOException $e) { die("$logPrefix - ERREUR MYSQL : " . $e->getMessage() . PHP_EOL); } /* * ============================================================ * CHARGEMENT DU REFERENTIEL CAC40 * * La table indice contient : * * indice * nom * isin * symbole * actif * * ============================================================ */ $sqlIndice = " SELECT nom, isin, symbole FROM indice WHERE indice = 'CAC40' AND actif = 1 "; $stmtIndice = $pdo->query($sqlIndice); $referentiel = []; while ($row = $stmtIndice->fetch()) { /* * On normalise le nom pour faciliter la correspondance * avec Investing. */ $cle = mb_strtolower( trim($row['nom']), 'UTF-8' ); $referentiel[$cle] = [ 'nom' => $row['nom'], 'isin' => $row['isin'], 'symbole' => $row['symbole'] ]; } echo "$logPrefix - Référentiel CAC40 : " . count($referentiel) . " valeurs\n"; if (count($referentiel) !== 40) { echo "$logPrefix - ATTENTION : le référentiel CAC40 " . "contient " . count($referentiel) . " valeurs au lieu de 40.\n"; } /* * ============================================================ * CURL * ============================================================ */ $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 5, CURLOPT_CONNECTTIMEOUT => 15, CURLOPT_TIMEOUT => 30, CURLOPT_ENCODING => '', CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_USERAGENT => 'Mozilla/5.0 (X11; Linux x86_64) ' . 'AppleWebKit/537.36 ' . '(KHTML, like Gecko) ' . 'Chrome/139.0 Safari/537.36', CURLOPT_HTTPHEADER => [ 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language: fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7', 'Cache-Control: no-cache', 'Pragma: no-cache' ] ]); $html = curl_exec($ch); $httpCode = curl_getinfo( $ch, CURLINFO_HTTP_CODE ); $curlError = curl_error($ch); curl_close($ch); /* * ============================================================ * VERIFICATION CURL * ============================================================ */ if ($html === false || $html === '') { die("$logPrefix - ERREUR CURL : " . $curlError . PHP_EOL); } if ($httpCode !== 200) { die("$logPrefix - ERREUR HTTP : $httpCode" . PHP_EOL); } echo "$logPrefix - Page récupérée : " . strlen($html) . " octets\n"; /* * ============================================================ * DOM * ============================================================ */ libxml_use_internal_errors(true); $dom = new DOMDocument(); if (!$dom->loadHTML($html)) { die("$logPrefix - ERREUR : impossible de parser le HTML\n"); } libxml_clear_errors(); $xpath = new DOMXPath($dom); /* * ============================================================ * RECHERCHE DU TABLEAU CAC40 * ============================================================ */ $tables = $xpath->query('//table'); $targetTable = null; foreach ($tables as $table) { $rowsTable = $xpath->query( './/tr', $table ); if ($rowsTable->length === 0) { continue; } $firstRow = $rowsTable->item(0); $cells = $xpath->query( './th|./td', $firstRow ); $headers = []; foreach ($cells as $cell) { $headers[] = trim( preg_replace( '/\s+/u', ' ', $cell->textContent ) ); } if ( in_array('Name', $headers, true) && in_array('Last', $headers, true) && in_array('High', $headers, true) && in_array('Low', $headers, true) && in_array('Chg. %', $headers, true) && in_array('Vol.', $headers, true) && in_array('Time', $headers, true) ) { $targetTable = $table; echo "$logPrefix - Tableau CAC40 trouvé\n"; echo "$logPrefix - Colonnes : " . implode('|', $headers) . "\n"; break; } } /* * ============================================================ * TABLEAU NON TROUVE * ============================================================ */ if ($targetTable === null) { file_put_contents( '/tmp/investing_cac40_error.html', $html ); die("$logPrefix - ERREUR : tableau CAC40 introuvable\n"); } /* * ============================================================ * COLONNES * ============================================================ */ $rows = $xpath->query( './/tr', $targetTable ); $headerCells = $xpath->query( './th|./td', $rows->item(0) ); $columns = []; foreach ($headerCells as $index => $cell) { $name = trim( preg_replace( '/\s+/u', ' ', $cell->textContent ) ); $columns[$name] = $index; } /* * ============================================================ * FONCTIONS * ============================================================ */ function cleanText(string $text): string { $text = html_entity_decode( $text, ENT_QUOTES | ENT_HTML5, 'UTF-8' ); $text = preg_replace( '/\s+/u', ' ', $text ); return trim($text); } function normalizeName(string $name): string { $name = cleanText($name); /* * Normalisation des apostrophes */ $name = str_replace( ['’', '`'], "'", $name ); /* * Suppression des espaces multiples */ $name = preg_replace( '/\s+/u', ' ', $name ); return mb_strtolower( trim($name), 'UTF-8' ); } function parseNumber(?string $value): ?float { if ($value === null) { return null; } $value = cleanText($value); if ($value === '') { return null; } $value = str_replace( ["\xc2\xa0", ' ', '%'], '', $value ); /* * Virgule française -> point */ if ( str_contains($value, ',') && !str_contains($value, '.') ) { $value = str_replace( ',', '.', $value ); } else { $value = str_replace( ',', '', $value ); } $value = preg_replace( '/[^0-9.\-+]/', '', $value ); if ( $value === '' || !is_numeric($value) ) { return null; } return (float)$value; } function parseVolume(?string $value): ?int { if ($value === null) { return null; } $value = cleanText($value); if ($value === '') { return null; } $value = str_replace( ["\xc2\xa0", ' '], '', $value ); $multiplier = 1; $last = strtoupper( substr($value, -1) ); if ($last === 'K') { $multiplier = 1000; $value = substr( $value, 0, -1 ); } elseif ($last === 'M') { $multiplier = 1000000; $value = substr( $value, 0, -1 ); } elseif ($last === 'B') { $multiplier = 1000000000; $value = substr( $value, 0, -1 ); } $value = str_replace( ',', '.', $value ); $value = preg_replace( '/[^0-9.]/', '', $value ); if ( $value === '' || !is_numeric($value) ) { return null; } return (int)round( (float)$value * $multiplier ); } /* * ============================================================ * PREPARATION INSERTION * ============================================================ * * IMPORTANT : * * Le symbole et l'ISIN viennent de la table indice. * * Investing ne sert qu'à fournir les cotations. * * ============================================================ */ $sql = " INSERT INTO cours_actions ( indice, nom, isin, symbole, cours, plus_haut, plus_bas, variation, variation_pct, volume, date_cours, source ) VALUES ( :indice, :nom, :isin, :symbole, :cours, :plus_haut, :plus_bas, :variation, :variation_pct, :volume, :date_cours, :source ) ON DUPLICATE KEY UPDATE nom = VALUES(nom), isin = VALUES(isin), symbole = VALUES(symbole), cours = VALUES(cours), plus_haut = VALUES(plus_haut), plus_bas = VALUES(plus_bas), variation = VALUES(variation), variation_pct = VALUES(variation_pct), volume = VALUES(volume), date_cours = VALUES(date_cours), source = VALUES(source) "; $stmt = $pdo->prepare($sql); /* * ============================================================ * DATE COURS * ============================================================ */ $dateCours = date( 'Y-m-d H:i:00' ); /* * ============================================================ * TRAITEMENT DES 40 VALEURS * ============================================================ */ $count = 0; $errors = 0; $notFound = []; /* * Ligne 0 = entêtes */ for ($r = 1; $r < $rows->length; $r++) { $row = $rows->item($r); $cells = $xpath->query( './td', $row ); /* * Une vraie ligne CAC40 possède 8 colonnes. */ if ($cells->length < 8) { continue; } /* * -------------------------------------------------------- * CELLULES * -------------------------------------------------------- */ $values = []; foreach ($cells as $cell) { $values[] = cleanText( $cell->textContent ); } /* * -------------------------------------------------------- * NOM INVESTING * -------------------------------------------------------- */ $nomInvesting = $values[$columns['Name'] ?? 0] ?? ''; if ($nomInvesting === '') { continue; } /* * -------------------------------------------------------- * RECHERCHE DANS LA TABLE INDICE * -------------------------------------------------------- */ $cle = normalizeName( $nomInvesting ); $ref = $referentiel[$cle] ?? null; /* * -------------------------------------------------------- * SI LE NOM N'EST PAS IDENTIQUE * * On tente quelques alias connus. * * Cela évite les problèmes du genre : * * "L'Oreal" / "L'Oréal" * "Saint Gobain" / "Saint-Gobain" * etc. * -------------------------------------------------------- */ if ($ref === null) { $aliases = [ 'l\'oreal' => 'l\'oréal', 'saint gobain' => 'saint-gobain', 'societe generale' => 'société générale', 'credit agricole' => 'crédit agricole', 'airbus group' => 'airbus', 'stellantis nv' => 'stellantis', 'veolia environnement' => 'veolia', 'louis vuitton' => 'lvmh', 'hermes international' => 'hermès international' ]; if (isset($aliases[$cle])) { $cleAlias = normalizeName( $aliases[$cle] ); $ref = $referentiel[$cleAlias] ?? null; } } /* * -------------------------------------------------------- * VALEUR ABSENTE DU REFERENTIEL * -------------------------------------------------------- */ if ($ref === null) { $notFound[] = $nomInvesting; echo "$logPrefix - ATTENTION : " . "référentiel inconnu : " . $nomInvesting . "\n"; continue; } /* * -------------------------------------------------------- * ISIN / SYMBOLE * -------------------------------------------------------- */ $isin = $ref['isin']; $symbole = $ref['symbole']; $nom = $ref['nom']; /* * -------------------------------------------------------- * COURS * -------------------------------------------------------- */ $cours = parseNumber( $values[$columns['Last']] ?? null ); /* * -------------------------------------------------------- * PLUS HAUT * -------------------------------------------------------- */ $plusHaut = parseNumber( $values[$columns['High']] ?? null ); /* * -------------------------------------------------------- * PLUS BAS * -------------------------------------------------------- */ $plusBas = parseNumber( $values[$columns['Low']] ?? null ); /* * -------------------------------------------------------- * VARIATION % * -------------------------------------------------------- */ $variationPct = parseNumber( $values[$columns['Chg. %']] ?? null ); /* * -------------------------------------------------------- * VOLUME * -------------------------------------------------------- */ $volume = parseVolume( $values[$columns['Vol.']] ?? null ); /* * -------------------------------------------------------- * VARIATION ABSOLUE * -------------------------------------------------------- */ $variation = null; if ( $cours !== null && $variationPct !== null && (100 + $variationPct) != 0 ) { $variation = $cours * $variationPct / (100 + $variationPct); } /* * -------------------------------------------------------- * INSERTION * -------------------------------------------------------- */ try { $stmt->execute([ ':indice' => 'CAC40', ':nom' => $nom, ':isin' => $isin, ':symbole' => $symbole, ':cours' => $cours, ':plus_haut' => $plusHaut, ':plus_bas' => $plusBas, ':variation' => $variation, ':variation_pct' => $variationPct, ':volume' => $volume, ':date_cours' => $dateCours, ':source' => 'Investing' ]); $count++; echo "$logPrefix - OK : " . str_pad( (string)$count, 2, '0', STR_PAD_LEFT ) . " - " . $nom . " [" . $symbole . "] " . $isin . " - " . ($cours ?? 'NULL') . " - " . ($variationPct ?? 'NULL') . "%\n"; } catch (PDOException $e) { $errors++; echo "$logPrefix - ERREUR SQL : " . $nom . " : " . $e->getMessage() . "\n"; } } /* * ============================================================ * RESULTAT * ============================================================ */ echo "\n"; echo "$logPrefix - CAC40 : " . $count . "/40 valeurs importées\n"; /* * Valeurs absentes du référentiel */ if (count($notFound) > 0) { echo "$logPrefix - VALEURS NON TROUVÉES DANS indice :\n"; foreach ($notFound as $nom) { echo " - " . $nom . "\n"; } } if ($errors > 0) { echo "$logPrefix - Erreurs SQL : " . $errors . "\n"; } /* * ============================================================ * SECURITE * ============================================================ */ if ($count !== 40) { echo "$logPrefix - ATTENTION : import incomplet !\n"; exit(1); } echo "$logPrefix - IMPORT CAC40 OK\n"; exit(0);