最好的方法来pipe理长时间运行的PHP脚本?
我有一个PHP脚本需要很长时间(5-30分钟)才能完成。 以防万一,这个脚本使用curl来从另一个服务器上抓取数据。 这就是花了这么长时间的原因。 它必须等待每个页面加载之前处理它,并转移到下一个。
我希望能够启动脚本,直到它完成,这将在数据库表中设置一个标志。
我需要知道的是如何在脚本运行完成之前结束http请求。 另外,是一个PHP脚本最好的方式来做到这一点?
当然可以用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中的ignore_user_abort
函数。 这基本上说:不要在乎用户做什么,运行这个脚本,直到它完成。 如果它是一个面向公众的站点,这是有点危险的(因为如果启动20次,最终可能同时运行20个++版本的脚本)。
“干净”的方式(至less恕我直言)是设置一个标志(例如在数据库),当你想启动的进程,并运行一个cronjob每小时(左右),以检查是否设置该标志。 如果设置了,长时间运行的脚本将会启动,如果没有设置,就不会发生。
你可以使用exec或者system来开始一个后台工作,然后在那里做这个工作。
此外,还有更好的方法来抓取您正在使用的networking。 您可以使用线程方法(多个线程一次执行一个页面),也可以使用一个事件循环(一次执行多个页面的一个线程)。 我个人使用Perl的方法是使用AnyEvent :: HTTP 。
ETA: symcbean解释了如何在这里正确地分离后台进程。
不,PHP不是最好的解决scheme。
我不确定Ruby或Perl,但是使用Python,您可以将您的页面刮板重写为multithreading,并且运行速度可能会提高至less20倍。 编写multithreading应用程序可能会有些困难,但是我写的第一个Python应用程序是multithreading的页面扫描器。 你可以简单地通过使用一个shell执行函数从PHP页面中调用Python脚本。
PHP可能是也可能不是最好的工具,但你知道如何使用它,而你的应用程序的其余部分是用它来编写的。 这两个特性,加上PHP“足够好”的事实,使得它成为使用它的一个非常强大的例子,而不是Perl,Ruby或者Python。
如果你的目标是学习另一种语言,然后select一个并使用它。 你提到的任何语言都能胜任,没问题。 我碰巧喜欢Perl,但是你喜欢的可能是不同的。
Symcbean对于如何在链接上pipe理后台进程有一些很好的build议。
总之,编写一个CLI PHP脚本来处理长位。 确保它以某种方式报告状态。 创build一个php页面来处理状态更新,使用AJAX或传统的方法。 你的kickoff脚本将启动在自己的会话中运行的进程,并返回进程正在进行的确认。
祝你好运。
您可以将其作为XHR(Ajax)请求发送。 与普通的HTTP请求不同,客户端通常不会有XHR的超时。
是的,你可以用PHP来做。 但除了PHP之外,使用队列pipe理器是明智之举。 这是战略:
-
把你的大任务分解成更小的任务。 在你的情况下,每个任务可能会加载一个页面。
-
将每个小任务发送到队列。
-
在某处运行你的队列工作者。
使用这个策略有以下优点:
-
对于长时间运行的任务,如果在运行过程中发生致命的问题,它有能力恢复 – 不需要从头开始。
-
如果您的任务不必按顺序运行,则可以运行多个工作人员同时运行任务。
你有多种select(这只是一些):
- RabbitMQ( https://www.rabbitmq.com/tutorials/tutorial-one-php.html )
- ZeroMQ( http://zeromq.org/bindings:php )
- 如果您使用的是Laravel框架,则内置队列( https://laravel.com/docs/5.4/queues ),其中包含适用于AWS SES,Redis,Beanstalkd
我同意这样的答案,说这应该在后台进程中运行。 但是,报告状态也很重要,以便用户知道工作正在完成。
当接收到启动stream程的PHP请求时,您可以在数据库中存储具有唯一标识符的任务表示。 然后,开始屏幕抓取过程,传递唯一标识符。 向iPhone应用程序报告任务已经启动,并且应该检查包含新任务ID的指定URL以获取最新状态。 iPhone应用程序现在可以轮询(甚至是“长轮询”)这个URL。 同时,后台进程将更新任务的数据库表示,因为它以完成百分比,当前步骤或任何其他状态指示符的方式工作。 当它完成后,它会设置一个完成的标志。
我意识到这是一个相当古老的问题,但想给它一个镜头。 这个脚本试图解决最初的启动呼叫,快速完成,并把重负荷切成更小的块。 我还没有testing过这个解决scheme。
<?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有点不同的解决scheme,主要是因为我有额外的要求,长期运行的过程需要作为另一个用户运行,而不是作为apache / www数据用户。
使用cron轮询后台任务表的第一个解决scheme:
- PHP网页插入到后台任务表中,状态为“已提交”
- cron每3分钟运行一次,使用另一个用户,运行PHP CLI脚本,检查后台任务表中的“SUBMITTED”行
- PHP CLI会将行中的状态列更新为'PROCESSING'并开始处理,完成之后它将被更新为'COMPLETED'
使用Linux inotify工具的第二种解决scheme:
- PHP网页使用用户设置的参数来更新控制文件,并给出一个任务ID
- shell脚本(作为非www用户)运行inotifywait将等待控制文件被写入
- 在写入控制文件之后,将会引发一个close_write事件,shell脚本将继续
- shell脚本执行PHP CLI来执行长时间运行的过程
- PHP CLI将输出写入由任务ID标识的日志文件,或者更新状态表中的进度
- PHP的网页可以轮询日志文件(基于任务ID)来显示长时间运行的进程的进度,或者也可以查询状态表
一些额外的信息可以在我的文章中find: http : //inventorsparadox.blogspot.co.id/2016/01/long-running-process-in-linux-using-php.html
我用Perl,双叉()和父进程分离做了类似的事情。 所有的http抓取工作都应该在分叉过程中完成。
使用代理委托请求。
我总是使用的是这些变体之一(因为不同的Linux有不同的规则处理输出/一些程序输出不同):
变种I @exec('./ myscript.php \ 1> / dev / null \ 2> / dev / null&');
Variant II @exec('php -f myscript.php \ 1> / dev / null \ 2> / dev / null&');
Variant III @exec('nohup myscript.php \ 1> / dev / null \ 2> / dev / null&');
你可能没有安装“nohup”。 但是例如,当我自动化FFMPEGvideo对话时,输出接口不知怎么的不是通过redirect输出stream1和2来处理的,所以我使用了nohup并redirect了输出。
如果你有很长的脚本,那么在每个任务的input参数的帮助下,分页工作(然后每个页面就像线程一样),即如果页面有1个lac product_keywords长处理循环,那么代替循环为一个关键字创build逻辑并通过这个关键字从magic或者cornjobpage.php(在下面的例子中)
对于后台工作者,我认为你应该尝试这种技术,它将有助于尽可能多地调用你喜欢的页面,所有页面将独立运行,而不用等待每个页面响应为asynchronous。
cornjobpage.php // mainpage
<?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