管理长时间运行的PHP脚本的最佳方法?

时间:2010-02-06 09:17:00

标签: php apache curl httprequest

我有一个PHP脚本需要很长时间(5-30分钟)才能完成。为了防止重要,脚本使用curl从另一台服务器中抓取数据。这就是它花了这么长时间的原因;它必须等待每个页面加载,然后再处理它并移动到下一页。

我希望能够启动脚本并让它完成,直到它完成,这将在数据库表中设置一个标志。

我需要知道的是如何在脚本运行完毕之前结束http请求。另外,php脚本是最好的方法吗?

16 个答案:

答案 0 :(得分:102)

当然可以用PHP完成,但是你不应该把它作为后台任务 - 新进程必须从启动它的进程组中分离出来。

由于人们不断给出这个常见问题解答的错误答案,我在这里写了一个更全面的答案:

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

来自评论:

  

简短版本为shell_exec('echo /usr/bin/php -q longThing.php | at now');,但原因在于此处包含的内容有点长。

答案 1 :(得分:11)

快速而肮脏的方法是在php中使用ignore_user_abort函数。这基本上说:不关心用户做什么,运行此脚本直到完成。如果它是一个面向公众的站点,这有点危险(因为有可能,如果它启动了20次,你最终会同时运行20个++版本的脚本)。

“干净”方式(至少是恕我直言)是设置一个标志(例如在数据库中),当你想要启动进程并每小时(或左右)运行一个cronjob来检查是否设置了该标志。如果设置了,则长时间运行的脚本会启动,如果未设置,则不会发生。

答案 2 :(得分:8)

您可以使用execsystem开始后台工作,然后再开始工作。

此外,还有更好的方法来抓取您正在使用的网络。您可以使用线程方法(多个线程一次执行一个页面),或者使用eventloop(一个线程一次执行多个页面)。我使用Perl的个人方法是使用AnyEvent::HTTP

ETA:symcbean解释了如何正确分离后台流程here

答案 3 :(得分:5)

不,PHP不是最佳解决方案。

我不确定Ruby或Perl,但是使用Python,您可以将页面刮板重写为多线程,并且它可能至少运行20倍。编写多线程应用程序可能有点挑战,但我编写的第一个Python应用程序是多线程页面刮刀。您可以使用其中一个shell执行函数从PHP页面中调用Python脚本。

答案 4 :(得分:5)

是的,您可以在PHP中完成。但除了PHP之外,最好使用队列管理器。这是策略:

  1. 将大型任务分解为较小的任务。在您的情况下,每个任务可以加载一个页面。

  2. 将每个小任务发送到队列。

  3. 在某处运行队列工作。

  4. 使用此策略具有以下优点:

    1. 对于长时间运行的任务,它可以在运行过程中发生致命问题时进行恢复 - 无需从头开始。

    2. 如果您的任务不必按顺序运行,则可以运行多个工作人员同时运行任务。

    3. 您有多种选择(仅限几个):

      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的驱动程序

答案 5 :(得分:3)

PHP可能是也可能不是最好的工具,但您知道如何使用它,而您的应用程序的其余部分都是使用它编写的。这两个特性,加上PHP“足够好”的事实,使用它,而不是Perl,Ruby或Python,是一个非常强大的案例。

如果您的目标是学习另一种语言,请选择一种并使用它。你提到的任何语言都可以完成这项工作,没问题。我碰巧喜欢Perl,但你喜欢的可能会有所不同。

Symcbean对如何在其链接上管理后台进程提出了一些很好的建议。

简而言之,编写一个CLI PHP脚本来处理长位。确保以某种方式报告状态。使用AJAX或传统方法创建一个php页面来处理状态更新。您的启动脚本将启动在其自己的会话中运行的进程,并返回该进程正在进行的确认。

祝你好运。

答案 6 :(得分:1)

我同意答案,说这应该在后台进程中运行。但是,报告状态也很重要,这样用户就知道工作正在完成。

当接收启动进程的PHP请求时,您可以在数据库中存储具有唯一标识符的任务表示。然后,启动屏幕抓取过程,向其传递唯一标识符。向iPhone应用程序报告该任务已经启动,并且应该检查包含新任务ID的指定URL以获取最新状态。 iPhone应用程序现在可以轮询(甚至“长轮询”)此URL。与此同时,后台进程将更新任务的数据库表示,因为它使用完成百分比,当前步骤或您想要的任何其他状态指示符。当它完成后,它将设置一个完整的标志。

答案 7 :(得分:1)

您可以将其作为XHR(Ajax)请求发送。与普通的HTTP请求不同,客户端通常没有XHR的超时。

答案 8 :(得分: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.';
}

答案 9 :(得分:1)

我想提出一个与symcbean有点不同的解决方案,主要是因为我还要求长时间运行的进程需要作为另一个用户运行,而不是作为apache / www-data用户运行

使用cron轮询后台任务表的第一个解决方案:

  • PHP网页插入后台任务表,状态&#39;提交&#39;
  • cron每隔3分钟运行一次,使用另一个用户运行PHP CLI脚本,该脚本检查后台任务表中是否提交了“提交的”#39;行
  • PHP CLI会将行中的状态列更新为&#39; PROCESSING&#39;并开始处理,完成后将更新为“完成”

使用Linux inotify工具的第二个解决方案:

  • PHP网页使用用户设置的参数更新控制文件,并提供任务ID
  • 运行inotifywait的shell脚本(作为非www用户)将等待写入控制文件
  • 写入控制文件后,将引发close_write事件,shell脚本将继续
  • shell脚本执行PHP CLI以执行长时间运行的过程
  • PHP CLI将输出写入由任务ID标识的日志文件,或者更新状态表中的进度
  • PHP网页可以轮询日志文件(基于任务ID)以显示长时间运行的进程的进度,或者它也可以查询状态表

我的帖子中可以找到一些其他信息:http://inventorsparadox.blogspot.co.id/2016/01/long-running-process-in-linux-using-php.html

答案 10 :(得分:0)

我用Perl做了类似的事情,双叉()并从父进程中分离。所有的http抓取工作都应该在分叉的过程中完成。

答案 11 :(得分:0)

使用代理委派请求。

答案 12 :(得分:0)

我总是使用的是这些变体之一(因为不同版本的Linux在处理输出/某些程序输出方面有不同的规则):

变体I     @exec('./ myscript.php \ 1&gt; / dev / null \ 2&gt; / dev / null&amp;');

变式II     @exec('php -f myscript.php \ 1&gt; / dev / null \ 2&gt; / dev / null&amp;');

变体III     @exec('nohup myscript.php \ 1&gt; / dev / null \ 2&gt; / dev / null&amp;');

你可能不会安装“nohup”。但是,例如,当我自动化FFMPEG视频转换时,输出接口不是通过重定向输出流1&amp;而不是100%处理的。 2,所以我使用nohup并重定向输出。

答案 13 :(得分: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

答案 14 :(得分: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

答案 15 :(得分:0)

如果脚本的期望输出是某种处理,而不是网页,那么我认为期望的解决方案是从shell运行脚本,就像

php my_script.php