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

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

(2021年4月4日)PHP8対応,リファラチェック追加

サンプル・プログラムの実行例

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

目次

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 にあらたな正規表現を加えるだけでよい。

 132: /**
 133:  * テキスト中からURLを取り出す
 134:  * @param   string $str 解析するテキスト
 135:  * @param   array  $url 取り出したURLを格納する配列
 136:  * @return  int 取り出したURL数 / FALSE(抽出に失敗)
 137: */
 138: function get_urls($str, &$urls) {
 139:     $PatTable = array(
 140:         "/<a(.*)href=\"?([\-_\.\!\~\*\'\(\)a-z0-9\;\/\?\:@&=\+\$\,\%\#]+)\"/i",
 141:         "/<img(.*)src=\"?([\-_\.\!\~\*\'\(\)a-z0-9\;\/\?\:@&=\+\$\,\%\#]+)\"/i"
 142:     );
 143: 
 144:     $i = 0;
 145:     foreach ($PatTable as $pat) {
 146:         //マッチするすべての部分文字列を取り出す
 147:         if (preg_match_all($pat, $str, $arr, PREG_SET_ORDER> 0) {
 148:             foreach ($arr as $key=>$val) {
 149:                 $urls[$i] = $arr[$key][2];
 150:                 $i++;
 151:             }
 152:         }
 153:     }
 154: 
 155:     return $i;
 156: }

解説:HTTPレスポンス

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

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

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

 210: /**
 211:  * HEADリクエストを行う
 212:  * @param   string $uri
 213:  * @return  array 'HTTP-Version', 'Status-Code', 'Reason-Phrase'
 214: */
 215: function get_http_header($uri) {
 216:     // URIから各情報を取得
 217:     $info = parse_url($uri);
 218: 
 219:     $scheme = $info['scheme'];
 220:     $host = $info['host'];
 221:     $port = isset($info['port']) ? $info['port': 80;
 222:     $path = isset($info['path']) ? $info['path': '';  //Ver.2.3
 223: 
 224:     //リクエストフィールド
 225:     $msg_req = "HEAD " . $path . " HTTP/1.0\r\n";
 226:     $msg_req ."Host: $host\r\n";
 227:     $msg_req ."User-Agent: H2C/1.0\r\n";
 228:     $msg_req ."\r\n";
 229: 
 230:     // スキームがHTTP(S)の時のみ実行する
 231:     if (preg_match('/https?/i', $scheme> 0) {         //Ver.2.4
 232:         $status = array();
 233: 
 234:         // 指定ホストに接続
 235:         if ($handle = @fsockopen($host, $port, $errno, $errstr, 1)) {
 236:             fputs($handle, $msg_req);
 237:             if (socket_set_timeout($handle, 3)) { 
 238:                 $line = 0;
 239:                 while(!feof($handle)) {
 240:                     // 1行めはステータスライン
 241:                     if ($line == 0) {
 242:                         $temp_stat = explode(' ', fgets($handle, 4096));
 243:                         $status['HTTP-Version']  = array_shift($temp_stat);
 244:                         $status['Status-Code']   = array_shift($temp_stat);
 245:                         $status['Reason-Phrase'] = implode(' ', $temp_stat);
 246: 
 247:                     // 2行目以降はコロンで分割
 248:                     } else {
 249:                         $temp_stat = explode(':', fgets$handle, 4096));
 250:                         $name = array_shift$temp_stat );
 251:                         // 通常:の後に1文字半角スペースがあるので除去
 252:                         $status[$name] = substr(implode(':', $temp_stat), 1);
 253:                     }
 254:                     $line++;
 255:                 }
 256: 
 257:             } else {
 258:                 $status['HTTP-Version']  = '---';
 259:                 $status['Status-Code']   = '902';
 260:                 $status['Reason-Phrase'] = "No Response";
 261:             }
 262: 
 263:             fclose($handle);
 264: 
 265:         } else {
 266:             $status['HTTP-Version']  = '---';
 267:             $status['Status-Code']   = '901';
 268:             $status['Reason-Phrase'] = "Unable To Connect";
 269:         }
 270: 
 271:     } else {
 272:         $status['HTTP-Version']  = '---';
 273:         $status['Status-Code']   = '903';
 274:         $status['Reason-Phrase'] = "Not HTTP Request";
 275:     }
 276: 
 277:     return $status;
 278: }

参考サイト

(この項おわり)
header