'/analysen/kaufen', 'hold' => '/analysen/halten', 'sell' => '/analysen/verkaufen' ]; public function __construct($options = []) { $this->baseUrl = 'https://www.finanzen.net'; $this->userAgent = $options['user_agent'] ?? 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'; $this->timeout = $options['timeout'] ?? 30; } public function getUrlForRatingType($type) { $type = strtolower($type); return isset($this->ratingUrls[$type]) ? $this->baseUrl . $this->ratingUrls[$type] : $this->baseUrl . '/analysen/kaufen'; } private function fetchHtml($ratingType = 'buy', $page = 1) { $url = $this->getUrlForRatingType($ratingType); if ($page > 1) { $url .= '?p=' . $page; } $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout); curl_setopt($ch, CURLOPT_ENCODING, ''); curl_setopt($ch, CURLOPT_USERAGENT, $this->userAgent); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language: de-DE,de;q=0.9,en;q=0.8', 'Accept-Encoding: gzip, deflate, br', 'Connection: keep-alive', ]); $html = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $error = curl_error($ch); curl_close($ch); if ($httpCode !== 200) { throw new Exception("HTTP $httpCode: Failed to fetch page - $error"); } return $html; } /** * Fetch current price from Yahoo Finance */ private function fetchPriceFromYahoo($companyName) { // Clean company name for search $searchTerm = preg_replace('/\s+(vz\.|vz|pref|preferred|plc|ltd|inc|corp|gmbh|ag|se|adr)$/i', '', $companyName); $searchTerm = preg_replace('/\s*\([^)]*\)/', '', $searchTerm); // First search for the symbol $searchUrl = "https://query1.finance.yahoo.com/v1/finance/search?q=" . urlencode($searchTerm) . ""esCount=3&newsCount=0"; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $searchUrl); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_TIMEOUT, 10); curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0'); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode === 200 && $response) { $data = json_decode($response, true); if (isset($data['quotes'][0]['symbol'])) { $symbol = $data['quotes'][0]['symbol']; // Now get the price for this symbol $priceUrl = "https://query1.finance.yahoo.com/v8/finance/chart/{$symbol}"; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $priceUrl); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_TIMEOUT, 10); curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0'); $priceResponse = curl_exec($ch); curl_close($ch); if ($priceResponse) { $priceData = json_decode($priceResponse, true); if (isset($priceData['chart']['result'][0]['meta'])) { $meta = $priceData['chart']['result'][0]['meta']; $currentPrice = $meta['regularMarketPrice'] ?? null; $previousClose = $meta['previousClose'] ?? null; $change = ($currentPrice && $previousClose) ? $currentPrice - $previousClose : null; $changePercent = ($change && $previousClose) ? ($change / $previousClose) * 100 : null; return [ 'symbol' => $symbol, 'price' => $currentPrice, 'change' => $change, 'change_percent' => $changePercent, 'currency' => $meta['currency'] ?? 'USD' ]; } } } } return null; } private function parseRatingsFromHtml($html) { $dom = new DOMDocument(); libxml_use_internal_errors(true); $dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'), LIBXML_NOERROR); libxml_clear_errors(); $xpath = new DOMXPath($dom); $rows = $xpath->query("//tr[contains(@class, 'table__tr')]"); $ratings = []; foreach ($rows as $row) { $cells = $xpath->query(".//td[contains(@class, 'table__td')]", $row); if ($cells->length < 4) continue; $date = trim($cells->item(0)->textContent); $link = $xpath->query(".//a", $cells->item(2)); if ($link->length === 0) continue; $linkText = trim($link->item(0)->textContent); $analyst = trim($cells->item(3)->textContent); $linkHref = $link->item(0)->getAttribute('href'); $analysisUrl = $this->baseUrl . $linkHref; if (preg_match('/^(.+?)\s+(Overweight|Outperform|Kaufen|Buy|Neutral|Sell|Verkaufen|Halten|Hold)$/i', $linkText, $matches)) { $companyName = $this->normalizeCompanyName(trim($matches[1])); $symbol = urlencode($companyName); // Fetch price from Yahoo Finance $priceInfo = $this->fetchPriceFromYahoo($companyName); $ratings[] = [ 'date' => $this->normalizeDate($date), 'date_display' => $date, 'company' => $companyName, 'symbol' => $companyName, 'symbol_urlencoded' => $symbol, 'rating' => $this->normalizeRating($matches[2]), 'rating_original' => $matches[2], 'analyst' => $this->normalizeAnalystName($analyst), 'analysis_url' => $analysisUrl, 'process_url' => "process.php?symbol=" . $symbol, 'link_text' => $linkText, 'price' => $priceInfo['price'] ?? null, 'price_change' => $priceInfo['change'] ?? null, 'price_change_percent' => $priceInfo['change_percent'] ?? null, 'currency' => $priceInfo['currency'] ?? 'USD', 'timestamp' => time() ]; } } return $ratings; } private function hasNextPage($html) { $dom = new DOMDocument(); libxml_use_internal_errors(true); $dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'), LIBXML_NOERROR); libxml_clear_errors(); $xpath = new DOMXPath($dom); $nextLinks = $xpath->query("//a[contains(@class, 'next') or contains(text(), 'nΓ€chste') or contains(@rel, 'next')]"); return $nextLinks->length > 0; } private function getTotalPages($html) { $dom = new DOMDocument(); libxml_use_internal_errors(true); $dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'), LIBXML_NOERROR); libxml_clear_errors(); $xpath = new DOMXPath($dom); $paginationText = $xpath->query("//div[contains(@class, 'pagination')]//text()"); foreach ($paginationText as $text) { if (preg_match('/von\s+(\d+)/i', $text->nodeValue, $matches)) { return (int)$matches[1]; } } return null; } public function parsePage($ratingType = 'buy', $page = 1) { $html = $this->fetchHtml($ratingType, $page); return [ 'ratings' => $this->parseRatingsFromHtml($html), 'has_next' => $this->hasNextPage($html), 'page' => $page, 'total_pages' => $this->getTotalPages($html) ]; } private function normalizeDate($date) { if (preg_match('/(\d{2})\.(\d{2})\.(\d{2})/', $date, $matches)) { return '20' . $matches[3] . '-' . $matches[2] . '-' . $matches[1]; } return $date; } private function normalizeCompanyName($name) { return trim(preg_replace('/\s+/', ' ', $name)); } private function normalizeRating($rating) { $mapping = ['Kaufen' => 'Buy', 'Verkaufen' => 'Sell', 'Halten' => 'Hold', 'Hold' => 'Hold']; return $mapping[$rating] ?? $rating; } private function normalizeAnalystName($name) { return trim(html_entity_decode(preg_replace('/\s+/', ' ', $name), ENT_QUOTES | ENT_HTML5, 'UTF-8')); } } // ==================== WEB INTERFACE ==================== $action = isset($_GET['action']) ? strtolower($_GET['action']) : 'buy'; $page = isset($_GET['page']) ? (int)$_GET['page'] : 1; $format = isset($_GET['format']) ? $_GET['format'] : 'html'; $sortBy = isset($_GET['sort']) ? $_GET['sort'] : 'price_desc'; $validActions = ['buy', 'hold', 'sell']; if (!in_array($action, $validActions)) { $action = 'buy'; } $parser = new FinanzenRatingsParser(); // Handle AJAX requests for infinite scroll if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') { header('Content-Type: application/json'); try { $result = $parser->parsePage($action, $page); // Sort by price if requested if (!empty($result['ratings']) && $sortBy === 'price_desc') { usort($result['ratings'], function($a, $b) { return ($b['price'] ?? 0) <=> ($a['price'] ?? 0); }); } elseif (!empty($result['ratings']) && $sortBy === 'price_asc') { usort($result['ratings'], function($a, $b) { return ($a['price'] ?? 0) <=> ($b['price'] ?? 0); }); } echo json_encode($result); } catch (Exception $e) { echo json_encode(['error' => $e->getMessage()]); } exit; } // Handle export formats if ($format === 'json' && !isset($_SERVER['HTTP_X_REQUESTED_WITH'])) { header('Content-Type: application/json'); header('Content-Disposition: attachment; filename="ratings_' . $action . '_' . date('Y-m-d') . '.json"'); $result = $parser->parsePage($action, 1); $allRatings = $result['ratings']; $currentPage = 2; while ($result['has_next'] && $currentPage <= 10) { $result = $parser->parsePage($action, $currentPage); $allRatings = array_merge($allRatings, $result['ratings']); $currentPage++; usleep(300000); } echo json_encode($allRatings, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); exit; } if ($format === 'csv' && !isset($_SERVER['HTTP_X_REQUESTED_WITH'])) { header('Content-Type: text/csv'); header('Content-Disposition: attachment; filename="ratings_' . $action . '_' . date('Y-m-d') . '.csv"'); $result = $parser->parsePage($action, 1); $allRatings = $result['ratings']; $currentPage = 2; while ($result['has_next'] && $currentPage <= 10) { $result = $parser->parsePage($action, $currentPage); $allRatings = array_merge($allRatings, $result['ratings']); $currentPage++; usleep(300000); } $fp = fopen('php://output', 'w'); fwrite($fp, "\xEF\xBB\xBF"); fputcsv($fp, ['Date', 'Company', 'Rating', 'Analyst', 'Price', 'Change', 'Analysis URL', 'Process URL']); foreach ($allRatings as $rating) { fputcsv($fp, [ $rating['date_display'], $rating['company'], $rating['rating'], $rating['analyst'], $rating['price'] ? $rating['currency'] . ' ' . number_format($rating['price'], 2) : 'N/A', $rating['price_change'] ? ($rating['price_change'] > 0 ? '+' : '') . number_format($rating['price_change'], 2) . ' (' . number_format($rating['price_change_percent'], 2) . '%)' : 'N/A', $rating['analysis_url'], $rating['process_url'] ]); } fclose($fp); exit; } $actionLabels = [ 'buy' => ['Kaufen (Buy)', 'positive', 'π’', '#28a745'], 'hold' => ['Halten (Hold)', 'neutral', 'π‘', '#ffc107'], 'sell' => ['Verkaufen (Sell)', 'negative', 'π΄', '#dc3545'] ]; $currentLabel = $actionLabels[$action]; ?>
Loading more ratings...