|
|
@@ -0,0 +1,231 @@
|
|
|
+<?php
|
|
|
+namespace App\Services\Parser\Portals;
|
|
|
+
|
|
|
+use Illuminate\Support\Facades\Http;
|
|
|
+
|
|
|
+class Encar
|
|
|
+{
|
|
|
+ /**
|
|
|
+ * Интервал между запросами в секундах.
|
|
|
+ * Переопределяет глобальный request_interval из config/parser.php.
|
|
|
+ */
|
|
|
+ const REQUEST_INTERVAL = 2;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Папка для временного хранения HTML-файлов
|
|
|
+ */
|
|
|
+ const TMP_DIR = __DIR__ . '/../tmp';
|
|
|
+
|
|
|
+ /**
|
|
|
+ * User-Agent для HTTP-запросов
|
|
|
+ */
|
|
|
+ const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36';
|
|
|
+
|
|
|
+ // -------------------------------------------------------------------------
|
|
|
+ // Public API
|
|
|
+ // -------------------------------------------------------------------------
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Точка входа: загружает страницы и сохраняет HTML во временную папку.
|
|
|
+ */
|
|
|
+ public function parse(): void
|
|
|
+ {
|
|
|
+ $contents = $this->getContent();
|
|
|
+ $this->saveContent($contents);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Читает все HTML-файлы из tmp и возвращает массив:
|
|
|
+ * [
|
|
|
+ * ['file' => '/absolute/path/to/file.html', 'html' => '<html>...'],
|
|
|
+ * ...
|
|
|
+ * ]
|
|
|
+ */
|
|
|
+ public function getResult(): array
|
|
|
+ {
|
|
|
+ $tmpDir = self::TMP_DIR;
|
|
|
+
|
|
|
+ if (! is_dir($tmpDir)) {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+
|
|
|
+ $files = glob($tmpDir . '/*.html') ?: [];
|
|
|
+ $results = [];
|
|
|
+
|
|
|
+ foreach ($files as $file) {
|
|
|
+ $html = file_get_contents($file);
|
|
|
+
|
|
|
+ if ($html === false) {
|
|
|
+ error_log("[Encar] Не удалось прочитать файл: $file");
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $results[] = [
|
|
|
+ 'file' => $file,
|
|
|
+ 'html' => $html,
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ return $results;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Удаляет все HTML-файлы из tmp.
|
|
|
+ * Метод готов, но пока не вызывается из Parser.
|
|
|
+ */
|
|
|
+ public function clearData(): void
|
|
|
+ {
|
|
|
+ $tmpDir = self::TMP_DIR;
|
|
|
+
|
|
|
+ if (! is_dir($tmpDir)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ $files = glob($tmpDir . '/*.html') ?: [];
|
|
|
+
|
|
|
+ foreach ($files as $file) {
|
|
|
+ if (! unlink($file)) {
|
|
|
+ error_log("[Encar] Не удалось удалить файл: $file");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // -------------------------------------------------------------------------
|
|
|
+ // Private
|
|
|
+ // -------------------------------------------------------------------------
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Список ссылок для парсинга
|
|
|
+ */
|
|
|
+ private function getLinks(): array
|
|
|
+ {
|
|
|
+ return [
|
|
|
+ 'https://fem.encar.com/cars/detail/41664398?pageid=dc_carsearch&listAdvType=pic&carid=41664398&view_type=checked&wtClick_korList=015&advClickPosition=kor_pic_p1_g1',
|
|
|
+ 'https://fem.encar.com/cars/detail/41939059?pageid=dc_carsearch&listAdvType=pic&carid=41939059&view_type=checked&wtClick_korList=015&advClickPosition=kor_pic_p1_g2',
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Загружает HTML-контент по всем ссылкам.
|
|
|
+ * Возвращает массив: [ ['url' => ..., 'html' => ...], ... ]
|
|
|
+ */
|
|
|
+ private function getContent(): array
|
|
|
+ {
|
|
|
+ $links = $this->getLinks();
|
|
|
+ $results = [];
|
|
|
+
|
|
|
+ foreach ($links as $index => $url) {
|
|
|
+ $html = $this->fetchUrl($url);
|
|
|
+
|
|
|
+ if ($html !== false) {
|
|
|
+ $body = $this->extractBody($html);
|
|
|
+
|
|
|
+ if ($body !== null) {
|
|
|
+ $results[] = [
|
|
|
+ 'url' => $url,
|
|
|
+ 'html' => $body,
|
|
|
+ ];
|
|
|
+ } else {
|
|
|
+ error_log("[Encar] Не удалось извлечь <body>: $url");
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ error_log("[Encar] Не удалось загрузить: $url");
|
|
|
+ }
|
|
|
+
|
|
|
+ // Пауза между запросами (кроме последнего)
|
|
|
+ if ($index < count($links) - 1) {
|
|
|
+ sleep(self::REQUEST_INTERVAL);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return $results;
|
|
|
+ }
|
|
|
+
|
|
|
+/**
|
|
|
+ * Извлекает содержимое тега <body> из HTML-строки.
|
|
|
+ * Возвращает null если тег не найден.
|
|
|
+ */
|
|
|
+ private function extractBody(string $html): ?string
|
|
|
+ {
|
|
|
+ $dom = new \DOMDocument();
|
|
|
+
|
|
|
+ // Подавляем ошибки парсинга (битый HTML, корейские символы и т.д.)
|
|
|
+ libxml_use_internal_errors(true);
|
|
|
+
|
|
|
+ // UTF-8 hint — иначе DOMDocument может неверно определить кодировку
|
|
|
+ $dom->loadHTML('<?xml encoding="UTF-8">' . $html, LIBXML_NOWARNING | LIBXML_NOERROR);
|
|
|
+
|
|
|
+ libxml_clear_errors();
|
|
|
+
|
|
|
+ $body = $dom->getElementsByTagName('body')->item(0);
|
|
|
+
|
|
|
+ if ($body === null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Сериализуем innerHTML тега <body> (без самих тегов <body></body>)
|
|
|
+ $innerHTML = '';
|
|
|
+ foreach ($body->childNodes as $child) {
|
|
|
+ $innerHTML .= $dom->saveHTML($child);
|
|
|
+ }
|
|
|
+
|
|
|
+ return trim($innerHTML);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Выполняет HTTP GET-запрос с кастомным User-Agent
|
|
|
+ */
|
|
|
+ private function fetchUrl(string $url): string | false
|
|
|
+ {
|
|
|
+ $response = Http::withHeaders([
|
|
|
+ 'User-Agent' => self::USER_AGENT,
|
|
|
+ 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
|
+ 'Accept-Language' => 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7',
|
|
|
+ 'Connection' => 'keep-alive',
|
|
|
+ ])
|
|
|
+ ->timeout(30)
|
|
|
+ ->withOptions(['allow_redirects' => true])
|
|
|
+ ->get($url);
|
|
|
+
|
|
|
+ if ($response->failed()) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ return $response->body();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Сохраняет полученный HTML в файлы в папке tmp
|
|
|
+ */
|
|
|
+ private function saveContent(array $contents): void
|
|
|
+ {
|
|
|
+ if (! is_dir(self::TMP_DIR)) {
|
|
|
+ mkdir(self::TMP_DIR, 0755, true);
|
|
|
+ }
|
|
|
+
|
|
|
+ foreach ($contents as $item) {
|
|
|
+ $carId = $this->extractCarId($item['url']);
|
|
|
+ $filename = self::TMP_DIR . '/' . $carId . '_' . time() . '.html';
|
|
|
+
|
|
|
+ $written = file_put_contents($filename, $item['html']);
|
|
|
+
|
|
|
+ if ($written === false) {
|
|
|
+ error_log("[Encar] Не удалось сохранить файл: $filename");
|
|
|
+ } else {
|
|
|
+ echo "[Encar] Сохранено: $filename" . PHP_EOL;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Извлекает carid из URL, либо возвращает md5-хэш URL
|
|
|
+ */
|
|
|
+ private function extractCarId(string $url): string
|
|
|
+ {
|
|
|
+ if (preg_match('/carid=(\d+)/', $url, $matches)) {
|
|
|
+ return $matches[1];
|
|
|
+ }
|
|
|
+
|
|
|
+ return md5($url);
|
|
|
+ }
|
|
|
+}
|