PHPでリンク切れを調べる

(1/1)
PHP では、ローカルディスクにあるファイルとインターネット上のコンテンツを同様に扱うことができる。
この特徴を利用して、ホームページ内に書かれているリンク先が存在しているかどうか、存在する場合はコンテンツのサイズがゼロでないかどうかをチェックするプログラムを作ってみることにする。
あわせて、W3C が提供する Web サービスを利用し、HTML が文法的に正しいかをチェックする。

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

PHPでリンク切れを調べる

サンプル・プログラム

解説:404エラーのチェック

0211: /**
0212:  * パターンに一致するものがあるかどうかチェックする
0213:  * @param string $pattern パターン(正規表現)
0214:  * @param int    $n       パターンに一致する場所(正規表現内の順序)
0215:  * @param string $str     チェック対象文字列
0216:  * @param int    $ln      解析中の行番号
0217:  * @param string $url     解析中のURL
0218:  * @param array  $items   チェック結果を格納する配列
0219:  * @param string $exclude チェック除外するURLパターン(正規表現;省略可)
0220: */
0221: function check_link($pattern$l$str$ln$url, &$items$exclude='') {
0222:     $n = preg_match_all($pattern$str$arr);
0223: 
0224:     for ($i = 0; $i < $n$i++) {
0225:         $target = reg_urls($arr[$l][$i]$url);
0226:         //除外パターンのチェック
0227:         if ($exclude !='') {
0228:             if (preg_match('/' . $exclude . '/ui', $target) > 0)  continue;
0229:         }
0230:         //ファイル・オープン
0231:         $cfp = @fopen($target, 'r');
0232:         $arr2['ln']  = $ln;
0233:         $arr2['url'] = $target;
0234:         $arr2['res'] = 'あり';
0235: 
0236:         if (function_exists('curl_exec')) {
0237:             list($code$size) = getHttpResponseCodeSize($target);
0238:             if ($code != 200) {
0239:                 $arr2['res'] = 'なし';
0240:                 $arr2['size'] = (-1);
0241:             //サイズ>0 または パス指定
0242:             } else if (($size > 0) || (preg_match('/\/$/ui', $target) > 0)) {
0243:                 $arr2['res'] = 'あり';
0244:                 $arr2['size'] = $size;
0245:             } else {
0246:                 $arr2['res'] = 'ゼロ';
0247:                 $arr2['size'] = 0;
0248:             }
0249:         } else if ($cfp == FALSE) {
0250:             $arr2['res'] = 'なし';
0251:             $arr2['size'] = (-1);
0252:         } else {
0253:             $size = contents_size($target);
0254:             if ($size <= 0) {
0255:                 $arr2['res'] = 'ゼロ';
0256:                 $arr2['size'] = 0;
0257:             }
0258:         }
0259:         $items[] = $arr2;
0260:     }
0261: }
0262: 
0263: /**
0264:  * リンク先の有無を表示する
0265:  * @param string $url      チェックするURL
0266:  * @param int    $flag_a   <a>タグのハイパーリンク先をチェックする
0267:  * @param int    $flag_img <img>タグの画像ソースをチェックする
0268:  * @param array  $items    チェック結果を格納する配列
0269:  * @param string $exclude  チェック除外するURLパターン(正規表現;省略可)
0270: */
0271: function check404($url$flag_a$flag_img, &$items$exclude='') {
0272:     $fp = @fopen($url, 'r');
0273:     if ($fp == FALSE)   return $url . ' は存在しない.';
0274: 
0275:     $ln = 1;
0276:     while (! feof($fp)) {
0277:         $s = fgets($fp);
0278:         if ($flag_a) {
0279:             $pattern = "/<a.+?href\=\"([^\"]+)\"/i";
0280:             check_link($pattern, 1, $s$ln$url$items$exclude);
0281:         }
0282:         if ($flag_img) {
0283:             $pattern = "/<img.+?src\=\"([^\"]+)\"/i";
0284:             check_link($pattern, 1, $s$ln$url$items$exclude);
0285:         }
0286:         $ln++;
0287:     }
0288:     fclose($fp);
0289: 
0290:     return '';
0291: }

このプログラムの肝となるのはユーザー定義関数 check404 である。

調査するコンテンツ(ホームページ)の URL を引数にし、その中に出現する <a> タグおよび <img> タグのリンク先が  fopen  できるかどうかを調べる。404 エラー(コンテンツが存在しない)なら、 fopen  は失敗する。
まず、調査するコンテンツの存在をチェックする。無ければエラーメッセージを返す。
次に、調査するコンテンツを 1行ずつ読み込み、<a> タグや <img> タグにパターンマッチする行があれば解析する。
<a> タグおよび <img> タグにパターンマッチするかどうかを調べるために正規表現を用いている。
実際のパターンマッチと存在性チェックは、ユーザー定義関数 check_link が行う。

ユーザー定義関数 check_link では、マッチした部分文字列(実際にはアドレスが入る)を取り出す。
このとき、相対アドレス指定(http:で始まらない)があると  fopen  関数が適用できないので、ユーザー定義関数 reg_urls により URL の正規化を行う。
正規化した URL が、除外パターン $exclude にマッチしていたら、チェックをスキップする。

存在性チェックは  fopen  関数を利用する。ネット上のコンテンツに対しては  file_exists  関数は利用できないためだ。
なお、404 エラーの場合、fopen 自身が失敗するので、@ を使ってエラー発生を抑えている。
ファイルが存在しなければ、fopen の戻り値は FALSE となる。

解説:HTTPレスポンスコード、サイズの取得

0175: /**
0176:  * HTTPレスポンスコード、サイズを取得
0177:  * @param string $url 解析中のURL
0178:  * @return array(HTTPレスポンスコード:200 正常/NULL 取得失敗, サイズ)
0179: */
0180: function getHttpResponseCodeSize($url) {
0181:     $header = NULL;
0182:     $options = array(
0183:         CURLOPT_RETURNTRANSFER => TRUE,
0184:         CURLOPT_HEADER         => TRUE,
0185:         CURLOPT_FOLLOWLOCATION => TRUE,
0186:         CURLOPT_ENCODING       => '',
0187:         CURLOPT_USERAGENT      => 'pahoo_php',
0188:         CURLOPT_SSL_VERIFYPEER => FALSE,
0189:         CURLOPT_SSL_VERIFYHOST => FALSE,
0190:         CURLOPT_AUTOREFERER    => TRUE,
0191:         CURLOPT_CONNECTTIMEOUT => 120,
0192:         CURLOPT_TIMEOUT        => 120,
0193:         CURLOPT_MAXREDIRS      => 10
0194:     );
0195:     $ch = curl_init($url);
0196:     curl_setopt_array($ch$options);
0197:     curl_exec($ch);
0198: 
0199:     if (! curl_errno($ch))   $header = curl_getinfo($ch);
0200:     curl_close($ch);
0201: 
0202:     //コンテンツ・サイズ
0203:     $size = $header['download_content_length'];
0204:     if (($header['http_code'] == 200) && ($size < 0)) {
0205:         $size = contents_size($url);
0206:     }
0207: 
0208:     return array($header['http_code'], $size);
0209: }

ユーザー定義関数 check_link でリンク先が存在していた場合でも、404 エラーなどでジャンプしている場合を想定し、ユーザー定義関数 getHttpResponseCode を用意した。この関数は、HTTP レスポンスコードを返す。
同様の機能を持つ組み込み関数  http_response_code  が用意されているが、PHP5 以上でないと利用できない。

そこで、HTTP レスポンスコードの取得は、cURL関数を利用することにした。
組み込み関数  curl_getinfo  を使って、HTTP CODE を受け取る。これが 200 であれば、正常受信できている。

次に、download_content_length を使って、HTTP通信で送られてくる Content-Length がゼロでないかどうかをチェックする。
ただし、HTTP サーバによっては Content-Length を送信しないことがあるので、その場合、自力でコンテンツ・サイズを求めるユーザー関数 contents_size を呼び出す。

解説:コンテンツ・サイズの取得

0158: /**
0159:  * http上のファイルサイズ取得
0160:  * @param string $url 対象コンテンツ
0161:  * @param long   ファイルサイズ
0162: */
0163: function contents_size($url) {
0164:     $data = '';
0165:     $fp = fopen($url, 'r');
0166:     if (! $fp)       return (-1);
0167:     while (! feof($fp)) {
0168:         $data .= fread($fp, 1024);
0169:     }
0170:     fclose($fp);
0171: 
0172:     return strlen($data);
0173: }

組み込み関数  filesize  ではネット上のファイルのサイズを取得することはできない。
そこで、コンテンツを読み込んでサイズを数えるユーザー関数 contents_size を用意した。

解説:URLの正規化

なお、ハイパーリンクに記述されている URL は相対パス(例:"./index.htm")のこともあるので、ステータスを調べるときに支障が出る。そこで、すべて絶対パスに正規化する必要がある。

URL の正規化処理は、ユーザー関数 reg_urls で行う。

0111: /**
0112:  * URLを正規化する(相対指定を絶対指定に変換する)
0113:  * @param string $url  正規化するURL
0114:  * @param string $sour 読み込んだコンテンツのURL
0115:  * @return string 正規化したURL / FALSE(正規化に失敗)
0116: */
0117: function reg_urls($url$sour) {
0118:     //httpで始まればスキップ
0119:     if (preg_match('/^http/', $url) != FALSE)    return $url;
0120: 
0121:     $sour = rtrim($sour, '/');      //末尾のスラッシュは除く
0122:     //相対指定の場合
0123:     if (preg_match('/\:\/\//', $url) == FALSE) {
0124:         $regs = parse_url($sour);
0125:         $dirname = isset($regs['path']) ? dirname($regs['path']) : '';
0126:         if (preg_match('/^\//', $url) > 0) {
0127:             $url = ltrim($url, '/');        //冒頭のスラッシュは除く
0128:             $dirname = '';
0129:         }
0130:         $url = $regs['scheme'] . '://' . $regs['host'] . $dirname . '/' . $url;
0131:     }
0132:     //相対指定を絶対指定に変換する
0133:     $regs = parse_url($url);
0134:     $aa = preg_split("/[\/\\\]/", $regs['path']);
0135:     $an = count($aa);
0136:     $bb = array();
0137:     $bn = 0;
0138:     for ($i = 1; $i < $an$i++) {
0139:         switch ($aa[$i]) {
0140:             case '.':
0141:                 break;
0142:             case '..':
0143:                 $bn--;
0144:                 if ($bn < 0)    return FALSE;
0145:                 break;
0146:             default:
0147:                 $bb[$bn] = $aa[$i];
0148:                 $bn++;
0149:                 break;
0150:         }
0151:     }
0152:     $ss = '';
0153:     for ($i = 0; $i < $bn$i++)    $ss = $ss . '/' . $bb[$i];
0154: 
0155:     return $regs['scheme'] . '://' . $regs['host'] . $ss;
0156: }

解説:HTMLチェッカー

指定した HTML が文法的に正しいかどうか、W3C が用意する Markup Validation Service を使ってチェックのが、ユーザー関数 htmlChecker である。
バリデーション結果をスクレイピングし、エラーとワーニングの数を返す。

0361: /**
0362:  * HTMLチェッカーのURL取得
0363:  * @param string $url チェックするURL
0364:  * @param string HTMLチェッカーのURL
0365: */
0366: function getURL_htmlChecker($url) {
0367:     return 'https://validator.w3.org/nu/?doc=' . urlencode($url);
0368: }
0369: 
0370: /**
0371:  * リンク先の有無を表示する
0372:  * @param string $url チェックするURL
0373:  * @param int エラー数+ワーニング数/FALSE
0374: */
0375: function htmlChecker($url) {
0376:     $url = getURL_htmlChecker($url);
0377:     $str = https($url);
0378:     if ($str == FALSE)  return FALSE;
0379: 
0380:     $error   = preg_match_all('/\<li class\=\"error\"\>/ui', $str);
0381:     $warning = preg_match_all('/\<li class\=\"info warning\"\>/ui', $str);
0382: 
0383:     return $error + $warning;
0384: }

解説:https通信

Markup Validation Service は、https通信、かつ User-Agent を要求することから、組み込み関数  fopen 、 file_get_contents  ではバリデーションを実行できない。
そこで、ユーザー関数 https を用意した。「PHP で住所から緯度経度を求める」で作った [#http:http] 関数の https 対応版である。

0293: /**
0294:  * HTTPS通信を行う
0295:  * @param string $url "https://" から始まるURL
0296:  * @param string $method GET,POST,HEAD (省略時はGET)
0297:  * @param string $headers その他の任意のヘッダ (省略時は"")
0298:  * @param array  $post POST変数を格納した連想配列("変数名"=>"値") (省略時はNULL)
0299:  * @param string $cookie Cookie(利用するときは常に$method="POST") (省略時は"")
0300:  * @return string 取得したコンテンツ/FALSE 取得エラー
0301: */
0302: function https($url$method='GET', $headers='', $post=NULL$cookie='') {
0303:     if ($cookie != '')  $method = 'POST';
0304:     $URL = parse_url($url);
0305: 
0306:     $URL['query'] = isset($URL['query']) ? $URL['query'] : '';      //クエリ
0307:     $URL['port']  = isset($URL['port'])  ? $URL['port']  : 443;       //ポート番号
0308: 
0309:     //リクエストライン
0310:     $request  = $method . ' ' . $URL['path'] . '?' . $URL['query'] . " HTTP/1.1\r\n";
0311: 
0312:     //リクエストヘッダ
0313:     $request .= 'Host: ' . $URL['host'] . "\r\n";
0314:     $request .= 'User-Agent: PHP/' . phpversion() . "\r\n";
0315: 
0316:     //追加ヘッダ
0317:     $request .= $headers;
0318: 
0319:     //POSTの時
0320:     if (strtoupper($method) == 'POST') {
0321:         while (list($name$value) = each($post)) {
0322:             $POST[] = $name . '=' . $value;
0323:         }
0324:         $postdata = implode('&', $POST);
0325:         $request .= "Content-Type: application/x-www-form-urlencoded\r\n";
0326:         $request .= 'Content-Length: ' . strlen($postdata) . "\r\n";
0327:         if ($cookie != '')  $request .= "Cookie: $cookie\r\n";
0328:         $request .= "\r\n";
0329:         $request .= $postdata;
0330:     } else {
0331:         $request .= "\r\n";
0332:     }
0333: 
0334:     //接続
0335:     $fp = fsockopen('ssl://' . $URL['host'], $URL['port']);
0336:     //エラー処理
0337:     if (! $fp)   return FALSE;
0338: 
0339:     //リクエスト送信
0340:     fputs($fp$request);
0341: 
0342:     //応答データ受信
0343:     $response = FALSE;
0344:     $s = trim(fgets($fp));
0345:     if (preg_match('/HTTP\/1\.[0-9]+ 2[0-9]+/i', $s) != 0) {
0346:         $response = '';
0347:         while (! feof($fp)) {
0348:             $s = trim(fgets($fp));
0349:             if ($s == '')  break;
0350:         }
0351:         while (! feof($fp)) {
0352:             $s = fgets($fp);
0353:             $response .= $s;
0354:         }
0355:     }
0356:     fclose($fp);
0357: 
0358:     return $response;
0359: }

参考サイト

(この項おわり)
header