Files

1014 lines
17 KiB
PHP
Raw Permalink Normal View History

2026-08-19 14:18:01 +00:00
<?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);