ProgressLoader

Прогресс выполнения тяжелой задачи в PHP

Случилось мне как-то иметь дело с тяжелым PHP-скриптом. Нужно было каким-то образом в браузере отображать прогресс выполнения задачи в то время, пока в достаточно длительном цикле на стороне PHP проводились расчёты. В таких случаях обычно прибегают к периодичному выводу строки вроде этой:

 

Этот вариант меня не устраивал по нескольким причинам, к тому же мне в принципе не нравится такой подход.

Итераций у меня было порядка 3000—5000. Я прикинул, что великоват трафик для такой несложной затеи. Кроме того, мне такой вариант казался очень некрасивым с технической точки зрения, а внешний вид страницы и вовсе получался уродлив: футер дойдет еще не скоро — после последнего уведомления о 100% выполнении задачи.

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

Несколько наводящих вопросов. Асинхронные HTTP-запросы возможны? — Да. Можно ли с помощью одного-единственного байта сообщить, что часть большой задачи выполнена? — Да. Можем ли мы постепенно (последовательно) получать и обрабатывать данные с помощью XMLHttpRequest.onreadystatechange ? — Да. Мы даже можем воспользоваться заголовками HTTP для передачи предварительного уведомления об общей продолжительности выполняемой задачи (если это возможно в принципе).

Решение простое. Основанная страница — это пульт управления. С пульта можно запустить и остановить задачу. Эта страница инициирует XMLHttpRequest — стартует выполнение основной задачи. В процессе выполнения этой задачи (внутри основного цикла) скрипт отправляет клиенту один байт — символ пробела. На пульте в обработчике onreadystatechange мы, получая байт за байтом, сможем делать вывод о прогрессе выполнения задачи.

Схема такая. Скрипт операции:

xhr.onreadystatechange = function() < if (this.readyState == 3) < var progress = this.responseText.length; document.getElementById('progress').style.width = progress + '%'; >>; 

Однако, итераций всего 50. Об этом мы знаем, потому что сами определили их количество в файле скрипта. А если не знаем или количество может меняться? При readyState == 2 мы можем получить информацию из заголовков. Давайте этим и воспользуемся для определения количества итераций:

А на пульте получим и запомним это значение:

var progressMax = 100; xhr.onreadystatechange = function() < if (this.readyState == 2) < progressMax = +this.getResponseHeader('X-Progress-Max') || progressMax; >else if (this.readyState == 3) < var progress = 100 * this.responseText.length / progressMax; document.getElementById('progress').style.width = progress + '%'; >>; 

Общая схема должна быть ясна. Поговорим теперь о подводных камнях.

Во-первых, если в PHP включена опция output_buffering , нужно это учесть. Здесь все просто: если она включена, то при запуске скрипта ob_get_level() будет больше 0. Нужно обойти буферизацию. Еще, если вы используете связку Nginx FastCGI PHP, нужно учесть, что и FastCGI и сам Nginx будут буферизовать вывод. Последний это будет делать в том случае, если собирается сжимать данные для отправки. Устраняется проблема просто (из PHP-скрипта):

header('X-Accel-Buffering: no', true); 

Кроме того, то ли Nginx, то ли FastCGI, то ли сам Chrome считают, что инициировать прием-передачу тела ответа, которое содержит всего-навсего один байт — слишком расточительно. Поэтому нужно предварить всю операцию дополнительными байтами. Нужно договориться, скажем, что первые 20 пробелов вообще ничего не должны означать. На стороне PHP их нужно просто «выплюнуть» в вывод, а в обработчике onreadystatechange их нужно проигнорировать. На мой взгляд — раз уж вся конфигурационная составляющая передается в заголовках — то и это число игнорируемых пробелов тоже лучше передать в заголовке. Назовем это padding-ом.

Читайте также:  Most common в питоне

На стороне клиента это тоже нужно учесть:

var progressMax = 100, progressPadding = 0; xhr.onreadystatechange = function() < if (this.readyState == 2) < progressMax = +this.getResponseHeader('X-Progress-Max') || progressMax; progressPadding = +this.getResponseHeader('X-Progress-Padding') || progressPadding; >else if (this.readyState == 3) < var progress = 100 * (this.responseText.length - progressPadding) / progressMax; document.getElementById('progress').style.width = progress + '%'; >>; 

Откуда число 20? Если подскажете — буду весьма признателен. Я его установил экспериментальным путем.

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

function ob_ignore($data, $flush = false) < $ob = array(); while (ob_get_level()) < array_unshift($ob, ob_get_contents()); ob_end_clean(); >echo $data; if ($flush) flush(); foreach ($ob as $ob_data) < ob_start(); echo $ob_data; >return count($ob); > 

С ее помощью можно обойти все уровни буферизации, вывести данные напрямую, после чего все буферы восстанавливаются.

Кстати, а почему именно пробел используется для уведомления о выполненной части задачи? Просто потому что почти любой формат представления данных в вебе такими пробелами не испортишь. Можно применить такой метод передачи уведомления о прогрессе операции, а после всего этого вывести отчет о результатах в JSON.

Если все привести в порядок, немного оптимизировать и дополнить код всеми возможностями, которые могут пригодиться, получится вот что:

function ProgressLoader(url, callbacks) < var _this = this; for (var k in callbacks) < if (typeof callbacks[k] != 'function') < callbacks[k] = false; >> delete k; function getXHR() < var xhr; try < xhr = new ActiveXObject("Msxml2.XMLHTTP"); >catch (e) < try < xhr = new ActiveXObject("Microsoft.XMLHTTP"); >catch (E) < xhr = false; >> if (!xhr && typeof XMLHttpRequest != 'undefined') < xhr = new XMLHttpRequest(); >return xhr; > this.xhr = getXHR(); this.xhr.open('GET', url, true); var contentLoading = false, progressPadding = 0, progressMax = -1, progress = 0, progressPerc = 0; this.xhr.onreadystatechange = function() < if (this.readyState == 2) < contentLoading = false; progressPadding = +this.getResponseHeader('X-Progress-Padding') || progressPadding; progressMax = +this.getResponseHeader('X-Progress-Max') || progressMax; if (callbacks.start) < callbacks.start.call(_this, this.status); >> else if (this.readyState == 3) < if (!contentLoading) < contentLoading = !!this.responseText .replace(/^\s+/, ''); // .trimLeft() — медленнее О_о >if (!contentLoading) < progress = this.responseText.length - progressPadding; progressPerc = progressMax >0 ? progress / progressMax : -1; if (callbacks.progress) < callbacks.progress.call(_this, this.status, progress, progressPerc, progressMax ); >> else if (callbacks.loading) < callbacks.loading.call(_this, this.status, this.responseText); >> else if (this.readyState == 4) < if (callbacks.end) < callbacks.end.call(_this, this.status, this.responseText); >> >; if (callbacks.abort) < this.xhr.onabort = callbacks.abort; >this.xhr.send(null); this.abort = function() < return this.xhr.abort(); >; this.getProgress = function() < return progress; >; this.getProgressMax = function() < return progressMax; >; this.getProgressPerc = function() < return progressPerc; >; return this; > 
 echo $data; if ($flush) flush(); foreach ($ob as $ob_data) < ob_start(); echo $ob_data; >return count($ob); > if (($work = @$_GET['work']) > 0) < header("X-Progress-Max: $work", true, 200); header("X-Progress-Padding: 20"); ob_ignore(str_repeat(' ', 20), true); for ($i = 0; $i < $work; $i++) < usleep(rand(100000, 500000)); ob_ignore(' ', true); >echo $work.' done!'; die(); > 
     progress, button     

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

Источник

Реализация пошаговой работы PHP-скрипта с помощью AJAX

Искал более-менее простое и универсальное средство для организации пошаговой работы скрипта, но так ничего и не нашел. Даже вопрос в QA задал, везде только общие фразы. Поэтому решил сам сделать такой инструмент.

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

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

Хотелось бы разбить обработку файла на несколько частей и запускать скрипт в работу уже по частям.

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

Читайте также:  Карта-изображение

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

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

index.html

     
Старт Заново

scriptoffset.php

 // Можно передавать в скрипт разный action и в соответствии с ним выполнять разные действия. $action = $_POST['action']; if (empty($action)) $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

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

Что в итоге

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

  • Мы можем установить количество обрабатываемых строк за одну итерацию (в самом скрипте);
  • Пользователю показывается настоящий прогресс-бар, а не бесконечная «крутилка» — если прогресс-бар стоит на середине, значит обработана половина файла;
  • Пользователь может остановить выполнение скрипта. В этом случае offset записывается в cookies на 10 мин, чтобы он мог продолжить работу скрипта с того же места.
  • Если пользователь обновит страницу, ему будет предложено продолжить работу скрипта с места остановки или начать заново (так же благодаря cookies).
Читайте также:  Fatal error in launcher unable to create process using python exe pip exe

UPD. Решение, адаптированное для MODX здесь.

Источник

Форум

Помогите, пожалуйста, разобраться с пошаговым выполненим php-скрипта.
Суть задачи следующая:
1. Требуется автоматизировать ввод картинок в Инфоблок (т.е. вставить картинку для анонса и детального просмотра) (папка с картинками лежит на сервере в папке images ~1000 картинок)
(этот пункт я реализовал )
2. Потом еще много чего нужно автоматизировать (скрипты я уже написал).

Но возникла проблема: время выполнения скриптов не укладывается в ограничение сервера.

Как реализовать пошаговое выполнение php-скрипта?

Посмотрите, как работают скрипты CSV-импорта — там пошаговость.
Вкратце — смотрите время, если подходит к концу — отдаете в страницу данные для следующего шага. На страничке делаете refresh.

Я в этом деле еще новичек

Поэтому не могли бы вы подробнее описать.

И подскажите путь к скриптам CSV-импорта.

Дайте, пожалуйста, какой-нибудь примерчик.

Как считать время, отдавать в страницу данные для следующего шага и делать refresh?

Центр поддержки

Продукты

Управление сайтом

Битрикс24

Интернет-магазин + CRM

Решения

Для интернет-магазинов

Каталог готовых решений

Внедрение

Выбрать партнера

Проверить партнера

Стать партнером

1С-Битрикс http://www.1c-bitrix.ru Общие вопросы info@1c-bitrix.ru Приобретение и лицензирование продуктов : sales@1c-bitrix.ru Маркетинг/мероприятия/PR marketing@1c-bitrix.ru Партнерская программа partners@1c-bitrix.ru Мы работаем с 10:00 до 19:00 по московскому времени. Офис в Москве 127287 Россия Московская область Москва 2-я Хуторская улица дом 38А строение 9 Офис в Калининграде +7 (4012) 51-05-64 Офис в Калининграде 236001 Россия Калининградская область Калининград Московский проспект 261 Офис в Киеве ukraine@1c-bitrix.ru Телефон в Киеве +3 (8044)221-55-33 Офис в Киеве 01033 Украина Калининградская область Киев улица Шота Руставели 39/41 офис 1507

Контент для лиц от 16 лет и старше

© 2001-2023 «Битрикс», «1С-Битрикс». Работает на 1С-Битрикс: Управление сайтом. Политика конфиденциальности

Источник

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

Доброго времени суток.
Как сделать пошаговое выполнение php-скрипта с выводом результата выполнения шагов в реальном времени без перезагрузки страницы?

BaNru

Гуру форума

chapser

Постоялец

Это понятно, что ajax. Как реализовать? Я в этом новичок. Суть в том, что скрипт выполняет несколько операций и только после этого выводит результат разом. Нужно. чтобы после выполнения каждой операции выводился результат. Вот в чем фишка.

yaski

web3
 Line to show."; echo str_pad('',4096)."\n"; ob_flush(); flush(); sleep(2); > echo "Done."; ob_end_flush(); ?>

chapser

Постоялец
 Line to show."; echo str_pad('',4096)."\n"; ob_flush(); flush(); sleep(2); > echo "Done."; ob_end_flush(); ?>

Если запускать скрипт напрямую — работает пошагово. Но, если запустить через ajax — выводит общий результат, а не по шагам. Нужно по шагам.

yaski

web3

chapser

Постоялец

chapser

Постоялец
 
, success: function (data) < if (data === '1') < $("#result").html(data); >else < $("#result").html(data); >> >); >);
$current = "1 шаг"; echo outputProgress($current); $current = "2 шаг"; echo outputProgress($current); $current = "Выполнено"; echo outputProgress($current); function outputProgress($current) < echo "" . $current . "
"; myFlush(); sleep(2); > function myFlush() < echo(str_repeat(' ', 256)); if (@ob_get_contents()) < @ob_end_flush(); >flush(); >

BaNru

Гуру форума

latteo

Эффективное использование PHP, MySQL

ob_* функции довольно часто не работают из-за буферизации или каких-то других настроек nginx/apach. Из-за этого от них отказался и пересел на консольные скрипты

Поиск по packagist привёл к вот такой поделке: Для просмотра ссылки Войди

Зарегистрируйся — идея норм, реализация страшненькая.

Источник

Оцените статью