MySQL GF * * Source : * https://www.investing.com/equities/americas * * Référentiel : * indice * * Données : * cours_actions * * IMPORTANT : * * - NE MODIFIE JAMAIS la table indice * - nom / ISIN / symbole viennent de indice * - la correspondance se fait sur le NOM * - "derived" est supprimé du nom Investing * * ============================================================ */ $dbHost = 'localhost'; $dbName = 'GF'; $dbUser = 'root'; $dbPass = 'sysadm-1963'; $url = 'https://www.investing.com/equities/americas'; $logPrefix = date('Y-m-d H:i:s'); echo "$logPrefix - Début import DOWJONES\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 * * On récupère uniquement les valeurs actives. * * Alphabet pourra être présent dans indice avec : * * actif = 0 * * tant que le changement n'est pas effectif. * * ============================================================ */ $sqlIndice = " SELECT nom, isin, symbole FROM indice WHERE indice = 'DOWJONES' AND actif = 1 "; $stmtIndice = $pdo->query($sqlIndice); $referentiel = []; while ($row = $stmtIndice->fetch()) { $cle = normalizeName( $row['nom'] ); $referentiel[$cle] = [ 'nom' => $row['nom'], 'isin' => $row['isin'], 'symbole' => $row['symbole'] ]; } echo "$logPrefix - Référentiel DOWJONES : " . count($referentiel) . " valeurs\n"; if (count($referentiel) !== 30) { echo "$logPrefix - ATTENTION : le référentiel " . "DOWJONES contient " . count($referentiel) . " valeurs au lieu de 30.\n"; } /* * ============================================================ * CURL * ============================================================ * * Investing peut répondre 403 selon le User-Agent. * * On utilise ici une requête proche d'un navigateur réel. * * ============================================================ */ $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.0.0 Safari/537.36', CURLOPT_HTTPHEADER => [ 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8', 'Accept-Language: en-US,en;q=0.9', '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) { file_put_contents( '/tmp/investing_dowjones_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 * * Colonnes attendues : * * Name * Last * High * Low * Chg. * Chg. % * Vol. * Time * * ============================================================ */ $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[] = cleanText( $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 DOWJONES trouvé\n"; echo "$logPrefix - Colonnes : " . implode( '|', $headers ) . "\n"; break; } } /* * ============================================================ * TABLEAU NON TROUVE * ============================================================ */ if ($targetTable === null) { file_put_contents( '/tmp/investing_dowjones_error.html', $html ); die("$logPrefix - ERREUR : tableau DOWJONES introuvable\n"); } /* * ============================================================ * COLONNES * ============================================================ */ $rows = $xpath->query( './/tr', $targetTable ); $headerCells = $xpath->query( './th|./td', $rows->item(0) ); $columns = []; foreach ($headerCells as $index => $cell) { $name = cleanText( $cell->textContent ); $columns[$name] = $index; } /* * ============================================================ * PREPARATION MYSQL * ============================================================ */ $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 / HEURE * * MySQL utilise UTC. * * On demande explicitement à MySQL son heure UTC. * * ============================================================ */ $stmtDate = $pdo->query( "SELECT UTC_TIMESTAMP() AS utc_now" ); $dateCours = $stmtDate->fetch()['utc_now']; $dateCours = substr( $dateCours, 0, 16 ) . ':00'; echo "$logPrefix - Date cours UTC : " . $dateCours . "\n"; /* * ============================================================ * TRAITEMENT DES VALEURS * ============================================================ */ $count = 0; $errors = 0; $notFound = []; /* * ============================================================ * LIGNE 0 = ENTETES * ============================================================ */ for ( $r = 1; $r < $rows->length; $r++ ) { $row = $rows->item($r); $cells = $xpath->query( './td', $row ); if ($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; } /* * -------------------------------------------------------- * SUPPRESSION DE "derived" * * Exemples : * * ACS derived * Apple derived * * devient : * * ACS * Apple * * -------------------------------------------------------- */ $nomNettoye = preg_replace( '/\s+derived\s*$/iu', '', $nomInvesting ); $nomNettoye = trim( $nomNettoye ); /* * -------------------------------------------------------- * RECHERCHE PAR NOM * -------------------------------------------------------- */ $cle = normalizeName( $nomNettoye ); $ref = $referentiel[$cle] ?? null; /* * -------------------------------------------------------- * ALIAS * * Les noms Investing peuvent légèrement différer. * -------------------------------------------------------- */ if ($ref === null) { $aliases = [ '3m' => '3M', 'amazon.com' => 'Amazon.com', 'american express' => 'American Express', 'amgen' => 'Amgen', 'apple' => 'Apple', 'boeing' => 'Boeing', 'caterpillar' => 'Caterpillar', 'chevron' => 'Chevron', 'cisco' => 'Cisco Systems', 'coca cola' => 'Coca-Cola', 'disney' => 'Disney', 'goldman sachs' => 'Goldman Sachs', 'home depot' => 'Home Depot', 'honeywell' => 'Honeywell International', 'ibm' => 'IBM', 'johnson johnson' => 'Johnson & Johnson', 'jpmorgan' => 'JPMorgan Chase', 'mcdonalds' => 'McDonald\'s', 'merck' => 'Merck & Co.', 'microsoft' => 'Microsoft', 'nike' => 'Nike', 'nvidia' => 'NVIDIA', 'procter gamble' => 'Procter & Gamble', 'salesforce' => 'Salesforce', 'sherwin williams' => 'Sherwin-Williams', 'travelers' => 'Travelers', 'unitedhealth' => 'UnitedHealth Group', 'verizon' => 'Verizon Communications', 'visa' => 'Visa', 'walmart' => 'Walmart', 'alphabet' => 'Alphabet' ]; if ( isset( $aliases[$cle] ) ) { $cleAlias = normalizeName( $aliases[$cle] ); $ref = $referentiel[$cleAlias] ?? null; } } /* * -------------------------------------------------------- * ABSENT DU REFERENTIEL * -------------------------------------------------------- */ if ($ref === null) { $notFound[] = $nomInvesting . ' / ' . $nomNettoye; echo "$logPrefix - ATTENTION : " . "référentiel inconnu : " . $nomInvesting . " -> " . $nomNettoye . "\n"; continue; } /* * -------------------------------------------------------- * REFERENTIEL * -------------------------------------------------------- */ $nom = $ref['nom']; $isin = $ref['isin']; $symbole = $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 * -------------------------------------------------------- */ $variation = null; if ( $cours !== null && $variationPct !== null && (100 + $variationPct) != 0 ) { $variation = $cours * $variationPct / (100 + $variationPct); } /* * -------------------------------------------------------- * INSERTION * -------------------------------------------------------- */ try { $stmt->execute([ ':indice' => 'DOWJONES', ':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 - DOWJONES : " . $count . "/30 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 * ============================================================ */ if ($count !== 30) { echo "$logPrefix - ATTENTION : import incomplet !\n"; exit(1); } echo "$logPrefix - IMPORT DOWJONES OK\n"; exit(0); /* * ============================================================ * 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 NOM * ------------------------------------------------------------ */ function normalizeName( string $name ): string { $name = cleanText( $name ); /* * Apostrophes */ $name = str_replace( [ '’', '`' ], "'", $name ); /* * Tirets */ $name = str_replace( [ '–', '—' ], '-', $name ); /* * Espaces autour des tirets */ $name = preg_replace( '/\s*-\s*/u', '-', $name ); /* * Suppression de certains caractères * de ponctuation pour faciliter * les correspondances. */ $name = preg_replace( '/[.,]/u', '', $name ); /* * Espaces multiples */ $name = preg_replace( '/\s+/u', ' ', $name ); return mb_strtolower( trim($name), 'UTF-8' ); } /* * ------------------------------------------------------------ * PARSE NUMBER * ------------------------------------------------------------ */ function parseNumber( ?string $value ): ?float { if ($value === null) { return null; } $value = cleanText( $value ); if ($value === '') { return null; } $value = str_replace( [ "\xc2\xa0", ' ', '%' ], '', $value ); /* * Format français */ 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; } /* * ------------------------------------------------------------ * PARSE VOLUME * ------------------------------------------------------------ */ 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 ); }