Для чего это вообще нужно?


Бывает необходимо обработать скриптом какой-то очень уж большой файл, например, для импорта. Естественно, время работы скрипта увеличивается пропорционально размеру файла или количеству строк в нем.

Хотелось бы разбить обработку файла на несколько частей и запускать скрипт в работу уже по частям.
9b743dabc1573d6479b81b8d9e30164b Принцип реализации давно известен — обмен данными между сервером и клиентом:
Клиент запускает скрипт, тот выполняет несколько итераций и возвращает клиенту номер строки, на которой он остановился. После этого клиент делает новый запрос, в котором передает скрипту этот номер и скрипт продолжает работу дальше.

Собственно сам код



Для работы нам понадобятся:

index.html

<html>
  <head>
    <title>ScriptOffset - инструмент для организации пошаговой работы скрипта</title>
    <script type="text/javascript" src="http://yandex.st/jquery/1.7.1/jquery.min.js"></script>
    <script type="text/javascript" src="/scriptoffset.js"></script>
    <link rel="stylesheet" type="text/css" href="/scriptoffset.css">
  </head>
  <body>
    <div class="form">
      <input id="url" name="url">
      <input id="offset" name="offset" type="hidden">

      <div class="progress" style="display: none;">
        <div class="bar" style="width: 0%;"></div>
      </div>

      <a href="#" id="runScript"  class="btn" data-action="run">Старт</a>
      <a href="#" id="refreshScript" class="btn" style="display: none;">Заново</a>
    </div>
  </body>
</html>

scriptoffset.php

<?php
// Отвечаем только на Ajax
if ($_SERVER['HTTP_X_REQUESTED_WITH'] != 'XMLHttpRequest') {return;}

// Можно передавать в скрипт разный action и в соответствии с ним выполнять разные действия.
$action = $_POST['action'];
if (empty($action)) {return;}

$count = 50;
$step = 1;

// Получаем от клиента номер итерации
$url = $_POST['url']; if (empty($url)) return;
$offset = $_POST['offset'];

// Проверяем, все ли строки обработаны
$offset = $offset + $step;
if ($offset >= $count) {
  $sucsess = 1;
} else {
  $sucsess = round($offset / $count, 2);
}

// И возвращаем клиенту данные (номер итерации и сообщение об окончании работы скрипта)
$output = Array('offset' => $offset, 'sucsess' => $sucsess);
echo json_encode($output);

scriptoffset.js

function setCookie (url, offset){
    	var ws=new Date();
        if (!offset && !url) {
                ws.setMinutes(10-ws.getMinutes());
            } else {
                ws.setMinutes(10+ws.getMinutes());
            }
        document.cookie="scriptOffsetUrl="+url+";expires="+ws.toGMTString();
        document.cookie="scriptOffsetOffset="+offset+";expires="+ws.toGMTString();
    }
    
function getCookie(name) {
        var cookie = " " + document.cookie;
        var search = " " + name + "=";
        var setStr = null;
        var offset = 0;
        var end = 0;
        if (cookie.length > 0) {
            offset = cookie.indexOf(search);
            if (offset != -1) {
                offset += search.length;
                end = cookie.indexOf(";", offset)
                if (end == -1) {
                    end = cookie.length;
                }
                setStr = unescape(cookie.substring(offset, end));
            }
        }
        return(setStr);
    }

function showProcess (url, sucsess, offset, action) {
        $('#url, #refreshScript').hide();
        $('.progress').show();
        $('#runScript').text('Стоп!');
        $('.bar').text(url);
        $('.bar').css('width', sucsess * 100 + '%');
        setCookie(url, offset);

        $('#runScript').click(function(){
                document.location.href=document.location.href
            });
        
        scriptOffset(url, offset, action);
    }

function scriptOffset (url, offset, action) {
        $.ajax({
            url: "http://bfmn.ru/scriptoffset/scriptoffset.php",
            type: "POST",
            data: {
                "action":action
              , "url":url
              , "offset":offset
            },
            success: function(data){
                data = $.parseJSON(data);
                if(data.sucsess < 1) {
                    showProcess(url, data.sucsess, data.offset, action);
                    } else {
                    setCookie();
                    $('.bar').css('width','100%');
                    $('.bar').text('OK');
                    $('#runScript').text('Еще');
                    }
            }
        });
    }
    
$(document).ready(function() {
    
    var url = getCookie("scriptOffsetUrl");
    var offset = getCookie("scriptOffsetOffset");
    
    if (url && url != 'undefined') {		
            $('#refreshScript').show();
            $('#runScript').text('Продолжить');
            $('#url').val(url);
            $('#offset').val(offset);
        }
    
    $('#runScript').click(function() {
        
            var action = $('#runScript').data('action');
            var offset = $('#offset').val();
            var url = $('#url').val();
            
            if ($('#url').val() != getCookie("scriptOffsetUrl")) {
                    setCookie();
                    scriptOffset(url, 0, action);
                } else {
                    scriptOffset(url, offset, action);
                }
            return false;
        });
        
    $('#refreshScript').click(function() {
        
            var action = $('#runScript').data('action');
            var url = $('#url').val();
        
            setCookie();
            scriptOffset(url, 0, action);
            return false;
        });
        
});

scriptoffset.css

input {
    font-size: 13px;
    margin: 0;
    padding: 0 3px;
    vertical-align: middle;
    border: 1px solid #CCCCCC;
    border-radius: 3px 3px 3px 3px;
    color: #808080;
    display: inline-block;
    font-size: 13px;
    height: 26px;
    line-height: 18px;
    width: 243px;
    -moz-transition: border 0.2s linear 0s, box-shadow 0.2s linear 0s;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1) inset;
}

.btn {
    font-size: 13px;
    padding: 5px 8px;
    background-color: #0064CD;
    background-image: -moz-linear-gradient(center top , #049CDB, #0064CD);
    background-repeat: repeat-x;
    border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
    color: #FFFFFF;
    display: inline-block;
    vertical-align: middle;
    border-radius: 3px 3px 3px 3px;
    text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
    text-decoration: none;
}

.btn:hover {
    background-position: 0 -15px;
}

.btn:active {
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25) inset, 0 1px 2px rgba(0, 0, 0, 0.05);
}

.progress {
    font-size: 13px;
    margin: 0;
    vertical-align: middle;
    background-color: #F7F7F7;
    background-image: -moz-linear-gradient(center top , #F5F5F5, #F9F9F9);
    background-repeat: repeat-x;
    border-radius: 4px 4px 4px 4px;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1) inset;
    height: 28px;
    width: 250px;
    overflow: hidden;
    display: inline-block;
}

.progress .bar {
    background-color: #0E90D2;
    background-image: -moz-linear-gradient(center top , #149BDF, #0480BE);
    background-size: 40px 40px;
    -moz-box-sizing: border-box;
    -moz-transition: width 0.6s ease 0s;
    background-repeat: repeat-x;
    box-shadow: 0 -1px 0 rgba(0, 0, 0, 0.15) inset;
    color: #FFFFFF;
    float: left;
    font-size: 12px;
    height: 100%;
    text-align: left;
    padding: 5px 8px;
    font-size: 13px;
    text-shadow: 1px 1px #333;
    white-space: nowrap;
}

div.form {
    margin: 150px auto 0;
    width: 500px;
}

Для оформления css взял несколько правил из Bootstrap.

Что в итоге


В поле url мы указываем, например, ссылку на файл, который нужно обработать, и запускаем скрипт. Появляется прогресс-бар, а мы сидим и ждем, когда он доползет до 100 %, чтобы увидеть результат работы.

При работе с этим решением:

  • Мы можем установить количество обрабатываемых строк за одну итерацию (в самом скрипте);
  • Пользователю показывается настоящий прогресс-бар, а не бесконечная «крутилка» — если прогресс-бар стоит на середине, значит обработана половина файла;
  • Пользователь может остановить выполнение скрипта. В этом случае offset записывается в cookies на 10 мин, чтобы он мог продолжить работу скрипта с того же места.
  • Если пользователь обновит страницу, ему будет предложено продолжить работу скрипта с места остановки или начать заново (так же благодаря cookies).



Если у сообщества есть примеры реализации подобного функционала или вообще готовые решения для пошаговой работы со скриптами, буду благодарен ссылкам в комментариях.

Php+Ajax полоса загрузки ProgressBar

17.01.2012
php ajax polosa zagruzki progressbar

 ProgressBar - это индикатор, который показывает скорость и процент выполнения любого процесса. ProgressBar также называют полосой загрузки, или индикатором загрузки. Обычно прогресс бар используют для отображения процесса скачивания или закачивания файлов, но существуют и другие способы применения. Почему то в интернете все ссылки связанные использованием ProgressBar на php, сводятся к загрузке файлов, неужели людям не нужно отслеживать процесс выполнения других задач? Например, таких как процент считывания XML файла, или процесс заполнения SQL таблицы данными. Немало важным индикатор отслеживания является и при работе с удаленным сервером.

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

Fatal error: Maximum execution time of 30 seconds exceeded in …

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

Пишем PHP скрипт для полосы загрузки с использованием AJAX

Представим, что у нас есть объемный алгоритм, который обрабатывает информацию по частям, и назовем его условно «сложной задачей». Чтобы было понятно, представьте что информацией является разбитый на части файл, который нужно скачать с сервера.

Создавать ProgressBar мы будем последовательно, выполнив для этого 3 шага.

AJAX обработчик (Шаг 1)

Сразу скажу, что для корректной работы AJAX технологии нам понадобится, уже готовый скрипт: ajax.js, в задачи которого входит отправка запросов на сервер и ожидание ответа. На самом деле он не большой, и можно было бы описать его более подробно, но боюсь, что это сделает статью слишком длинной. О работе с AJAX я напишу подробнее в другой статье, а пока просто выложу код упомянутого выше скрипта:

function XmlHttp()
{
    var xmlhttp;
    try{xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");}
    catch(e)
    {
     try {xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");}
     catch (E) {xmlhttp = false;}
    }
    if (!xmlhttp && typeof XMLHttpRequest!='undefined')
    {
     xmlhttp = new XMLHttpRequest();
    }
      return xmlhttp;
}
 
function ajax(param)
{
    if (window.XMLHttpRequest) req = new XmlHttp();
    send="";               
    for (var i in param.data) send+= i+"="+param.data[i]+"&";      
    req.open("POST", param.url, true);              
    req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
    req.send(send);
    req.onreadystatechange = function()
    {
       if (req.readyState == 4 && req.status == 200) //если ответ положительный
       {
         if(param.success)param.success(req.responseText);
       }
    }
}

И так, скачайте данный скрипт, и положите его в корневую папку вашего сайта, либо в ручную создайте пустой файл ajax.js и вставьте в него приведенный выше код.

Пишем скрипт PHP выполняющий «сложную задачу» (Шаг 2)

Вторым действием создадим скрипт index.php , который будет отвечать за вывод полосы загрузки на страницу, а также будет выполнять расчет «сложной задачи». В корневой папке сайта создайте пустой файл index.phр. Откройте его и вставьте следующий код:

<?
session_start();//открываем сессию для записи
if($_POST["difficult_task"]){      
    $part=231; //общее количество задач
    $_SESSION['sucsess_part']++; //количество выполненных подзадач
    echo floor(($_SESSION['sucsess_part']*100)/$part);//процент выполненния общей задачи
}
?>
else{?>
…
<?}?>

 

Эта часть кода описывает процесс выполнения «сложной задачи», если интерпретировать ее на задачу скачивания файла, то переменная $part должна содержать количество частей разбитого файла. Счетчик уже скачанных частей должен храниться в сессии на сервере, поэтому сразу открываем ее для записи функцией session_start(), для того чтобы в дальнейшем увеличивать $_SESSION['sucsess_part'] на один, каждый раз при успешной закачке одной из частей файла. Функция floor() округляет выполненные проценты скачивания до целого числа в меньшую сторону.

Вот и весь скрипт расчета сложной задачи, конечно на практике данный скрипт разрастётся сотнями строк, но для общего примера хватит и данного кода. В принципе правильнее было бы выделить этот скрипт отдельно и обращаться к нему с html странички с помощью ajax.js, но т.к. он слишком мал, то будет понятнее включить обработчик «сложной задачи» непосредственно в код страницы.

Организация процесса обновления полосы загрузки и вывод её на страницу (3 шаг)

Код вывода HTML страницы и progressbar будет заключен между скобками else из предыдущего шага:

else{?>
…
<?}?>

Скопируйте следующий кусок кода и вставьте его вместо многоточия.

<html>
        <head>
            <title>Сложная задача</title>
            <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
            <script type="text/javascript" src="/ajax.js"></script>     
            <script>
                function ProgressBar(persent){
                if(persent<100){ //если задача не достигла 100% готовности, отправляем запрос на ее выполнение
                      ajax({
                               url:"http://<?=$_SERVER['HTTP_HOST']."/progressbar/index.php";?>", //путь к скрипту, который обрабатывает задачу
                               data:  //данные передаваемые в POST запросе
                               {
                                   difficult_task:"difficult_task",                                  
                               },
                               success:function(data){  //функция обратного вызова, выполняется в случае успехной отработки скрипта                        
                                document.getElementById("text").innerHTML="<br/>Завершено <b>"+data+"%</b>"; // выводим в информационный блок количество выполненных %
                                document.getElementById("bar").style.width=data+"%"; //растягиваем полосу загрузки    
                                document.getElementById("persent").innerHTML=data+"%";  // изменяем счетчик процентов, на полосе загрузки                          
                                ProgressBar(parseInt(data));// рекурсивно вызываем этуже функцию, она будет выполняться пока не выполнит 100%
                                }
                            })
                        }
                        else{//если задача выполненна на 100%, то выводим информацию об этом.
                            document.getElementById("status").innerHTML="";
                            document.getElementById("text").innerHTML="<br/> Задача успешно выполнена!<br/>Для повторного запуска нужно сбросить сессию. (Перезапустите браузер)";
                            document.getElementById("bar").style.width="0%";       
                            document.getElementById("load").style.display="none";          
                            document.getElementById("bar").style.display="none";   
                            document.getElementById("btn").style.display="none";       
                        }                          
                    }  
                </script>      
        </head>
        <body>
            <form method="post" action="">         
                <div id="status"></div>                
                    <div id="load" style="text-align: center; color: white; display: block; height: 20px; width: 200px; background:blue; border: solid 1px black;">
                        <div id="bar" style="display: block; height: 20px; width: 0%; background:green;" ><div id="persent" style="position: relative; float:left; width:200px;">0%</div></div>
                    </div>
                <br/>
                <div id="text"></div>
                <br/>
                <input id="btn" type='button' value='Выполнить сложную задачу' onclick='ProgressBar(0);'>
                <br/>
            </form>
        </body>
    </html>

Данный код выводит на страницу кнопку с названием ‘Выполнить сложную задачу’, при нажатии на которую запускается процесс обработки сложной задачи с использованием ajax запросов. В коде присутствуют комментарии, поэтому не стану их дублировать, просто советую внимательно посмотреть что там написанно.

Если вы все сделали правильно, то у вас в корне сайта должны лежать два файла: index.php и ajax.js, в противном случаем вы можете скачать их одним архивом.

Запустите index.php вы увидите кнопку ‘Выполнить сложную задачу’. Нажмите на нее, и синяя полоса загрузки начнет заполняться зеленым цветом.

polosa zagruzki progressbar

При достижении 100% вы увидите сообщение о успешном выполнении «сложной задачи».

progressbar ajax

Помните о том, что счетчик выполнения хранится в сессии на стороне сервера, поэтому для того, чтобы вновь посмотреть работу скрипта вам нужно будет перезапустить браузер, либо дописать скрипт самостоятельно таким образом, чтобы сессия сбрасывалась при достижении 100% результата.

Сегодня в статье с ужасным названием php+ajax полоса загрузки progressbar
мы рассмотрели, очень полезный на мой взгляд, механизм отслеживания выполнения задачи в процентах с выводом графического индикатора. Полоса загрузки, или попросту progressbar, будет полезен при синхронизации данных, при скачивании или закачивании файлов, а также при удаленной работе с чужим сайтом.