Skip to content

Instantly share code, notes, and snippets.

@hackimov
Last active March 16, 2026 18:42
Show Gist options
  • Select an option

  • Save hackimov/a06215e24cf98cec99dd32589d45f2f6 to your computer and use it in GitHub Desktop.

Select an option

Save hackimov/a06215e24cf98cec99dd32589d45f2f6 to your computer and use it in GitHub Desktop.
<?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