| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182 |
- <?php
- namespace App\Services\Parser;
- use Exception;
- use Illuminate\Support\Facades\Http;
- class Parser
- {
- /**
- * Пространство имён папки Portals
- */
- const PORTALS_NAMESPACE = 'App\\Services\\Parser\\Portals\\';
- /**
- * Конфигурация (из config/parser.php)
- */
- private array $config;
- public function __construct()
- {
- $this->config = config('parser');
- }
- // -------------------------------------------------------------------------
- // Public API
- // -------------------------------------------------------------------------
- /**
- * Главная точка входа.
- * 1. Обходит активные порталы из конфига
- * 2. Вызывает parse() на каждом → данные оседают в tmp
- * 3. Читает результаты через getResult()
- * 4. Отправляет HTML в ChatGPT и получает JSON
- * 5. Возвращает массив готовых данных для создания Car
- */
- public function run(): array
- {
- $cars = [];
- foreach ($this->getActivePortals() as $portalInstance) {
- // Шаг 1: загружаем страницы и сохраняем HTML во временные файлы
- $portalInstance->parse();
- // Шаг 2: читаем HTML из tmp
- $results = $portalInstance->getResult();
- // Шаг 3: каждый HTML → ChatGPT → массив полей Car
- foreach ($results as $item) {
- try {
- $carData = $this->processWithGpt($item['html']);
- if ($carData !== null) {
- $cars[] = $carData;
- }
- } catch (Exception $e) {
- error_log('[Parser] GPT error for file ' . ($item['file'] ?? '?') . ': ' . $e->getMessage());
- }
- }
- // clearData() — не вызываем пока, но метод готов
- // $portalInstance->clearData();
- }
- return $cars;
- }
- // -------------------------------------------------------------------------
- // Portal loading
- // -------------------------------------------------------------------------
- /**
- * Возвращает массив инстансов активных порталов из конфига.
- * Пример конфига:
- * 'portals' => ['Encar' => true, 'Drom' => false]
- *
- * @return object[]
- */
- private function getActivePortals(): array
- {
- $portals = $this->config['portals'] ?? [];
- $instances = [];
- foreach ($portals as $name => $enabled) {
- if (! $enabled) {
- continue;
- }
- $class = self::PORTALS_NAMESPACE . $name;
- if (! class_exists($class)) {
- error_log("[Parser] Класс портала не найден: $class");
- continue;
- }
- $instance = new $class();
- // Убеждаемся что у класса есть нужные методы
- if (! method_exists($instance, 'parse') || ! method_exists($instance, 'getResult')) {
- error_log("[Parser] Портал $name не реализует parse() / getResult()");
- continue;
- }
- $instances[] = $instance;
- }
- return $instances;
- }
- // -------------------------------------------------------------------------
- // GPT integration
- // -------------------------------------------------------------------------
- /**
- * Отправляет HTML в ChatGPT, получает JSON, декодирует в массив.
- * Возвращает null если GPT вернул невалидный JSON.
- */
- private function processWithGpt(string $html): ?array
- {
- $prompt = $this->buildPrompt($html);
- $raw = $this->callChatGpt($prompt);
- // Убираем возможные markdown-обёртки ```json … ```
- $json = preg_replace('/^```(?:json)?\s*/i', '', trim($raw));
- $json = preg_replace('/\s*```$/', '', $json);
- $data = json_decode($json, true);
- if (! is_array($data)) {
- error_log('[Parser] GPT вернул невалидный JSON: ' . substr($raw, 0, 200));
- return null;
- }
- return $data;
- }
- /**
- * Подставляет HTML в промт из конфига (плейсхолдер {html})
- */
- private function buildPrompt(string $html): string
- {
- $template = $this->config['gpt_prompt'] ?? '{html}';
- // Обрезаем HTML чтобы не превысить контекст модели (~100k символов достаточно)
- $trimmedHtml = mb_substr($html, 0, 80000);
- return str_replace('{html}', $trimmedHtml, $template);
- }
- /**
- * Отправляет запрос к OpenAI Chat Completions API и возвращает текст ответа.
- *
- * @throws Exception при сетевых ошибках или ошибках API
- */
- private function callChatGpt(string $prompt): string
- {
- $cfg = $this->config['openai'] ?? [];
- $apiKey = $cfg['api_key'] ?? config('services.openai.key', '');
- $model = $cfg['model'] ?? 'gpt-4o';
- $maxTok = $cfg['max_tokens'] ?? 2000;
- $temp = $cfg['temperature'] ?? 0;
- if (empty($apiKey)) {
- throw new Exception('OPENAI_API_KEY не задан в конфиге или .env');
- }
- $response = Http::withToken($apiKey)
- ->timeout(60)
- ->post('https://api.openai.com/v1/chat/completions', [
- 'model' => $model,
- 'messages' => [
- ['role' => 'user', 'content' => $prompt],
- ],
- ]);
- if ($response->failed()) {
- $error = $response->json('error.message', 'unknown');
- throw new Exception("OpenAI API error [{$response->status()}]: {$error}");
- }
- return $response->json('choices.0.message.content', '');
- }
- }
|