Найкращий спосіб керувати тривалим php-сценарієм?


80

У мене є PHP-скрипт, який вимагає тривалого часу (5-30 хвилин). Про всяк випадок, якщо це має значення, скрипт використовує curl для вилучення даних з іншого сервера. Ось чому так довго триває; йому слід зачекати, поки кожна сторінка завантажиться, перш ніж обробляти її та переходити до наступної.

Я хочу мати можливість ініціювати сценарій і нехай це буде до кінця, що встановить прапор у таблиці бази даних.

Мені потрібно знати, як закінчити запит http до завершення роботи скрипта. Крім того, чи найкращим способом це зробити php-скрипт?


1
Хоча ви не згадали про це мовами, що підтримуються вашим сервером, я здогадуюсь, якщо у вас є можливість запускати Ruby та Perl, ви, ймовірно, можете додати Node.js, і це для мене звучить як ідеальний варіант використання для Javascript : ваш скрипт буде проводити більшу частину часу в очікуванні завершення запитів, що є областю, в якій перевершує асинхронна парадигма. Жодні потоки не означають легкої синхронізації, паралельність означає швидкість.
djfm

Ви можете зробити це за допомогою PHP. Я б використовував Goutteі Guzzleдля реалізації потоків паралельності. Ви також можете заглянути, Gearmanщоб запустити паралельні запити у формі робочих.
Андре Гарсія,

Відповіді:


114

Звичайно, це можна зробити за допомогою PHP, однак вам НЕ слід робити це як фонове завдання - новий процес повинен бути віддалений від групи процесів, де він ініціюється.

Оскільки люди продовжують давати однакову неправильну відповідь на цей FAQ, я написав тут більш повну відповідь:

http://symcbean.blogspot.com/2010/02/php-and-long-running-processes.html

З коментарів:

Коротка версія - це shell_exec('echo /usr/bin/php -q longThing.php | at now');лише причини, за якими тут трохи довше.


Ця публікація в блозі - справжня відповідь. Виконавці та система PHP мають занадто багато потенційних підводних каменів.
incredimike

2
будь-який шанс скопіювати відповідні деталі у відповідь? занадто багато старих відповідей, які посилаються на мертві блоги. Цей блог ще не помер (ще), але одного дня буде.
Мерфі

5
Коротка версія - це shell_exec('echo /usr/bin/php -q longThing.php | at now');лише причини, за якими тут трохи довше.
symcbean

1
Відповідь із високим рівнем голосу на питання, що проголосувало за високу оцінку, але відповідь не містить значно більше, ніж посилання на допис у блозі. Будь ласка, додайте фактичну відповідь відповідно до meta.stackexchange.com/questions/8231/… та / або довідкового центру
Нанна

1
Чи можу я знати, що робить цей параметр -q?
Кірен Сіва

11

Швидким і брудним способом було б використовувати ignore_user_abort функції в php. Це в основному говорить: Не хвилює, що робить користувач, запускайте цей сценарій, поки він не буде закінчений. Це дещо небезпечно, якщо це веб-сайт, який стоїть перед громадськістю (оскільки можливо, у вас вийде одночасно запущено 20 ++ версій сценарію, якщо він ініціюється 20 разів).

"Чистий" спосіб (принаймні IMHO) полягає у встановленні прапора (наприклад, у db), коли ви хочете ініціювати процес і запускати cronjob щогодини (або близько того), щоб перевірити, чи встановлений цей прапор. Якщо він встановлений, запускається тривалий сценарій, якщо його НЕ встановлено, нічого не відбувається.


Отже, метод "ignore_user_abort" дозволить користувачеві закрити вікно браузера, але чи є щось, що я міг би зробити, щоб він повернув клієнту відповідь HTTP, перш ніж він закінчиться працювати?
kbanman

1
@kbanman Так. Вам потрібно закрити з'єднання: header("Connection: close", true);. І не забудьте змити ()
Benubird

8

Ви можете використовувати exec або систему, щоб розпочати фонове завдання, а потім виконати роботу в цьому.

Крім того, є кращі підходи до вишкрібання павутини, ніж той, який ви використовуєте. Ви можете використовувати різьбовий підхід (декілька потоків, що роблять по одній сторінці за раз), або один, використовуючи eventloop (один потік робить кілька сторінок одночасно). Мій особистий підхід до використання Perl - використання AnyEvent :: HTTP .

ETA: symcbean пояснив, як тут правильно від'єднати фоновий процес .


5
Майже правильно. Просто використання exec або системи повернеться, щоб вкусити вас за дупу. Детальніше див. У моїй відповіді.
symcbean

5

Ні, PHP - не найкраще рішення.

Я не впевнений щодо Ruby чи Perl, але за допомогою Python ви можете переписати скребок сторінки на багатопотоковий, і він, ймовірно, працюватиме принаймні в 20 разів швидше. Написати багатопотокові програми може бути дещо складним завданням, але найпершим додатком Python, який я написав, був скрепер сторінок із потоковим потоком. І ви можете просто викликати скрипт Python з вашої сторінки PHP, використовуючи одну з функцій виконання оболонки.


Фактична частина обробки мого вишкрібання є дуже ефективною. Як я вже згадував вище, саме завантаження кожної сторінки вбиває мене. Мені було цікаво, чи призначений PHP для запуску протягом таких тривалих періодів.
kbanman

Я трохи упереджений, тому що з моменту вивчення Python я відверто ненавиджу PHP. Однак якщо ви скребуєте більше однієї сторінки (послідовно), ви майже впевнені, що отримаєте кращу продуктивність, роблячи це паралельно з багатопотоковою програмою.
jamieb

1
Ви можете надіслати мені приклад такого скребка для сторінок? Це допомогло б мені вдосталь побачити, оскільки я ще не торкався Python.
kbanman

Якби мені довелося його переписати, я б просто використав eventlet. Це робить мій код приблизно в 10 разів простішим: eventlet.net/doc
jamieb

5

Так, ви можете це зробити в PHP. Але на додаток до PHP було б розумно використовувати менеджер черг. Ось стратегія:

  1. Розбийте своє велике завдання на менші завдання. У вашому випадку кожне завдання може завантажувати одну сторінку.

  2. Надішліть кожне невелике завдання в чергу.

  3. Запустити своїх працівників черги кудись.

Використання цієї стратегії має наступні переваги:

  1. Для тривалих запущених завдань він має можливість відновлення, якщо в середині запуску виникає фатальна проблема - не потрібно починати спочатку.

  2. Якщо ваші завдання не повинні виконуватися послідовно, ви можете запустити декілька робітників для запуску завдань одночасно.

У вас є безліч варіантів (це лише декілька):

  1. RabbitMQ ( https://www.rabbitmq.com/tutorials/tutorial-one-php.html )
  2. ZeroMQ ( http://zeromq.org/bindings:php )
  3. Якщо ви використовуєте фреймворк Laravel, черги вбудовані ( https://laravel.com/docs/5.4/queues ) із драйверами для AWS SES, Redis, Beanstalkd

3

PHP може бути чи не найкращим інструментом, але ви знаєте, як ним користуватися, а решта вашої програми написана за його допомогою. Ці дві якості в поєднанні з тим, що PHP є "достатньо хорошим", роблять досить вагомим аргументом для його використання замість Perl, Ruby або Python.

Якщо ваша мета - вивчити іншу мову, виберіть одну та використовуйте її. Будь-яка мова, яку ви згадали, виконає цю роботу, не проблема. Мені здається, Perl подобається, але те, що вам подобається, може бути іншим.

За його посиланням Symcbean має кілька хороших порад щодо управління фоновими процесами.

Коротше кажучи, напишіть сценарій CLI PHP для обробки довгих бітів. Переконайтеся, що він якимось чином повідомляє про стан. Створіть php-сторінку, щоб обробляти оновлення стану, використовуючи AJAX або традиційні методи. Ваш початковий сценарій запустить процес, що запускається під час його власного сеансу, і поверне підтвердження про те, що процес триває.

Удачі.


1

Я погоджуюся з відповідями, які говорять, що це слід виконувати у фоновому режимі. Але також важливо, щоб ви повідомляли про стан, щоб користувач знав, що робота виконується.

Отримуючи PHP-запит на початок процесу, ви можете зберегти у базі даних подання завдання з унікальним ідентифікатором. Потім розпочніть процес вишкрібання екрана, передавши йому унікальний ідентифікатор. Повідомте програмі iPhone про те, що завдання запущено, і що йому слід перевірити вказану URL-адресу, що містить новий ідентифікатор завдання, щоб отримати останній статус. Додаток iPhone тепер може опитувати (або навіть "довге опитування") цю URL-адресу. Тим часом фоновий процес оновлював базу даних завдання, оскільки вона працювала із відсотком завершення, поточним кроком або іншими показниками стану, які ви хочете. І коли він закінчить, він встановить завершений прапор.


1

Ви можете надіслати його як запит XHR (Ajax). Зазвичай у клієнтів немає часу очікування для XHR, на відміну від звичайних запитів HTTP.


1

Я розумію, що це досить давнє запитання, але я хотів би спробувати. Цей сценарій намагається вирішити як початковий дзвінок, щоб швидко закінчити, так і подрібнити важке навантаження на менші шматки. Я не тестував це рішення.

<?php
/**
 * crawler.php located at http://mysite.com/crawler.php
 */

// Make sure this script will keep on runing after we close the connection with
// it.
ignore_user_abort(TRUE);


function get_remote_sources_to_crawl() {
  // Do a database or a log file query here.

  $query_result = array (
    1 => 'http://exemple.com',
    2 => 'http://exemple1.com',
    3 => 'http://exemple2.com',
    4 => 'http://exemple3.com',
    // ... and so on.
  );

  // Returns the first one on the list.
  foreach ($query_result as $id => $url) {
    return $url;
  }
  return FALSE;
}

function update_remote_sources_to_crawl($id) {
  // Update my database or log file list so the $id record wont show up
  // on my next call to get_remote_sources_to_crawl()
}

$crawling_source = get_remote_sources_to_crawl();

if ($crawling_source) {


  // Run your scraping code on $crawling_source here.


  if ($your_scraping_has_finished) {
    // Update you database or log file.
    update_remote_sources_to_crawl($id);

    $ctx = stream_context_create(array(
      'http' => array(
        // I am not quite sure but I reckon the timeout set here actually
        // starts rolling after the connection to the remote server is made
        // limiting only how long the downloading of the remote content should take.
        // So as we are only interested to trigger this script again, 5 seconds 
        // should be plenty of time.
        'timeout' => 5,
      )
    ));

    // Open a new connection to this script and close it after 5 seconds in.
    file_get_contents('http://' . $_SERVER['HTTP_HOST'] . '/crawler.php', FALSE, $ctx);

    print 'The cronjob kick off has been initiated.';
  }
}
else {
  print 'Yay! The whole thing is done.';
}

@symcbean Я прочитав запропоновану вами публікацію і хотів би почути ваші думки щодо цього альтернативного рішення.
Francisco Luz

По-перше, ви дали мені початкову ідею для мого першого бота (teehee). По-друге, як Ви знайшли ефективність свого рішення? Ви працювали з цим далі і дізналися щось більше? Мені цікаво здійснити щось подібне до виїмки через 26 000 зображень (1,3 Гб), виконати різні операції тощо. Це займе певний час. Твоє єдине рішення, яке не здається хакі, використовувати exec () здригатися або вимагати Linux (деякі з нас, хто програє, все ще повинні використовувати Windows). Я вважаю за краще вчитися у твого хедбашингу, аніж у мене власного: P
Just Plain High

@HighPriestessofTheTech Привіт, друже, я не пішов далі. На той час, коли я писав це, я просто проводив мислительний експеримент.
Франциско Луз

1
О боже ... Отже, я буду вчитися на власному хедбашингу ... Повідомлю вас, як це відбувається;)
Just Plain High

1
Я спробував це, і я вважаю це дуже корисним.
Олексій

1

Я хотів би запропонувати рішення, яке трохи відрізняється від рішення Symcbean, головним чином тому, що у мене є додаткова вимога, що тривалий процес повинен запускатися як інший користувач, а не як користувач apache / www-data.

Перше рішення за допомогою cron для опитування фонової таблиці завдань:

  • Веб-сторінка PHP вставляється у фонову таблицю завдань, указуючи "НАДАНО"
  • cron запускається один раз на 3 хвилини, використовуючи іншого користувача, запустивши PHP CLI-скрипт, який перевіряє фонову таблицю завдань на наявність РОЗДАНИХ рядків
  • PHP CLI оновить стовпець стану в рядку на «ОБРОБКА» та розпочне обробку, після завершення він буде оновлений до «ЗАВЕРШЕНО»

Друге рішення з використанням функції inotify Linux:

  • Веб-сторінка PHP оновлює файл управління з параметрами, встановленими користувачем, а також надає ідентифікатор завдання
  • Сценарій оболонки (як користувач, що не користується www), що працює на inotifywait, буде чекати, поки файл управління буде записаний
  • після написання керуючого файлу буде викликано подію close_write, сценарій оболонки продовжиться
  • сценарій оболонки виконує PHP CLI для тривалого процесу
  • PHP CLI записує вихідні дані у файл журналу, ідентифікований ідентифікатором завдання, або ж оновлює прогрес у таблиці стану
  • Веб-сторінка PHP може опитувати файл журналу (на основі ідентифікатора завдання), щоб показати хід тривалого процесу, або також може запитувати таблицю стану

Деяку додаткову інформацію можна знайти в моєму дописі: http://inventorsparadox.blogspot.co.id/2016/01/long-running-process-in-linux-using-php.html


0

Я робив подібні речі з Perl, подвійною форкою () та від'єднанням від батьківського процесу. Вся робота з отримання http повинна виконуватися в роздільному процесі.



0

те, що Я ЗАВЖДИ використовую, є одним із таких варіантів (оскільки різні версії Linux мають різні правила щодо обробки вихідних даних / деяких програм виводяться по-різному):

Варіант I @exec ('./ myscript.php \ 1> / dev / null \ 2> / dev / null &');

Варіант II @exec ('php -f myscript.php \ 1> / dev / null \ 2> / dev / null &');

Варіант III @exec ('nohup myscript.php \ 1> / dev / null \ 2> / dev / null &');

Можливо, вам не доведеться встановлювати "nohup". Але, наприклад, коли я автоматизував відеоконверсії FFMPEG, вихідний інтерфейс якимось чином не оброблявся на 100% шляхом перенаправлення вихідних потоків 1 і 2, тому я використовував nohup І перенаправляв вихід.


0

якщо у вас довгий сценарій, розділіть роботу сторінки за допомогою вхідного параметра для кожного завдання. (тоді кожна сторінка діє як нитка), тобто якщо сторінка має довгий цикл процесу 1 lac product_keywords, то замість циклу зробіть логіку для одного ключового слова та передайте це ключове слово від magic або cornjobpage.php (у наступному прикладі)

а для фонового працівника, я думаю, вам слід спробувати цю техніку, це допоможе зателефонувати стільки сторінок, які вам подобаються, всі сторінки будуть запускатися одночасно незалежно, не чекаючи кожної відповіді сторінки як асинхронну.

cornjobpage.php // головна сторінка

    <?php

post_async("http://localhost/projectname/testpage.php", "Keywordname=testValue");
//post_async("http://localhost/projectname/testpage.php", "Keywordname=testValue2");
//post_async("http://localhost/projectname/otherpage.php", "Keywordname=anyValue");
//call as many as pages you like all pages will run at once independently without waiting for each page response as asynchronous.
            ?>
            <?php

            /*
             * Executes a PHP page asynchronously so the current page does not have to wait for it to     finish running.
             *  
             */
            function post_async($url,$params)
            {

                $post_string = $params;

                $parts=parse_url($url);

                $fp = fsockopen($parts['host'],
                    isset($parts['port'])?$parts['port']:80,
                    $errno, $errstr, 30);

                $out = "GET ".$parts['path']."?$post_string"." HTTP/1.1\r\n";//you can use POST instead of GET if you like
                $out.= "Host: ".$parts['host']."\r\n";
                $out.= "Content-Type: application/x-www-form-urlencoded\r\n";
                $out.= "Content-Length: ".strlen($post_string)."\r\n";
                $out.= "Connection: Close\r\n\r\n";
                fwrite($fp, $out);
                fclose($fp);
            }
            ?>

testpage.php

    <?
    echo $_REQUEST["Keywordname"];//case1 Output > testValue
    ?>

PS: якщо ви хочете надіслати параметри url як цикл, дотримуйтесь цієї відповіді: https://stackoverflow.com/a/41225209/6295712


0

Не найкращий підхід, як багато хто зазначав тут, але це може допомогти:

ignore_user_abort(1); // run script in background even if user closes browser
set_time_limit(1800); // run it for 30 minutes

// Long running script here

0

Якщо бажаним результатом вашого сценарію є якась обробка, а не веб-сторінка, то я вважаю, що бажаним рішенням є запуск вашого сценарію з оболонки, просто як

php my_script.php

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.