cURL 是一个利用URL语法规定来传输文件和数据的工具,支持很多协议和选项,如HTTP、FTP、TELNET等,能提供 URL 请求相关的各种细节信息。最爽的是,PHP 也支持 cURL 库。
本文将介绍 cURL 的一些高级特性,以及在 PHP 中如何运用它。
是的,我们可以通过其他办法获取网页内容。大多数时候,我因为想偷懒,都直接用简单的 PHP 的 file_get_contents() 函数:
$content = file_get_contents("http://www.awaimai.com"); $lines = file("http://www.awaimai.com"); readfile(http://www.awaimai.com);
不过,这种做法缺乏灵活性和有效的错误处理。而且,你也不能用它完成一些高难度任务,比如处理:coockies、验证、表单提交、文件上传等等。
首先,我们得先要确定 PHP 是否开启了这个库,你可以通过使用phpinfo()函数来得到这一信息。如果在网页上看到下面的输出,那么表示 cURL 库已开启。
如果 curl 没有开启,那么就需要开启这个库。如果是在Windows平台下,那么非常简单,你需要改一改 php.ini 文件的设置,找到 php_curl.dll,并取消前面的分号注释就行了。如下所示:
# 取消下面的注释 extension=php_curl.dll
如果是 Linux 服务器,需要重新编译 PHP ,编译时在configure命令上加上--with-curl参数。
在学习更为复杂的功能之前,先来看一下在 PHP 中建立 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 请求的各个细节。要一次性全部看完并理解可能比较困难,所以今天我们只试一下那些更常用也更有用的选项。
你可以在 cur_exec() 后加一段检查错误的语句(虽然这并不是必需的):
$output = curl_exec($ch); if ($output === FALSE) { echo "cURL Error: " . curl_error($ch); }
请注意,比较的时候我们用的是=== FALSE,而非== FALSE。因为我们得区分空输出和布尔值FALSE,后者才是真正的错误。
利用curl_getinfo()能够在 cURL 执行后获取请求的有关信息,当然,这也是一个可选的设置项,:
curl_exec($ch); $info = curl_getinfo($ch); echo '获取'. $info['url'] . '耗时'. $info['total_time'] . '秒';
返回的数组中包括了以下信息:
在第一个例子中,我们将提供一段用于侦测服务器是否有基于浏览器的重定向的代码。例如,有些网站会根据是否是手机浏览器甚至用户来自哪个国家来重定向网页。
我们利用 CURLOPT_HTTPHEADER选项来设定发送出的HTTP请求头信息(http headers),包括user agent信息和默认语言。然后看看这些特定网站是否会把我们重定向到不同的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; Windows NT 6.1; en-US; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 (.NET CLR 3.5.30729)", "language" => "en-us,en;q=0.5" ), "iphone" => array ( "user_agent" => "Mozilla/5.0 (iPhone; U; CPU like Mac OS X; en) AppleWebKit/420+ (KHTML, like Gecko) Version/3.0 Mobile/1A537a Safari/419.3", "language" => "en" ), "french" => array ( "user_agent" => "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; GTB6; .NET CLR 2.0.50727)", "language" => "fr,fr-FR;q=0.5" ) ); foreach ($urls as $url) { echo "URL: $url\n<br />"; foreach ($browsers as $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); // 只需返回HTTP header 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: redirects to $matches[1]\n<br />"; } else { echo "$test_name: no redirection\n<br />"; } } echo "\n\n<br /><br />"; }
首先,我们建立一组需要测试的URL,接着指定一组需要测试的浏览器信息。最后通过循环测试各种URL和浏览器匹配可能产生的情况。
因为我们指定了CURLOPT_NOBODY选项,所以返回的输出内容则只包括HTTP头信息(被存放于 $output 中)。利用一个简单的正则,我们检查这个头信息中是否包含了Location:字样。
运行这段代码应该会返回如下结果:
URL: http://www.bbc.com standard: no redirection iphone: no redirection french: no redirection URL: http://www.baidu.com standard: redirects to https://www.baidu.com/ iphone: no redirection french: redirects to https://www.baidu.com/ URL: http://www.ubuntu.com standard: redirects to http://www.ubuntu.com/index_kylin iphone: redirects to http://www.ubuntu.com/index_kylin french: redirects to http://www.ubuntu.com/index_kylin
当发起 GET 请求时,数据可以通过“查询字串”(Query String)传递给一个URL。例如,在必应(鉴于 Google 需要翻墙,用 Bing 代替)中搜索时,搜索关键即为 URL 的查询字串的一部分:
http://www.bing.com?q=awaimai.com
这种情况下你可能并不需要 cURL 来模拟。把这个URL丢给 file_get_contents() 就能得到相同结果。
不过有一些HTML表单是用 POST 方法提交的。这种表单提交时,数据是通过 HTTP请求体(request body) 发送,而不是查询字串。例如,当使用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 捕捉了这个输出。
上传文件和前面的 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 ) )
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。
还是来看一看怎么把这一功能用到实处吧:
想象一下你有一个文章数目庞大的博客,这些文章中包含了大量外部网站链接。一段时间之后,因为这样那样的原因,这些链接中相当数量都失效了。要么是被和谐了,要么是整个站点都被功夫网了…
我们下面建立一个脚本,分析所有这些链接,找出打不开或者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('Could not connect: ' . mysql_error()); } if (!mysql_select_db($db_name)) { die('Could not select db: ' . mysql_error()); } // 找出所有含有链接的文章 $sql = "SELECT post_content FROM wp_posts WHERE post_content LIKE '%href=%' AND post_status = 'publish' AND post_type = 'post'"; $res = mysql_query($sql) or die(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('No URL to check'); }
我们首先配置好数据库,一系列要排除的域名($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了? } else if ($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 "==Dead URLs==\n"; echo implode("\n",$dead_urls) . "\n\n"; echo "==404 URLs==\n"; echo implode("\n",$not_found_urls) . "\n\n"; echo "==Working URLs==\n"; echo implode("\n",$working_urls); // 15. 向批处理器添加url function add_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++; return true; } else { // 没有新的URL需要处理了 return false; } }
下面解释一下以上代码。列表的序号对应着代码注释中的顺序数字。
我把这个脚本在我的博客上跑了一遍(测试需要,有一些错误链接是故意加上的),共检查约40个URL,只耗费两秒不到。当需要检查更加大量的URL时,其省心省力的效果可想而知!如果你同时打开10个连接,还能再快上10倍!另外,你还可以利用cURL批处理的无隔断特性,来处理大量URL请求,而不会阻塞你的Web脚本。
如果某个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);
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);
你可以用代理发起 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);
可以在一个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); function progress_function($ch, $str) { echo $str; return strlen($str); }
这个回调函数必须返回字串的长度,不然此功能将无法正常使用。在URL响应接收的过程中,只要收到一个数据包,这个函数就会被调用。
今天我们一起学习了cURL库的强大功能和灵活的扩展性,希望你喜欢。下一次要发起URL请求时,考虑下cURL吧!谢谢!
参考资料:
主要内容:多级页面分析,爬虫增量抓取,程序代码实现前面讲解的爬虫案例都是单级页面数据抓取,但有些时候,只抓取一个单级页面是无法完成数据提取的。本节讲解如何使用爬虫抓取多级页面的数据。 在爬虫的过程中,多级页面抓取是经常遇见的。下面以抓取二级页面为例,对每级页面的作用进行说明: 一级页面提供了获取二级页面的访问链接。 二级页面作为详情页用来提取所需数据。 一级页面以 标签的形式链接到二级页面,只有在二级页面才可以提取到所需数据。 多级页面分析 下面
问题内容: 我已经通过Scrapy文档今天一直在进行,并试图获得一个工作版本- https://docs.scrapy.org/en/latest/intro/tutorial.html#our-first- spider -在现实世界的例子。我的示例稍有不同,它有2个下一页,即 start_url>城市页面>单位页面 这是我要从中获取数据的单位页面。 我的代码: 但是当我运行它时,我得到: 因此
问题内容: 该网站在一个列表中列出了250多种课程。我想获取每个课程的名称,然后使用php将其插入到我的mysql数据库中。这些课程如下所示: 有没有办法在PHP中做到这一点,而不是让我陷入疯狂的数据输入梦night? 问题答案: 正则表达式效果很好。 请参阅文档 preg_match。
问题内容: 请告知如何抓取AJAX页面。 问题答案: 概述: 首先,所有屏幕抓取都需要手动查看要从中提取资源的页面。在处理AJAX时,通常只需要分析一些内容,而不仅仅是HTML。 在处理AJAX时,这仅意味着您想要的值不在您请求的初始HTML文档中,而是将执行javascript,该javascript请求服务器提供您想要的其他信息。 因此,您通常可以简单地分析javascript并查看javas
在我的硕士论文中,我正在探索通过web自动化从网站中提取数据的可能性。步骤如下: 登录网站(https://www.metal.com/Copper/201102250376) 输入用户名和密码 单击登录 将日期更改为2020年1月1日 刮取生成的表格数据,然后将其保存到csv文件中 用我电脑上的特定名称保存到特定文件夹 运行相同的序列,在同一浏览器窗口的新选项卡中下载其他材料的其他历史价格数据
我正试图从亚马逊印度网站上搜集数据。在以下情况下,我无法使用yield()方法收集响应并解析元素:1)我必须从一个产品页面移动到另一个审阅页面2)我必须从一个审阅页面移动到另一个审阅页面 产品页 评论页 代码流: 1) customerReviewData()调用getCustomerRatingsAndComments(响应) 2) GetCustomerRatings和Comments(res