PHP

Асинхронный поиск кода по файлам сайта через Server‑Sent Events (SSE), PHP, JS

Готовый скрипт для асинхронного поиска текста в файлах сайта на PHP через Server-Sent Events (SSE). Обход директорий, фильтрация расширений, результаты в реальном времени

sqlka.ruОпубликовано: Чтение: 7 мин.
Асинхронный поиск кода по файлам сайта через Server‑Sent Events (SSE), PHP, JS

При разработке и поддержке сайтов часто возникает необходимость найти все вхождения определённого фрагмента кода в файлах проекта. Обычные инструменты поиска в хостинг‑панелях работают медленно или неудобны, а grep через SSH доступен не всегда. Данный скрипт реализует асинхронный поиск прямо в браузере с использованием технологии Server‑Sent Events (SSE). Результаты отображаются по мере нахождения, без ожидания полного завершения сканирования.

Возможности скрипта

  • Рекурсивный обход всех файлов в корневой директории сайта.
  • Фильтрация файлов по расширениям (можно легко изменить под свои нужды).
  • Исключение ненужных директорий (cache, logs, vendor и др.) для ускорения и предотвращения просмотра служебных файлов.
  • Асинхронная отправка найденных фрагментов через SSE – интерфейс не блокируется, результаты появляются сразу.
  • Возможность прервать поиск со стороны клиента (закрытие вкладки или явное прерывание).
  • Подсветка строк, в которых найдено совпадение, с выводом самого кода.

Принцип работы

Скрипт состоит из двух частей: PHP‑обработчика (генерирует события) и JavaScript‑клиента (принимает и отображает результаты). При отправке формы с искомым текстом браузер открывает соединение через EventSource к тому же файлу с параметром q. PHP‑скрипт:

  1. Устанавливает заголовки для SSE и отключает буферизацию вывода.
  2. Проверяет наличие корневой директории.
  3. Создаёт итератор для рекурсивного обхода всех файлов.
  4. Пропускает исключённые директории и файлы с неразрешёнными расширениями.
  5. Для каждого подходящего файла построчно читает содержимое и ищет подстроку (регистронезависимо).
  6. При нахождении отправляет клиенту событие с типом result, содержащее путь к файлу, номер строки и саму строку.
  7. Периодически проверяет, не закрыто ли соединение (чтобы прервать поиск при обрыве связи).
  8. По завершении отправляет финальное событие с общим количеством найденных совпадений.

Полный код скрипта

Сохраните приведённый ниже код в отдельный файл (например, search.php) в корне вашего сайта. Убедитесь, что веб‑сервер имеет права на чтение всех файлов, по которым планируется поиск.

<?php
// ============================================//
// Асинхронный поиск кода по всему сайту через SSE
// ============================================//

// Разрешённые расширения файлов
$allowedExtensions = ['php', 'html', 'htm', 'js', 'css', 'txt', 'inc', 'tpl'];

// Директории, которые нужно исключить (относительно DOCUMENT_ROOT)
$excludePaths = [
    '/cache',
    '/logs',
    '/tmp',
    '/vendor',
    '/node_modules',
    '/.git'
];

if (isset($_GET['q'])) {
    // Режим Server-Sent Events
    header('Content-Type: text/event-stream');
    header('Cache-Control: no-cache');
    header('X-Accel-Buffering: no'); 

    set_time_limit(0);
    ob_implicit_flush(1);
    ob_end_flush();

    $searchQuery = $_GET['q'];
    $rootDir = $_SERVER['DOCUMENT_ROOT'];

    function sendEvent($data) {
        echo "data: " . json_encode($data, JSON_UNESCAPED_UNICODE) . "\n\n";
    }

    function shouldStop() {
        return connection_aborted();
    }

    sendEvent(['type' => 'start', 'message' => 'Поиск начат...']);

    $foundCount = 0;

    if (!is_dir($rootDir)) {
        sendEvent(['type' => 'error', 'message' => 'Корневая директория не найдена']);
        exit;
    }

    $iterator = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($rootDir, RecursiveDirectoryIterator::SKIP_DOTS),
        RecursiveIteratorIterator::SELF_FIRST
    );

    foreach ($iterator as $file) {
        if (shouldStop()) {
            sendEvent(['type' => 'abort', 'message' => 'Поиск прерван клиентом']);
            exit;
        }

        $relativePath = str_replace($rootDir, '', $file->getPathname());

        $skip = false;
        foreach ($excludePaths as $exclude) {
            if (strpos($relativePath, $exclude) === 0) {
                $skip = true;
                break;
            }
        }
        if ($skip) continue;

        if ($file->isFile() && in_array($file->getExtension(), $allowedExtensions)) {
            $handle = fopen($file->getPathname(), 'r');
            if ($handle) {
                $lineNum = 0;
                while (($line = fgets($handle)) !== false) {
                    $lineNum++;
                    if (stripos($line, $searchQuery) !== false) {
                        $foundCount++;
                        sendEvent([
                            'type' => 'result',
                            'file' => $relativePath,
                            'line' => $lineNum,
                            'code' => rtrim($line)
                        ]);
                    }

                    if ($lineNum % 100 == 0 && shouldStop()) {
                        fclose($handle);
                        sendEvent(['type' => 'abort', 'message' => 'Поиск прерван клиентом']);
                        exit;
                    }
                }
                fclose($handle);
            }
        }
    }

    sendEvent(['type' => 'end', 'found' => $foundCount]);
    exit;
}
?>

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <title>Поиск кода по сайту</title>
    <style>
        body { font-family: sans-serif; margin: 20px; }
        #results { list-style: none; padding: 0; }
        #results li { margin: 10px 0; padding: 10px; background: #f5f5f5; border-left: 4px solid #007bff; }
        .filename { font-weight: bold; margin-bottom: 5px; }
        .code { font-family: monospace; white-space: pre-wrap; }
        #progress { width: 100%; background: #eee; margin: 10px 0; display: none; }
        #progress div { width: 0%; height: 20px; background: #007bff; }
        input, button { padding: 8px; font-size: 16px; }
    </style>
</head>
<body>
    <h1>Поиск фрагментов кода по сайту</h1>
    <form id="searchForm">
        <input type="text" id="query" name="query" placeholder="Введите искомый текст..." required>
        <button type="submit">Найти</button>
    </form>
    <div id="progress"><div></div></div>
    <div id="status"></div>
    <div id="counter"></div>
    <ul id="results"></ul>

    <script>
        let eventSource = null;

        document.getElementById('searchForm').addEventListener('submit', function(e) {
            e.preventDefault();
            const query = document.getElementById('query').value.trim();
            if (!query) return;

            if (eventSource) {
                eventSource.close();
            }

            document.getElementById('results').innerHTML = '';
            document.getElementById('status').textContent = 'Поиск запущен...';
            document.getElementById('counter').textContent = '';
            document.getElementById('progress').style.display = 'block';
            document.getElementById('progress').firstChild.style.width = '0%';

            let resultCount = 0;
            const resultsList = document.getElementById('results');
            const statusDiv = document.getElementById('status');
            const counterDiv = document.getElementById('counter');
            const progressBar = document.getElementById('progress').firstChild;

            eventSource = new EventSource('?q=' + encodeURIComponent(query));

            eventSource.onmessage = function(e) {
                const data = JSON.parse(e.data);
                console.log('SSE message:', data);

                switch (data.type) {
                    case 'start':
                        statusDiv.textContent = data.message;
                        break;

                    case 'result':
                        resultCount++;
                        const li = document.createElement('li');
                        li.innerHTML = `<div class="filename">${escapeHtml(data.file)} (строка ${data.line})</div>
                                        <div class="code">${escapeHtml(data.code)}</div>`;
                        resultsList.appendChild(li);
                        counterDiv.textContent = `Найдено совпадений: ${resultCount}`;
                        break;

                    case 'end':
                        statusDiv.textContent = `Поиск завершён. Всего найдено: ${data.found}`;
                        progressBar.style.width = '100%';
                        eventSource.close();
                        break;

                    case 'error':
                    case 'abort':
                        statusDiv.textContent = data.message;
                        progressBar.style.width = '100%';
                        eventSource.close();
                        break;
                }
            };

            eventSource.onerror = function(e) {
                statusDiv.textContent = 'Ошибка соединения или поиск прерван.';
                progressBar.style.width = '100%';
                eventSource.close();
            };
        });

        function escapeHtml(unsafe) {
            return unsafe.replace(/[&<>"']/g, function(m) {
                if (m === '&') return '&';
                if (m === '<') return '<';
                if (m === '>') return '>';
                if (m === '"') return '"';
                if (m === "'") return "'";
                return m;
            });
        }
    </script>
</body>
</html>

Настройка скрипта

Перед использованием отредактируйте две переменные в начале PHP‑части:

  • $allowedExtensions – список расширений файлов, в которых будет выполняться поиск.
  • $excludePaths – массив относительных путей (относительно корня сайта), которые нужно исключить из обхода (например, /bitrix/cache).

Если ваш проект использует другие служебные директории, добавьте их в исключения, чтобы ускорить поиск и избежать лишнего вывода.

Использование

  1. Поместите файл в корень сайта (или в любую доступную папку, но тогда пути в исключениях должны быть указаны корректно).
  2. Откройте его в браузере (например, http://вашсайт/search.php).
  3. Введите искомый текст (регистр не имеет значения) и нажмите «Найти».
  4. По мере сканирования результаты будут появляться на странице. Вы можете прервать процесс, закрыв вкладку или перезагрузив страницу.

Технические детали

  • SSE (Server‑Sent Events) – технология, позволяющая серверу отправлять клиенту текстовые события по одному долгоживущему HTTP‑соединению. В отличие от WebSocket, SSE работает только в сторону клиента и реализуется встроенным объектом EventSource в браузере.
  • Прерывание поиска – скрипт периодически вызывает connection_aborted(), чтобы проверить, не закрыто ли соединение со стороны клиента. Это позволяет остановить сканирование, если пользователь ушёл со страницы.
  • Производительность – поиск по всем файлам может занимать много времени и нагружать диск. Рекомендуется использовать скрипт только в административных целях и не запускать одновременно несколько таких поисков.
  • Безопасность – скрипт не имеет никаких ограничений доступа. Если вы размещаете его на рабочем сайте, обязательно защитите его паролем с помощью HTTP‑аутентификации или поместите в папку, недоступную для посторонних.

Заключение

Данный инструмент пригодится разработчикам и администраторам сайтов для быстрого поиска фрагментов кода без использования консоли. Асинхронная отправка результатов делает работу комфортной даже при большом количестве файлов. Вы можете легко адаптировать скрипт под свои нужды: изменить набор расширений, добавить поддержку дополнительных типов файлов или улучшить интерфейс вывода.

Хотите освоить SQL для работы с данными?

В SQLka можно изучать SQL последовательно или готовиться к собеседованиям. Запросы выполняются прямо в браузере — устанавливать базу данных не нужно.

sqlka.ru

support@sqlka.ru

Дальше по теме

PHP

Получаем список популярных поисковых запросов из Яндекс.Вебмастер через PHP API

Практическое руководство по использованию API Яндекс.Вебрастера на PHP для получения ТОП популярных поисковых запросов по вашим сайтам. Примеры кода, авторизация OAuth, обработка ответов.

sqlka.ruЧтение: 9 мин.
← Все статьи