開發前的準備
安裝linux系統(ubuntu14.04),在vmware虛拟機下安裝一個ubuntu;
安裝php5.6或以上版本;
安裝curl、pcntl擴充。
使用php的curl擴充抓取頁面資料
php的curl擴充是php支援的允許你與各種伺服器使用各種類型的協定進行連接配接和通信的庫。
本程式是抓取知乎的使用者資料,要能通路使用者個人頁面,需要使用者登入後的才能通路。當我們在浏覽器的頁面中點選一個使用者頭像連結進入使用者個人中心頁面
的時候,之是以能夠看到使用者的資訊,是因為在點選連結的時候,浏覽器幫你将本地的cookie帶上一齊送出到新的頁面,是以你就能進入到使用者的個人中心頁
面。是以實作通路個人頁面之前需要先獲得使用者的cookie資訊,然後在每次curl請求的時候帶上cookie資訊。在擷取cookie資訊方面,我是
用了自己的cookie,在頁面中可以看到自己的cookie資訊:
一個個地複制,以"__utma=?;__utmb=?;"這樣的形式組成一個cookie字元串。接下來就可以使用該cookie字元串來發送請求。
初始的示例:
$url = 'http://www.zhihu.com/people/mora-hu/about'; //此處mora-hu代表使用者id
$ch = curl_init($url); //初始化會話
curl_setopt($ch, curlopt_header, 0);
curl_setopt($ch, curlopt_cookie, $this->config_arr['user_cookie']); //設定請求cookie
curl_setopt($ch, curlopt_useragent, $_server['http_user_agent']);
curl_setopt($ch, curlopt_returntransfer, 1); //将curl_exec()擷取的資訊以檔案流的形式傳回,而不是直接輸出。
curl_setopt($ch, curlopt_followlocation, 1);
$result = curl_exec($ch);
return $result; //抓取的結果
運作上面的代碼可以獲得mora-hu使用者的個人中心頁面。利用該結果再使用正規表達式對頁面進行處理,就能擷取到姓名,性别等所需要抓取的資訊。
圖檔防盜鍊
在對傳回結果進行正則處理後輸出個人資訊的時候,發現在頁面中輸出使用者頭像時無法打開。經過查閱資料得知,是因為知乎對圖檔做了防盜鍊處理。解決方案就是請求圖檔的時候在請求頭裡僞造一個referer。
在使用正規表達式擷取到圖檔的連結之後,再發一次請求,這時候帶上圖檔請求的來源,說明該請求來自知乎網站的轉發。具體例子如下:
function getimg($url, $u_id)
{
if (file_exists('./images/' . $u_id . ".jpg"))
{
return "images/$u_id" . '.jpg';
}
if (empty($url))
return '';
$context_options = array(
'http' =>
array(
'header' => "referer:http://www.zhihu.com"//帶上referer參數
)
);
$context = stream_context_create($context_options);
$img = file_get_contents('http:' . $url, false, $context);
file_put_contents('./images/' . $u_id . ".jpg", $img);
return "images/$u_id" . '.jpg';
}
抓取了自己的個人資訊後,就需要再通路使用者的關注者和關注了的使用者清單擷取更多的使用者資訊。然後一層一層地通路。可以看到,在個人中心頁面裡,有兩個連結如下:
這裡有兩個連結,一個是關注了,另一個是關注者,以“關注了”的連結為例。用正則比對去比對到相應的連結,得到url之後用curl帶上cookie再發一次請求。抓取到使用者關注了的用于清單頁之後,可以得到下面的頁面:
分析頁面的html結構,因為隻要得到使用者的資訊,是以隻需要框住的這一塊的div内容,使用者名都在這裡面。可以看到,使用者關注了的頁面的url是:
不同的使用者的這個url幾乎是一樣的,不同的地方就在于使用者名那裡。用正則比對拿到使用者名清單,一個一個地拼url,然後再逐個發請求(當然,一個一個是比較慢的,下面有解決方案,這個稍後會說到)。進入到新使用者的頁面之後,再重複上面的步驟,就這樣不斷循環,直到達到你所要的資料量。
linux統計檔案數量
腳本跑了一段時間後,需要看看究竟擷取了多少圖檔,當資料量比較大的時候,打開檔案夾檢視圖檔數量就有點慢。腳本是在linux環境下運作的,是以可以使用linux的指令來統計檔案數量:
ls -l | grep "^-" | wc -l
其中, ls -l 是長清單輸出該目錄下的檔案資訊(這裡的檔案可以是目錄、連結、裝置檔案等); grep "^-" 過濾長清單輸出資訊, "^-" 隻保留一般檔案,如果隻保留目錄是 "^d" ; wc -l 是統計輸出資訊的行數。下面是一個運作示例:
插入mysql時重複資料的處理
程式運作了一段時間後,發現有很多使用者的資料是重複的,是以需要在插入重複使用者資料的時候做處理。處理方案如下:
1)插入資料庫之前檢查資料是否已經存在資料庫;
2)添加唯一索引,插入時使用 insert into ... on duplicate key update...
3)添加唯一索引,插入時使用 insert ingnore into...
4)添加唯一索引,插入時使用 replace into...
使用curl_multi實作多線程抓取頁面
剛開始單程序而且單個curl去抓取資料,速度很慢,挂機爬了一個晚上隻能抓到2w的資料,于是便想到能不能在進入新的使用者頁面發curl請求的時候一次性請求多個使用者,後來發現了curl_multi這個好東西。curl_multi這類函數可以實作同時請求多個url,而不是一個個請求,這類似于linux系統中一個程序開多條線程執行的功能。下面是使用curl_multi實作多線程爬蟲的示例:
$mh = curl_multi_init(); //傳回一個新curl批處理句柄
for ($i = 0; $i < $max_size; $i++)
{
$ch = curl_init(); //初始化單個curl會話
curl_setopt($ch, curlopt_header, 0);
curl_setopt($ch, curlopt_url, 'http://www.zhihu.com/people/' . $user_list[$i] . '/about');
curl_setopt($ch, curlopt_cookie, self::$user_cookie);
curl_setopt($ch, curlopt_useragent, 'mozilla/5.0 (windows nt 6.1; wow64) applewebkit/537.36 (khtml, like gecko) chrome/44.0.2403.130 safari/537.36');
curl_setopt($ch, curlopt_returntransfer, true);
curl_setopt($ch, curlopt_followlocation, 1);
$requestmap[$i] = $ch;
curl_multi_add_handle($mh, $ch); //向curl批處理會話中添加單獨的curl句柄
}
$user_arr = array();
do {
//運作目前 curl 句柄的子連接配接
while (($cme = curl_multi_exec($mh, $active)) == curlm_call_multi_perform);
if ($cme != curlm_ok) {break;}
//擷取目前解析的curl的相關傳輸資訊
while ($done = curl_multi_info_read($mh))
{
$info = curl_getinfo($done['handle']);
$tmp_result = curl_multi_getcontent($done['handle']);
$error = curl_error($done['handle']);
$user_arr[] = array_values(getuserinfo($tmp_result));
//保證同時有$max_size個請求在處理
if ($i < sizeof($user_list) && isset($user_list[$i]) && $i < count($user_list))
{
$ch = curl_init();
curl_setopt($ch, curlopt_header, 0);
curl_setopt($ch, curlopt_url, 'http://www.zhihu.com/people/' . $user_list[$i] . '/about');
curl_setopt($ch, curlopt_cookie, self::$user_cookie);
curl_setopt($ch, curlopt_useragent, 'mozilla/5.0 (windows nt 6.1; wow64) applewebkit/537.36 (khtml, like gecko) chrome/44.0.2403.130 safari/537.36');
curl_setopt($ch, curlopt_returntransfer, true);
curl_setopt($ch, curlopt_followlocation, 1);
$requestmap[$i] = $ch;
curl_multi_add_handle($mh, $ch);
$i++;
}
curl_multi_remove_handle($mh, $done['handle']);
}
if ($active)
curl_multi_select($mh, 10);
} while ($active);
curl_multi_close($mh);
return $user_arr;
http 429 too many requests
使用curl_multi函數可以同時發多個請求,但是在執行過程中使同時發200個請求的時候,發現很多請求無法傳回了,即發現了丢包的情況。進一步分析,使用 curl_getinfo 函數列印每個請求句柄資訊,該函數傳回一個包含http
response資訊的關聯數組,其中有一個字段是http_code,表示請求傳回的http狀态碼。看到有很多個請求的http_code都是429,這個傳回碼的意思是發送太多請求了。我猜是知乎做了防爬蟲的防護,于是我就拿其他的網站來做測試,發現一次性發200個請求時沒問題的,證明了我的猜測,知乎在這方面做了防護,即一次性的請求數量是有限制的。于是我不斷地減少請求數量,發現在5的時候就沒有丢包情況了。說明在這個程式裡一次性最多隻能發5個請求,雖然不多,但這也是一次小提升了。
使用redis儲存已經通路過的使用者
抓取使用者的過程中,發現有些使用者是已經通路過的,而且他的關注者和關注了的使用者都已經擷取過了,雖然在資料庫的層面做了重複資料的處理,但是程式還是會使用curl發請求,這樣重複的發送請求就有很多重複的網絡開銷。還有一個就是待抓取的使用者需要暫時儲存在一個地方以便下一次執行,剛開始是放到數組裡面,後來發現要在程式裡添加多程序,在多程序程式設計裡,子程序會共享程式代碼、函數庫,但是程序使用的變量與其他程序所使用的截然不同。不同程序之間的變量是分離的,不能被其他程序讀取,是以是不能使用數組的。是以就想到了使用redis緩存來儲存已經處理好的使用者以及待抓取的使用者。這樣每次執行完的時候都把使用者push到一個already_request_queue隊列中,把待抓取的使用者(即每個使用者的關注者和關注了的使用者清單)push到request_queue裡面,然後每次執行前都從request_queue裡pop一個使用者,然後判斷是否在already_request_queue裡面,如果在,則進行下一個,否則就繼續執行。
在php中使用redis示例:
<?php
$redis = new redis();
$redis->connect('127.0.0.1', '6379');
$redis->set('tmp', 'value');
if ($redis->exists('tmp'))
echo $redis->get('tmp') . "\n";
使用php的pcntl擴充實作多程序
改用了curl_multi函數實作多線程抓取使用者資訊之後,程式運作了一個晚上,最終得到的資料有10w。還不能達到自己的理想目标,于是便繼續優化,後來發現php裡面有一個pcntl擴充可以實作多程序程式設計。下面是多程式設計程式設計的示例:
//php多程序demo
//fork10個程序
for ($i = 0; $i < 10; $i++) {
$pid = pcntl_fork();
if ($pid == -1) {
echo "could not fork!\n";
exit(1);
if (!$pid) {
echo "child process $i running\n";
//子程序執行完畢之後就退出,以免繼續fork出新的子程序
exit($i);
//等待子程序執行完畢,避免出現僵屍程序
while (pcntl_waitpid(0, $status) != -1) {
$status = pcntl_wexitstatus($status);
echo "child $status completed\n";
在linux下檢視系統的cpu資訊
實作了多程序程式設計之後,就想着多開幾條程序不斷地抓取使用者的資料,後來開了8調程序跑了一個晚上後發現隻能拿到20w的資料,沒有多大的提升。于是查閱資料發現,根據系統優化的cpu性能調優,程式的最大程序數不能随便給的,要根據cpu的核數和來給,最大程序數最好是cpu核數的2倍。是以需要檢視cpu的資訊來看看cpu的核數。在linux下檢視cpu的資訊的指令:
cat /proc/cpuinfo
其中,model name表示cpu類型資訊,cpu cores表示cpu核數。這裡的核數是1,因為是在虛拟機下運作,配置設定到的cpu核數比較少,是以隻能開2條程序。最終的結果是,用了一個周末就抓取了110萬的使用者資料。
多程序程式設計中redis和mysql連接配接問題
在多程序條件下,程式運作了一段時間後,發現資料不能插入到資料庫,會報mysql too many connections的錯誤,redis也是如此。
下面這段代碼會執行失敗:
for ($i = 0; $i < 10; $i++) {
$pid = pcntl_fork();
if ($pid == -1) {
echo "could not fork!\n";
exit(1);
}
if (!$pid) {
$redis = predis::getinstance();
// do something
exit;
}
根本原因是在各個子程序建立時,就已經繼承了父程序一份完全一樣的拷貝。對象可以拷貝,但是已建立的連接配接不能被拷貝成多個,由此産生的結果,就是各個程序都使用同一個redis連接配接,各幹各的事,最終産生莫名其妙的沖突。
解決方法:
程式不能完全保證在fork程序之前,父程序不會建立redis連接配接執行個體。是以,要解決這個問題隻能靠子程序本身了。試想一下,如果在子程序中擷取的執行個體隻與目前程序相關,那麼這個問題就不存在了。于是解決方案就是稍微改造一下redis類執行個體化的靜态方式,與目前程序id綁定起來。
改造後的代碼如下:
<a><?php </a>
<a> public static function getinstance() { </a>
<a> static $instances = array(); </a>
<a> $key = getmypid();//擷取目前程序id </a>
<a> if ($empty($instances[$key])) { </a>
<a> $inctances[$key] = new self(); </a>
<a> } </a>
<a> </a>
<a> return $instances[$key]; </a>
<a> } </a>
<a></a>
php統計腳本執行時間
因為想知道每個程序花費的時間是多少,是以寫個函數統計腳本執行時間:
function microtime_float()
list($u_sec, $sec) = explode(' ', microtime());
return (floatval($u_sec) + floatval($sec));
$start_time = microtime_float();
//do something
usleep(100);
$end_time = microtime_float();
$total_time = $end_time - $start_time;
$time_cost = sprintf("%.10f", $total_time);
echo "program cost total " . $time_cost . "s\n";
資料分析
抓取了110萬的資料後,小小做了一些資料分析,結果如下:
若文中有不正确的地方,望各位指出以便改正。
作者:aintnot
來源:51cto