760 lines
23 KiB
PHP
760 lines
23 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* Importiert aktuelle T-CrB-Beobachtungen aus AAVSO WebObs in die Tabelle
|
|
* `aavso_tcrb_observations`.
|
|
*
|
|
* Nutzung:
|
|
* php scripts/import/import_tcrb.php
|
|
* php scripts/import/import_tcrb.php --pages=3
|
|
* php scripts/import/import_tcrb.php --html=C:\pfad\webobs.html --dry-run
|
|
* php scripts/import/import_tcrb.php --dir=C:\Users\ich\Desktop\TCrB
|
|
*
|
|
* Typischer Cron-Aufruf:
|
|
* php /pfad/zum/projekt/scripts/import/import_tcrb.php --pages=2
|
|
*/
|
|
|
|
error_reporting(E_ALL);
|
|
ini_set('display_errors', '1');
|
|
set_time_limit(0);
|
|
|
|
const DEFAULT_TCRB_URL = 'https://apps.aavso.org/webobs/results/?star=t+CRB&num_results=25&obs_types=ccd&page=1';
|
|
const DEFAULT_TABLE_NAME = 'aavso_tcrb_observations';
|
|
const DEFAULT_IMPORT_SOURCE = 'aavso-tcrb';
|
|
|
|
$rootDir = dirname(__DIR__, 2);
|
|
$config = require $rootDir . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'database.php';
|
|
|
|
if (!is_array($config)) {
|
|
writeMessage('config/database.php muss ein Array mit DB-Zugangsdaten zurueckgeben.', true);
|
|
exit(1);
|
|
}
|
|
|
|
$config = normalizeDbConfig($config);
|
|
|
|
foreach (['host', 'dbname', 'user', 'pass'] as $requiredKey) {
|
|
if (!array_key_exists($requiredKey, $config) || $config[$requiredKey] === '') {
|
|
writeMessage("DB-Konfiguration unvollstaendig: {$requiredKey} fehlt.", true);
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
$options = parseCliArguments(getCliArguments());
|
|
|
|
if (($options['help'] ?? false) === true) {
|
|
writeUsage();
|
|
exit(0);
|
|
}
|
|
|
|
$dsn = sprintf(
|
|
'mysql:host=%s;port=%d;dbname=%s;charset=%s',
|
|
$config['host'],
|
|
(int) $config['port'],
|
|
$config['dbname'],
|
|
$config['charset']
|
|
);
|
|
|
|
$pdo = new PDO(
|
|
$dsn,
|
|
$config['user'],
|
|
$config['pass'],
|
|
[
|
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
|
]
|
|
);
|
|
|
|
$tableName = DEFAULT_TABLE_NAME;
|
|
|
|
if (!tableExists($pdo, $tableName)) {
|
|
writeMessage("Die Zieltabelle `{$tableName}` existiert noch nicht. Wir koennen sie im naechsten Schritt gemeinsam anlegen.", true);
|
|
exit(1);
|
|
}
|
|
|
|
ensureImportRunsTableExists($pdo);
|
|
|
|
$insertSql = <<<SQL
|
|
INSERT INTO `{$tableName}` (
|
|
`source_row_key`,
|
|
`star_name`,
|
|
`jd`,
|
|
`observed_at_utc`,
|
|
`calendar_date_text`,
|
|
`magnitude`,
|
|
`magnitude_error`,
|
|
`filter_code`,
|
|
`observer_code`,
|
|
`notes_raw`,
|
|
`imported_at`
|
|
) VALUES (
|
|
:source_row_key,
|
|
:star_name,
|
|
:jd,
|
|
:observed_at_utc,
|
|
:calendar_date_text,
|
|
:magnitude,
|
|
:magnitude_error,
|
|
:filter_code,
|
|
:observer_code,
|
|
:notes_raw,
|
|
NOW()
|
|
)
|
|
ON DUPLICATE KEY UPDATE
|
|
`star_name` = VALUES(`star_name`),
|
|
`observed_at_utc` = VALUES(`observed_at_utc`),
|
|
`calendar_date_text` = VALUES(`calendar_date_text`),
|
|
`magnitude` = VALUES(`magnitude`),
|
|
`magnitude_error` = VALUES(`magnitude_error`),
|
|
`filter_code` = VALUES(`filter_code`),
|
|
`observer_code` = VALUES(`observer_code`),
|
|
`notes_raw` = VALUES(`notes_raw`),
|
|
`imported_at` = NOW(),
|
|
`updated_at` = NOW()
|
|
SQL;
|
|
|
|
$statement = $pdo->prepare($insertSql);
|
|
|
|
$pagesToImport = max(1, (int) ($options['pages'] ?? 1));
|
|
$sourceHtmlPath = $options['html'] ?? null;
|
|
$sourceDirectoryPath = $options['dir'] ?? null;
|
|
$dryRun = (bool) ($options['dry-run'] ?? false);
|
|
$baseUrl = (string) ($options['url'] ?? DEFAULT_TCRB_URL);
|
|
$totalImported = 0;
|
|
$totalUpserts = 0;
|
|
$recordsInserted = 0;
|
|
$recordsUpdated = 0;
|
|
$importRunId = null;
|
|
|
|
if ($sourceHtmlPath !== null && $sourceDirectoryPath !== null) {
|
|
writeMessage('Bitte entweder --html oder --dir verwenden, nicht beides gleichzeitig.', true);
|
|
exit(1);
|
|
}
|
|
|
|
$importSources = [];
|
|
|
|
if ($sourceDirectoryPath !== null) {
|
|
foreach (findHtmlFilesInDirectory($sourceDirectoryPath) as $filePath) {
|
|
$importSources[] = [
|
|
'label' => basename($filePath),
|
|
'html' => loadHtmlFromFile($filePath),
|
|
'is_local_file' => true,
|
|
];
|
|
}
|
|
|
|
if (empty($importSources)) {
|
|
writeMessage("Im Ordner wurden keine importierbaren HTML/TXT-Dateien gefunden: {$sourceDirectoryPath}", true);
|
|
exit(1);
|
|
}
|
|
} elseif ($sourceHtmlPath !== null) {
|
|
$importSources[] = [
|
|
'label' => basename($sourceHtmlPath),
|
|
'html' => loadHtmlFromFile($sourceHtmlPath),
|
|
'is_local_file' => true,
|
|
];
|
|
} else {
|
|
for ($pageNumber = 1; $pageNumber <= $pagesToImport; $pageNumber++) {
|
|
$importSources[] = [
|
|
'label' => 'Seite ' . $pageNumber,
|
|
'html' => downloadUrl(buildPagedUrl($baseUrl, $pageNumber)),
|
|
'is_local_file' => false,
|
|
];
|
|
}
|
|
}
|
|
|
|
if (!$dryRun) {
|
|
$stmtImportStart = $pdo->prepare("
|
|
INSERT INTO app_import_runs (
|
|
source,
|
|
source_url,
|
|
started_at,
|
|
status,
|
|
message
|
|
) VALUES (
|
|
:source,
|
|
:source_url,
|
|
NOW(),
|
|
'running',
|
|
NULL
|
|
)
|
|
");
|
|
$stmtImportStart->execute([
|
|
':source' => DEFAULT_IMPORT_SOURCE,
|
|
':source_url' => $sourceDirectoryPath !== null ? $sourceDirectoryPath : ($sourceHtmlPath !== null ? $sourceHtmlPath : $baseUrl),
|
|
]);
|
|
$importRunId = (int) $pdo->lastInsertId();
|
|
}
|
|
|
|
foreach ($importSources as $sourceIndex => $source) {
|
|
$pageNumber = $sourceIndex + 1;
|
|
$html = (string) $source['html'];
|
|
|
|
$pageData = parseWebObsPage($html);
|
|
|
|
if (empty($pageData['observations'])) {
|
|
writeMessage(($source['label'] ?? ('Seite ' . $pageNumber)) . ': keine Beobachtungen gefunden.');
|
|
if (($source['is_local_file'] ?? false) === true) {
|
|
continue;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
writeMessage(sprintf(
|
|
'%s: %d Beobachtungen fuer %s gefunden.',
|
|
(string) ($source['label'] ?? ('Seite ' . $pageNumber)),
|
|
count($pageData['observations']),
|
|
$pageData['target_name'] !== '' ? $pageData['target_name'] : 'T CrB'
|
|
));
|
|
|
|
if ($dryRun) {
|
|
$totalImported += count($pageData['observations']);
|
|
continue;
|
|
}
|
|
|
|
$pdo->beginTransaction();
|
|
|
|
try {
|
|
foreach ($pageData['observations'] as $observation) {
|
|
$statement->execute([
|
|
':source_row_key' => $observation['source_row_key'],
|
|
':star_name' => $observation['star_name'],
|
|
':jd' => $observation['jd'],
|
|
':observed_at_utc' => $observation['observed_at_utc'],
|
|
':calendar_date_text' => $observation['calendar_date_text'],
|
|
':magnitude' => $observation['magnitude'],
|
|
':magnitude_error' => $observation['magnitude_error'],
|
|
':filter_code' => $observation['filter_code'],
|
|
':observer_code' => $observation['observer_code'],
|
|
':notes_raw' => $observation['notes_raw'],
|
|
]);
|
|
|
|
$affectedRows = (int) $statement->rowCount();
|
|
$totalUpserts += $affectedRows;
|
|
if ($affectedRows === 1) {
|
|
$recordsInserted++;
|
|
} elseif ($affectedRows >= 2) {
|
|
$recordsUpdated++;
|
|
}
|
|
$totalImported++;
|
|
}
|
|
|
|
$pdo->commit();
|
|
} catch (Throwable $e) {
|
|
if ($pdo->inTransaction()) {
|
|
$pdo->rollBack();
|
|
}
|
|
|
|
if ($importRunId !== null) {
|
|
$stmtImportError = $pdo->prepare("
|
|
UPDATE app_import_runs
|
|
SET
|
|
finished_at = NOW(),
|
|
records_received = :records_received,
|
|
records_inserted = :records_inserted,
|
|
records_updated = :records_updated,
|
|
status = 'error',
|
|
message = :message
|
|
WHERE id = :id
|
|
");
|
|
$stmtImportError->execute([
|
|
':records_received' => $totalImported,
|
|
':records_inserted' => $recordsInserted,
|
|
':records_updated' => $recordsUpdated,
|
|
':message' => substr($e->getMessage(), 0, 65000),
|
|
':id' => $importRunId,
|
|
]);
|
|
}
|
|
|
|
writeMessage('Import fehlgeschlagen bei ' . (string) ($source['label'] ?? ('Seite ' . $pageNumber)) . ': ' . $e->getMessage(), true);
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
if ($dryRun) {
|
|
writeMessage("Dry-Run abgeschlossen. Parsbare Beobachtungen: {$totalImported}.");
|
|
exit(0);
|
|
}
|
|
|
|
if ($importRunId !== null) {
|
|
$stmtImportFinish = $pdo->prepare("
|
|
UPDATE app_import_runs
|
|
SET
|
|
finished_at = NOW(),
|
|
records_received = :records_received,
|
|
records_inserted = :records_inserted,
|
|
records_updated = :records_updated,
|
|
status = 'success',
|
|
message = NULL
|
|
WHERE id = :id
|
|
");
|
|
$stmtImportFinish->execute([
|
|
':records_received' => $totalImported,
|
|
':records_inserted' => $recordsInserted,
|
|
':records_updated' => $recordsUpdated,
|
|
':id' => $importRunId,
|
|
]);
|
|
}
|
|
|
|
writeMessage("Import abgeschlossen. Verarbeitete Beobachtungen: {$totalImported}. DB-Upserts: {$totalUpserts}.");
|
|
exit(0);
|
|
|
|
function parseCliArguments(array $argv): array
|
|
{
|
|
$options = [
|
|
'pages' => 1,
|
|
'dry-run' => false,
|
|
'help' => false,
|
|
];
|
|
|
|
foreach (array_slice($argv, 1) as $argument) {
|
|
if ($argument === '--dry-run') {
|
|
$options['dry-run'] = true;
|
|
continue;
|
|
}
|
|
|
|
if ($argument === '--help' || $argument === '-h') {
|
|
$options['help'] = true;
|
|
continue;
|
|
}
|
|
|
|
if (str_starts_with($argument, '--pages=')) {
|
|
$options['pages'] = (int) substr($argument, 8);
|
|
continue;
|
|
}
|
|
|
|
if (str_starts_with($argument, '--url=')) {
|
|
$options['url'] = (string) substr($argument, 6);
|
|
continue;
|
|
}
|
|
|
|
if (str_starts_with($argument, '--html=')) {
|
|
$options['html'] = (string) substr($argument, 7);
|
|
continue;
|
|
}
|
|
|
|
if (str_starts_with($argument, '--dir=')) {
|
|
$options['dir'] = (string) substr($argument, 6);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return $options;
|
|
}
|
|
|
|
function getCliArguments(): array
|
|
{
|
|
if (isset($GLOBALS['argv']) && is_array($GLOBALS['argv'])) {
|
|
return $GLOBALS['argv'];
|
|
}
|
|
|
|
if (isset($_SERVER['argv']) && is_array($_SERVER['argv'])) {
|
|
return $_SERVER['argv'];
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
function writeUsage(): void
|
|
{
|
|
$usageLines = [
|
|
'Nutzung:',
|
|
' php scripts/import/import_tcrb.php',
|
|
' php scripts/import/import_tcrb.php --pages=3',
|
|
' php scripts/import/import_tcrb.php --html=C:\pfad\webobs.html --dry-run',
|
|
' php scripts/import/import_tcrb.php --dir=C:\Users\ich\Desktop\TCrB',
|
|
'',
|
|
'Optionen:',
|
|
' --pages=N Anzahl der zu ladenden Ergebnisseiten, Standard: 1',
|
|
' --url=URL Basis-URL fuer den Abruf, Standard: AAVSO T CrB CCD',
|
|
' --html=PFAD Liest die erste Seite aus einer lokalen HTML-Datei statt aus dem Web',
|
|
' --dir=PFAD Liest alle HTML/TXT-Dateien aus einem Ordner in Dateinamen-Reihenfolge ein',
|
|
' --dry-run Parst nur und schreibt nichts in die DB',
|
|
' --help Diese Hilfe anzeigen',
|
|
];
|
|
|
|
foreach ($usageLines as $line) {
|
|
writeMessage($line);
|
|
}
|
|
}
|
|
|
|
function normalizeDbConfig(array $config): array
|
|
{
|
|
$config['port'] = isset($config['port']) && (int) $config['port'] > 0 ? (int) $config['port'] : 3306;
|
|
$config['charset'] = isset($config['charset']) && $config['charset'] !== '' ? (string) $config['charset'] : 'utf8mb4';
|
|
|
|
return $config;
|
|
}
|
|
|
|
function tableExists(PDO $pdo, string $tableName): bool
|
|
{
|
|
$stmt = $pdo->prepare('
|
|
SELECT COUNT(*)
|
|
FROM information_schema.TABLES
|
|
WHERE TABLE_SCHEMA = DATABASE()
|
|
AND TABLE_NAME = :table_name
|
|
');
|
|
$stmt->execute([':table_name' => $tableName]);
|
|
|
|
return (int) $stmt->fetchColumn() > 0;
|
|
}
|
|
|
|
function ensureImportRunsTableExists(PDO $pdo): void
|
|
{
|
|
$pdo->exec("
|
|
CREATE TABLE IF NOT EXISTS `app_import_runs` (
|
|
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
|
`source` varchar(32) NOT NULL,
|
|
`source_url` varchar(500) DEFAULT NULL,
|
|
`started_at` datetime NOT NULL,
|
|
`finished_at` datetime DEFAULT NULL,
|
|
`records_received` int(11) NOT NULL DEFAULT 0,
|
|
`records_inserted` int(11) NOT NULL DEFAULT 0,
|
|
`records_updated` int(11) NOT NULL DEFAULT 0,
|
|
`status` varchar(32) NOT NULL,
|
|
`message` text DEFAULT NULL,
|
|
PRIMARY KEY (`id`),
|
|
KEY `idx_app_import_runs_source` (`source`),
|
|
KEY `idx_app_import_runs_started_at` (`started_at`)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
|
");
|
|
}
|
|
|
|
function loadHtmlFromFile(string $path): string
|
|
{
|
|
if (!is_file($path)) {
|
|
throw new RuntimeException("HTML-Datei nicht gefunden: {$path}");
|
|
}
|
|
|
|
$html = file_get_contents($path);
|
|
if ($html === false || trim($html) === '') {
|
|
throw new RuntimeException("HTML-Datei konnte nicht gelesen werden: {$path}");
|
|
}
|
|
|
|
return $html;
|
|
}
|
|
|
|
function findHtmlFilesInDirectory(string $directoryPath): array
|
|
{
|
|
if (!is_dir($directoryPath)) {
|
|
throw new RuntimeException("Ordner nicht gefunden: {$directoryPath}");
|
|
}
|
|
|
|
$entries = scandir($directoryPath);
|
|
if ($entries === false) {
|
|
throw new RuntimeException("Ordner konnte nicht gelesen werden: {$directoryPath}");
|
|
}
|
|
|
|
$filePaths = [];
|
|
foreach ($entries as $entry) {
|
|
if ($entry === '.' || $entry === '..') {
|
|
continue;
|
|
}
|
|
|
|
$fullPath = rtrim($directoryPath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $entry;
|
|
if (!is_file($fullPath)) {
|
|
continue;
|
|
}
|
|
|
|
$extension = strtolower((string) pathinfo($fullPath, PATHINFO_EXTENSION));
|
|
if (!in_array($extension, ['html', 'htm', 'txt'], true)) {
|
|
continue;
|
|
}
|
|
|
|
$filePaths[] = $fullPath;
|
|
}
|
|
|
|
natsort($filePaths);
|
|
|
|
return array_values($filePaths);
|
|
}
|
|
|
|
function buildPagedUrl(string $baseUrl, int $pageNumber): string
|
|
{
|
|
$parts = parse_url($baseUrl);
|
|
if ($parts === false) {
|
|
throw new RuntimeException('Ungueltige URL: ' . $baseUrl);
|
|
}
|
|
|
|
$query = [];
|
|
parse_str((string) ($parts['query'] ?? ''), $query);
|
|
$query['page'] = $pageNumber;
|
|
|
|
$scheme = $parts['scheme'] ?? 'https';
|
|
$host = $parts['host'] ?? '';
|
|
$path = $parts['path'] ?? '';
|
|
$queryString = http_build_query($query);
|
|
|
|
return $scheme . '://' . $host . $path . ($queryString !== '' ? '?' . $queryString : '');
|
|
}
|
|
|
|
function downloadUrl(string $url): string
|
|
{
|
|
if (DIRECTORY_SEPARATOR === '\\' && function_exists('shell_exec')) {
|
|
$urlBase64 = base64_encode($url);
|
|
$powerShellScript = str_replace(
|
|
'__URL_BASE64__',
|
|
$urlBase64,
|
|
<<<'PS'
|
|
$ErrorActionPreference = 'Stop'
|
|
$ProgressPreference = 'SilentlyContinue'
|
|
$url = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('__URL_BASE64__'))
|
|
$response = Invoke-WebRequest -Uri $url -UseBasicParsing -Headers @{ 'User-Agent' = 'SkyView-TCrB-Importer/1.0' }
|
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
$response.Content
|
|
PS
|
|
);
|
|
|
|
$encodedCommandBinary = function_exists('iconv')
|
|
? iconv('UTF-8', 'UTF-16LE', $powerShellScript)
|
|
: $powerShellScript;
|
|
$encodedCommand = base64_encode($encodedCommandBinary === false ? $powerShellScript : $encodedCommandBinary);
|
|
$command = 'powershell -NoProfile -ExecutionPolicy Bypass -EncodedCommand ' . $encodedCommand;
|
|
$output = shell_exec($command);
|
|
|
|
if ($output !== null && trim($output) !== '') {
|
|
return $output;
|
|
}
|
|
}
|
|
|
|
if (function_exists('curl_init')) {
|
|
$ch = curl_init($url);
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_FOLLOWLOCATION => true,
|
|
CURLOPT_CONNECTTIMEOUT => 15,
|
|
CURLOPT_TIMEOUT => 60,
|
|
CURLOPT_USERAGENT => 'SkyView-TCrB-Importer/1.0',
|
|
]);
|
|
|
|
$response = curl_exec($ch);
|
|
if ($response === false) {
|
|
$error = curl_error($ch);
|
|
throw new RuntimeException('Download fehlgeschlagen: ' . $error);
|
|
}
|
|
|
|
$httpCode = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
|
|
|
|
if ($httpCode < 200 || $httpCode >= 300) {
|
|
throw new RuntimeException("Download fehlgeschlagen. HTTP-Status: {$httpCode}");
|
|
}
|
|
|
|
return (string) $response;
|
|
}
|
|
|
|
$context = stream_context_create([
|
|
'http' => [
|
|
'method' => 'GET',
|
|
'header' => implode("\r\n", [
|
|
'User-Agent: SkyView-TCrB-Importer/1.0',
|
|
]),
|
|
'timeout' => 60,
|
|
'ignore_errors' => true,
|
|
],
|
|
]);
|
|
|
|
$response = @file_get_contents($url, false, $context);
|
|
if ($response === false) {
|
|
throw new RuntimeException('Download fehlgeschlagen: keine Antwort erhalten.');
|
|
}
|
|
|
|
return $response;
|
|
}
|
|
|
|
function parseWebObsPage(string $html): array
|
|
{
|
|
libxml_use_internal_errors(true);
|
|
|
|
$dom = new DOMDocument();
|
|
$loaded = $dom->loadHTML($html);
|
|
libxml_clear_errors();
|
|
|
|
if ($loaded === false) {
|
|
throw new RuntimeException('HTML konnte nicht geparst werden.');
|
|
}
|
|
|
|
$xpath = new DOMXPath($dom);
|
|
|
|
$targetName = '';
|
|
$obsInfoNode = $xpath->query("//p[@id='obsinfo']")->item(0);
|
|
if ($obsInfoNode instanceof DOMNode) {
|
|
$obsInfoText = normalizeWhitespace($obsInfoNode->textContent);
|
|
if (preg_match('/for\s+(.+?)\s+from\s+/i', $obsInfoText, $matches) === 1) {
|
|
$targetName = trim($matches[1]);
|
|
}
|
|
}
|
|
|
|
$observations = [];
|
|
$rowNodes = $xpath->query("//table[contains(@class,'observations')]/tbody/tr[contains(@class,'obs')]");
|
|
if ($rowNodes === false) {
|
|
return [
|
|
'target_name' => $targetName,
|
|
'observations' => [],
|
|
];
|
|
}
|
|
|
|
foreach ($rowNodes as $rowNode) {
|
|
if (!$rowNode instanceof DOMElement) {
|
|
continue;
|
|
}
|
|
|
|
$rowId = trim((string) $rowNode->getAttribute('id'));
|
|
$tdNodes = $xpath->query('./td', $rowNode);
|
|
if ($tdNodes === false || $tdNodes->length < 8) {
|
|
continue;
|
|
}
|
|
|
|
$starName = normalizeWhitespace($tdNodes->item(1)?->textContent ?? '');
|
|
$jdText = normalizeWhitespace($tdNodes->item(2)?->textContent ?? '');
|
|
$calendarDateText = normalizeWhitespace($tdNodes->item(3)?->textContent ?? '');
|
|
$magnitudeText = normalizeWhitespace($tdNodes->item(4)?->textContent ?? '');
|
|
$magnitudeErrorText = normalizeWhitespace($tdNodes->item(5)?->textContent ?? '');
|
|
$filterCode = normalizeWhitespace($tdNodes->item(6)?->textContent ?? '');
|
|
$observerCode = normalizeWhitespace($tdNodes->item(7)?->textContent ?? '');
|
|
|
|
if ($jdText === '' || $magnitudeText === '') {
|
|
continue;
|
|
}
|
|
|
|
$detailRow = $xpath->query("//tr[@id='{$rowId}-detail']")->item(0);
|
|
$notesRaw = null;
|
|
|
|
if ($detailRow instanceof DOMElement) {
|
|
$detailCells = $xpath->query(".//table[contains(@class,'obs-detail')]/tbody/tr/td", $detailRow);
|
|
if ($detailCells !== false && $detailCells->length >= 6) {
|
|
$notesRaw = normalizeWhitespace($detailCells->item(5)?->textContent ?? '');
|
|
$notesRaw = $notesRaw !== '' && $notesRaw !== '—' ? $notesRaw : null;
|
|
}
|
|
}
|
|
|
|
$magnitude = parseStrictDecimal($magnitudeText);
|
|
if ($magnitude === null) {
|
|
continue;
|
|
}
|
|
|
|
$jd = (float) str_replace(',', '.', $jdText);
|
|
$observedAtUtc = jdToUtcString($jd);
|
|
|
|
$observations[] = [
|
|
'source_row_key' => $rowId !== '' ? $rowId : null,
|
|
'star_name' => $starName !== '' ? $starName : ($targetName !== '' ? $targetName : 'T CrB'),
|
|
'jd' => number_format($jd, 6, '.', ''),
|
|
'observed_at_utc' => $observedAtUtc,
|
|
'calendar_date_text' => $calendarDateText !== '' ? $calendarDateText : null,
|
|
'magnitude' => $magnitude,
|
|
'magnitude_error' => $magnitudeErrorText !== '' ? parseStrictDecimal($magnitudeErrorText) : null,
|
|
'filter_code' => $filterCode !== '' ? $filterCode : null,
|
|
'observer_code' => $observerCode !== '' ? $observerCode : null,
|
|
'notes_raw' => $notesRaw,
|
|
];
|
|
}
|
|
|
|
return [
|
|
'target_name' => $targetName,
|
|
'observations' => $observations,
|
|
];
|
|
}
|
|
|
|
function normalizeWhitespace(string $value): string
|
|
{
|
|
$value = html_entity_decode($value, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
|
$value = preg_replace('/\s+/u', ' ', trim($value));
|
|
|
|
return $value ?? '';
|
|
}
|
|
|
|
function normalizeNumeric(string $value): string
|
|
{
|
|
return str_replace(',', '.', trim($value));
|
|
}
|
|
|
|
function parseStrictDecimal(string $value): ?string
|
|
{
|
|
$normalized = normalizeNumeric($value);
|
|
if ($normalized === '') {
|
|
return null;
|
|
}
|
|
|
|
if (preg_match('/^[+-]?\d+(?:\.\d+)?$/', $normalized) !== 1) {
|
|
return null;
|
|
}
|
|
|
|
return $normalized;
|
|
}
|
|
|
|
function jdToUtcString(float $jd): string
|
|
{
|
|
$jd += 0.5;
|
|
$z = (int) floor($jd);
|
|
$f = $jd - $z;
|
|
|
|
if ($z >= 2299161) {
|
|
$alpha = (int) floor(($z - 1867216.25) / 36524.25);
|
|
$a = $z + 1 + $alpha - (int) floor($alpha / 4);
|
|
} else {
|
|
$a = $z;
|
|
}
|
|
|
|
$b = $a + 1524;
|
|
$c = (int) floor(($b - 122.1) / 365.25);
|
|
$d = (int) floor(365.25 * $c);
|
|
$e = (int) floor(($b - $d) / 30.6001);
|
|
|
|
$day = $b - $d - (int) floor(30.6001 * $e) + $f;
|
|
$month = $e < 14 ? $e - 1 : $e - 13;
|
|
$year = $month > 2 ? $c - 4716 : $c - 4715;
|
|
|
|
$dayInteger = (int) floor($day);
|
|
$dayFraction = $day - $dayInteger;
|
|
|
|
$totalSeconds = (int) round($dayFraction * 86400);
|
|
if ($totalSeconds >= 86400) {
|
|
$totalSeconds = 86399;
|
|
}
|
|
|
|
$hours = intdiv($totalSeconds, 3600);
|
|
$minutes = intdiv($totalSeconds % 3600, 60);
|
|
$seconds = $totalSeconds % 60;
|
|
|
|
return sprintf(
|
|
'%04d-%02d-%02d %02d:%02d:%02d',
|
|
$year,
|
|
$month,
|
|
$dayInteger,
|
|
$hours,
|
|
$minutes,
|
|
$seconds
|
|
);
|
|
}
|
|
|
|
function writeMessage(string $message, bool $isError = false): void
|
|
{
|
|
$prefix = '[' . date('Y-m-d H:i:s') . '] ';
|
|
$stream = openOutputStream($isError);
|
|
fwrite($stream, $prefix . $message . PHP_EOL);
|
|
}
|
|
|
|
/**
|
|
* `STDOUT` und `STDERR` sind in manchen CGI-/Web-Kontexten nicht definiert.
|
|
* Daher greifen wir auf die entsprechenden php:// Streams zurueck.
|
|
*
|
|
* @return resource
|
|
*/
|
|
function openOutputStream(bool $isError)
|
|
{
|
|
if ($isError && defined('STDERR')) {
|
|
return STDERR;
|
|
}
|
|
|
|
if (!$isError && defined('STDOUT')) {
|
|
return STDOUT;
|
|
}
|
|
|
|
$stream = fopen($isError ? 'php://stderr' : 'php://stdout', 'wb');
|
|
|
|
if ($stream === false) {
|
|
throw new RuntimeException('Ausgabestream konnte nicht geoeffnet werden.');
|
|
}
|
|
|
|
return $stream;
|
|
}
|