87

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

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

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

4

16 回答 16

119

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

由于人们一直对这个常见问题给出相同的错误答案,我在这里写了一个更完整的答案:

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

从评论:

简短的版本是shell_exec('echo /usr/bin/php -q longThing.php | at now');“为什么”的原因,在这里包含有点长。

于 2010-02-06T23:35:39.757 回答
11

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

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

于 2010-02-06T09:26:53.613 回答
8

您可以使用execsystem启动后台作业,然后在其中完成工作。

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

ETA:symcbean在这里解释了如何正确分离后台进程。

于 2010-02-06T09:49:55.837 回答
5

不,PHP 不是最好的解决方案。

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

于 2010-02-06T09:35:51.573 回答
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 的驱动程序
于 2017-05-23T05:06:59.930 回答
3

PHP 可能是也可能不是最好的工具,但您知道如何使用它,并且您的应用程序的其余部分都是使用它编写的。这两个品质,再加上 PHP “足够好”这一事实,为使用它而不是 Perl、Ruby 或 Python 提供了一个非常有力的理由。

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

Symcbean 在他的链接上有一些关于如何管理后台进程的好建议。

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

祝你好运。

于 2010-02-08T06:28:55.450 回答
1

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

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

于 2010-02-06T19:58:59.103 回答
1

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

于 2010-02-06T23:51:28.473 回答
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.';
}
于 2013-06-27T01:24:59.440 回答
1

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

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

  • PHP 网页插入到后台任务表中,状态为“已提交”
  • cron 每 3 分钟运行一次,使用另一个用户,运行 PHP CLI 脚本,检查后台任务表中的“已提交”行
  • PHP CLI 会将行中的状态列更新为 'PROCESSING' 并开始处理,完成后它将更新为 'COMPLETED'

使用 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

于 2016-01-31T12:32:25.993 回答
0

我已经用 Perl、双 fork() 和从父进程分离做了类似的事情。所有的 http 获取工作都应该在分叉的过程中完成。

于 2010-02-06T19:41:36.533 回答
0

使用代理来委派请求。

于 2010-10-29T22:17:03.617 回答
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 视频转换时,输出接口在某种程度上不是通过重定向输出流 1 和 2 来 100% 处理的,所以我使用 nohup 并重定向了输出。

于 2011-09-07T10:42:47.483 回答
0

如果你有很长的脚本,那么在每个任务的输入参数的帮助下划分页面工作。(然后每个页面都像线程一样)即如果页面有 1 lac product_keywords 长进程循环,那么而不是循环为一个关键字制作逻辑并传递这个关键字来自魔术或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);
            }
            ?>

测试页.php

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

PS:如果您想将 url 参数作为循环发送,请遵循以下答案:https ://stackoverflow.com/a/41225209/6295712

于 2016-12-19T15:32:26.220 回答
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
于 2019-01-29T19:27:20.043 回答
0

如果您的脚本所需的输出是一些处理,而不是网页,那么我相信所需的解决方案是从 shell 运行您的脚本,就像

php my_script.php

于 2019-09-13T09:35:43.617 回答