目次
サンプル・プログラムの実行例
存在チェックを実施したくないURLを、除外パターンとして正規表現で複数指定することができる。パターンはPHPの正規表現と同様、スラッシュで囲む /.../ こと。2つ以上のパターンを指定したいときは、改行で区切る。たとえば、"/e-soul/webtech/php02/" で始まるURLを除外したければ、
/https\:\/\/www\.pahoo\.org\/e\-soul\/webtech\/php02\//と入力する。
サンプル・プログラム
| check404.php | サンプル・プログラム本体。 |
| check404sub.py | Pythonサブプログラム |
| pahooInputData.php | データ入力に関わる関数群。 使い方は「数値入力とバリデーション」「文字入力とバリデーション」などを参照。include_path が通ったディレクトリに配置すること。 |
| pahooScraping.php | スクレイピングS処理に関わるクラス pahooScraping。 スクレイピング処理に関わるクラスの使い方は「PHPで作るRSSビューア」を参照。include_path が通ったディレクトリに配置すること。 |
| バージョン | 更新日 | 内容 |
|---|
| バージョン | 更新日 | 内容 |
|---|---|---|
| 2.0.1 | 2025/08/11 | getParam() bug-fix |
| 2.0.0 | 2025/08/11 | pahooLoadEnv() 追加 |
| 1.9.0 | 2025/07/26 | getParam() 引数に$trim追加 |
| 1.8.1 | 2025/03/15 | validRegexPattern() debug |
| 1.8.0 | 2024/11/12 | validRegexPattern() 追加 |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 1.2.1 | 2024/10/31 | __construct() 文字化け対策 |
| 1.2.0 | 2024/09/29 | getValueFistrXPath() 属性値でない指定に対応 |
| 1.1.0 | 2023/10/15 | getValueFistrXPath() 追加 |
| 1.0.1 | 2023/09/29 | __construct() bug-fix |
| 1.0.0 | 2023/09/18 | 初版 |
準備:初期値など
check404.php
59: // 初期値(START) ============================================================
60:
61: // 表示幅(ピクセル)
62: define('WIDTH', 600);
63:
64: // 偽装ユーザー
65: define('USER_AGENT', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36');
66:
67: // スクレイピング処理に関わるクラス:include_pathが通ったディレクトリに配置
68: require_once('pahooScraping.php');
69:
70: // デフォルトのスクレイピング対象クエリ
71: $text = [
72: "//a/@href",
73: "//img/@src",
74: "//link/@href",
75: "//script/@src",
76: "//meta[@name='og:url']/@content",
77: "//meta[@name='og:image']/@content",
78: "//meta[@name='twitter:image']/@content",
79: "//link[@rel='canonical']/@href",
80: ];
81: define('XPATH_LIST', $text);
82:
83: // デフォルトの除外パターン(チェック対象外URL)リスト
84: define('DEF_EXCLUDE', '');
85:
86: // Pythonサブ・プログラム【変更不可】
87: define('PYTHON_SUBPROGRAM', 'python check404sub.py ');
88:
89: // 初期値(END) ==============================================================
後述するユーザー関数の幾つかで、ブラウザからアクセスが来ているように見せかけるために、USER_AGENT の偽装値を設定しておく。
リンク先を検出するのに、XPath式を利用する。XPath式 およびユーザー定義クラス pahooScraping については「PHPでDOMDocumentを使ってスクレイピング」をご覧いただきたい。
下表に示すタグにあるリンク先をデフォルトでチェックするよう、定数 XPATH_LIST に代入してある。過不足があれば、適宜編集してほしい。
| XPath式 | 意味 |
|---|---|
| //a/@href | aタグのhref属性にあるリンク |
| //img/@src | imgタグのsec属性にあるリンク |
| //link/@href | linkタグのhref属性にあるリンク |
| //script/@src | scriptaタグのsrc属性にあるリンク |
| //meta[@name='og:url']/@content | metaタグのname属性がog:urlのものでcontent属性にあるリンク |
| //meta[@name='og:image']/@content | metaタグのname属性がog:imageのものでcontent属性にあるリンク |
| //meta[@name='twitter:image']/@content | metaタグのname属性がtwitter:imageのものでcontent属性にあるリンク |
| //link[@rel='canonical']/@href | linkタグのrel属性がcanonicalのものでhref属性にあるリンク |
準備:pahooInputData 関数群
また、各種クラウドサービスに登録したときに取得するアカウント情報、アプリケーションパスワードなどを登録した .pahooEnv ファイルから読み込む関数 pahooLoadEnv を備えている。こちらについては、「各種クラウド連携サービス(WebAPI)の登録方法」をご覧いただきたい。
準備:pahooScraping クラス
pahooScraping.php
13: class pahooScraping {
14: private $dom; //DOMDocument
15: private $xpath; //DOMXPath
16: private $language; //使用言語(初期値は日本語)
17: private $error; //エラー・メッセージ
18: private $errorTable; //エラー番号 => エラー・メッセージ 一覧
19:
20: /**
21: * コンストラクタ
22: * @param string $contents 解析対象テキスト(UTF-8)
23: * @return object /NULL:PHP8未満のため実行できない.
24: */
25: function __construct($contents) {
26: //PHPのバージョンをチェックする.
27: if (! $this->isphp8over()) {
28: $this->error = 111;
29: throw new Exception($this->getErrorMessage());
30: return NULL;
31: }
32: //プロパティの初期値を設定する.
33: try {
34: // Unicodeの範囲を指定
35: $map = [ 0x80, 0xFFFF, 0, 0xFFFF ];
36: // DOMの文字化け対策:UTF-8文字を数値エンティティに変換
37: $contents = mb_encode_numericentity($contents, $map, 'UTF-8');
38: // DOMDocument クラス
39: $this->dom = new DOMDocument();
40: libxml_use_internal_errors(TRUE);
41: @$this->dom->loadHTML($contents);
42: libxml_clear_errors();
43: $this->xpath = new DOMXPath($this->dom);
44: } catch (Exception $e) {
45: $this->error = 121;
46: throw new Exception($e->getMessage());
47: return FALSE;
48: }
49:
50: //エラー番号 => エラー・メッセージ 一覧
51: $this->errorTable = array(
52: 111 => array('PHP8以上が必要です', 'PHP8 or higher is required'),
53: 121 => array('DOMエラーです', 'DOM error'),
54: 999 => array('不明のエラー', 'unknown error'),
55: );
56: }
PHPのクラスについては「PHPでクラスを使ってテキストの読みやすさを調べる」を参照されたい。
準備:Python実行環境
Python実行環境のインストール方法については、「1.2 実行環境と開発環境/各バージョンのサポート期限 - Python入門」をご覧いただきたい。
解説:404エラーのチェック
check404.php
463: /**
464: * 指定コンテンツに含まれるリンク先の有無を配列に格納する
465: * @param string $sour コンテンツURL
466: * @param array $items チェック結果を格納する配列
467: * @param string $errmsg エラーメッセージを格納
468: * @param string $queryList チェック対象のXPath式の配列
469: * @param string $exclude チェック除外するURLパターン
470: * (正規表現の配列;省略時はNULL)
471: * @param bool $usePython Pythonサブプログラムを使うかどうか(省略時:使わない)
472: * @return bool TRUE:処理成功/FALSE:失敗(引数のエラーなど)
473: */
474: function check404($sour, &$items, &$errmsg, $queryList=XPATH_LIST, $excludes=NULL, $usePython=FALSE) {
475: $context = getContext($sour);
476: $contents = @file_get_contents($sour, FALSE, $context);
477: if ($contents == FALSE) {
478: $errmsg = $url . ' は存在しない.';
479: return FALSE;
480: }
481:
482: // スクレイピング開始
483: $pcr = new pahooScraping($contents);
484: $index = 0;
485:
486: // チェック対象のXPath式を1つずつ調べていく
487: foreach ($queryList as $query) {
488: $imageNode = $pcr->queryXPath($query);
489: // XPath式に合致するノードを1つずつ調べていく
490: foreach ($imageNode as $node) {
491: // URLチェックを行うフラグを立てる
492: $flagCheck = TRUE;
493: // ノードにあるURLを正規化する
494: $url = normalizeURLs((string)$node->nodeValue, $sour);
495: // それがURLでなければスキップ
496: if ($url == FALSE) {
497: continue;
498: }
499: // そのURLが除外URL配列に含まれていたら
500: // この後の処理をスキップするようフラグを降ろす
501: if ($excludes != NULL) {
502: foreach ($excludes as $exclude) {
503: if (preg_match($exclude, $url)) {
504: $flagCheck = FALSE;
505: break;
506: }
507: }
508: }
509: if ($flagCheck) {
510: // そのURLが存在するかどうか、サイズなどの情報を配列に格納
511: checkLink($url, (int)$node->getLineNo(), $items, $usePython);
512: $index++;
513: }
514: }
515: }
516: // スクレイピング終了
517: $pcr = NULL;
518:
519: // 行番号の小さい順に並び替える
520: usort($items, function ($a, $b) {
521: return $a['ln'] > $b['ln'] ? (+1) : (-01);
522: });
523: }
調査するコンテンツのURLを引数 $sour に渡し、与えられた XPath式(複数)を使って、そのコンテンツの中に含まれているリンク先URを取りだし、それがが存在するかどうかを、後述するユーザー関数 checkLink を使って調べる。
まず、調査対象の $sour が存在するかどうかを、組み込み関数 file_get_contents を使って検証する。このとき、第三引数に後述するユーザー関数 getContext で生成したストリームコンテキストを渡すことで、ブラウザからアクセスしているように見せかける。
次に、ユーザー定義クラス pahooScraping を使ってスクレイピングを開始する。
for文を使い、引数で渡された XPath式 の連想配列を1つずつ調べていく。XPath式 に合致するパターンがあれば、そのノードを配列 $imageNode に代入する。
for文を使い、ノード配列 $imageNode を1つずつ調べていく。
まず、URLチェックを行うフラグ変数 $flagCheck を立てる。
次に、XPath式を使って得られるリンク先は "http(s)://" ではじまらない相対リンクのこともあるので、後述するユーザー関数 normalizeURLs を使って "http(s)://" ではじまるように正規化してから変数 $url に代入する。
$url が引数として渡された除外URL配列 $excludes に合致したら、これ以降の処理をスキップするよう、フラグ変数 $flackCheck を降ろす。
最後に、フラグ変数 $flagCheck が立っていれば、得られたURLが存在するかどうかを、後述するユーザー関数 checkLink を使ってチェックし、結果を連想配列 $items に格納する。
以上の二重ループを抜けたら、結果を格納している連想配列 $items を、行番号の小さい順に並び替える。
解説:ストリームコンテキスト作成
check404.php
264: /**
265: * 偽装用のcontextを返す
266: * @param string $url 対象URL
267: * @param object コンテクスト
268: */
269: function getContext($url) {
270: // Refererを生成する
271: $parsedUrl = parse_url($url);
272: $referer = $parsedUrl['scheme'] . '://' . $parsedUrl['host'] . '/';
273:
274: // 偽装ヘッダ
275: $header = array(
276: 'Content-Type: text/html; charset=UTF-8',
277: 'User-Agent: ' . USER_AGENT,
278: 'Referer: ' . $referer
279: );
280: $ssl = array(
281: 'verify_peer' => FALSE,
282: 'verify_peer_name' => FALSE
283: );
284: $opts = array(
285: 'http' => array(
286: 'method' => 'GET',
287: 'header' => implode("\r\n", $header),
288: 'ssl' => $ssl,
289: 'follow_location' => 1, // リダイレクトを追跡する
290: 'max_redirects' => 10, // 最大リダイレクト回数
291: )
292: );
293:
294: return stream_context_create($opts);
295: }
本プログラムでは、ブラウザがサーバと通信している状態を再現して404エラーが起きるかどうかをチェックするために、これらの情報を組み込み関数 stream_context_create を使って用意する。用意する情報は、getContext に書かれているとおりだ。
Referer として、対象URLのホスト名を利用する。
これらはアクセス偽装と呼ばれる手法だが、別に悪い意味はないので、念のため。
解説:指定したURLが存在するかどうかを連想配列に格納
check404.php
387: /**
388: * 指定したURLが存在するかどうかを配列に格納する
389: * @param string $url チェックするURL(正規化済)
390: * @param int $ln 解析中の行番号
391: * @param array $items チェック結果を格納する配列
392: * @param bool $usePython Pythonサブプログラムを使うかどうか(省略時:使わない)
393: */
394: function checkLink($url, $ln, &$items, $usePython=FALSE) {
395: $arr = array();
396:
397: // URLが $items に含まれていたら行番号以外をコピーして終了
398: // ネットへの余計なアクセスを減らすため
399: foreach ($items as $item) {
400: if ($item['url'] == $url) {
401: $arr['ln'] = $ln;
402: $arr['url'] = $url;
403: $arr['res'] = $item['res'];
404: $arr['size'] = $item['size'];
405: $items[] = $arr;
406: return;
407: }
408: }
409:
410: // cURL関数を使って存在チェックする
411: if (function_exists('curl_exec')) {
412: list($code, $size) = getHttpResponseCodeSize($url);
413: if (($code != NULL) && ($code != 200)) {
414: $arr['res'] = 'なし';
415: $size = (-1);
416: // サイズ>0 または パス指定
417: } else if (($size > 0) || (preg_match('/\/$/ui', $url) > 0)) {
418: $arr['res'] = 'あり';
419: } else {
420: $arr['res'] = 'ゼロ';
421: $size = 0;
422: }
423:
424: // getContentsSize関数を使って存在チェックする
425: } else {
426: $size = getContentsSize($url);
427: if ($size < 0) {
428: $arr['res'] = 'なし';
429: } else if ($size == 0) {
430: $arr['res'] = 'ゼロ';
431: } else {
432: $arr['res'] = 'あり';
433: }
434: }
435:
436: // Pythonサブプログラムを使って存在チェックする
437: if ((($arr['res'] == 'なし') || ($arr['res'] == 'ゼロ')) && $usePython) {
438: // Pythonスクリプト実行
439: $cmd = PYTHON_SUBPROGRAM . ' ' . escapeshellarg($url);
440: $output = shell_exec($cmd);
441: // JSONを配列に変換
442: $data = json_decode($output, TRUE);
443: // 結果
444: $status = $data['status'];
445: $size = $data['size'];
446: if (($status != NULL) && ($status != 200)) {
447: $arr['res'] = 'なし';
448: $size = (-1);
449: } else if ($size == 0) {
450: $arr['res'] = 'ゼロ';
451: } else {
452: $arr['res'] = 'あり';
453: }
454: }
455:
456: // 配列に追加する
457: $arr['url'] = $url; // 対象URL
458: $arr['ln'] = $ln; // 対象URLがある行番号
459: $arr['size'] = $size; // コンテンツ・サイズ(バイト)
460: $items[] = $arr;
461: }
存在するかどうかをチェックするのに、3つの場合分けをしている。
- すでに連想配列 $items に登録されていれば、その内容をコピーする(行番号だけ解析中のものにする)。これは余計なネットワークアクセスを減らすための工夫だ。
- PHP処理系にcURL関数が実装されていれば、cURL関数を使って存在チェックする。
- それ以外の場合は、ユーザー関数 getContentsSize を使って存在チェックする。
- url‥‥対象URL
- ln‥‥対象URLがある行番号(表示用)
- size‥‥コンテンツ・サイズ(バイト)
- res‥‥あり/なし/ゼロ(表示用)
解説:HTTPレスポンスコード、サイズを取得
check404.php
315: /**
316: * HTTPレスポンスコード、サイズを取得
317: * @param string $url チェックするURL
318: * @return array(HTTPステータスコード:200 正常/NULL 取得失敗, サイズ)
319: */
320: function getHttpResponseCodeSize($url) {
321: // Refererを生成する
322: $parsedUrl = parse_url($url);
323: $referer = $parsedUrl['scheme'] . '://' . $parsedUrl['host'] . '/';
324:
325: // ヘッダー設定
326: $header = array(
327: 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
328: 'Accept-Language: ja,en-US;q=0.9,en;q=0.8',
329: 'Cache-Control: max-age=0',
330: 'User-Agent: ' . USER_AGENT,
331: 'Referer: ' . $referer,
332: );
333:
334: // cURL関数を使ってコンテンツをオープンする
335: $options = array(
336: CURLOPT_RETURNTRANSFER => TRUE, // ホスト名の検証を無効化
337: CURLOPT_HEADER => TRUE, // HTTPステータスコードを返す
338: CURLOPT_HTTPHEADER => $header, // ヘッダ情報
339: CURLOPT_SSL_VERIFYPEER => FALSE, // SSL証明書の検証を無効化
340: CURLOPT_SSL_VERIFYHOST => FALSE, // ホスト名の検証を無効化
341: CURLOPT_FOLLOWLOCATION => TRUE, // リダイレクト先に自動アクセス
342: CURLOPT_AUTOREFERER => TRUE, // リダイレクト先にReferer設定
343: CURLOPT_MAXREDIRS => 10, // リダイレクトを追跡する最大回数
344: CURLOPT_CONNECTTIMEOUT => 120, // 接続開始までの最大待ち時間
345: CURLOPT_TIMEOUT => 120, // リクエスト終了までの最大時間
346: CURLOPT_ENCODING => '', // ヘッダのエンコーディンング
347: );
348: $ch = curl_init($url);
349: curl_setopt_array($ch, $options);
350: curl_exec($ch);
351:
352: // cURLエラーチェック
353: if (curl_errno($ch)) {
354: // PHP8.5:非推奨関数
355: if (PHP_VERSION_ID < 80500) {
356: curl_close($ch);
357: }
358: return array(NULL, NULL);
359: }
360: $header = curl_getinfo($ch);
361: // PHP8.5:非推奨関数
362: if (PHP_VERSION_ID < 80500) {
363: curl_close($ch);
364: }
365:
366: // コンテンツ・サイズを求める
367: $size = $header['download_content_length'];
368: // HTTPステータスが200でサイズが求められなかった場合の処置
369: if (($header['http_code'] == 200) && ($size < 0)) {
370: $size = getContentsSize($url);
371: if ($size < 0) {
372: $options[CURLOPT_HEADER] = FALSE;
373: $ch = curl_init($url);
374: curl_setopt_array($ch, $options);
375: $res = curl_exec($ch);
376: // PHP8.5:非推奨関数
377: if (PHP_VERSION_ID < 80500) {
378: curl_close($ch);
379: }
380: $size = strlen($res);
381: }
382: }
383:
384: return array($header['http_code'], $size);
385: }
httpヘッダ は、配列 $http に格納しておく。getContext 関数同様、User-Agent や Referを偽装する。
cURL関数に設定するオプションは、コメントの通りである。CURLOPT_ENCODING は、コンテンツがgzipで圧縮されている場合に備えての指定である。
アクセスに成功したら、 curl_getinfo 関数で得られる download_content_length をコンテンツ・サイズとして返す。
もし、HTTPステータスコードが正常(200)なのに、この値が取得できなかった場合は、後述するユーザー定義関数 getContentsSize を使ってサイズを取得する。
解説:http上のファイルサイズ取得
check404.php
297: /**
298: * http上のファイルサイズ取得
299: * @param string $url 対象コンテンツ
300: * @param long ファイルサイズ
301: */
302: function getContentsSize($url) {
303: $data = '';
304: $context = getContext($url);
305: $fp = @fopen($url, 'r', FALSE, $context);
306: if (! $fp) return (-1);
307: while (! feof($fp)) {
308: $data .= fread($fp, 1024);
309: }
310: fclose($fp);
311:
312: return strlen($data);
313: }
ユーザー関数 getContext で生成したストリームコンテキストを使って、ブラウザからアクセスしているように見せかけて fopen 関数を実行し、[fread:phph_function] 関数を使ってコンテンツを読み込み、そのサイズを返す。
解説:URLを正規化する
まず、httpではじまるアドレスであれば絶対アドレスなので、即リターンする。
次に、基準となる絶対アドレス $sour と正規化するURLを結合し、変数 $url に再代入する。このとき、パス分離記号のスラッシュ "/" が二重にならないよう調整している。
この段階では、変数 $url の中に、相対パス指定を意味するドット . または .. が含まれている。続く処理で、スラッシュ "/" を目印にパスを分離し、ドット標記を消し込んでいく。
check404.php
199: /**
200: * URLを正規化する
201: * 相対アドレスを絶対アドレスに変換する
202: * @param string $url 正規化するURL
203: * @param string $sour 読み込んだコンテンツのURL(絶対アドレス)
204: * @return string 正規化したURL / FALSE(正規化に失敗;URLではない)
205: */
206: function normalizeURLs($url, $sour) {
207: // 前後の空白を除く
208: $url = trim($url);
209: // httpで始まればそのまま返す
210: if (preg_match('/^http/', $url) != FALSE) {
211: return $url;
212: }
213:
214: // 末尾のスラッシュを除く
215: $sour = rtrim($sour, '/');
216:
217: // 基準となる絶対アドレスと正規化するURLを結合する
218: if (preg_match('/\:\/\//', $url) == FALSE) {
219: $regs = parse_url($sour);
220: // URLとして分離できなければFALSE
221: if (!isset($regs['scheme']) || !isset($regs['host'])) {
222: return FALSE;
223: }
224: $dirname = isset($regs['path']) ? dirname($regs['path']) : '';
225: if (preg_match('/^\//', $url) > 0) {
226: $url = ltrim($url, '/'); // 冒頭のスラッシュは除く
227: $dirname = '';
228: }
229: $url = $regs['scheme'] . '://' . $regs['host'] . $dirname . '/' . $url;
230: }
231:
232: // ドット . または .. を置換する
233: $regs = parse_url($url);
234: // URLとして分離できなければFALSE
235: if (!isset($regs['scheme']) || !isset($regs['host'])) {
236: return FALSE;
237: }
238: $aa = preg_split("/[\/\\\]/", $regs['path']);
239: $an = count($aa);
240: $bb = array();
241: $bn = 0;
242: for ($i = 1; $i < $an; $i++) {
243: switch ($aa[$i]) {
244: case '.':
245: break;
246: case '..':
247: $bn--;
248: if ($bn < 0) return FALSE;
249: break;
250: default:
251: $bb[$bn] = $aa[$i];
252: $bn++;
253: break;
254: }
255: }
256: $ss = '';
257: for ($i = 0; $i < $bn; $i++) {
258: $ss = $ss . '/' . $bb[$i];
259: }
260:
261: return $regs['scheme'] . '://' . $regs['host'] . $ss;
262: }
解説:HTMLバリーデーション
| URL |
|---|
| https://validator.w3.org/nu/ |
check404.php
525: /**
526: * 指定したURLをHTMLバリーデーションする.
527: * W3C Markup Validator Web Service APIを利用する.
528: * @param string $url バリデーションするURL
529: * @return array(エラー数,警告数,情報数)/FALSE:APIコール失敗
530: */
531: function validateHTML($url) {
532: // W3C Markup Validator Web Service API
533: $webapi = 'https://validator.w3.org/nu/?';
534: // API引数
535: $params = array(
536: 'doc' => $url,
537: 'out' => 'json',
538: );
539: // HTTPヘッダ
540: $headers = array(
541: 'Content-Type: text/html; charset=UTF-8',
542: 'User-Agent: Mozilla/5.0 (PHP Validator)',
543: );
544:
545: // W3C Markup Validator Web Service APIを呼び出す.
546: $options = array(
547: CURLOPT_URL => $webapi . http_build_query($params),
548: CURLOPT_RETURNTRANSFER => TRUE, // ホスト名の検証を無効化
549: CURLOPT_HTTPHEADER => $headers, // ヘッダ情報
550: CURLOPT_SSL_VERIFYPEER => FALSE, // SSL証明書の検証を無効化
551: CURLOPT_SSL_VERIFYHOST => FALSE, // ホスト名の検証を無効化
552: CURLOPT_FOLLOWLOCATION => TRUE, // リダイレクト先に自動アクセス
553: CURLOPT_AUTOREFERER => TRUE, // リダイレクト先にReferer設定
554: CURLOPT_MAXREDIRS => 10, // リダイレクトを追跡する最大回数
555: CURLOPT_CONNECTTIMEOUT => 120, // 接続開始までの最大待ち時間
556: CURLOPT_TIMEOUT => 120, // リクエスト終了までの最大時間
557: );
558:
559: $ch = curl_init();
560: curl_setopt_array($ch, $options);
561: $response = curl_exec($ch);
562: // PHP8.5:非推奨関数
563: if (PHP_VERSION_ID < 80500) {
564: curl_close($ch);
565: }
566:
567: // エラーチェック
568: if ($response == FALSE) {
569: return FALSE;
570: }
571: // JSONデコード
572: $json = json_decode($response);
573:
574: // エラーチェック
575: if (! isset($json->messages)) {
576: return FALSE;
577: } else if (count($json->messages) == 0) {
578: return array(0, 0, 0);
579: }
580:
581: // エラー、警告、情報をカウントする.
582: $error = $warning = $info = 0;
583: foreach ($json->messages as $message) {
584: // エラー
585: if (preg_match('/error/ui', (string)$message->type) > 0) {
586: $error++;
587: // 警告
588: } else if (preg_match('/warning/ui', (string)$message->type) > 0) {
589: $warning++;
590: // 情報
591: } else if (preg_match('/info/ui', (string)$message->type) > 0) {
592: $info++;
593: }
594: }
595:
596: return array($error, $info, $warning);
597: }
解説:Pythonサブプログラム
import sys
import json
from curl_cffi import requests
# サブルーチン ==============================================================
def fetchStatusAndSize(url: str, timeout: int = 10):
"""指定URLのHTTPステータス・コードとコンテンツ・サイズを返す
Args:
url: コンテンツのURL
timeout: タイムアウト時間(秒)(省略時:10秒)
Returns:
tuple: HTTPステータス・コード, コンテンツ・サイズ(byte)
"""
try:
# まず HEAD を試す
r = requests.head(url, timeout=timeout, allow_redirects=True, impersonate="chrome")
status_code = r.status_code
content_length = r.headers.get("Content-Length")
# Content-Length があればそれを使う
if content_length is not None:
return status_code, int(content_length)
# 無ければ GET にフォールバック
r = requests.get(url, timeout=timeout, allow_redirects=True, impersonate="chrome")
return r.status_code, len(r.content)
except Exception:
return None, None
# メイン・プログラム ========================================================
if __name__ == "__main__":
# 引数があればそれを使う
if len(sys.argv) > 1:
url = sys.argv[1]
else:
# なければキーボード入力
url = input("URLを入力してください: ").strip()
# 空入力対策
if not url:
print(json.dumps({"status": None, "size": None}))
sys.exit(0)
status, size = fetchStatusAndSize(url)
result = {
"status": status,
"size": size
}
print(json.dumps(result))
PHP関数では「なし」となってしまう URL に対して、Pythonサブプログラム "check404sub.py" を実行する。
"check404sub.py" は、コマンドラインに URL を与えた場合、その URL にアクセスし、HTTPステータス・コードとコンテンツ・サイズを JSON形式で返す。コマンドラインにURLがないとき(単独で "check404sub.py" を実行することを想定)は、キーボードから URL を入力する。
URLから HTTPステータス・コードとコンテンツ・サイズを求めるのがユーザー定義関数 fetchStatusAndSize である。
まず、request.head を実行し、Content-Length を取得する。引数 impersonate="chrome" を指定することで、ユーザーエージェントだけでなく、通信レベルでブラウザ Chrome を再現する。
解説:メイン・プログラム
なお、パラメータ取得時に、対象URLがURL文字列であるかどうか、"pahooInputData.php" にある validURL関数を使ってバリデーションする。
また、除外パターンを行分割して配列 $excludes に格納し、各要素に対して正規表現であるかどうかのバリデーションを行う。バリデーションを行うのは、以下に示すユーザー定義関数 validRegexPattern である。
check404.php
724: // メイン・プログラム ======================================================
725:
726: // パラメータを取得する
727: $items = array();
728: $errmsg = '';
729: $htmlerror = $htmlwarning = $htmlinfo = '?';
730:
731: $flag = getParam('flag', FALSE, '');
732: $flag = ($flag == '1') ? FALSE : TRUE;
733: $checkhtml = getParam('checkhtml', FALSE, '');
734: $checkhtml = ($checkhtml == '1') ? TRUE : FALSE;
735: $usePython = getParam('python', FALSE, '');
736: $usePython = ($usePython == '1') ? TRUE : FALSE;
737:
738: // URLのバリデーション
739: $url = getParam('url', FALSE, '');
740: if (isButton('exec') && (validURL($url, $errmsg) == FALSE)) {
741: $errmsg = $url . ' はURLではありません';
742: }
743:
744: // 除外パターンのバリデーション
745: $excludeList = NULL;
746: $exclude = getParam('exclude', FALSE, DEF_EXCLUDE);
747: if (isButton('exec') && ($exclude != '')) {
748: $excludes = explode("\n", $exclude);
749: foreach ($excludes as $key=>$val) {
750: $val = trim($val);
751: if ($val != '') {
752: if (validRegexPattern($val)) {
753: $excludeList[$key] = $val;
754: } else {
755: $errmsg = $val . ' は正規表現ではありません';
756: break;
757: }
758: }
759: }
760: if (count($excludeList) == 0) {
761: $excludeList = NULL;
762: }
763: }
764:
765: // 404チェックを実行する
766: if ($errmsg == '') {
767: $errmsg= ($url != '') ? check404($url, $items, $errmsg, XPATH_LIST, $excludeList, $usePython) : '';
768: // HTMLバリデーションを行う。
769: if (($url != '') && ($errmsg == '') && $checkhtml) {
770: $arr = validateHTML($url);
771: if ($arr != FALSE) {
772: $htmlerror = $arr[0];
773: $htmlinfo = $arr[1];
774: $htmlwarning = $arr[2];
775: }
776: }
777: }
778:
779: // 表示HTMLを作成する
780: $HtmlBody = makeCommonBody($url, $items, $htmlerror, $htmlwarning, $htmlinfo, $errmsg, $flag, $checkhtml, $exclude, $usePython);
781:
782: // 画面に表示する
783: echo $HtmlHeader;
784: echo $HtmlBody;
785: echo $HtmlFooter;
786:
787: /*
pahooInputData.php
535: /**
536: * 正規表現のバリデーションを行う.
537: * @参考URL https://www.pahoo.org/e-soul/webtech/php06/php05-23-01.shtm
538: * @param string $reg 正規表現;スラッシュ /.../ で囲むこと.
539: * @param string $text マッチングさせる文字列;デフォルトでは空文字
540: * @return bool TRUE:正規表現/FALSE:正規表現ではない
541: */
542: function validRegexPattern($reg, $text='') {
543: $res = @preg_match($reg, $text);
544: return ($res === FALSE) ? FALSE : TRUE;
545: }
参考サイト
- PHPでリンク切れを調べる(その2):ぱふぅ家のホームページ

この特徴を利用して、ホームページ内に書かれているリンク先が存在しているかどうか、存在する場合はコンテンツのサイズがゼロでないかどうかをチェックするプログラムを作ってみることにする。ボット対策を行っているサイトに対しては、Pythonサブプログラムを使うことで存在チェックができるようにした。
あわせて、W3Cが提供するWebサービスを利用し、HTMLが文法的に正しいかをチェックする。
(2026年8月23日)Pythonサブプログラムによるチェック範囲拡大
(2026年4月26日)Pythonサブプログラム "check404sub.py" 追加, validateHTML - User-Agent変更
(2025年12月28日)PHP8.5対応:curl_closeを使わない