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 庫已開啓。

phpinfo_curl

如果 curl 沒有開啓,那麼就需要開啓這個庫。如果是在Windows平臺下,那麼非常簡單,你需要改一改 php.ini 文件的設置,找到 php_curl.dll,並取消前面的分號註釋就行了。如下所示:

# 取消下面的註釋
extension=php_curl.dll

如果是 Linux 服務器,需要重新編譯 PHP ,編譯時在configure命令上加上--with-curl參數。

3 基本結構

在學習更爲複雜的功能之前,先來看一下在 PHP 中建立 cURL 請求的基本步驟:

  1. 初始化
  2. 設置選項
  3. 執行並獲取結果
  4. 釋放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 "cURL Error: " . 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請求頭信息(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

7 用POST方法發送數據

當發起 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 捕捉了這個輸出。

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
    )

)

9 cURL批處理(multi cURL)

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.1 WordPress 鏈接檢查器

想象一下你有一個文章數目龐大的博客,這些文章中包含了大量外部網站鏈接。一段時間之後,因爲這樣那樣的原因,這些鏈接中相當數量都失效了。要麼是被和諧了,要麼是整個站點都被功夫網了…

我們下面建立一個腳本,分析所有這些鏈接,找出打不開或者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;
	}
}

下面解釋一下以上代碼。列表的序號對應着代碼註釋中的順序數字。

  1. 新建一個批處理句柄。
  2. 稍後我們將創建一個把URL加入批處理器的函數 add_url_to_multi_handle() 。每當這個函數被調用,就有一個新url被加入批處理器。一開始,我們給批處理器添加了10個URL(這一數字由 $max_connections 所決定)。
  3. 運行 curl_multi_exec() 進行初始化工作是必須的,只要它返回 CURLM_CALL_MULTI_PERFORM 就還有事情要做。這麼做主要是爲了創建連接,它不會等待完整的URL響應。
  4. 只要批處理中還有活動連接主循環就會一直持續。
  5. curl_multi_select() 會一直等待,直到某個URL查詢產生活動連接。
  6. cURL的活兒又來了,主要是獲取響應數據。
  7. 檢查各種信息。當一個URL請求完成時,會返回一個數組。
  8. 在返回的數組中有一個 cURL 句柄。我們利用其獲取單個 cURL 請求的相應信息。
  9. 如果這是一個死鏈或者請求超時,不會返回http狀態碼。
  10. 如果這個頁面找不到了,會返回404狀態碼。
  11. 其他情況我們都認爲這個鏈接是可用的(當然,你也可以再檢查一下500錯誤之類…)。
  12. 從該批次移除這個 cURL 句柄,因爲它已經沒有利用價值了,關了它!
  13. 很好,現在可以另外加一個URL進來了。再一次地,初始化工作又開始進行…
  14. 嗯,該乾的都幹了。關閉批處理器,生成報告。
  15. 回過頭來看給批處理器添加新 URL 的函數。這個函數每調用一次,靜態變量 $index 就遞增一次,這樣我們才能知道還剩多少 URL 沒處理。

我把這個腳本在我的博客上跑了一遍(測試需要,有一些錯誤鏈接是故意加上的),共檢查約40個URL,只耗費兩秒不到。當需要檢查更加大量的URL時,其省心省力的效果可想而知!如果你同時打開10個連接,還能再快上10倍!另外,你還可以利用cURL批處理的無隔斷特性,來處理大量URL請求,而不會阻塞你的Web腳本。

10 另一些有用的cURL 選項

10.1 HTTP 認證

如果某個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.2 FTP 上傳

PHP 自帶有 FTP 類庫, 但你也能用 cURL:

// 打開一個文件指針
$file = fopen("/path/to/file", "r");
$size = filesize("/path/to/file");

// url裏包含了大部分所需信息
$url = "ftp://username:[email protected]: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);

function progress_function($ch, $str) {
	echo $str;
	return strlen($str);
}

這個回調函數必須返回字串的長度,不然此功能將無法正常使用。在URL響應接收的過程中,只要收到一個數據包,這個函數就會被調用。

11 小結

今天我們一起學習了cURL庫的強大功能和靈活的擴展性,希望你喜歡。下一次要發起URL請求時,考慮下cURL吧!謝謝!

參考地址: https://www.awaimai.com/610.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章