PHPでリンク切れを調べる(その2)

(1/1)
以前、コンテンツ内のリンク切れを調べるプログラムを作ったが、リンク先でリダイレクトしていたりすると、期待する結果が得られないケースがある。そこで今回は、HTTP レスポンスを調べ、より実用的なプログラムに改良してみることにした。

HTTP レスポンス

HTTP通信とは、
  • クライアントが URL を含む HTTP リクエストを送出する
  • サーバ側がHTTP レスポンスを返す
――この繰り返しである。

 fopen  関数を使ってインターネットにアクセスする際、表に見えてこないが、実際にはリクエスト・ヘッダとレスポンス・ヘッダがやり取りされている。そして、レスポンス・ヘッダの中には、リクエストしたコンテンツがどうなっているかを示すステータスが記録されている。
このステータスを解析することで、リンク切れなのかどうか判断できる。

HTTP レスポンスの詳細については、HTTP Status Codeを参照してほしい。
今回は、HTTP レスポンスがクライアントエラー(4xx番台)とサーバエラー(5xx番台)が「リンク切れ」であると定義し、プログラムを作っていくことにする。

サンプル・プログラム

サンプル・プログラムは、指定した URL に含まれるリンク先(URL)を検出し、リンク切れが起きていないかどうか一覧表に表示する。また、チェックボックスを ON にすることで、クライアントエラー(4xx番台)とサーバエラー(5xx番台)以外のすべてのリンク先とステータスを表示するようにした。

解説:URLの検出

まずリンク URL の検出が必要だが、これは正規表現で画像ファイルの URL を取り出すプログラムで説明した方法を利用し、ユーザー関数 get_url に実装した。URL を絶対パスに正規化するために、「PHP でリンク切れを調べる」で作ったユーザー関数 reg_urls を流用した。

タグ "<a href~>" と タグ "<img src~>" のハイパーリンクを検出対象とするため、複数の正規表現をテーブルとして、配列変数 $PatTable に用意するというアレンジを加えている。
もし <object> タグなども検出対象にしたいのであれば、配列変数 $PatTable にあらたな正規表現を加えるだけでよい。

0094: }
0095: 
0096: /**
0097:  * テキスト中からURLを取り出す
0098:  * @param string $str 解析するテキスト
0099:  * @param array  $url 取り出したURLを格納する配列
0100:  * @return int 取り出したURL数 / FALSE(抽出に失敗)
0101: */
0102: function get_urls($str, &$urls) {
0103:     $PatTable = array(
0104:         "/<a(.*)href=\"?([\-_\.\!\~\*\'\(\)a-z0-9\;\/\?\:@&=\+\$\,\%\#]+)\"/i",
0105:         "/<img(.*)src=\"?([\-_\.\!\~\*\'\(\)a-z0-9\;\/\?\:@&=\+\$\,\%\#]+)\"/i"
0106:     );
0107: 
0108:     $i = 0;
0109:     foreach ($PatTable as $pat) {
0110:         //マッチするすべての部分文字列を取り出す
0111:         if (preg_match_all($pat$str$arrPREG_SET_ORDER) > 0) {
0112:             foreach ($arr as $key=>$val) {
0113:                 $urls[$i] = $arr[$key][2];
0114:                 $i++;
0115:             }
0116:         }
0117:     }
0118: 

解説:HTTPレスポンス

PHP 5 では、 get_headers  関数を使って指定した URL のレスポンスを得ることができる。
残念ながら PHP 4 にこの関数はないので、同等の関数をユーザー定義することにした。それがユーザー関数 get_http_header である。

ユーザー関数 get_http_header は、配列変数に HTTP バージョン、ステータス・コード、リーズンの 3 つを返す。

こうして、ユーザー関数 get_url とユーザー関数 get_http_header を呼び出すことで、リンク切れのチェックができる。
これを行うのがユーザー関数 check_links である。
あとは、押下されたボタンによって処理を分岐させるメイン・プログラム部分と、表示プログラムを用意すればいい。

0174: /**
0175:  * HEADリクエストを行う
0176:  * @param string $uri
0177:  * @return array 'HTTP-Version', 'Status-Code', 'Reason-Phrase'
0178: */
0179: function get_http_header($uri) {
0180:     // URIから各情報を取得
0181:     $info = parse_url($uri);
0182: 
0183:     $scheme = $info['scheme'];
0184:     $host = $info['host'];
0185:     $port = isset($info['port']) ? $info['port'] : 80;
0186:     $path = isset($info['path']) ? $info['path'] : '';   //Ver.2.3
0187: 
0188:     //リクエストフィールド
0189:     $msg_req = "HEAD " . $path . " HTTP/1.0\r\n";
0190:     $msg_req .= "Host: $host\r\n";
0191:     $msg_req .= "User-Agent: H2C/1.0\r\n";
0192:     $msg_req .= "\r\n";
0193: 
0194:     // スキームがHTTP(S)の時のみ実行する
0195:     if (preg_match('/https?/i', $scheme) > 0) {           //Ver.2.4
0196:         $status = array();
0197: 
0198:         // 指定ホストに接続
0199:         if ($handle = @fsockopen($host$port$errno$errstr, 1)) {
0200:             fputs($handle$msg_req);
0201:             if (socket_set_timeout($handle, 3)) { 
0202:                 $line = 0;
0203:                 while(!feof($handle)) {
0204:                     // 1行めはステータスライン
0205:                     if ($line == 0) {
0206:                         $temp_stat = explode(' ', fgets($handle, 4096));
0207:                         $status['HTTP-Version']  = array_shift($temp_stat);
0208:                         $status['Status-Code']   = array_shift($temp_stat);
0209:                         $status['Reason-Phrase'] = implode(' ', $temp_stat);
0210: 
0211:                     // 2行目以降はコロンで分割
0212:                     } else {
0213:                         $temp_stat = explode(':', fgets$handle, 4096));
0214:                         $name = array_shift$temp_stat );
0215:                         // 通常:の後に1文字半角スペースがあるので除去
0216:                         $status[$name] = substr(implode(':', $temp_stat), 1);
0217:                     }
0218:                     $line++;
0219:                 }
0220: 
0221:             } else {
0222:                 $status['HTTP-Version'] = '---';
0223:                 $status['Status-Code'] = '902';
0224:                 $status['Reason-Phrase'] = "No Response";
0225:             }
0226: 
0227:             fclose($handle);
0228: 
0229:         } else {
0230:             $status['HTTP-Version'] = '---';
0231:             $status['Status-Code'] = '901';
0232:             $status['Reason-Phrase'] = "Unable To Connect";
0233:         }
0234: 
0235:     } else {
0236:         $status['HTTP-Version'] = '---';
0237:         $status['Status-Code'] = '903';
0238:         $status['Reason-Phrase'] = "Not HTTP Request";
0239:     }
0240: 
0241:     return $status;
0242: }

参考サイト

(この項おわり)
header