MySQL GF * * Source : * https://www.investing.com/equities/spain * * Référentiel : * indice * * Données : * cours_actions * * IMPORTANT : * * - Le nom Investing sert uniquement à retrouver la valeur * dans la table indice. * * - Le symbole Investing n'est JAMAIS utilisé comme référence. * * - nom / isin / symbole proviennent exclusivement de indice. * * - Aucun changement n'est effectué dans la table indice. * * - Les dates sont générées en UTC. * * ============================================================ */ /* * ============================================================ * CONFIGURATION * ============================================================ */ $dbHost = 'localhost'; $dbName = 'GF'; $dbUser = 'root'; $dbPass = 'sysadm-1963'; $url = 'https://www.investing.com/equities/spain'; /* * ============================================================ * UTC * * MySQL travaille en UTC chez toi. * ============================================================ */ date_default_timezone_set('UTC'); $logPrefix = date('Y-m-d H:i:s'); echo "$logPrefix - Début import IBEX35\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 IBEX35 * * La table indice contient : * * indice * nom * isin * symbole * actif * * ============================================================ */ $sqlIndice = " SELECT nom, isin, symbole FROM indice WHERE indice = 'IBEX35' AND actif = 1 "; $stmtIndice = $pdo->query( $sqlIndice ); $referentiel = []; while ($row = $stmtIndice->fetch()) { /* * La clé du tableau est le nom normalisé. */ $cle = normalizeName( (string)$row['nom'] ); $referentiel[$cle] = [ 'nom' => $row['nom'], 'isin' => $row['isin'], 'symbole' => $row['symbole'] ]; } echo "$logPrefix - Référentiel IBEX35 : " . count($referentiel) . " valeurs\n"; if (count($referentiel) !== 35) { echo "$logPrefix - ATTENTION : le référentiel IBEX35 " . "contient " . count($referentiel) . " valeurs au lieu de 35.\n"; } /* * ============================================================ * CURL * ============================================================ */ $ch = curl_init($url); curl_setopt_array( $ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 10, CURLOPT_CONNECTTIMEOUT => 15, CURLOPT_TIMEOUT => 45, CURLOPT_ENCODING => '', CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_2TLS, /* * User-Agent navigateur réel. */ CURLOPT_USERAGENT => 'Mozilla/5.0 (X11; Linux x86_64; rv:153.0) ' . 'Gecko/20100101 Firefox/153.0', CURLOPT_HTTPHEADER => [ 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8', 'Accept-Language: fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7', 'Accept-Encoding: gzip, deflate, br', 'Cache-Control: no-cache', 'Pragma: no-cache', 'Upgrade-Insecure-Requests: 1', 'Sec-Fetch-Dest: document', 'Sec-Fetch-Mode: navigate', 'Sec-Fetch-Site: none', 'Sec-Fetch-User: ?1' ] ] ); $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) { /* * Sauvegarde de la page reçue pour diagnostic. */ file_put_contents( '/tmp/investing_ibex35_http_error.html', $html ); 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 IBEX35 * * Investing utilise : * * Name * Last * High * Low * Chg. * Chg. % * Vol. * Time * * ============================================================ */ $tables = $xpath->query( '//table' ); $targetTable = null; foreach ($tables as $table) { /* * IMPORTANT : * * $table vient de DOMXPath::query(). * On ne fait aucun getAttribute() dessus. */ $rowsTable = $xpath->query( './/tr', $table ); if ( $rowsTable === false || $rowsTable->length === 0 ) { continue; } $firstRow = $rowsTable->item(0); if ($firstRow === null) { continue; } $cells = $xpath->query( './th|./td', $firstRow ); if ( $cells === false ) { continue; } $headers = []; foreach ($cells as $cell) { $headers[] = cleanText( $cell->textContent ); } /* * Tableau IBEX35 recherché. */ 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 IBEX35 trouvé\n"; echo "$logPrefix - Colonnes : " . implode( '|', $headers ) . "\n"; break; } } /* * ============================================================ * TABLEAU NON TROUVE * ============================================================ */ if ( $targetTable === null ) { file_put_contents( '/tmp/investing_ibex35_error.html', $html ); die("$logPrefix - ERREUR : tableau IBEX35 introuvable\n"); } /* * ============================================================ * COLONNES * ============================================================ */ $rows = $xpath->query( './/tr', $targetTable ); if ( $rows === false || $rows->length === 0 ) { die("$logPrefix - ERREUR : aucune ligne trouvée\n"); } $headerCells = $xpath->query( './th|./td', $rows->item(0) ); $columns = []; if ($headerCells !== false) { foreach ( $headerCells as $index => $cell ) { $name = cleanText( $cell->textContent ); $columns[$name] = $index; } } /* * ============================================================ * FONCTIONS * ============================================================ */ /* * ------------------------------------------------------------ * Nettoyage texte * ------------------------------------------------------------ */ 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 ); } /* * ------------------------------------------------------------ * Normalisation des noms * * Exemple : * * "ACS derived" * ↓ * "acs" * * "Téléfónica" * ↓ * "telefonica" * * "Caixabank" * ↓ * "caixabank" * * ------------------------------------------------------------ */ function normalizeName( string $name ): string { $name = cleanText( $name ); /* * Supprimer "derived" ajouté par Investing. * * On accepte aussi d'éventuels espaces avant. */ $name = preg_replace( '/\s+derived\s*$/iu', '', $name ); /* * Minuscules. */ $name = mb_strtolower( $name, 'UTF-8' ); /* * Suppression des accents. */ $name = strtr( $name, [ 'à' => 'a', 'á' => 'a', 'â' => 'a', 'ä' => 'a', 'ã' => 'a', 'å' => 'a', 'ç' => 'c', 'è' => 'e', 'é' => 'e', 'ê' => 'e', 'ë' => 'e', 'ì' => 'i', 'í' => 'i', 'î' => 'i', 'ï' => 'i', 'ñ' => 'n', 'ò' => 'o', 'ó' => 'o', 'ô' => 'o', 'ö' => 'o', 'õ' => 'o', 'ù' => 'u', 'ú' => 'u', 'û' => 'u', 'ü' => 'u', 'ý' => 'y', 'ÿ' => 'y', 'œ' => 'oe', 'æ' => 'ae' ] ); /* * Apostrophes. */ $name = str_replace( [ "'", '’', '`' ], '', $name ); /* * Tout ce qui n'est pas alphanumérique * devient un espace. */ $name = preg_replace( '/[^a-z0-9]+/u', ' ', $name ); /* * Espaces multiples. */ $name = preg_replace( '/\s+/u', ' ', $name ); return trim( $name ); } /* * ------------------------------------------------------------ * Nombre * ------------------------------------------------------------ */ function parseNumber( ?string $value ): ?float { if ( $value === null ) { return null; } $value = cleanText( $value ); if ( $value === '' ) { return null; } /* * Espaces insécables et espaces. */ $value = str_replace( [ "\xc2\xa0", ' ', '%' ], '', $value ); /* * Gestion décimale. * * Investing utilise généralement le point. * * Exemple : * 24.755 * -0.74% */ if ( str_contains( $value, ',' ) && !str_contains( $value, '.' ) ) { $value = str_replace( ',', '.', $value ); } else { $value = str_replace( ',', '', $value ); } /* * Garder uniquement nombre. */ $value = preg_replace( '/[^0-9.\-+]/', '', $value ); if ( $value === '' || !is_numeric( $value ) ) { return null; } return (float)$value; } /* * ------------------------------------------------------------ * Volume * * Exemples : * * 1.25K * 3.4M * 1.2B * * ------------------------------------------------------------ */ function parseVolume( ?string $value ): ?int { if ( $value === null ) { return null; } $value = cleanText( $value ); if ( $value === '' ) { return null; } /* * Espaces. */ $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 ); } /* * Décimale. */ 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 (int)round( (float)$value * $multiplier ); } /* * ============================================================ * ALIAS INVESTING -> NOM DE LA TABLE indice * * Les clés sont déjà normalisées. * * ============================================================ */ $aliases = [ /* * ACS */ 'acs' => 'acs', /* * BBVA */ 'bbva' => 'banco bilbao vizcaya argentaria', /* * Caixabank */ 'caixabank' => 'caixabank', /* * Enagas */ 'enagas' => 'enagas', /* * Naturgy */ 'naturgy energy' => 'naturgy energy group', /* * Redeia */ 'redeia corporacion' => 'redeia', /* * Indra */ 'indra a' => 'indra sistemas', /* * Telefonica */ 'telefonica' => 'telefonica', /* * Banco Sabadell */ 'banco de sabadell' => 'banco sabadell', /* * Santander */ 'santander' => 'banco santander', /* * IAG */ 'iag' => 'international airlines group', /* * Amadeus */ 'amadeus' => 'amadeus it group', /* * Rovi */ 'laborat rovi' => 'laboratorios rovi', /* * Merlin */ 'merlin properties sa' => 'merlin properties', /* * Acciona Energía */ 'corporacion acciona energias renovables' => 'acciona energia', /* * Aena */ 'aena' => 'aena', /* * Cellnex */ 'cellnex telecom' => 'cellnex telecom', /* * Unicaja */ 'unicaja banco' => 'unicaja banco', /* * ArcelorMittal */ 'arcelormittal' => 'arcelormittal', /* * Fluidra */ 'fluidra' => 'fluidra', /* * Logista */ 'logista' => 'logista', /* * Solaria */ 'solaria' => 'solaria', /* * Puig */ 'puig brands' => 'puig brands' ]; /* * ============================================================ * PREPARATION SQL * ============================================================ */ $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 * * UTC. * ============================================================ */ $dateCours = gmdate( 'Y-m-d H:i:00' ); /* * ============================================================ * TRAITEMENT DES 35 VALEURS * ============================================================ */ $count = 0; $errors = 0; $notFound = []; /* * Ligne 0 = entêtes. */ for ( $r = 1; $r < $rows->length; $r++ ) { $row = $rows->item( $r ); if ( $row === null ) { continue; } /* * Récupération des cellules. */ $cells = $xpath->query( './td', $row ); if ( $cells === false || $cells->length < 7 ) { continue; } /* * -------------------------------------------------------- * VALEURS DES CELLULES * -------------------------------------------------------- */ $values = []; foreach ( $cells as $cell ) { $values[] = cleanText( $cell->textContent ); } /* * -------------------------------------------------------- * NOM INVESTING * -------------------------------------------------------- */ $nomInvesting = $values[$columns['Name'] ?? 0] ?? ''; if ( $nomInvesting === '' ) { continue; } /* * -------------------------------------------------------- * RECHERCHE PAR NOM * * JAMAIS par symbole Investing. * -------------------------------------------------------- */ $cle = normalizeName( $nomInvesting ); $ref = $referentiel[$cle] ?? null; /* * -------------------------------------------------------- * ALIAS * -------------------------------------------------------- */ if ( $ref === null && isset( $aliases[$cle] ) ) { $cleAlias = normalizeName( $aliases[$cle] ); $ref = $referentiel[$cleAlias] ?? null; } /* * -------------------------------------------------------- * REFERENTIEL INCONNU * -------------------------------------------------------- */ if ( $ref === null ) { $notFound[] = $nomInvesting; echo "$logPrefix - ATTENTION : " . "référentiel inconnu : " . $nomInvesting . "\n"; continue; } /* * -------------------------------------------------------- * IDENTITE DE LA VALEUR * * TOUT vient de indice. * -------------------------------------------------------- */ $nom = (string)$ref['nom']; $isin = (string)$ref['isin']; $symbole = (string)$ref['symbole']; /* * -------------------------------------------------------- * 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 * * Si le cours actuel vaut C et que la variation * est P %, alors : * * cours précédent = C / (1 + P/100) * * variation = C - cours précédent * * -------------------------------------------------------- */ $variation = null; if ( $cours !== null && $variationPct !== null && (100 + $variationPct) != 0 ) { $variation = $cours * $variationPct / (100 + $variationPct); } /* * -------------------------------------------------------- * INSERTION * -------------------------------------------------------- */ try { $stmt->execute( [ ':indice' => 'IBEX35', ':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 ? $cours : 'NULL' ) . " - " . ( $variationPct !== null ? $variationPct : 'NULL' ) . "%\n"; } catch ( PDOException $e ) { $errors++; echo "$logPrefix - ERREUR SQL : " . $nom . " : " . $e->getMessage() . "\n"; } } /* * ============================================================ * RESULTAT * ============================================================ */ echo "\n"; echo "$logPrefix - IBEX35 : " . $count . "/35 valeurs importées\n"; /* * ------------------------------------------------------------ * VALEURS NON TROUVEES * ------------------------------------------------------------ */ if ( count($notFound) > 0 ) { echo "$logPrefix - VALEURS NON TROUVÉES DANS indice :\n"; foreach ( $notFound as $nom ) { echo " - " . $nom . "\n"; } } /* * ------------------------------------------------------------ * ERREURS SQL * ------------------------------------------------------------ */ if ( $errors > 0 ) { echo "$logPrefix - Erreurs SQL : " . $errors . "\n"; } /* * ============================================================ * SECURITE * * L'import est considéré comme valide uniquement * si les 35 valeurs ont été correctement importées. * ============================================================ */ if ( $count !== 35 ) { echo "$logPrefix - ATTENTION : import incomplet !\n"; exit(1); } echo "$logPrefix - IMPORT IBEX35 OK\n"; exit(0);