Last active
March 16, 2026 18:42
-
-
Save hackimov/a06215e24cf98cec99dd32589d45f2f6 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| declare(strict_types=1); | |
| function validateLargeJsonFile(string $filePath, int $chunkSize = 8192): array | |
| { | |
| $handle = fopen($filePath, 'rb'); | |
| if ($handle === false) { | |
| return [ | |
| 'valid' => false, | |
| 'error' => "Не удалось открыть файл: $filePath", | |
| 'errors' => [], | |
| ]; | |
| } | |
| // ── Стек скобок вместо двух счётчиков ──────────────────────────────────── | |
| $stack = new SplStack(); // хранит '{' или '[' | |
| $inString = false; | |
| $escape = false; | |
| $totalBytes = 0; | |
| $lineNumber = 1; | |
| $errorMessages = []; | |
| // ── Состояние корневого уровня ──────────────────────────────────────────── | |
| // null = ещё не встретили ни одного токена | |
| // 'open'= внутри корневого объекта/массива/строки/скаляра | |
| // 'done'= корневой токен закрыт, ждём EOF | |
| $rootState = null; | |
| $rootIsScalar = false; // true для строк и скаляров (не {} и не []) | |
| try { | |
| while (!feof($handle)) { | |
| $chunk = fread($handle, $chunkSize); | |
| if ($chunk === false) { | |
| $errorMessages[] = "Ошибка чтения на байте $totalBytes"; | |
| break; | |
| } | |
| $len = strlen($chunk); | |
| $totalBytes += $len; | |
| for ($i = 0; $i < $len; $i++) { | |
| $char = $chunk[$i]; | |
| if ($char === "\n") { | |
| $lineNumber++; | |
| } | |
| // ════════════════════════════════════════════════════════ | |
| // РЕЖИМ ВНУТРИ СТРОКИ | |
| // ════════════════════════════════════════════════════════ | |
| if ($inString) { | |
| if ($escape) { | |
| $escape = false; | |
| continue; | |
| } | |
| if ($char === '\\') { | |
| $escape = true; | |
| continue; | |
| } | |
| // [ДОРАБОТКА 3] Буквальный перенос строки внутри строки — запрещён JSON-spec | |
| if ($char === "\n" || $char === "\r") { | |
| $errorMessages[] = "Буквальный перенос строки внутри строки на строке $lineNumber"; | |
| $inString = false; // восстанавливаемся, чтобы продолжить анализ | |
| continue; | |
| } | |
| if ($char === '"') { | |
| $inString = false; | |
| // Если корневой токен — строка, она только что закрылась | |
| if ($rootIsScalar && $rootState === 'open') { | |
| $rootState = 'done'; | |
| } | |
| } | |
| continue; | |
| } | |
| // ════════════════════════════════════════════════════════ | |
| // РЕЖИМ ВНЕ СТРОКИ | |
| // ════════════════════════════════════════════════════════ | |
| // Пробельные символы вне строки — всегда пропускаем | |
| if ($char === ' ' || $char === "\t" || $char === "\n" || $char === "\r") { | |
| continue; | |
| } | |
| // [ДОРАБОТКА 2] Если корневой токен уже закрыт — любой непробельный символ ошибка | |
| if ($rootState === 'done') { | |
| $errorMessages[] = "Лишние данные после корневого элемента на строке $lineNumber (символ: '$char')"; | |
| // Сбрасываем, чтобы не спамить одной и той же ошибкой на каждый символ | |
| $rootState = 'open'; | |
| } | |
| switch ($char) { | |
| case '"': | |
| $inString = true; | |
| if ($rootState === null) { | |
| $rootState = 'open'; | |
| $rootIsScalar = true; // строка как корневой элемент | |
| } | |
| break; | |
| case '{': | |
| case '[': | |
| // [ДОРАБОТКА 1] Стек: кладём открывающую скобку | |
| $stack->push($char); | |
| if ($rootState === null) { | |
| $rootState = 'open'; | |
| $rootIsScalar = false; | |
| } | |
| break; | |
| case '}': | |
| case ']': | |
| // [ДОРАБОТКА 1] Проверяем соответствие скобок через стек | |
| $expected = ($char === '}') ? '{' : '['; | |
| if ($stack->isEmpty()) { | |
| $errorMessages[] = "Лишняя '$char' на строке $lineNumber — стек скобок пуст"; | |
| } elseif ($stack->top() !== $expected) { | |
| $errorMessages[] = "Несоответствие скобок на строке $lineNumber: " | |
| . "ожидалось закрытие '" . ($stack->top() === '{' ? '}' : ']') . "', " | |
| . "получено '$char'"; | |
| $stack->pop(); // убираем, чтобы продолжить анализ | |
| } else { | |
| $stack->pop(); | |
| } | |
| // Если стек опустел — корневой элемент закрыт | |
| if ($stack->isEmpty() && $rootState === 'open' && !$rootIsScalar) { | |
| $rootState = 'done'; | |
| } | |
| break; | |
| default: | |
| // Скалярные значения: числа, true, false, null | |
| if ($rootState === null && trim($char) !== '') { | |
| $rootState = 'done'; // скаляр — однотокенный, сразу считаем закрытым | |
| $rootIsScalar = true; | |
| } | |
| break; | |
| } | |
| } | |
| unset($chunk); | |
| if ($totalBytes % (1024 * 1024 * 100) < $chunkSize) { | |
| $mb = number_format($totalBytes / 1024 / 1024, 1); | |
| echo "Обработано: {$mb} МБ, строка: {$lineNumber}\r"; | |
| gc_collect_cycles(); | |
| } | |
| } | |
| } finally { | |
| fclose($handle); | |
| } | |
| echo PHP_EOL; | |
| // ── Финальные проверки ──────────────────────────────────────────────────── | |
| if ($rootState === null) { | |
| $errorMessages[] = 'Файл пустой или не содержит JSON-структуры'; | |
| } | |
| // [ДОРАБОТКА 1] Незакрытые скобки из стека | |
| if (!$stack->isEmpty()) { | |
| $unclosed = []; | |
| foreach ($stack as $bracket) { | |
| $unclosed[] = $bracket; | |
| } | |
| $errorMessages[] = "Незакрытые скобки в конце файла: " . implode(' ', array_reverse($unclosed)); | |
| } | |
| if ($inString) { | |
| $errorMessages[] = 'Незакрытая строка в конце файла'; | |
| } | |
| return [ | |
| 'valid' => empty($errorMessages), | |
| 'total_bytes' => $totalBytes, | |
| 'total_mb' => round($totalBytes / 1024 / 1024, 2), | |
| 'lines' => $lineNumber, | |
| 'errors' => $errorMessages, | |
| ]; | |
| } | |
| // ─── Запуск ─────────────────────────────────────────────────────────────────── | |
| ini_set('memory_limit', '32M'); | |
| set_time_limit(0); | |
| gc_enable(); | |
| $filePath = $argv[1] ?? 'Zalupa50GB.json'; | |
| $chunkSize = (int)($argv[2] ?? 8192); | |
| echo "=== JSON Validator ===" . PHP_EOL; | |
| echo "Файл: $filePath" . PHP_EOL; | |
| echo str_repeat('-', 40) . PHP_EOL; | |
| $start = microtime(true); | |
| $result = validateLargeJsonFile($filePath, $chunkSize); | |
| $elapsed = round(microtime(true) - $start, 2); | |
| echo str_repeat('=', 40) . PHP_EOL; | |
| echo "РЕЗУЛЬТАТ: " . ($result['valid'] ? 'ВАЛИДНЫЙ JSON' : 'НЕВАЛИДНЫЙ JSON') . PHP_EOL; | |
| echo "Размер: {$result['total_mb']} МБ" . PHP_EOL; | |
| echo "Строк: {$result['lines']}" . PHP_EOL; | |
| echo "Время: {$elapsed} сек" . PHP_EOL; | |
| if (!empty($result['errors'])) { | |
| echo PHP_EOL . "Ошибки:" . PHP_EOL; | |
| foreach ($result['errors'] as $err) { | |
| echo " • $err" . PHP_EOL; | |
| } | |
| } | |
| echo str_repeat('=', 40) . PHP_EOL; | |
| exit($result['valid'] ? 0 : 1); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment