我有一个需要很长时间(5-30分钟)才能完成的PHP脚本。以防万一,脚本正在使用curl从另一台服务器上刮取数据。这就是为什么要花这么长时间的原因。它必须等待每个页面加载,然后再处理并移至下一页。
我希望能够启动脚本并放任其完成,这将在数据库表中设置一个标志。
我需要知道的是如何能够在脚本完成运行之前结束http请求。此外,php脚本是做到这一点的最佳方法吗?
我有一个需要很长时间(5-30分钟)才能完成的PHP脚本。以防万一,脚本正在使用curl从另一台服务器上刮取数据。这就是为什么要花这么长时间的原因。它必须等待每个页面加载,然后再处理并移至下一页。
我希望能够启动脚本并放任其完成,这将在数据库表中设置一个标志。
我需要知道的是如何能够在脚本完成运行之前结束http请求。此外,php脚本是做到这一点的最佳方法吗?
Goutte
并Guzzle
实现并发线程。您也可以查看Gearman
以并行形式启动并行请求。
Answers:
当然可以使用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');
只是其中包含一些内容的原因。
shell_exec('echo /usr/bin/php -q longThing.php | at now');
只是其中包含一些内容的原因。
快速而肮脏的方法是ignore_user_abort
在php中使用该函数。基本上是这样说的:不管用户做什么,运行脚本直到完成。如果它是一个面向公众的站点,这将有些危险(因为如果启动20次,最终有可能同时运行20 ++版本的脚本)。
“干净”的方式(至少是IMHO)是在您要启动进程时设置一个标志(例如,在数据库中),并每小时(或大约一个小时)运行一次cronjob来检查该标志是否已设置。如果已设置,则将运行长时间运行的脚本;如果未设置,则不会发生任何情况。
header("Connection: close", true);
。而且不要忘了冲洗()
您可以使用exec或system来启动后台作业,然后在其中进行工作。
另外,还有更好的方法来抓取您正在使用的Web。您可以使用线程方法(多个线程一次执行一页),也可以使用事件循环(一个线程一次执行多个页面)。我个人使用Perl的方法是使用AnyEvent :: HTTP。
不,PHP不是最佳解决方案。
我不确定Ruby或Perl,但使用Python可以将页面抓取器重写为多线程,并且它的运行速度至少快20倍。编写多线程应用程序可能有些困难,但是我编写的第一个Python应用程序是多线程页面刮板。而且,您可以使用外壳程序执行功能之一在PHP页面中简单地调用Python脚本。
是的,您可以在PHP中完成。但是除了PHP,使用队列管理器是明智的。这是策略:
将大型任务分解为较小的任务。在您的情况下,每个任务可能只加载一个页面。
将每个小任务发送到队列。
在队列中运行队列工作器。
使用此策略具有以下优点:
对于长时间运行的任务,如果在运行过程中出现致命问题,它具有恢复的能力-无需从头开始。
如果不必按顺序运行任务,则可以运行多个工作程序来同时运行任务。
您有多种选择(仅几个):
PHP可能不是最好的工具,但您知道如何使用它,其余的应用程序都是使用PHP编写的。这两种特性,再加上PHP“足够好”的事实,为使用它而不是Perl,Ruby或Python奠定了坚实的基础。
如果您的目标是学习另一种语言,则选择一种语言并使用它。您提到的任何语言都可以胜任,没问题。我碰巧喜欢Perl,但是您喜欢的东西可能有所不同。
Symcbean在他的链接上对如何管理后台进程提供了一些很好的建议。
简而言之,编写一个CLI PHP脚本来处理较长的位。确保它以某种方式报告状态。使用AJAX或传统方法制作一个php页面来处理状态更新。您的启动脚本将启动进程在其自己的会话中运行,并返回确认进程正在运行。
祝好运。
我意识到这是一个非常老的问题,但想尝试一下。该脚本尝试解决最初的启动呼叫以快速完成并将重负载切成较小的块的情况。我尚未测试此解决方案。
<?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的解决方案略有不同的解决方案,主要是因为我还有其他要求,即长时间运行的进程需要以其他用户而不是apache / www-data用户的身份运行。
使用cron轮询后台任务表的第一个解决方案:
使用Linux inotify工具的第二个解决方案:
一些其他信息可以在我的帖子中找到:http : //inventorsparadox.blogspot.co.id/2016/01/long-running-process-in-linux-using-php.html
我对Perl,double fork()以及从父进程中分离都做了类似的事情。所有http提取工作均应在分支过程中完成。
我总是使用的是这些变体之一(因为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 AND重定向了输出。
如果您有较长的脚本,则在每个任务的输入参数的帮助下划分页面工作。(然后,每个页面的行为都类似于线程),即,如果页面具有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
正如这里许多人所说,这不是最好的方法,但这可能会有所帮助:
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