Parser.php 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. <?php
  2. namespace App\Services\Parser;
  3. use Exception;
  4. use Illuminate\Support\Facades\Http;
  5. class Parser
  6. {
  7. /**
  8. * Пространство имён папки Portals
  9. */
  10. const PORTALS_NAMESPACE = 'App\\Services\\Parser\\Portals\\';
  11. /**
  12. * Конфигурация (из config/parser.php)
  13. */
  14. private array $config;
  15. public function __construct()
  16. {
  17. $this->config = config('parser');
  18. }
  19. // -------------------------------------------------------------------------
  20. // Public API
  21. // -------------------------------------------------------------------------
  22. /**
  23. * Главная точка входа.
  24. * 1. Обходит активные порталы из конфига
  25. * 2. Вызывает parse() на каждом → данные оседают в tmp
  26. * 3. Читает результаты через getResult()
  27. * 4. Отправляет HTML в ChatGPT и получает JSON
  28. * 5. Возвращает массив готовых данных для создания Car
  29. */
  30. public function run(): array
  31. {
  32. $cars = [];
  33. foreach ($this->getActivePortals() as $portalInstance) {
  34. // Шаг 1: загружаем страницы и сохраняем HTML во временные файлы
  35. $portalInstance->parse();
  36. // Шаг 2: читаем HTML из tmp
  37. $results = $portalInstance->getResult();
  38. // Шаг 3: каждый HTML → ChatGPT → массив полей Car
  39. foreach ($results as $item) {
  40. try {
  41. $carData = $this->processWithGpt($item['html']);
  42. if ($carData !== null) {
  43. $cars[] = $carData;
  44. }
  45. } catch (Exception $e) {
  46. error_log('[Parser] GPT error for file ' . ($item['file'] ?? '?') . ': ' . $e->getMessage());
  47. }
  48. }
  49. // clearData() — не вызываем пока, но метод готов
  50. // $portalInstance->clearData();
  51. }
  52. return $cars;
  53. }
  54. // -------------------------------------------------------------------------
  55. // Portal loading
  56. // -------------------------------------------------------------------------
  57. /**
  58. * Возвращает массив инстансов активных порталов из конфига.
  59. * Пример конфига:
  60. * 'portals' => ['Encar' => true, 'Drom' => false]
  61. *
  62. * @return object[]
  63. */
  64. private function getActivePortals(): array
  65. {
  66. $portals = $this->config['portals'] ?? [];
  67. $instances = [];
  68. foreach ($portals as $name => $enabled) {
  69. if (! $enabled) {
  70. continue;
  71. }
  72. $class = self::PORTALS_NAMESPACE . $name;
  73. if (! class_exists($class)) {
  74. error_log("[Parser] Класс портала не найден: $class");
  75. continue;
  76. }
  77. $instance = new $class();
  78. // Убеждаемся что у класса есть нужные методы
  79. if (! method_exists($instance, 'parse') || ! method_exists($instance, 'getResult')) {
  80. error_log("[Parser] Портал $name не реализует parse() / getResult()");
  81. continue;
  82. }
  83. $instances[] = $instance;
  84. }
  85. return $instances;
  86. }
  87. // -------------------------------------------------------------------------
  88. // GPT integration
  89. // -------------------------------------------------------------------------
  90. /**
  91. * Отправляет HTML в ChatGPT, получает JSON, декодирует в массив.
  92. * Возвращает null если GPT вернул невалидный JSON.
  93. */
  94. private function processWithGpt(string $html): ?array
  95. {
  96. $prompt = $this->buildPrompt($html);
  97. $raw = $this->callChatGpt($prompt);
  98. // Убираем возможные markdown-обёртки ```json … ```
  99. $json = preg_replace('/^```(?:json)?\s*/i', '', trim($raw));
  100. $json = preg_replace('/\s*```$/', '', $json);
  101. $data = json_decode($json, true);
  102. if (! is_array($data)) {
  103. error_log('[Parser] GPT вернул невалидный JSON: ' . substr($raw, 0, 200));
  104. return null;
  105. }
  106. return $data;
  107. }
  108. /**
  109. * Подставляет HTML в промт из конфига (плейсхолдер {html})
  110. */
  111. private function buildPrompt(string $html): string
  112. {
  113. $template = $this->config['gpt_prompt'] ?? '{html}';
  114. // Обрезаем HTML чтобы не превысить контекст модели (~100k символов достаточно)
  115. $trimmedHtml = mb_substr($html, 0, 80000);
  116. return str_replace('{html}', $trimmedHtml, $template);
  117. }
  118. /**
  119. * Отправляет запрос к OpenAI Chat Completions API и возвращает текст ответа.
  120. *
  121. * @throws Exception при сетевых ошибках или ошибках API
  122. */
  123. private function callChatGpt(string $prompt): string
  124. {
  125. $cfg = $this->config['openai'] ?? [];
  126. $apiKey = $cfg['api_key'] ?? config('services.openai.key', '');
  127. $model = $cfg['model'] ?? 'gpt-4o';
  128. $maxTok = $cfg['max_tokens'] ?? 2000;
  129. $temp = $cfg['temperature'] ?? 0;
  130. if (empty($apiKey)) {
  131. throw new Exception('OPENAI_API_KEY не задан в конфиге или .env');
  132. }
  133. $response = Http::withToken($apiKey)
  134. ->timeout(60)
  135. ->post('https://api.openai.com/v1/chat/completions', [
  136. 'model' => $model,
  137. 'messages' => [
  138. ['role' => 'user', 'content' => $prompt],
  139. ],
  140. ]);
  141. if ($response->failed()) {
  142. $error = $response->json('error.message', 'unknown');
  143. throw new Exception("OpenAI API error [{$response->status()}]: {$error}");
  144. }
  145. return $response->json('choices.0.message.content', '');
  146. }
  147. }