From bb14c859970e712d4bc8af43c2d3a21002e5f83a Mon Sep 17 00:00:00 2001 From: jfgiraud Date: Wed, 19 Aug 2026 14:18:01 +0000 Subject: [PATCH] Commit initial --- import_cac40.php | 1013 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1013 insertions(+) create mode 100755 import_cac40.php diff --git a/import_cac40.php b/import_cac40.php new file mode 100755 index 0000000..a933024 --- /dev/null +++ b/import_cac40.php @@ -0,0 +1,1013 @@ + 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);