2016年12月31日 星期六

如何剖析不標準的網頁

最近在寫 PHP 爬蟲程式時遇到一個比較特殊的網頁, 需要費一點功夫來處理, 雖然也不算太麻煩, 但因為值得以後處理類似網頁時參考, 於是就抽出一點時間記錄一下. 這網頁就是鉅亨網的類股行情表 :

# 鉅亨網類股行情表

此網頁將集中市場每日盤後各產業分類的成交量, 比重, 以及指數漲跌幅等資料做成一個報表, 且可查詢近一周的資訊. 紀錄各分類所佔比重變化可以知道資金往哪個產業移動, 若比重逐漸增加, 表示該產業逐漸成長增溫, 資金流向此產業將推升股價, 特別是該產業的龍頭股; 反之則是該產業逐漸降溫, 有可能走入景氣循環的下坡, 市場籌碼漸漸抽出. 另外從台股產業龍頭的電子股與金融股也可以看出市場交易的冷熱情形, 電子類股占比若低於 50% 表示市場過冷; 而金融類股占比若超過 10% 表示市場過熱.

檢視此網頁的原始碼可知, 此網頁的類股資料放在 <!-- 資料區塊:start --> 與 <!-- 資料區塊:end --> 之間的 <div> 區塊裡, 裡面含有三個表格, 而我們需要的類股資料放在第三個表格中的 tbody 元素裡面, 因此我會以 thead 末尾的幾個欄位標題當作開頭標誌, 以 </tbody> 當作結束標誌, 這樣就能輕易抓出 tbody 內部的一堆 tr 與 td 所構成的儲存格內容. 擷取網頁的程式碼如下 :

//===擷取目標網頁 : 鉅亨網類股行情表===
$target="http://www.cnyes.com/twstock/a_price4.aspx";
echo "<br>擷取鉅亨網類股行情報表 : <a href='$target' target='_blank'>原始網頁</a><br>";
$ref=$target;
$web_page=http_get($target,$ref); //下載網頁檔
$file=$web_page['FILE'];
$file=preg_replace("/([\s]{2,})/","",$file); //先去除多餘空白以利尋標

這裡我們用 preg_replace() 函數將原始網頁中的多餘空白去除, 包含跳行字元, 因此取得的原始碼字元串會變得很緊緻, 有利於後續剖析. 接下來要抓出 tbody 內的儲存格資料, 程式碼如下 :

//擷取表格內容 : 最後一個 table
$start='市值(億)</th><th>增減</th></tr></thead><tbody>';
$end="</tbody>";
$data=return_between($file, $start, $end, EXCL);

這裡 $start 就是該表格的 thead 最後面的幾個欄位直到 <tbody>, 具有唯一性 (才不會找錯資料), 呼叫 return_between() 函數後會傳回開頭與結尾中間的儲存格內容, 包含 tr 與 td 元素. 不過這些元素含有樣式屬性與元素 (font), 必須去除才好處理, 程式碼如下 :

//純化表格:去除屬性與不需要的元素
$data=str_replace(' bgcolor="#FFFFFF"', "", $data);; //去除 <tr> 屬性
$data=str_replace(' nowrap align="center"', "", $data);; //去除 <td> 屬性
$data=str_replace(' nowrap align="right"', "", $data);; //去除 <td> 屬性
$data=remove($data, "<font", ">"); //去除 font
$data=str_replace('</font>', "", $data);; //去除 font

經過這樣處理後的 tr 與 td 序列就全部乾淨了, 只含有 tr, td 以及資料. 但是仔細看這些 tr 與 td 元素發現, 這些表格元素不是標準的 HTML 格式, 例如部分 tr 為 <tr >, 裡面含有一個空格; 其次它沒有 tr 與 td 的結束標籤, 格式如下 :

<tr >
  <td>電子
  <td>324.97
  <td>64.69
  ...
<tr>
  <td>金融
  <td>37.33
  <td>7.43
  ...

缺乏結束標籤會造成剖析上的困難, 因為 parse_array() 與 remove() 函數都需要開頭與結束標籤來尋標. 解決之道是對其進行矯正, 修正為標準 HTML 格式. 程式碼如下 :

//修正 HTML 格式
$data=str_replace('<tr >', "<tr>", $data);; //矯正 <tr >
$data=str_replace('<tr><td>', "</tr><tr><td>", $data); //補上 </tr>
$data=substr($data, 5); //刪除最前面多加的 </tr>
$data .= "</tr>";
echo "<br>類股比重漲跌<br><table border=1>".$data."</table><br>";

這裡首先用 str_replace() 將 <tr > 全部改成 <tr>, 去除 tr 中的空格, 其次用 str_replace() 將 <tr><td> 全部改成 </tr><tr><td>, 亦即補上缺漏的 </tr>, 但是這樣一來最前面就會多出一個 </tr> 了, 因此須用 substr() 把前面 5 個字元刪掉. 這樣就把 $data 變成標準的 HTML 格式了.

咦, td 不是還缺結束標籤嗎? 是的, 但這可以不需要處理, 因為拆分欄位不需要再次使用 parse_array(), 只要用 <td> 當拆分符去拆解每一列, 就可以取得所需要的比重與漲幅欄位了, 程式碼如下 :

$arr=parse_array($data, "<tr", "/tr>");  //拆分每一列 (共 30 列)
for ($i=0; $i<count($arr); $i++) { //拜訪每一列 (每一支股票)
  $brr=explode("<td>", $arr[$i]); //拆分每一欄
  $sector=$brr[1];  //類別
  $ratio=$brr[3];   //比重
  $delta=$brr[7];   //漲幅
  echo "$sector : 比重=$ratio% 漲跌=$delta%<br>";
  }

這裡先拆分 tr 列, 然後在迴圈中用 explode 拆分行, 類別欄位在第 1 欄 (索引 1), 比重在第 3 欄 (索引 3), 而指數漲幅在第 7 欄 (索引 7). 咦, 第一欄不是索引 0 嗎? 沒錯, 但因為用 <td> 當分界符, 所以索引 0 是一個空值. 舉個簡單的範例來說明會比較明白 :

$str="<td>1<td>2<td>3";  //沒有結尾 </td> 的儲存格
$arr=explode("<td>",$str);  //[0]為空白, [1]為 1,...
print_r($arr);

在這個範例中, 以  <td> 拆分字串時, 第一個  <td>  前面的空值 "" 是第一個被拆除來的子字串, 它會變成索引 0, 而第一欄的資料會在索引 1, 依此類推.

網頁格式不一定是標準的 HTML, 剖析處理時要視情況個別處理, 以上只是一種特例.

沒有留言 :