Files
extractions-actions/import_cac40.php
T
2026-08-19 14:18:01 +00:00

1014 lines
17 KiB
PHP
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
/*
* ============================================================
* IMPORT CAC 40
* Investing.com -> 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);