При разработке и поддержке сайтов часто возникает необходимость найти все вхождения определённого фрагмента кода в файлах проекта. Обычные инструменты поиска в хостинг‑панелях работают медленно или неудобны, а grep через SSH доступен не всегда. Данный скрипт реализует асинхронный поиск прямо в браузере с использованием технологии Server‑Sent Events (SSE). Результаты отображаются по мере нахождения, без ожидания полного завершения сканирования.
Возможности скрипта
- Рекурсивный обход всех файлов в корневой директории сайта.
- Фильтрация файлов по расширениям (можно легко изменить под свои нужды).
- Исключение ненужных директорий (cache, logs, vendor и др.) для ускорения и предотвращения просмотра служебных файлов.
- Асинхронная отправка найденных фрагментов через SSE – интерфейс не блокируется, результаты появляются сразу.
- Возможность прервать поиск со стороны клиента (закрытие вкладки или явное прерывание).
- Подсветка строк, в которых найдено совпадение, с выводом самого кода.
Принцип работы
Скрипт состоит из двух частей: PHP‑обработчика (генерирует события) и JavaScript‑клиента (принимает и отображает результаты). При отправке формы с искомым текстом браузер открывает соединение через EventSource к тому же файлу с параметром q. PHP‑скрипт:
- Устанавливает заголовки для SSE и отключает буферизацию вывода.
- Проверяет наличие корневой директории.
- Создаёт итератор для рекурсивного обхода всех файлов.
- Пропускает исключённые директории и файлы с неразрешёнными расширениями.
- Для каждого подходящего файла построчно читает содержимое и ищет подстроку (регистронезависимо).
- При нахождении отправляет клиенту событие с типом result, содержащее путь к файлу, номер строки и саму строку.
- Периодически проверяет, не закрыто ли соединение (чтобы прервать поиск при обрыве связи).
- По завершении отправляет финальное событие с общим количеством найденных совпадений.
Полный код скрипта
Сохраните приведённый ниже код в отдельный файл (например, 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: Arial, sans-serif; margin: 20px; }
#searchForm { margin-bottom: 20px; }
#query { width: 300px; padding: 5px; }
button { padding: 5px 15px; }
#status { font-weight: bold; margin-top: 10px; }
#results { list-style: none; padding: 0; }
#results li {
border-bottom: 1px solid #eee;
padding: 10px 0;
font-size: 14px;
}
.filename {
color: #0066cc;
font-weight: bold;
cursor: pointer;
text-decoration: underline;
}
.code {
background: #f5f5f5;
padding: 5px;
margin-top: 5px;
font-family: monospace;
white-space: pre-wrap;
}
#counter { margin: 10px 0; color: #333; }
#progress { width: 100%; background: #f0f0f0; height: 4px; margin: 10px 0; display: none; }
#progress div { height: 4px; background: #4CAF50; width: 0; }
</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).
Если ваш проект использует другие служебные директории, добавьте их в исключения, чтобы ускорить поиск и избежать лишнего вывода.
Использование
- Поместите файл в корень сайта (или в любую доступную папку, но тогда пути в исключениях должны быть указаны корректно).
- Откройте его в браузере (например, http://вашсайт/search.php).
- Введите искомый текст (регистр не имеет значения) и нажмите «Найти».
- По мере сканирования результаты будут появляться на странице. Вы можете прервать процесс, закрыв вкладку или перезагрузив страницу.
Технические детали
- SSE (Server‑Sent Events) – технология, позволяющая серверу отправлять клиенту текстовые события по одному долгоживущему HTTP‑соединению. В отличие от WebSocket, SSE работает только в сторону клиента и реализуется встроенным объектом EventSource в браузере.
- Прерывание поиска – скрипт периодически вызывает connection_aborted(), чтобы проверить, не закрыто ли соединение со стороны клиента. Это позволяет остановить сканирование, если пользователь ушёл со страницы.
- Производительность – поиск по всем файлам может занимать много времени и нагружать диск. Рекомендуется использовать скрипт только в административных целях и не запускать одновременно несколько таких поисков.
- Безопасность – скрипт не имеет никаких ограничений доступа. Если вы размещаете его на рабочем сайте, обязательно защитите его паролем с помощью HTTP‑аутентификации или поместите в папку, недоступную для посторонних.
Заключение
Данный инструмент пригодится разработчикам и администраторам сайтов для быстрого поиска фрагментов кода без использования консоли. Асинхронная отправка результатов делает работу комфортной даже при большом количестве файлов. Вы можете легко адаптировать скрипт под свои нужды: изменить набор расширений, добавить поддержку дополнительных типов файлов или улучшить интерфейс вывода.