2017年1月5日 星期四

撰寫 PHP 網頁爬蟲程式的注意事項

最近 PHP 專案中的網頁爬蟲程式都已寫完, 接下來要完善分析邏輯. 趁著記憶猶新, 抽點時間把撰寫網頁爬蟲該注意的一些事項, 以常用的財經網站為例紀錄如下 :

1. 確定網頁編碼格式 :

每一個網頁都要指定內容的編碼格式, 早期中文網頁都使用 big5 碼, 現在幾乎都改用 Unicode 的 utf-8 格式. 首先要檢視目標網頁 meta 標籤裡面的 charset 屬性是 big5 還是 utf-8, 這決定了 PHP 程式要存成哪一種檔案格式. 例如集保公司集保戶股權分散表查詢網頁就是用 big5 碼 :

集保戶股權分散表查詢


而鉅亨網類股行情表網頁則是採用 utf-8 格式 :

# 鉅亨網類股行情表


如果要抓的目標網頁是 big5 編碼, 則 PHP 爬蟲程式存檔時要用 ANSI 格式存檔, 例如抓取集保戶股權分散表的程式即是, 下面是 EditPlus 編輯器的存檔畫面 :


若目標網頁是 utf-8 編碼, 則 PHP 程式當然要以 utf-8 格式存檔 :


2. 確定取得網頁的方法為 GET 或 POST :

以程式擷取網頁內容必須根據目標網頁取得資料的方法是 GET 或 POST, 才能知道該呼叫 cURL 的 GET 函數 http_get() 還是 POST 函數 http_post_form(), 因為後端伺服器可能只針對網頁表單使用之方法建置相對應的回應處理函數, 用錯方法伺服器不會回應所要的資料.

例如鉅亨網的類股行情表網頁使用 GET 方法, 它不需要使用者做任何選擇或輸入, 只要連線網址即可獲得全部資料. 但是集保戶股權分散表網頁則是使用 POST 方法, 使用者連線到其網址時只是顯示查詢表單, 必須輸入股號或股名後, 按查詢鈕才會傳回資料. 如果程式中用 GET 方法傳遞參數給伺服器的話將無法取得資料, 仍然傳回查詢表單而已.



而鉅亨網的類股行情表不需表單查詢, 自然是使用 GET 方法.

3. 確定表單要傳遞那些參數 :

有些網頁使用 GET 方法取得, 且不需傳遞參數給伺服器, 這部分就可跳過不管. 但不論是 GET 或 POST 方法都可以傳遞表單參數, 只是 GET 所傳遞的參數會顯現在網址列上, 而 POST 則不會, 因為它是放在 HTTP 的內容中傳送.

如何得知表單向後端伺服器傳送了那些參數呢? 可以在 Firefox 瀏覽器中按 F12, 再重新載入網頁, 點選下方的 "網路", 然後點左下角的查詢網址, 點右下角的 "參數" 即可顯示所有傳遞之參數 :


注意, 最底下的 sub 參數其值為怪碼, 原因可能是 Firefox 的 F12 開發工具使用 utf-8 格式, 而目標網頁卻是 big5 編碼之故. 查看網頁原始碼可知此名為 sub 之參數為 submit 按鈕, 其值為 "查詢", 也就是上面的怪碼. 這在擷取此網頁資料時即為重要, 因為後端伺服器似乎會檢查有無傳出此參數, 且其值必須為 big5 的  "查詢".

以上三點是成功擷取網頁的關鍵. 除此之外, 取得網頁資料後要進一步剖析之前, 可能還是需要用到編碼轉換函數 iconv() 來做適度的轉換, 例如在 ANSI 編碼的 PHP 程式中若要輸出從資料庫讀取的中文資料, 必須使用 iconv() 函數轉成 big5 碼才行, 否則會輸出怪碼 :

$stock_name=iconv("UTF-8","BIG5",$RS[$i]["stock_name"]);

這是因為 MySQL 資料庫裡一般都是使用 Unicode 來儲存中文之故.

另外, 如果欲擷取的目標網頁雖然是 big5 編碼, 但只要用 GET 方法即可取得的話, 那麼 PHP 程式就不需要以 ANSI 格式存檔, 可以直接存成 utf-8 格式, 以便與 MySQL 順利接軌, 但是所取得的網頁內容必須用 iconv() 函數轉成 utf-8 格式再分析處理 :

$file=iconv("BIG5","UTF-8",$web_page['FILE']); //轉成 utf-8 格式

其實集保戶股權分布資料網路上有現成整理好的圖表, 例如 :

神秘金字塔

但是若要套用自己的分析邏輯的話, 必須自行記錄追蹤這些資料, 建立自己的數據庫才能辦到. 追蹤集保戶的股權分佈情形有助於了解散戶與大股東的持股動態, 可做為投資決策的參考, 特別是持有千張以上的大股東股權占比變化, 因為公司大股東最了解自家經營狀況.

集保公司的查詢網頁我從去年就測試過, 但是卻一直未能成功, 即使我使用 POST 也是無濟於事. 後來忙著玩 Arduino 就漸漸淡忘這件事了. 直到去年 11 月 17/18 去上 Python 進階班, 老師介紹如何用 Python 寫爬蟲程式, 又勾起了回憶, 於是改用 Python 重新嘗試擷取集保網頁, 但仍然功敗垂成. 我請老師幫我看看到底哪裡出問題, 也是束手無策.

受訓回來後經多方嘗試, 終於搞定這網頁, 原因就出在上面所述的編碼方式與存檔格式問題上, 這也影響到所傳遞的參數值能否通過伺服器檢查, 這些條件都滿足了才會傳回我們所期望的回應.

最後, 擷取集保網頁還有一個問題 (也是最重要的問題), 就是在 POST 表單時須傳送欲擷取之日期 (格式例如 20161230), 但這些日期是放在查詢網頁的下拉式選單中以倒序排列, 集保公司大約每周會更新資料. 解決辦法是在資料庫中設置一個 last_tdcc_date 欄位來儲存最近的更新日期, 先存入一個初始日期, 然後在每次擷取時剖析這個下拉式選單的 option 部分, 其第一筆資料便是最近的統計日期, 在擷取結束時更新這個 last_tdcc_date 欄位即可, 下一次擷取時就會抓最新日期的資料了.


不過處理這部分遇到一個小麻煩, 如上圖所示, 第一個 option (最近統計日期) 預設為 selected, 而且後面有一個跳行, 使用傳統的消除 2 個以上空白的方式無法去除這個跳行, 必須減為去除一個以上空白才行 :

  $start='<select size="1" name="SCA_DATE">';
  $end="</select>";
  $options=return_between($file, $start, $end, EXCL);
  $options=preg_replace("/[\s]{1,}/","",$options); //去除 1 個以上空白

這樣就完美地解決這問題了.

2 則留言 :

李碩軒 提到...

版主您好,我的自己寫了一份網頁,環境是以PHP 5結合Microsoft SQL server,以mssql_query function做為資料存取。
如果前端網頁用meta charset設定為UTF-8,以POST方式傳到後台,然後執行mssql_query,但中間卻要經由iconv function從UTF-8轉成BIG-5才能儲存中文字到資料庫中,對照您說的內容,我有兩種方法解決,一就是以iconv作轉換解決,另一解法其實就是前台就以meta charset big5訂定即可解決此問題,是這樣理解嗎?

小狐狸事務所 提到...

我測試的結果是, 要完全模仿目標網頁的情況, 若它是 big5, 則 PHP 程式的 HTML 部分也要設成 big5 才能正確解譯所下載的網頁內容, 且 PHP存檔要存成 ANSI, 不是 utf-8, 但取得的存入 MySQL 資料庫通常是 utf-8, 所以取得的網頁內容要從 big5 用 iconv 轉成 utf-8.