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

0132: /**
0133:  * テキスト中からURLを取り出す
0134:  * @param string $str 解析するテキスト
0135:  * @param array  $url 取り出したURLを格納する配列
0136:  * @return int 取り出したURL数 / FALSE(抽出に失敗)
0137: */
0138: function get_urls($str, &$urls) {
0139:     $PatTable = array(
0140:         "/<a(.*)href=\"?([\-_\.\!\~\*\'\(\)a-z0-9\;\/\?\:@&=\+\$\,\%\#]+)\"/i",
0141:         "/<img(.*)src=\"?([\-_\.\!\~\*\'\(\)a-z0-9\;\/\?\:@&=\+\$\,\%\#]+)\"/i"
0142:     );
0143: 
0144:     $i = 0;
0145:     foreach ($PatTable as $pat) {
0146:         //マッチするすべての部分文字列を取り出す
0147:         if (preg_match_all($pat$str$arrPREG_SET_ORDER) > 0) {
0148:             foreach ($arr as $key=>$val) {
0149:                 $urls[$i] = $arr[$key][2];
0150:                 $i++;
0151:             }
0152:         }
0153:     }
0154: 
0155:     return $i;
0156: }

解説:HTTPレスポンス

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

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

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

0210: /**
0211:  * HEADリクエストを行う
0212:  * @param string $uri
0213:  * @return array 'HTTP-Version', 'Status-Code', 'Reason-Phrase'
0214: */
0215: function get_http_header($uri) {
0216:     // URIから各情報を取得
0217:     $info = parse_url($uri);
0218: 
0219:     $scheme = $info['scheme'];
0220:     $host = $info['host'];
0221:     $port = isset($info['port']) ? $info['port'] : 80;
0222:     $path = isset($info['path']) ? $info['path'] : '';   //Ver.2.3
0223: 
0224:     //リクエストフィールド
0225:     $msg_req = "HEAD " . $path . " HTTP/1.0\r\n";
0226:     $msg_req .= "Host: $host\r\n";
0227:     $msg_req .= "User-Agent: H2C/1.0\r\n";
0228:     $msg_req .= "\r\n";
0229: 
0230:     // スキームがHTTP(S)の時のみ実行する
0231:     if (preg_match('/https?/i', $scheme) > 0) {           //Ver.2.4
0232:         $status = array();
0233: 
0234:         // 指定ホストに接続
0235:         if ($handle = @fsockopen($host$port$errno$errstr, 1)) {
0236:             fputs($handle$msg_req);
0237:             if (socket_set_timeout($handle, 3)) { 
0238:                 $line = 0;
0239:                 while(!feof($handle)) {
0240:                     // 1行めはステータスライン
0241:                     if ($line == 0) {
0242:                         $temp_stat = explode(' ', fgets($handle, 4096));
0243:                         $status['HTTP-Version']  = array_shift($temp_stat);
0244:                         $status['Status-Code']   = array_shift($temp_stat);
0245:                         $status['Reason-Phrase'] = implode(' ', $temp_stat);
0246: 
0247:                     // 2行目以降はコロンで分割
0248:                     } else {
0249:                         $temp_stat = explode(':', fgets$handle, 4096));
0250:                         $name = array_shift$temp_stat );
0251:                         // 通常:の後に1文字半角スペースがあるので除去
0252:                         $status[$name] = substr(implode(':', $temp_stat), 1);
0253:                     }
0254:                     $line++;
0255:                 }
0256: 
0257:             } else {
0258:                 $status['HTTP-Version']  = '---';
0259:                 $status['Status-Code']   = '902';
0260:                 $status['Reason-Phrase'] = "No Response";
0261:             }
0262: 
0263:             fclose($handle);
0264: 
0265:         } else {
0266:             $status['HTTP-Version']  = '---';
0267:             $status['Status-Code']   = '901';
0268:             $status['Reason-Phrase'] = "Unable To Connect";
0269:         }
0270: 
0271:     } else {
0272:         $status['HTTP-Version']  = '---';
0273:         $status['Status-Code']   = '903';
0274:         $status['Reason-Phrase'] = "Not HTTP Request";
0275:     }
0276: 
0277:     return $status;
0278: }

参考サイト

(この項おわり)
header