目次
サンプル・プログラムの実行例
サンプル・プログラム
解説:404エラーのチェック
376: /**
377: * リンク先の有無を表示する
378: * @param string $url チェックするURL
379: * @param int $flag_a <a>タグのハイパーリンク先をチェックする
380: * @param int $flag_img <img>タグの画像ソースをチェックする
381: * @param array $items チェック結果を格納する配列
382: * @param string $exclude チェック除外するURLパターン(正規表現;省略可)
383: */
384: function check404($url, $flag_a, $flag_img, &$items, $exclude='') {
385: $context = get_context();
386: $fp = @fopen($url, 'r', FALSE, $context);
387: if ($fp == FALSE) return $url . ' は存在しない.';
388:
389: $ln = 1;
390: while (! feof($fp)) {
391: $s = fgets($fp);
392: if ($flag_a) {
393: $pattern = "/<a.+?href\=\"([^\"]+)\"/i";
394: check_link($pattern, 1, $s, $ln, $url, $items, $exclude);
395: }
396: if ($flag_img) {
397: $pattern = "/<img.+?src\=\"([^\"]+)\"/i";
398: check_link($pattern, 1, $s, $ln, $url, $items, $exclude);
399: }
400: $ln++;
401: }
402: fclose($fp);
403:
404: return '';
405: }
調査するコンテンツ(ホームページ)のURLを引数にし、その中に出現する <a> タグおよび <img> タグのリンク先が fopen できるかどうかを調べる。404エラー(コンテンツが存在しない)なら、 fopen は失敗する。
まず、調査するコンテンツの存在をチェックする。無ければエラーメッセージを返す。
次に、調査するコンテンツを1行ずつ読み込み、<a> タグや <img> タグにパターンマッチする行があれば解析する。
<a> タグおよび <img> タグにパターンマッチするかどうかを調べるために正規表現を用いている。
実際のパターンマッチと存在性チェックは、ユーザー定義関数 check_link が行う。
なお、ブラウザ以外からのアクセスを拒否するページもあるので、 fopen 関数実行時に、あたかもブラウザからアクセスしたようにみせかけるために、HTTPヘッダを送信する。そのためのデータを fopen 関数に渡すのが、 stream_context_create 関数である。HTTPヘッダは配列 $header に用意しておく。
また、SSLサーバ証明書の有効期限切れなども無視するよう、検証要求をスキップするために、配列 $ssl に値をセットしてある。
解説:パターンに一致するものがあるかどうか
324: /**
325: * パターンに一致するものがあるかどうかチェックする
326: * @param string $pattern パターン(正規表現)
327: * @param int $n パターンに一致する場所(正規表現内の順序)
328: * @param string $str チェック対象文字列
329: * @param int $ln 解析中の行番号
330: * @param string $url 解析中のURL
331: * @param array $items チェック結果を格納する配列
332: * @param string $exclude チェック除外するURLパターン(正規表現;省略可)
333: */
334: function check_link($pattern, $l, $str, $ln, $url, &$items, $exclude='') {
335: $n = preg_match_all($pattern, $str, $arr);
336:
337: for ($i = 0; $i < $n; $i++) {
338: $target = reg_urls($arr[$l][$i], $url);
339: //除外パターンのチェック
340: if ($exclude !='') {
341: if (preg_match('/' . $exclude . '/ui', $target) > 0) continue;
342: }
343: //ファイル・オープン
344: $cfp = @fopen($target, 'r');
345: $arr2['ln'] = $ln;
346: $arr2['url'] = $target;
347: $arr2['res'] = 'あり';
348:
349: if (function_exists('curl_exec')) {
350: list($code, $size) = getHttpResponseCodeSize($target);
351: if (($code != NULL) && ($code != 200)) {
352: $arr2['res'] = 'なし';
353: $arr2['size'] = (-1);
354: //サイズ>0 または パス指定
355: } else if (($size > 0) || (preg_match('/\/$/ui', $target) > 0)) {
356: $arr2['res'] = 'あり';
357: $arr2['size'] = $size;
358: } else {
359: $arr2['res'] = 'ゼロ';
360: $arr2['size'] = 0;
361: }
362: } else if ($cfp == FALSE) {
363: $arr2['res'] = 'なし';
364: $arr2['size'] = (-1);
365: } else {
366: $size = contents_size($target);
367: if ($size <= 0) {
368: $arr2['res'] = 'ゼロ';
369: $arr2['size'] = 0;
370: }
371: }
372: $items[] = $arr2;
373: }
374: }
このとき、相対アドレス指定(http:で始まらない)があると fopen 関数が適用できないので、ユーザー定義関数 reg_urls によりURLの正規化を行う。
正規化したURLが、除外パターン $exclude にマッチしていたら、チェックをスキップする。
存在性チェックは fopen 関数を利用する。ネット上のコンテンツに対しては file_exists 関数は利用できないためだ。
なお、404エラーの場合、fopen 自身が失敗するので、@ を使ってエラー発生を抑えている。
ファイルが存在しなければ、fopen の戻り値は FALSE となる。
解説:HTTPレスポンスコード、サイズの取得
272: /**
273: * HTTPレスポンスコード、サイズを取得
274: * @param string $url 解析中のURL
275: * @return array(HTTPレスポンスコード:200 正常/NULL 取得失敗, サイズ)
276: */
277: function getHttpResponseCodeSize($url) {
278: //Cookie格納ファイル
279: $temp_cookie = tempnam(sys_get_temp_dir(), 'cookie');
280:
281: $header = NULL;
282: $options = array(
283: CURLOPT_RETURNTRANSFER => TRUE,
284: CURLOPT_HEADER => TRUE,
285: CURLOPT_FOLLOWLOCATION => TRUE,
286: CURLOPT_ENCODING => '',
287: CURLOPT_USERAGENT => USER_AGENT,
288: CURLOPT_SSL_VERIFYPEER => FALSE,
289: CURLOPT_SSL_VERIFYHOST => FALSE,
290: CURLOPT_AUTOREFERER => TRUE,
291: CURLOPT_CONNECTTIMEOUT => 120,
292: CURLOPT_TIMEOUT => 120,
293: CURLOPT_MAXREDIRS => 10,
294: );
295: $ch = curl_init($url);
296: curl_setopt_array($ch, $options);
297: curl_exec($ch);
298:
299: //cURLエラーチェック
300: if (curl_errno($ch)) {
301: curl_close($ch);
302: return array(NULL, NULL);
303: }
304: $header = curl_getinfo($ch);
305: curl_close($ch);
306:
307: //コンテンツ・サイズ
308: $size = $header['download_content_length'];
309: if (($header['http_code'] == 200) && ($size < 0)) {
310: $size = contents_size($url);
311: if ($size < 0) {
312: $options[CURLOPT_HEADER] = FALSE;
313: $ch = curl_init($url);
314: curl_setopt_array($ch, $options);
315: $res = curl_exec($ch);
316: curl_close($ch);
317: $size = strlen($res);
318: }
319: }
320:
321: return array($header['http_code'], $size);
322: }
同様の機能を持つ組み込み関数 http_response_code が用意されているが、PHP5以上でないと利用できない。
そこで、HTTPレスポンスコードの取得は、cURL関数を利用することにした。
組み込み関数 curl_getinfo を使って、HTTP CODE を受け取る。これが200であれば、正常受信できている。
次に、download_content_length を使って、HTTP通信で送られてくる Content-Length がゼロでないかどうかをチェックする。
ただし、HTTPサーバによっては Content-Length を送信しないことがあるので、その場合、自力でコンテンツ・サイズを求めるユーザー関数 contents_size を呼び出す。
解説:コンテンツ・サイズの取得
254: /**
255: * http上のファイルサイズ取得
256: * @param string $url 対象コンテンツ
257: * @param long ファイルサイズ
258: */
259: function contents_size($url) {
260: $data = '';
261: $context = get_context();
262: $fp = @fopen($url, 'r', FALSE, $context);
263: if (! $fp) return (-1);
264: while (! feof($fp)) {
265: $data .= fread($fp, 1024);
266: }
267: fclose($fp);
268:
269: return strlen($data);
270: }
解説:URLの正規化
URLの正規化処理は、ユーザー関数 reg_urls で行う。
180: /**
181: * URLを正規化する(相対指定を絶対指定に変換する)
182: * @param string $url 正規化するURL
183: * @param string $sour 読み込んだコンテンツのURL
184: * @return string 正規化したURL / FALSE(正規化に失敗)
185: */
186: function reg_urls($url, $sour) {
187: //httpで始まればスキップ
188: if (preg_match('/^http/', $url) != FALSE) return $url;
189:
190: $sour = rtrim($sour, '/'); //末尾のスラッシュは除く
191: //相対指定の場合
192: if (preg_match('/\:\/\//', $url) == FALSE) {
193: $regs = parse_url($sour);
194: $dirname = isset($regs['path']) ? dirname($regs['path']) : '';
195: if (preg_match('/^\//', $url) > 0) {
196: $url = ltrim($url, '/'); //冒頭のスラッシュは除く
197: $dirname = '';
198: }
199: $url = $regs['scheme'] . '://' . $regs['host'] . $dirname . '/' . $url;
200: }
201: //相対指定を絶対指定に変換する
202: $regs = parse_url($url);
203: $aa = preg_split("/[\/\\\]/", $regs['path']);
204: $an = count($aa);
205: $bb = array();
206: $bn = 0;
207: for ($i = 1; $i < $an; $i++) {
208: switch ($aa[$i]) {
209: case '.':
210: break;
211: case '..':
212: $bn--;
213: if ($bn < 0) return FALSE;
214: break;
215: default:
216: $bb[$bn] = $aa[$i];
217: $bn++;
218: break;
219: }
220: }
221: $ss = '';
222: for ($i = 0; $i < $bn; $i++) $ss = $ss . '/' . $bb[$i];
223:
224: return $regs['scheme'] . '://' . $regs['host'] . $ss;
225: }
解説:HTMLチェッカー
バリデーション結果をスクレイピングし、エラーとワーニングの数を返す。
407: /**
408: * 指定したURLをHTMLバリーデーションする.
409: * W3C Markup Validator Web Service APIを利用する.
410: * @param string $url バリデーションするURL
411: * @return array(エラー数,ワーニング数)/FALSE:APIコール失敗
412: */
413: function validateHTML($url) {
414: //W3C Markup Validator Web Service API
415: $webapi = 'https://validator.w3.org/nu/?';
416: //API引数
417: $params = array(
418: 'doc' => $url,
419: 'out' => 'json',
420: );
421: //HTTPヘッダ
422: $headers = array(
423: 'Content-Type: text/html; charset=UTF-8',
424: 'User-Agent: ' . USER_AGENT,
425: );
426:
427: //W3C Markup Validator Web Service APIを呼び出す.
428: $ch = curl_init();
429: curl_setopt($ch, CURLOPT_URL, $webapi . http_build_query($params));
430: curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
431: curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
432: curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
433: curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
434: curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
435: curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
436: curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
437:
438: $response = curl_exec($ch);
439: curl_close($ch);
440:
441: //エラーチェック
442: if ($response == FALSE) return FALSE;
443: //JSONデコード
444: $json = json_decode($response);
445: //エラーチェック
446: if (! $json->messages) return FALSE;
447:
448: //エラーおよびワーニングをカウントする.
449: $error = $warning = 0;
450: foreach ($json->messages as $message) {
451: //エラー
452: if (preg_match('/error/ui', (string)$message->type) > 0) {
453: $error++;
454: //ワーニング
455: } else if (preg_match('/info/ui', (string)$message->type) > 0) {
456: $warning++;
457: }
458: }
459:
460: return array($error, $warning);
461: }
参考サイト
- PHPでリンク切れを調べる(その2):ぱふぅ家のホームページ
- PHPで住所から緯度経度を求める:ぱふぅ家のホームページ
この特徴を利用して、ホームページ内に書かれているリンク先が存在しているかどうか、存在する場合はコンテンツのサイズがゼロでないかどうかをチェックするプログラムを作ってみることにする。
あわせて、W3Cが提供するWebサービスを利用し、HTMLが文法的に正しいかをチェックする。
(2022年1月10日)コンテンツ・サイズ取得の例外に対応
(2021年6月5日)get_context()追加,USER_AGENT 変更。
(2020年12月19日)HTTPヘッダ偽装を追加。
(2020年12月5日)PHP 8対応。