Encar.php 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. <?php
  2. namespace App\Services\Parser\Portals;
  3. use Illuminate\Support\Facades\Http;
  4. class Encar
  5. {
  6. /**
  7. * Интервал между запросами в секундах.
  8. * Переопределяет глобальный request_interval из config/parser.php.
  9. */
  10. const REQUEST_INTERVAL = 2;
  11. /**
  12. * Папка для временного хранения HTML-файлов
  13. */
  14. const TMP_DIR = __DIR__ . '/../tmp';
  15. /**
  16. * User-Agent для HTTP-запросов
  17. */
  18. 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';
  19. // -------------------------------------------------------------------------
  20. // Public API
  21. // -------------------------------------------------------------------------
  22. /**
  23. * Точка входа: загружает страницы и сохраняет HTML во временную папку.
  24. */
  25. public function parse(): void
  26. {
  27. $contents = $this->getContent();
  28. $this->saveContent($contents);
  29. }
  30. /**
  31. * Читает все HTML-файлы из tmp и возвращает массив:
  32. * [
  33. * ['file' => '/absolute/path/to/file.html', 'html' => '<html>...'],
  34. * ...
  35. * ]
  36. */
  37. public function getResult(): array
  38. {
  39. $tmpDir = self::TMP_DIR;
  40. if (! is_dir($tmpDir)) {
  41. return [];
  42. }
  43. $files = glob($tmpDir . '/*.html') ?: [];
  44. $results = [];
  45. foreach ($files as $file) {
  46. $html = file_get_contents($file);
  47. if ($html === false) {
  48. error_log("[Encar] Не удалось прочитать файл: $file");
  49. continue;
  50. }
  51. $results[] = [
  52. 'file' => $file,
  53. 'html' => $html,
  54. ];
  55. }
  56. return $results;
  57. }
  58. /**
  59. * Удаляет все HTML-файлы из tmp.
  60. * Метод готов, но пока не вызывается из Parser.
  61. */
  62. public function clearData(): void
  63. {
  64. $tmpDir = self::TMP_DIR;
  65. if (! is_dir($tmpDir)) {
  66. return;
  67. }
  68. $files = glob($tmpDir . '/*.html') ?: [];
  69. foreach ($files as $file) {
  70. if (! unlink($file)) {
  71. error_log("[Encar] Не удалось удалить файл: $file");
  72. }
  73. }
  74. }
  75. // -------------------------------------------------------------------------
  76. // Private
  77. // -------------------------------------------------------------------------
  78. /**
  79. * Список ссылок для парсинга
  80. */
  81. private function getLinks(): array
  82. {
  83. return [
  84. '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',
  85. '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',
  86. ];
  87. }
  88. /**
  89. * Загружает HTML-контент по всем ссылкам.
  90. * Возвращает массив: [ ['url' => ..., 'html' => ...], ... ]
  91. */
  92. private function getContent(): array
  93. {
  94. $links = $this->getLinks();
  95. $results = [];
  96. foreach ($links as $index => $url) {
  97. $html = $this->fetchUrl($url);
  98. if ($html !== false) {
  99. $body = $this->extractBody($html);
  100. if ($body !== null) {
  101. $results[] = [
  102. 'url' => $url,
  103. 'html' => $body,
  104. ];
  105. } else {
  106. error_log("[Encar] Не удалось извлечь <body>: $url");
  107. }
  108. } else {
  109. error_log("[Encar] Не удалось загрузить: $url");
  110. }
  111. // Пауза между запросами (кроме последнего)
  112. if ($index < count($links) - 1) {
  113. sleep(self::REQUEST_INTERVAL);
  114. }
  115. }
  116. return $results;
  117. }
  118. /**
  119. * Извлекает содержимое тега <body> из HTML-строки.
  120. * Возвращает null если тег не найден.
  121. */
  122. private function extractBody(string $html): ?string
  123. {
  124. $dom = new \DOMDocument();
  125. // Подавляем ошибки парсинга (битый HTML, корейские символы и т.д.)
  126. libxml_use_internal_errors(true);
  127. // UTF-8 hint — иначе DOMDocument может неверно определить кодировку
  128. $dom->loadHTML('<?xml encoding="UTF-8">' . $html, LIBXML_NOWARNING | LIBXML_NOERROR);
  129. libxml_clear_errors();
  130. $body = $dom->getElementsByTagName('body')->item(0);
  131. if ($body === null) {
  132. return null;
  133. }
  134. // Сериализуем innerHTML тега <body> (без самих тегов <body></body>)
  135. $innerHTML = '';
  136. foreach ($body->childNodes as $child) {
  137. $innerHTML .= $dom->saveHTML($child);
  138. }
  139. return trim($innerHTML);
  140. }
  141. /**
  142. * Выполняет HTTP GET-запрос с кастомным User-Agent
  143. */
  144. private function fetchUrl(string $url): string | false
  145. {
  146. $response = Http::withHeaders([
  147. 'User-Agent' => self::USER_AGENT,
  148. 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
  149. 'Accept-Language' => 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7',
  150. 'Connection' => 'keep-alive',
  151. ])
  152. ->timeout(30)
  153. ->withOptions(['allow_redirects' => true])
  154. ->get($url);
  155. if ($response->failed()) {
  156. return false;
  157. }
  158. return $response->body();
  159. }
  160. /**
  161. * Сохраняет полученный HTML в файлы в папке tmp
  162. */
  163. private function saveContent(array $contents): void
  164. {
  165. if (! is_dir(self::TMP_DIR)) {
  166. mkdir(self::TMP_DIR, 0755, true);
  167. }
  168. foreach ($contents as $item) {
  169. $carId = $this->extractCarId($item['url']);
  170. $filename = self::TMP_DIR . '/' . $carId . '_' . time() . '.html';
  171. $written = file_put_contents($filename, $item['html']);
  172. if ($written === false) {
  173. error_log("[Encar] Не удалось сохранить файл: $filename");
  174. } else {
  175. echo "[Encar] Сохранено: $filename" . PHP_EOL;
  176. }
  177. }
  178. }
  179. /**
  180. * Извлекает carid из URL, либо возвращает md5-хэш URL
  181. */
  182. private function extractCarId(string $url): string
  183. {
  184. if (preg_match('/carid=(\d+)/', $url, $matches)) {
  185. return $matches[1];
  186. }
  187. return md5($url);
  188. }
  189. }