/
scoder
/
tplink_parser
Обзор
Документация
Войти
/
scoder
/
tplink_parser
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
develop
lib/tplinkparser.php
642 строки
22 KB
scoder
Добавлнение логирования и файла логов
16 июл 2026, 16:58
16 июл 2026, 16:58
05627be
Код
Авторство
О чём код?
<?php namespace Tplink\Parser; use \Tplink\Parser\IblockCreator; class TplinkParser { /** * Обходит страницы, автоматически определяет число страниц, и парсит товары. * @param string $baseUrl — базовый URL категории или раздела. * @param int $maxPages — максимум страниц для обхода. * @return array — массив товаров. */ // public static function parseProductPages($baseUrl, $maxPages = 4) // { // $iblockId = IblockCreator::installStructure(); // if ($iblockId <= 0) // return false; // $allProducts = []; // // Загружаем первую страницу, чтобы определить число страниц // echo "Обрабатываю: {$baseUrl}\n"; // $firstPageHtml = self::fetchHtml($baseUrl); // if (!$firstPageHtml) { // echo "Нет доступа к странице.\n"; // return $allProducts; // } // $maxPage = self::getMaxPage($firstPageHtml); // if ($maxPage > $maxPages) { // $maxPage = $maxPages; // } // for ($page = 1; $page <= $maxPage; $page++) { // $url = $baseUrl . ($page > 1 ? '?page=' . $page : ''); // echo "Обрабатываю: {$url}\n"; // $html = self::fetchHtml($url); // if (!$html) { // echo "Не удалось загрузить страницу: {$url}\n"; // continue; // } // $productsOnPage = self::parseProductList($html, $iblockId); // $countGoods = count($productsOnPage); // echo "Страница {$page}: {$countGoods} товаров\n"; // $allProducts = array_merge($allProducts, $productsOnPage); // } // echo "Обработано страниц: " . $maxPage . "\n"; // return $allProducts; // } public static function parseProductPages($baseUrl, $maxPages = 4) { $iblockId = IblockCreator::installStructure(); if ($iblockId <= 0) return false; $log = [ "started_at" => date('Y-m-d H:i:s'), "source_url" => $baseUrl, "finished_at" => null, "counters" => [ "new" => 0, "updated" => 0, "unchanged" => 0, "missing" => 0, "errors" => 0, ], "items" => [], "errors" => [], ]; $allProducts = []; echo "Обрабатываю: {$baseUrl}\n"; // Загружаем первую страницу, чтобы определить число страниц $firstPageHtml = self::fetchHtml($baseUrl); if (!$firstPageHtml) { $log['errors'][] = "Не удалось загрузить стартовую страницу."; $log['finished_at'] = date('Y-m-d H:i:s'); self::saveLog($log); return $allProducts; } $maxPage = self::getMaxPage($firstPageHtml); if ($maxPage > $maxPages) { $maxPage = $maxPages; } for ($page = 1; $page <= $maxPage; $page++) { $url = $baseUrl . ($page > 1 ? '?page=' . $page : ''); echo "Обрабатываю: {$url}\n"; $html = self::fetchHtml($url); if (!$html) { $log['errors'][] = "Не удалось загрузить страницу: {$url}"; continue; } $productsOnPage = self::parseProductList($html, $iblockId); $countGoods = count($productsOnPage); echo "Страница {$page}: {$countGoods} товаров\n"; foreach ($productsOnPage as $product) { $log['items'][] = [ 'article' => isset($product['model']) ? $product['model'] : '', 'status' => 'parsed', // тут можно менять при обработке 'url' => isset($product['link']) ? $product['link'] : '', 'changed_fields' => [], // при реальных изменениях сюда можно добавлять ]; } // Обновляем счетчик товаров $log['counters']['new'] += $countGoods; } $log['finished_at'] = date('Y-m-d H:i:s'); // Сохраняем лог self::saveLog($log); echo "Обработано страниц: " . $maxPage . "\n"; return $allProducts; } // Метод для сохранения лога protected static function saveLog($log) { $logFileName = 'tplink_import_' . date('Y-m-d_H-i-s') . '.json'; $logFilePath = $_SERVER['DOCUMENT_ROOT'] . $logFileName; file_put_contents($logFileName, json_encode($log, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); } /** * Получает максимальное число страниц из HTML пагинации. */ protected static function getMaxPage($html) { $dom = new \DOMDocument(); libxml_use_internal_errors(true); $dom->loadHTML($html); libxml_clear_errors(); $xpath = new \DOMXPath($dom); $pageNodes = $xpath->query("//li[contains(@class, 'tp-product-pagination-item')]"); $maxPage = 1; foreach ($pageNodes as $li) { $text = trim($li->textContent); if (is_numeric($text)) { $num = (int)$text; if ($num > $maxPage) { $maxPage = $num; } } } return $maxPage; } /** * Вспомогательный метод для получения HTML по URL. */ protected static function fetchHtml($url) { $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0'); $html = curl_exec($ch); curl_close($ch); return $html ?: false; } /** * Парсит список товаров со страницы. */ protected static function parseProductList($html, $iblockId = 0) { $products = []; $dom = new \DOMDocument(); libxml_use_internal_errors(true); $dom->loadHTML($html); libxml_clear_errors(); $xpath = new \DOMXPath($dom); $productNodes = $xpath->query("//*[contains(@class, 'product-item')]"); foreach ($productNodes as $product) { // Получение ссылки $linkNodes = $xpath->query(".//a[contains(@class, 'ga-click tp-product-link')]", $product); $link = ''; if ($linkNodes->length > 0) { $href = $linkNodes->item(0)->getAttribute('href'); $link = (strpos($href, 'http') !== 0) ? 'https://www.tp-link.com' . $href : $href; } // Название $titleNodes = $xpath->query(".//*[contains(@class, 'product-title')]", $product); $name = $titleNodes->length > 0 ? trim($titleNodes->item(0)->textContent) : 'Нет названия'; // Модель $modelNodes = $xpath->query(".//div[contains(@class, 'tp-product-model')]", $product); $model = $modelNodes->length > 0 ? trim($modelNodes->item(0)->textContent) : ''; // Характеристики $attributes = []; $specDivs = $xpath->query(".//div[contains(@class, 'tp-product-spec') and @aria-hidden='true']", $product); if ($specDivs->length > 0) { $liNodes = $xpath->query(".//ul/li", $specDivs->item(0)); foreach ($liNodes as $li) { $attributes[] = trim($li->textContent); } } // Детальная страница $supportLink = ''; if ($link) { $supportLink = self::findSupportLink($link); } // Обработка поддержки и версий $versions = []; if ($supportLink) { // Получаем supportHtml $supportHtml = self::fetchHtml($supportLink); if ($supportHtml) { $versions = self::parseVersions($supportHtml); // Проверяем есть ли версии if (!empty($versions)) { // Для каждой версии — ищем ресурсы foreach ($versions as &$version) { if (!empty($version['link'])) { $verHtml = self::fetchHtml($version['link']); if ($verHtml) { $version['resources'] = self::parseDownloadResources($verHtml); } else { $version['resources'] = []; } } else { $version['resources'] = []; } } } else { // Версий нет — ищем ресурсы на support странице $resourcesFromSupportPage = self::parseDownloadResources($supportHtml); $versions[] = [ 'resources' => $resourcesFromSupportPage ]; // Можно присоединить как отдельный ключ, например // $supportResources = $resourcesFromSupportPage; } } } // Получение категории $category = self::parseCategory($link); // Получение свойств $properties = self::parseProperties($link); $itemData = [ 'name' => $name, 'model' => $model, 'link' => $link, 'attributes' => $attributes, 'category' => $category, 'properties' => $properties, 'versions' => $versions, ]; self::saveElements($itemData, $iblockId); $products[] = $itemData; } return $products; } // Вспомогательные методы: protected static function parseCategory($productUrl) { if (!$productUrl) return ''; $html = self::fetchHtml($productUrl); if (!$html) return ''; $dom = new \DOMDocument(); libxml_use_internal_errors(true); $dom->loadHTML($html); libxml_clear_errors(); $xpath = new \DOMXPath($dom); $divs = $xpath->query("//div[contains(@class, 'product-message')]"); if ($divs->length > 0) { $aTags = $xpath->query(".//a", $divs->item(0)); if ($aTags->length > 0) { return trim($aTags->item(0)->textContent); } } return ''; } protected static function parseProperties($productUrl) { $props = []; if (!$productUrl) return $props; $html = self::fetchHtml($productUrl); if (!$html) return $props; $dom = new \DOMDocument(); libxml_use_internal_errors(true); $dom->loadHTML($html); libxml_clear_errors(); $xpath = new \DOMXPath($dom); $divs = $xpath->query("//div[contains(@class, 'specifications')]"); if ($divs->length > 0) { $tables = $xpath->query(".//table", $divs->item(0)); foreach ($tables as $table) { $rows = $xpath->query(".//tr", $table); foreach ($rows as $row) { $thNode = $xpath->query(".//th", $row); $tdNode = $xpath->query(".//td", $row); if ($thNode->length > 0 && $tdNode->length > 0) { $key = trim($thNode->item(0)->textContent); $rawValue = trim($tdNode->item(0)->textContent); if (strpos($rawValue, "\n") !== false) { $value = array_map('trim', preg_split('/\r?\n/', $rawValue)); } else { $value = $rawValue; } $props[$key] = $value; } } } } return $props; } protected static function findSupportLink($productUrl) { $html = self::fetchHtml($productUrl); if (!$html) return ''; $dom = new \DOMDocument(); libxml_use_internal_errors(true); $dom->loadHTML($html); libxml_clear_errors(); $xpath = new \DOMXPath($dom); $divs = $xpath->query("//div[contains(@class, 'product-menu')]"); if ($divs->length > 0) { $aTags = $xpath->query(".//a[contains(@class, 'support')]", $divs->item(0)); if ($aTags->length > 0) { $href = $aTags->item(0)->getAttribute('href'); return (strpos($href, 'http') !== 0) ? 'https://www.tp-link.com' . $href : $href; } } return ''; } protected static function parseVersions($html) { $versions = []; $dom = new \DOMDocument(); libxml_use_internal_errors(true); $dom->loadHTML($html); libxml_clear_errors(); $xpath = new \DOMXPath($dom); $listNodes = $xpath->query("//ul[@id='version-list']"); foreach ($listNodes as $ul) { $aTags = $xpath->query(".//a", $ul); foreach ($aTags as $a) { $href = $a->getAttribute('href'); $text = trim($a->textContent); $versions[] = [ 'name' => $text, 'link' => (strpos($href, 'http') === 0) ? $href : ('https://www.tp-link.com' . $href), ]; } } return $versions; } protected static function parseDownloadResources($html) { $resources = []; $dom = new \DOMDocument(); libxml_use_internal_errors(true); $dom->loadHTML($html); libxml_clear_errors(); $xpath = new \DOMXPath($dom); $dlNodes = $xpath->query("//dl[contains(@class, 'download-resource')]"); foreach ($dlNodes as $dl) { $liNodes = $xpath->query(".//li", $dl); $liTexts = []; foreach ($liNodes as $li) { $liTexts[] = trim($li->textContent); } $resources[] = $liTexts; } return $resources; } /** * Генерирует все комбинации названия модели, региона и версии железа. * Если регион и версия отсутствуют, возвращает только название модели. * * @param array $deviceData Полный массив устройства (содержит 'model' и 'versions') * @return array Плоский массив готовых строк комбинаций */ protected static function generateModelCombinations(array $deviceData): array { // Базовая проверка: если нет имени модели, возвращаем пустой массив if (empty($deviceData['model'])) { return []; } $modelName = trim($deviceData['model']); // Если нет массива версий, возвращаем просто имя модели if (empty($deviceData['versions']) || !is_array($deviceData['versions'])) { return [$modelName]; } $combinations = []; foreach ($deviceData['versions'] as $versionBlock) { // --- 1. ОПРЕДЕЛЕНИЕ ВЕРСИИ ОБОРУДОВАНИЯ --- $detectedVersion = null; // По умолчанию null if (!empty($versionBlock['name'])) { $detectedVersion = strtoupper(trim($versionBlock['name'])); } elseif (!empty($versionBlock['resources']) && is_array($versionBlock['resources'])) { foreach ($versionBlock['resources'] as $group) { if (!is_array($group)) continue; foreach ($group as $filename) { if (preg_match('/\bV\d+(?:\.\d+)?\b|_[vV]\d+(?:\.\d+)?_/u', $filename, $matches)) { // Берем $matches[0], так как preg_match возвращает массив строк $detectedVersion = strtoupper(trim($matches[0], '_')); break 2; // Версию нашли, выходим из циклов ресурсов } } } } // --- 2. ПОИСК ВСЕХ РЕГИОНОВ В ЭТОЙ ВЕРСИИ --- $regions = []; if (!empty($versionBlock['resources']) && is_array($versionBlock['resources'])) { foreach ($versionBlock['resources'] as $group) { if (!is_array($group)) continue; foreach ($group as $filename) { if (preg_match('/\(([A-Z]{2,4}[^\)]*)\)/u', $filename, $matches)) { // Берем $matches[1] (строку внутри скобок) $regions[] = trim($matches[1]); } } } } // Удаляем дубликаты регионов для текущей версии блока $uniqueRegions = array_values(array_unique($regions)); // --- 3. ГЕНЕРАЦИЯ СТРОКИ С УЧЕТОМ ОТСУТСТВИЯ ДАННЫХ --- if (!empty($uniqueRegions)) { // Если регионы ЕСТЬ foreach ($uniqueRegions as $region) { if ($detectedVersion) { // Есть и регион, и версия $combinations[] = "{$modelName}({$region}) {$detectedVersion}"; } else { // Есть только регион $combinations[] = "{$modelName}({$region})"; } } } else { // Если регионов НЕТ if ($detectedVersion) { // Региона нет, но есть версия $combinations[] = "{$modelName} {$detectedVersion}"; } else { // Нет ни региона, ни версии $combinations[] = $modelName; } } } // Возвращаем уникальные строки (на случай, если разные версии дали одинаковый текстовый результат) // Используем self:: для вызова, если это необходимо, но здесь просто возвращаем массив return array_values(array_unique($combinations)); } /** * Функция парсинга характеристик роутера. * * @param string $specsText Текст со свойствами устройства * @return array Массив с ключами WIFI_STANDARD, WIFI_SPEED, WAN_SPEED, LAN_PORTS */ protected static function parseRouterSpecsArray(array $specs): array { $result = [ 'WIFI_STANDARD' => null, 'WIFI_SPEED' => null, 'WAN_SPEED' => null, 'LAN_PORTS' => 0 ]; // 1. Определение стандарта Wi-Fi // Ищем в 'Скорость Wi-Fi' или в 'Стандарты' (если такой ключ есть) $wifiText = ($specs['Скорость Wi-Fi'] ?? '') . ' ' . ($specs['Стандарты'] ?? ''); $standardsOrder = [ 'be' => 'Wi-Fi 7 (802.11be)', 'ax' => 'Wi-Fi 6 (802.11ax)', 'ac' => 'Wi-Fi 5 (802.11ac)', 'n' => 'Wi-Fi 4 (802.11n)', 'g' => '802.11g', 'b' => '802.11b' ]; if (preg_match_all('/802\.11([a-z]+)/i', $wifiText, $matches)) { $foundLetters = array_map('strtolower', $matches[1]); foreach ($standardsOrder as $letter => $fullName) { if (in_array($letter, $foundLetters)) { $result['WIFI_STANDARD'] = $fullName; break; } } } // 2. Определение класса скорости Wi-Fi (BE19000, AX3000 и т.д.) if (!empty($specs['Скорость Wi-Fi']) && preg_match('/\b(BE|AX|AC|N)\s*(\d+)/i', $specs['Скорость Wi-Fi'], $matches)) { $result['WIFI_SPEED'] = strtoupper($matches[1] . $matches[2]); } // Обработка блока портов Ethernet if (!empty($specs['Порты Ethernet'])) { $portsBlock = $specs['Порты Ethernet']; // 3. Определение скорости WAN-порта if (preg_match('/(?:WAN|WAN\/LAN).*?(\d+(?:\/\d+)*(?:\.\d+)?)\s*(Гбит\/с|Мбит\/с)/iu', $portsBlock, $matches)) { $result['WAN_SPEED'] = $matches[1] . ' ' . $matches[2]; } // 4. Подсчет количества LAN-портов $individualPorts = preg_split('/[•\n,]+/', $portsBlock); foreach ($individualPorts as $portLine) { if (preg_match('/(\d+)\s*(?:×|x|\s+)\s*порт/iu', $portLine, $matches)) { $count = (int)$matches[1]; // Пропускаем чистый WAN if (preg_match('/\bWAN\b/iu', $portLine) && !preg_match('/LAN/iu', $portLine)) { continue; } $result['LAN_PORTS'] += $count; } } } return $result; } public static function saveElements(array $itemData, $iblockId = 0): array { $result = []; $combinations = self::generateModelCombinations($itemData); if (!empty($combinations) && $iblockId > 0 ) { $sub_data = self::parseRouterSpecsArray($itemData['properties']); foreach ($combinations as $modelXmlId) { $xmls[] = $modelXmlId; // Данные, которые прилетели из парсера модели TP-Link $modelName = $itemData['model']; // todo // Массив свойств (код свойства => значение) $modelProperties = [ 'FULL_ARTICLE' => $modelXmlId, 'ARTICLE' => $itemData['model'], 'CATEGORY' => $itemData['category'], 'WIFI_STANDARD' => $sub_data['WIFI_STANDARD'], 'WIFI_SPEED' => $sub_data['WIFI_SPEED'], 'WAN_SPEED' => $sub_data['WAN_SPEED'], 'LAN_PORTS' => $sub_data['LAN_PORTS'], 'SOURCE_URL' => $itemData['link'], 'SYNCED_AT' => (new \Bitrix\Main\Type\DateTime())->format('c'), // ISO 8601 'MISSING_AT_SOURCE' => 'N' // Признак «на источнике не найден при последнем запуске» (Y/N) ]; // Дополнительные стандартные поля Битрикса (необязательно) $additional = [ 'PREVIEW_TEXT' => $itemData['name'], // 'SORT' => 100 ]; // Создаем или обновляем элемент $elementId = \Tplink\Parser\IblockCreator::saveElement( $iblockId, $modelXmlId, $modelName, $modelProperties, $additional ); $status = "N"; if ($elementId > 0) { $status = "Y"; $result[] = $elementId; } self::setLogs($itemData, $modelProperties, $status); } if (!empty($xmls)) { \Tplink\Parser\IblockCreator::resetElements( $iblockId, $xmls ); } } return $result; } protected static function setLogs(array $itemData, array $modelProperties, $status = "N") { $logs = [ "article" => $modelProperties["FULL_ARTICLE"], "name" => $itemData["name"], "category" => $itemData["category"], "source_url" => $itemData["link"], "status" => $status, ]; // Формируем строку и добавляем перенос в конец $csvRow = implode(';', array_values($logs)) . PHP_EOL; // Записываем в файл file_put_contents('tplink_import_second_run.csv', $csvRow, FILE_APPEND | LOCK_EX); } }