PHP cURL库函数抓取页面内容
cURL是一个利用URL语法规定来传输文件和数据的工具,支持很多协议和选项,如HTTP、FTP、TELNET等,能提供URL请求相关的各种细节信息。最爽的是,PHP也支持cURL库。
本文将介绍cURL的一些高级特性,以及在PHP中如何运用它。
1为什么要用cURL?
是的,我们可以通过其他办法获取网页内容。大多数时候,我因为想偷懒,都直接用简单的PHP的file_get_contents()函数:
$content=file_get_contents("http://www.awaimai.com"); $lines=file("http://www.awaimai.com"); readfile(http://www.awaimai.com);
不过,这种做法缺乏灵活性和有效的错误处理。而且,你也不能用它完成一些高难度任务,比如处理:coockies、验证、表单提交、文件上传等等。
2启用cURL
首先,我们得先要确定PHP是否开启了这个库,你可以通过使用phpinfo()函数来得到这一信息。如果在网页上看到下面的输出,那么表示cURL库已开启。
如果curl没有开启,那么就需要开启这个库。如果是在Windows平台下,那么非常简单,你需要改一改php.ini文件的设置,找到php_curl.dll,并取消前面的分号注释就行了。如下所示:
#取消下面的注释 extension=php_curl.dll
如果是Linux服务器,需要重新编译PHP,编译时在configure命令上加上--with-curl参数。
3基本结构
在学习更为复杂的功能之前,先来看一下在PHP中建立cURL请求的基本步骤:
- 初始化
- 设置选项
- 执行并获取结果
- 释放cURL句柄
实现代码如下:
//1.初始化 $ch=curl_init(); //2.设置选项 curl_setopt($ch,CURLOPT_URL,"http://www.awaimai.com");//设置要抓取的页面地址 curl_setopt($ch,CURLOPT_RETURNTRANSFER,1);//抓取结果直接返回(如果为0,则直接输出内容到页面) curl_setopt($ch,CURLOPT_HEADER,0);//不需要页面的HTTP头 //3.执行并获取HTML文档内容,可用echo输出内容 $output=curl_exec($ch); //4.释放curl句柄 curl_close($ch);
第二步(也就是curl_setopt())最为重要,一切玄妙均在此。有一长串cURL参数可供设置,它们能指定URL请求的各个细节。要一次性全部看完并理解可能比较困难,所以今天我们只试一下那些更常用也更有用的选项。
4检查错误
你可以在cur_exec()后加一段检查错误的语句(虽然这并不是必需的):
$output=curl_exec($ch); if($output===FALSE){ echo"cURLError:".curl_error($ch); }
请注意,比较的时候我们用的是===FALSE,而非==FALSE。因为我们得区分空输出和布尔值FALSE,后者才是真正的错误。
5获取信息
利用curl_getinfo()能够在cURL执行后获取请求的有关信息,当然,这也是一个可选的设置项,:
curl_exec($ch); $info=curl_getinfo($ch); echo'获取'.$info['url'].'耗时'.$info['total_time'].'秒';
返回的数组中包括了以下信息:
- url //资源网络地址
- content_type //内容编码
- http_code //HTTP状态码
- header_size //header的大小
- request_size //请求的大小
- filetime //文件创建时间
- ssl_verify_result //SSL验证结果
- redirect_count //跳转技术
- total_time //总耗时
- namelookup_time //DNS查询耗时
- connect_time //等待连接耗时
- pretransfer_time //传输前准备耗时
- size_upload //上传数据的大小
- size_download //下载数据的大小
- speed_download //下载速度
- speed_upload //上传速度
- download_content_length //下载内容的长度
- upload_content_length //上传内容的长度
- starttransfer_time //开始传输的时间
- redirect_time //重定向耗时
6基于浏览器的重定向
在第一个例子中,我们将提供一段用于侦测服务器是否有基于浏览器的重定向的代码。例如,有些网站会根据是否是手机浏览器甚至用户来自哪个国家来重定向网页。
我们利用CURLOPT_HTTPHEADER选项来设定发送出的HTTP请求头信息(httpheaders),包括useragent信息和默认语言。然后看看这些特定网站是否会把我们重定向到不同的URL。
//测试用的URL $urls=array( "http://www.bbc.com", "http://www.baidu.com", "http://www.ubuntu.com" ); //测试用的浏览器信息 $browsers=array( "standard"=>array( "user_agent"=>"Mozilla/5.0(Windows;U;WindowsNT6.1;en-US;rv:1.9.1.6)Gecko/20091201Firefox/3.5.6(.NETCLR3.5.30729)", "language"=>"en-us,en;q=0.5" ), "iphone"=>array( "user_agent"=>"Mozilla/5.0(iPhone;U;CPUlikeMacOSX;en)AppleWebKit/420+(KHTML,likeGecko)Version/3.0Mobile/1A537aSafari/419.3", "language"=>"en" ), "french"=>array( "user_agent"=>"Mozilla/4.0(compatible;MSIE7.0;WindowsNT5.1;GTB6;.NETCLR2.0.50727)", "language"=>"fr,fr-FR;q=0.5" ) ); foreach($urlsas$url){ echo"URL:$url\n<br/>"; foreach($browsersas$test_name=>$browser){ $ch=curl_init(); //设置url curl_setopt($ch,CURLOPT_URL,$url); //设置浏览器的特定header curl_setopt($ch,CURLOPT_HTTPHEADER,array( "User-Agent:{$browser['user_agent']}", "Accept-Language:{$browser['language']}" )); //页面内容我们并不需要 curl_setopt($ch,CURLOPT_NOBODY,1); //只需返回HTTPheader curl_setopt($ch,CURLOPT_HEADER,1); //返回结果,而不是输出它 curl_setopt($ch,CURLOPT_RETURNTRANSFER,1); $output=curl_exec($ch); curl_close($ch); //有重定向的HTTP头信息吗? if(preg_match("!Location:(.*)!",$output,$matches)){ echo"$test_name:redirectsto$matches[1]\n<br/>"; }else{ echo"$test_name:noredirection\n<br/>"; } } echo"\n\n<br/><br/>"; }
首先,我们建立一组需要测试的URL,接着指定一组需要测试的浏览器信息。最后通过循环测试各种URL和浏览器匹配可能产生的情况。
因为我们指定了CURLOPT_NOBODY选项,所以返回的输出内容则只包括HTTP头信息(被存放于$output中)。利用一个简单的正则,我们检查这个头信息中是否包含了Location:字样。
运行这段代码应该会返回如下结果:
URL:http://www.bbc.com standard:noredirection iphone:noredirection french:noredirection URL:http://www.baidu.com standard:redirectstohttps://www.baidu.com/ iphone:noredirection french:redirectstohttps://www.baidu.com/ URL:http://www.ubuntu.com standard:redirectstohttp://www.ubuntu.com/index_kylin iphone:redirectstohttp://www.ubuntu.com/index_kylin french:redirectstohttp://www.ubuntu.com/index_kylin
7用POST方法发送数据
当发起GET请求时,数据可以通过“查询字串”(QueryString)传递给一个URL。例如,在必应(鉴于Google需要翻墙,用Bing代替)中搜索时,搜索关键即为URL的查询字串的一部分:
http://www.bing.com?q=awaimai.com
这种情况下你可能并不需要cURL来模拟。把这个URL丢给file_get_contents()就能得到相同结果。
不过有一些HTML表单是用POST方法提交的。这种表单提交时,数据是通过HTTP请求体(requestbody)发送,而不是查询字串。例如,当使用ThinkPHP网站的搜索功能时,无论输入什么关键字,总是被POST到如下页面:
http://www.thinkphp.cn/Search/
你可以用PHP脚本来模拟这种URL请求。首先,新建一个可以接受并显示POST数据的文件,我们给它命名为post_output.php,脚本内容为:
print_r($_POST);
接下来,写一段PHP脚本来执行cURL请求:
$url="http://localhost/post_output.php"; $post_data=array( "foo"=>"bar", "query"=>"Nettuts", "action"=>"Submit" ); $ch=curl_init(); curl_setopt($ch,CURLOPT_URL,$url); curl_setopt($ch,CURLOPT_RETURNTRANSFER,1); //我们在POST数据哦! curl_setopt($ch,CURLOPT_POST,1); //加上POST变量 curl_setopt($ch,CURLOPT_POSTFIELDS,$post_data); $output=curl_exec($ch); curl_close($ch); echo$output;
执行代码后应该会得到以下结果:
Array ( [foo]=>bar [query]=>Nettuts [action]=>Submit )
这段脚本发送一个POST请求给post_output.php,这个页面$_POST变量并返回,我们利用cURL捕捉了这个输出。
8文件上传
上传文件和前面的POST十分相似。因为所有的文件上传表单都是通过POST方法提交的。首先新建一个接收文件的页面,命名为upload_output.php,页面内容:
print_r($_FILES);
以下是真正执行文件上传任务的脚本,命名为upload.php,内容:
$url="http://localhost/upload_output.php"; $post_data=array( "foo"=>"bar", //要上传的本地文件地址 "upload"=>"@C:/wamp/www/test.zip" ); $ch=curl_init(); curl_setopt($ch,CURLOPT_URL,$url); curl_setopt($ch,CURLOPT_RETURNTRANSFER,1); curl_setopt($ch,CURLOPT_POST,1); curl_setopt($ch,CURLOPT_POSTFIELDS,$post_data); $output=curl_exec($ch); curl_close($ch); echo$output;
如果你需要上传一个文件,只需要把文件路径赋给upload,作为POST变量传过去,不过记得在前面加上@符号。执行这段脚本应该会得到类似如下输出:
Array ( [upload]=>Array ( [name]=>test.zip [type]=>application/octet-stream [tmp_name]=>C:\Windows\php1BB4.tmp [error]=>0 [size]=>487235 ) )
9cURL批处理(multicURL)
cURL还有一个高级特性:批处理句柄(handle)。这一特性允许你同时或异步地打开多个URL连接。下面是来自来自php.net的示例代码:
//创建两个cURL资源 $ch1=curl_init(); $ch2=curl_init(); //指定URL和适当的参数 curl_setopt($ch1,CURLOPT_URL,"http://lxr.php.net/"); curl_setopt($ch1,CURLOPT_HEADER,0); curl_setopt($ch2,CURLOPT_URL,"http://www.php.net/"); curl_setopt($ch2,CURLOPT_HEADER,0); //创建cURL批处理句柄 $mh=curl_multi_init(); //加上前面两个资源句柄 curl_multi_add_handle($mh,$ch1); curl_multi_add_handle($mh,$ch2); //预定义一个状态变量 $active=null; //执行批处理 do{ $mrc=curl_multi_exec($mh,$active); }while($mrc==CURLM_CALL_MULTI_PERFORM); while($active&&$mrc==CURLM_OK){ if(curl_multi_select($mh)!=-1){ do{ $mrc=curl_multi_exec($mh,$active); }while($mrc==CURLM_CALL_MULTI_PERFORM); } } //关闭各个句柄 curl_multi_remove_handle($mh,$ch1); curl_multi_remove_handle($mh,$ch2); curl_multi_close($mh);
这里要做的就是打开多个cURL句柄并指派给一个批处理句柄。然后你就只需在一个while循环里等它执行完毕。
这个示例中有两个主要循环。第一个do-while循环重复调用curl_multi_exec()。这个函数是无隔断(non-blocking)的,但会尽可能少地执行。它返回一个状态值,只要这个值等于常量CURLM_CALL_MULTI_PERFORM,就代表还有一些刻不容缓的工作要做(例如,把对应URL的http头信息发送出去)。也就是说,我们需要不断调用该函数,直到返回值发生改变。
而接下来的while循环,只在$active变量为true时继续。这一变量之前作为第二个参数传给了curl_multi_exec(),代表只要批处理句柄中是否还有活动连接。接着,我们调用curl_multi_select(),在活动连接(例如接受服务器响应)出现之前,它都是被“屏蔽”的。这个函数成功执行后,我们又会进入另一个do-while循环,继续下一条URL。
还是来看一看怎么把这一功能用到实处吧:
9.1WordPress链接检查器
想象一下你有一个文章数目庞大的博客,这些文章中包含了大量外部网站链接。一段时间之后,因为这样那样的原因,这些链接中相当数量都失效了。要么是被和谐了,要么是整个站点都被功夫网了…
我们下面建立一个脚本,分析所有这些链接,找出打不开或者404的网站/网页,并生成一个报告。
请注意,以下并不是一个真正可用的WordPress插件,仅仅是一段独立功能的脚本而已,仅供演示,谢谢。
好,开始吧。首先,从数据库中读取所有这些链接:
//配置MySQL数据库 $db_host='localhost'; $db_user='root'; $db_pass=''; $db_name='wordpress'; $excluded_domains=array('localhost','www.mydomain.com'); $max_connections=10; //初始化一些变量 $url_list=array(); $working_urls=array(); $dead_urls=array(); $not_found_urls=array(); $active=null; //连到MySQL if(!mysql_connect($db_host,$db_user,$db_pass)){ die('Couldnotconnect:'.mysql_error()); } if(!mysql_select_db($db_name)){ die('Couldnotselectdb:'.mysql_error()); } //找出所有含有链接的文章 $sql="SELECTpost_contentFROMwp_posts WHEREpost_contentLIKE'%href=%' ANDpost_status='publish' ANDpost_type='post'"; $res=mysql_query($sql)ordie(mysql_error()); while($d=mysql_fetch_assoc($res)){ //用正则匹配链接 if(preg_match_all("!href=\"(.*?)\"!",$d['post_content'],$matches)){ foreach($matches[1]as$url){ //剔除排除的域名 $tmp=parse_url($url); if(in_array($tmp['host'],$excluded_domains)){ continue; } //保存URL $url_list[]=$url; } } } //移除重复链接 $url_list=array_values(array_unique($url_list)); if(!$url_list){ die('NoURLtocheck'); }
我们首先配置好数据库,一系列要排除的域名($excluded_domains),以及最大同时连接数量($max_connections)。然后,连接数据库,获取文章和包含的链接,把它们收集到一个数组中($url_list)。
下面的代码有点复杂了,因此我将一小步一小步地详细解释:
//1.批处理器 $mh=curl_multi_init(); //2.加入需批量处理的URL for($i=0;$i<$max_connections;$i++){ add_url_to_multi_handle($mh,$url_list); } //3.初始处理 do{ $mrc=curl_multi_exec($mh,$active); }while($mrc==CURLM_CALL_MULTI_PERFORM); //4.主循环 while($active&&$mrc==CURLM_OK){ //5.有活动连接 if(curl_multi_select($mh)!=-1){ //6.干活 do{ $mrc=curl_multi_exec($mh,$active); }while($mrc==CURLM_CALL_MULTI_PERFORM); //7.有信息否? if($mhinfo=curl_multi_info_read($mh)){ //意味着该连接正常结束 //8.从curl句柄获取信息 $chinfo=curl_getinfo($mhinfo['handle']); //9.死链么? if(!$chinfo['http_code']){ $dead_urls[]=$chinfo['url']; //10.404了? }elseif($chinfo['http_code']==404){ $not_found_urls[]=$chinfo['url']; //11.还能用 }else{ $working_urls[]=$chinfo['url']; } //12.移除句柄 curl_multi_remove_handle($mh,$mhinfo['handle']); curl_close($mhinfo['handle']); //13.加入新URL,干活 if(add_url_to_multi_handle($mh,$url_list)){ do{ $mrc=curl_multi_exec($mh,$active); }while($mrc==CURLM_CALL_MULTI_PERFORM); } } } } //14.完了 curl_multi_close($mh); echo"==DeadURLs==\n"; echoimplode("\n",$dead_urls)."\n\n"; echo"==404URLs==\n"; echoimplode("\n",$not_found_urls)."\n\n"; echo"==WorkingURLs==\n"; echoimplode("\n",$working_urls); //15.向批处理器添加url functionadd_url_to_multi_handle($mh,$url_list){ static$index=0; //如果还剩url没用 if($url_list[$index]){ //新建curl句柄 $ch=curl_init(); //配置url curl_setopt($ch,CURLOPT_URL,$url_list[$index]); //不想输出返回的内容 curl_setopt($ch,CURLOPT_RETURNTRANSFER,1); //重定向到哪儿我们就去哪儿 curl_setopt($ch,CURLOPT_FOLLOWLOCATION,1); //不需要内容体,能够节约带宽和时间 curl_setopt($ch,CURLOPT_NOBODY,1); //加入到批处理器中 curl_multi_add_handle($mh,$ch); //拨一下计数器,下次调用该函数就能添加下一个url了 $index++; returntrue; }else{ //没有新的URL需要处理了 returnfalse; } }
下面解释一下以上代码。列表的序号对应着代码注释中的顺序数字。
- 新建一个批处理句柄。
- 稍后我们将创建一个把URL加入批处理器的函数add_url_to_multi_handle()。每当这个函数被调用,就有一个新url被加入批处理器。一开始,我们给批处理器添加了10个URL(这一数字由$max_connections所决定)。
- 运行curl_multi_exec()进行初始化工作是必须的,只要它返回CURLM_CALL_MULTI_PERFORM就还有事情要做。这么做主要是为了创建连接,它不会等待完整的URL响应。
- 只要批处理中还有活动连接主循环就会一直持续。
- curl_multi_select()会一直等待,直到某个URL查询产生活动连接。
- cURL的活儿又来了,主要是获取响应数据。
- 检查各种信息。当一个URL请求完成时,会返回一个数组。
- 在返回的数组中有一个cURL句柄。我们利用其获取单个cURL请求的相应信息。
- 如果这是一个死链或者请求超时,不会返回http状态码。
- 如果这个页面找不到了,会返回404状态码。
- 其他情况我们都认为这个链接是可用的(当然,你也可以再检查一下500错误之类…)。
- 从该批次移除这个cURL句柄,因为它已经没有利用价值了,关了它!
- 很好,现在可以另外加一个URL进来了。再一次地,初始化工作又开始进行…
- 嗯,该干的都干了。关闭批处理器,生成报告。
- 回过头来看给批处理器添加新URL的函数。这个函数每调用一次,静态变量$index就递增一次,这样我们才能知道还剩多少URL没处理。
我把这个脚本在我的博客上跑了一遍(测试需要,有一些错误链接是故意加上的),共检查约40个URL,只耗费两秒不到。当需要检查更加大量的URL时,其省心省力的效果可想而知!如果你同时打开10个连接,还能再快上10倍!另外,你还可以利用cURL批处理的无隔断特性,来处理大量URL请求,而不会阻塞你的Web脚本。
10另一些有用的cURL选项
10.1HTTP认证
如果某个URL请求需要基于HTTP的身份验证,你可以使用下面的代码:
$url="http://www.somesite.com/members/"; $ch=curl_init(); curl_setopt($ch,CURLOPT_URL,$url); curl_setopt($ch,CURLOPT_RETURNTRANSFER,1); //发送用户名和密码 curl_setopt($ch,CURLOPT_USERPWD,"myusername:mypassword"); //你可以允许其重定向 curl_setopt($ch,CURLOPT_FOLLOWLOCATION,1); //下面的选项让cURL在重定向后 //也能发送用户名和密码 curl_setopt($ch,CURLOPT_UNRESTRICTED_AUTH,1); $output=curl_exec($ch); curl_close($ch);
10.2FTP上传
PHP自带有FTP类库,但你也能用cURL:
//打开一个文件指针 $file=fopen("/path/to/file","r"); $size=filesize("/path/to/file"); //url里包含了大部分所需信息 $url="ftp://username:password@mydomain.com:21/path/to/new/file"; $ch=curl_init(); curl_setopt($ch,CURLOPT_URL,$url); curl_setopt($ch,CURLOPT_RETURNTRANSFER,1); //上传相关的选项 curl_setopt($ch,CURLOPT_UPLOAD,1); curl_setopt($ch,CURLOPT_INFILE,$file); curl_setopt($ch,CURLOPT_INFILESIZE,$size); //是否开启ASCII模式(上传文本文件时有用) curl_setopt($ch,CURLOPT_FTPASCII,1); $output=curl_exec($ch); curl_close($ch);
10.3代理/翻墙请求
你可以用代理发起cURL请求:
$ch=curl_init(); curl_setopt($ch,CURLOPT_URL,'http://www.example.com'); curl_setopt($ch,CURLOPT_RETURNTRANSFER,1); //指定代理地址 curl_setopt($ch,CURLOPT_PROXY,'11.11.11.11:8080'); //如果需要的话,提供用户名和密码 curl_setopt($ch,CURLOPT_PROXYUSERPWD,'username:password'); $output=curl_exec($ch); curl_close($ch);
10.4回调函数
可以在一个URL请求过程中,让cURL调用某指定的回调函数。例如,在内容或者响应下载的过程中,立刻开始利用数据,而不用等到完全下载完。
$ch=curl_init(); curl_setopt($ch,CURLOPT_URL,'http://net.tutsplus.com'); curl_setopt($ch,CURLOPT_WRITEFUNCTION,"progress_function"); curl_exec($ch); curl_close($ch); functionprogress_function($ch,$str){ echo$str; returnstrlen($str); }
这个回调函数必须返回字串的长度,不然此功能将无法正常使用。在URL响应接收的过程中,只要收到一个数据包,这个函数就会被调用。
11小结
今天我们一起学习了cURL库的强大功能和灵活的扩展性,希望你喜欢。下一次要发起URL请求时,考虑下cURL吧!谢谢!
参考资料:
- 使用PHP的cURL库
- TechniquesforMasteringcURL