Понедельник - пятница с 10:00 до 18:00
Главная » Блог » Перекодировка файлов в 1С-Битрикс Windows-1251 в UTF-8 с паузами
Полезное

Перекодировка файлов в 1С-Битрикс Windows-1251 в UTF-8 с паузами

AJAX-версия для перекодировки файлов Windows-1251 в UTF-8 в 1С-Битрикс. Она состоит из двух файлов:

  1. convert_page.php — страница с интерфейсом и JS, которая по очереди дёргает worker, ждёт 5 секунд и продолжает.

  2. convert_worker.php — обрабатывает файлы порцией по 10 секунд и завершается (никаких sleep() внутри — пауза делается на клиенте).

Такая схема работает на любом хостинге: каждый HTTP-запрос короткий, буферизация не важна, пауза реально видна в логе.

Файл 1: convert_page.php

<?require($_SERVER['DOCUMENT_ROOT']."/bitrix/header.php"); ?>

<div style="font-family: monospace; padding: 20px;">
    <h2>Перекодировка файлов в UTF-8</h2>
    <div style="margin-bottom: 10px;">
        <button id="btn-start" onclick="startConvert()">▶ Старт</button>
        <button id="btn-stop" onclick="stopConvert()" disabled>■ Стоп</button>
        <button id="btn-reset" onclick="resetState()">⟲ Сбросить прогресс</button>
    </div>
    <div style="margin-bottom: 10px;">
        Статус: <span id="status">ожидание</span> |
        Обработано: <span id="counter">0</span> |
        Осталось примерно: <span id="remaining">?</span>
    </div>
    <div style="margin-bottom: 5px;">
        <progress id="progress" value="0" max="100" style="width: 100%; height: 20px;"></progress>
    </div>
    <pre id="log" style="background:#111; color:#0f0; padding:10px; height:500px; overflow:auto; border:1px solid #333;"></pre>
</div>

<script>
let running = false;
let totalFiles = 0;
let processedTotal = 0;

const WORKER_URL = '/convert_worker.php';

function log(msg, color) {
    const el = document.getElementById('log');
    const line = document.createElement('div');
    if (color) line.style.color = color;
    line.textContent = '[' + new Date().toLocaleTimeString() + '] ' + msg;
    el.appendChild(line);
    el.scrollTop = el.scrollHeight;
}

function setStatus(s) {
    document.getElementById('status').textContent = s;
}

function updateProgress(processed, total) {
    processedTotal = processed;
    totalFiles = total;
    document.getElementById('counter').textContent = processed;
    const pct = total > 0 ? Math.round(processed / total * 100) : 0;
    document.getElementById('progress').value = pct;
    document.getElementById('remaining').textContent = (total - processed);
}

function startConvert() {
    if (running) return;
    running = true;
    document.getElementById('btn-start').disabled = true;
    document.getElementById('btn-stop').disabled = false;
    document.getElementById('btn-reset').disabled = true;
    setStatus('работаем...');
    log('=== Запуск ===', '#ff0');
    step();
}

function stopConvert() {
    running = false;
    setStatus('остановлено пользователем');
    log('=== Остановлено ===', '#f80');
    document.getElementById('btn-start').disabled = false;
    document.getElementById('btn-stop').disabled = true;
    document.getElementById('btn-reset').disabled = false;
}

function resetState() {
    if (!confirm('Сбросить прогресс? Все файлы будут проверяться заново.')) return;
    fetch(WORKER_URL + '?action=reset')
        .then(r => r.json())
        .then(d => {
            log('Прогресс сброшен', '#f80');
            updateProgress(0, 0);
        })
        .catch(e => log('Ошибка сброса: ' + e, '#f00'));
}

function step() {
    if (!running) return;

    setStatus('обрабатываю порцию...');

    fetch(WORKER_URL + '?action=step')
        .then(r => r.json())
        .then(data => {
            if (data.error) {
                log('ОШИБКА: ' + data.error, '#f00');
                stopConvert();
                return;
            }

            // Выводим новые строки лога
            if (data.log && data.log.length) {
                data.log.forEach(line => log(line));
            }

            updateProgress(data.processed, data.total);

            if (data.done) {
                setStatus('готово');
                log('=== ВСЁ ГОТОВО. Обработано: ' + data.processed + ' ===', '#0f0');
                running = false;
                document.getElementById('btn-start').disabled = false;
                document.getElementById('btn-stop').disabled = true;
                document.getElementById('btn-reset').disabled = false;
                return;
            }

            // Пауза 5 секунд и следующий шаг
            setStatus('пауза 5 сек...');
            log('--- пауза 5 сек ---', '#888');
            setTimeout(step, 5000);
        })
        .catch(err => {
            log('Сетевая ошибка: ' + err, '#f00');
            setStatus('ошибка');
            running = false;
            document.getElementById('btn-start').disabled = false;
            document.getElementById('btn-stop').disabled = true;
        });
}
</script>

<?require($_SERVER['DOCUMENT_ROOT']."/bitrix/footer.php"); ?>

Файл 2: convert_worker.php

Положите его в корень сайта (рядом с convert_page.php).

<?php
// Worker не подключает header/footer Битрикса — это чистый JSON-ответ.
// При необходимости можно подключить пролог, если нужны настройки, но обычно не нужно.

header('Content-Type: application/json; charset=utf-8');
header('X-Accel-Buffering: no');

set_time_limit(0);

$stateFile = $_SERVER['DOCUMENT_ROOT'] . '/convert_state.json';
$lockFile  = $_SERVER['DOCUMENT_ROOT'] . '/convert_state.lock';

$directory = $_SERVER['DOCUMENT_ROOT'] . '/';
$skipPaths = [
    $_SERVER['DOCUMENT_ROOT'] . '/upload/',
    $_SERVER['DOCUMENT_ROOT'] . '/bitrix/',
];
$allowedExtensions = ['php', 'js', 'css'];

$workTime = 10; // сколько секунд работать в одной порции

$action = isset($_GET['action']) ? $_GET['action'] : 'step';

// ==== Сброс прогресса ====
if ($action === 'reset') {
    if (file_exists($stateFile)) @unlink($stateFile);
    echo json_encode(['ok' => true]);
    exit;
}

// ==== Простая блокировка, чтобы два таба не работали одновременно ====
$fp = @fopen($lockFile, 'c');
if ($fp && !flock($fp, LOCK_EX | LOCK_NB)) {
    echo json_encode([
        'error' => 'Другой процесс уже выполняет перекодировку. Закройте другие вкладки.',
    ]);
    exit;
}

// ==== Загрузка/сохранение состояния ====
function loadState($stateFile) {
    if (file_exists($stateFile)) {
        $data = json_decode(file_get_contents($stateFile), true);
        if (is_array($data) && isset($data['processed']) && is_array($data['processed'])) {
            return $data['processed'];
        }
    }
    return [];
}

function saveState($stateFile, $processed) {
    file_put_contents($stateFile, json_encode(['processed' => $processed], JSON_UNESCAPED_UNICODE));
}

function collectFiles($dir, $skipPaths, $allowedExtensions, &$result = []) {
    $files = @scandir($dir);
    if ($files === false) return $result;

    foreach ($files as $file) {
        if ($file === '.' || $file === '..') continue;

        $path = $dir . '/' . $file;

        if (in_array($path, $skipPaths)) continue;

        // Не заходим в файл состояния и lock
        if (basename($path) === 'convert_state.json' || basename($path) === 'convert_state.lock') {
            continue;
        }

        if (is_dir($path)) {
            collectFiles($path, $skipPaths, $allowedExtensions, $result);
        } elseif (is_file($path)) {
            $ext = pathinfo($path, PATHINFO_EXTENSION);
            if (in_array($ext, $allowedExtensions)) {
                $result[] = $path;
            }
        }
    }
    return $result;
}

// ==== Основная обработка порции ====
$processed    = loadState($stateFile);
$processedMap = array_flip($processed);

$allFiles   = collectFiles($directory, $skipPaths, $allowedExtensions);
$totalFiles = count($allFiles);

$log          = [];
$startTime    = microtime(true);
$countInBatch = 0;
$done         = false;

foreach ($allFiles as $path) {

    if (isset($processedMap[$path])) continue;

    // Если время порции вышло — прекращаем, сохраняем состояние и отдаём JSON
    if ((microtime(true) - $startTime) >= $workTime) {
        $done = false;
        break;
    }

    $content = @file_get_contents($path);
    if ($content === false) {
        $log[] = "Не удалось прочитать: $path";
        $processedMap[$path] = true;
        $countInBatch++;
        continue;
    }

    $encoding = mb_detect_encoding($content, ['CP1251', 'Windows-1251', 'UTF-8'], true);

    if ($encoding === 'CP1251' || $encoding === 'Windows-1251') {
        $contentUtf8 = mb_convert_encoding($content, 'UTF-8', 'CP1251');
        @file_put_contents($path, $contentUtf8);
        $log[] = "Перекодирован: $path";
    } elseif ($encoding === 'UTF-8') {
        $log[] = "Уже UTF-8: $path";
    } else {
        $log[] = "Неизвестная кодировка ($encoding): $path";
    }

    $processedMap[$path] = true;
    $countInBatch++;
}

// Если прошли весь список — значит всё готово
if (count($processedMap) >= $totalFiles) {
    $done = true;
}

saveState($stateFile, array_keys($processedMap));

// Освобождаем блокировку
if ($fp) {
    flock($fp, LOCK_UN);
    fclose($fp);
}

echo json_encode([
    'done'      => $done,
    'processed' => count($processedMap),
    'total'     => $totalFiles,
    'batch'     => $countInBatch,
    'log'       => $log,
], JSON_UNESCAPED_UNICODE);

Как это работает

  1. Пользователь открывает convert_page.php в браузере и жмёт «Старт».

  2. JS вызывает convert_worker.php?action=step.

  3. Worker:

    • загружает список уже обработанных файлов из convert_state.json;

    • обходит дерево, пропуская upload/ и bitrix/;

    • обрабатывает файлы, пока не пройдёт 10 секунд;

    • сохраняет состояние;

    • возвращает JSON с новыми строками лога и прогрессом;

    • завершается — никакого sleep() внутри.

  4. JS выводит новые строки в лог, обновляет прогресс-бар, ждёт 5 секунд через setTimeout и вызывает worker снова.

  5. Когда все файлы обработаны — worker вернёт done: true, JS покажет «ВСЁ ГОТОВО» и остановится.

Почему это надёжно

Мелочи

Читать все статьи