目次
サンプル・プログラムの実行例
存在チェックを実施したくないURLを、除外パターンとして正規表現で複数指定することができる。パターンはPHPの正規表現と同様、スラッシュで囲む /.../ こと。2つ以上のパターンを指定したいときは、改行で区切る。たとえば、"/e-soul/webtech/php02/" で始まるURLを除外したければ、
/https\:\/\/www\.pahoo\.org\/e\-soul\/webtech\/php02\//と入力する。
サンプル・プログラム
check404.php | サンプル・プログラム本体。 |
pahooInputData.php | データ入力に関わる関数群。 使い方は「数値入力とバリデーション」「文字入力とバリデーション」などを参照。include_path が通ったディレクトリに配置すること。 |
pahooScraping.php | スクレイピングS処理に関わるクラス pahooScraping。 スクレイピング処理に関わるクラスの使い方は「PHPで作るRSSビューア」を参照。include_path が通ったディレクトリに配置すること。 |
バージョン | 更新日 | 内容 |
---|---|---|
5.0.0 | 2024/11/15 | 全面改訂:XPath式導入, アクセス回数低減 |
4.6.0 | 2023/03/21 | htmlChecker→validateHTMLに変更 |
4.5 | 2022/01/10 | コンテンツ・サイズ取得の例外に対応 |
4.41 | 2021/06/12 | スタイルシートへのリンク変更 |
4.4 | 2021/06/05 | getContext()追加,USER_AGENT 変更 |
バージョン | 更新日 | 内容 |
---|---|---|
1.8.0 | 2024/11/12 | validRegexPattern() 追加 |
1.7.0 | 2024/10/09 | validURL() validEmail() 追加 |
1.6.0 | 2024/10/07 | isButton() -- buttonタグに対応 |
1.5.0 | 2024/01/28 | exitIfExceedVersion() 追加 |
1.4.2 | 2024/01/28 | exitIfLessVersion() メッセージ修正 |
バージョン | 更新日 | 内容 |
---|---|---|
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: define('WIDTH', 600);
62:
63: // 偽装ユーザー
64: 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');
65:
66: // スクレイピング処理に関わるクラス:include_pathが通ったディレクトリに配置
67: require_once('pahooScraping.php');
68:
69: // デフォルトのスクレイピング対象クエリ
70: $text = [
71: "//a/@href",
72: "//img/@src",
73: "//link/@href",
74: "//script/@src",
75: "//meta[@name='og:url']/@content",
76: "//meta[@name='og:image']/@content",
77: "//meta[@name='twitter:image']/@content",
78: "//link[@rel='canonical']/@href",
79: ];
80: define('XPATH_LIST', $text);
81:
82: // デフォルトの除外パターン(チェック対象外URL)リスト
83: define('DEF_EXCLUDE', '');
84:
85: // 初期値(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属性にあるリンク |
解説:404エラーのチェック
check404.php
396: /**
397: * 指定コンテンツに含まれるリンク先の有無を配列に格納する
398: * @param string $sour コンテンツURL
399: * @param array $items チェック結果を格納する配列
400: * @param string $errmsg エラーメッセージを格納
401: * @param string $queryList チェック対象のXPath式の配列
402: * @param string $exclude チェック除外するURLパターン
403: * (正規表現の配列;省略時はNULL)
404: * @return bool TRUE:処理成功/FALSE:失敗(引数のエラーなど)
405: */
406: function check404($sour, &$items, &$errmsg, $queryList=XPATH_LIST, $excludes=NULL) {
407: $context = getContext();
408: $contents = @file_get_contents($sour, FALSE, $context);
409: if ($contents == FALSE) {
410: $errmsg = $url . ' は存在しない.';
411: return FALSE;
412: }
413:
414: // スクレイピング開始
415: $pcr = new pahooScraping($contents);
416: $index = 0;
417:
418: // チェック対象のXPath式を1つずつ調べていく
419: foreach ($queryList as $query) {
420: $imageNode = $pcr->queryXPath($query);
421: // XPath式に合致するノードを1つずつ調べていく
422: foreach ($imageNode as $node) {
423: // URLチェックを行うフラグを立てる
424: $flagCheck = TRUE;
425: // ノードにあるURLを正規化する
426: $url = normalizeURLs((string)$node->nodeValue, $sour);
427: // それがURLでなければスキップ
428: if ($url == FALSE) {
429: continue;
430: }
431: // そのURLが除外URL配列に含まれていたら
432: // この後の処理をスキップするようフラグを降ろす
433: if ($excludes != NULL) {
434: foreach ($excludes as $exclude) {
435: if (preg_match($exclude, $url)) {
436: $flagCheck = FALSE;
437: break;
438: }
439: }
440: }
441: if ($flagCheck) {
442: // そのURLが存在するかどうか、サイズなどの情報を配列に格納
443: checkLink($url, (int)$node->getLineNo(), $items);
444: $index++;
445: }
446: }
447: }
448: // スクレイピング終了
449: $pcr = NULL;
450:
451: // 行番号の小さい順に並び替える
452: usort($items, function ($a, $b) {
453: return $a['ln'] > $b['ln'] ? (+1) : (-01);
454: });
455: }
調査するコンテンツの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
240: /**
241: * 偽装用のcontextを返す
242: * @param なし
243: * @param object コンテクスト
244: */
245: function getContext() {
246: //偽装ヘッダ
247: $header = array(
248: 'Content-Type: text/html; charset=UTF-8',
249: 'User-Agent: ' . USER_AGENT,
250: 'Referer: https://www.yahoo.co.jp/'
251: );
252: $ssl = array(
253: 'verify_peer' => FALSE,
254: 'verify_peer_name' => FALSE
255: );
256: $opts = array(
257: 'http' => array(
258: 'method' => 'GET',
259: 'header' => implode("\r\n", $header),
260: 'ssl' => $ssl
261: )
262: );
263:
264: return stream_context_create($opts);
265: }
本プログラムでは、ブラウザがサーバと通信している状態を再現して404エラーが起きるかどうかをチェックするために、これらの情報を組み込み関数 stream_context_creat を使って用意する。用意する情報は、getContext に書かれているとおりだ。
これはアクセス偽装と呼ばれる手法だが、別に悪い意味はないので、念のため。
解説:指定したURLが存在するかどうかを連想配列に格納
check404.php
341: /**
342: * 指定したURLが存在するかどうかを配列に格納する
343: * @param string $url チェックするURL(正規化済)
344: * @param int $ln 解析中の行番号
345: * @param array $items チェック結果を格納する配列
346: */
347: function checkLink($url, $ln, &$items) {
348: $arr = array();
349:
350: // URLが $items に含まれていたら行番号以外をコピーして終了
351: // ネットへの余計なアクセスを減らすため
352: foreach ($items as $item) {
353: if ($item['url'] == $url) {
354: $arr['ln'] = $ln;
355: $arr['url'] = $url;
356: $arr['res'] = $item['res'];
357: $arr['size'] = $item['size'];
358: $items[] = $arr;
359: return;
360: }
361: }
362:
363: // cURL関数を使って存在チェックする
364: if (function_exists('curl_exec')) {
365: list($code, $size) = getHttpResponseCodeSize($url);
366: if (($code != NULL) && ($code != 200)) {
367: $arr['res'] = 'なし';
368: $size = (-1);
369: //サイズ>0 または パス指定
370: } else if (($size > 0) || (preg_match('/\/$/ui', $url) > 0)) {
371: $arr['res'] = 'あり';
372: } else {
373: $arr['res'] = 'ゼロ';
374: $size = 0;
375: }
376:
377: // getContentsSize関数を使って存在チェックする
378: } else {
379: $size = getContentsSize($url);
380: if ($size < 0) {
381: $arr['res'] = 'なし';
382: } else if ($size == 0) {
383: $arr['res'] = 'ゼロ';
384: } else {
385: $arr['res'] = 'あり';
386: }
387: }
388:
389: // 配列に追加する
390: $arr['url'] = $url; // 対象URL
391: $arr['ln'] = $ln; // 対象URLがある行番号
392: $arr['size'] = $size; // コンテンツ・サイズ(バイト)
393: $items[] = $arr;
394: }
存在するかどうかをチェックするのに、3つの場合分けをしている。
- すでに連想配列 $items に登録されていれば、その内容をコピーする(行番号だけ解析中のものにする)。これは余計なネットワークアクセスを減らすための工夫だ。
- PHP処理系にcURL関数が実装されていれば、cURL関数を使って存在チェックする。
- それ以外の場合は、ユーザー関数 getContentsSize を使って存在チェックする。
- url‥‥対象URL
- ln‥‥対象URLがある行番号(表示用)
- size‥‥コンテンツ・サイズ(バイト)
- res‥‥あり/なし/ゼロ(表示用)
解説:HTTPレスポンスコード、サイズを取得
check404.php
285: /**
286: * HTTPレスポンスコード、サイズを取得
287: * @param string $url チェックするURL
288: * @return array(HTTPステータスコード:200 正常/NULL 取得失敗, サイズ)
289: */
290: function getHttpResponseCodeSize($url) {
291: // ヘッダー設定
292: $header = array(
293: 'Content-Type: text/html; charset=UTF-8',
294: 'User-Agent: ' . USER_AGENT,
295: 'Referer: https://www.yahoo.co.jp/'
296: );
297:
298: // cURL関数を使ってコンテンツをオープンする
299: $options = array(
300: CURLOPT_RETURNTRANSFER => TRUE, // ホスト名の検証を無効化
301: CURLOPT_HEADER => TRUE, // HTTPステータスコードを返す
302: CURLOPT_HTTPHEADER => $header, // ヘッダ情報
303: CURLOPT_SSL_VERIFYPEER => FALSE, // SSL証明書の検証を無効化
304: CURLOPT_SSL_VERIFYHOST => FALSE, // ホスト名の検証を無効化
305: CURLOPT_FOLLOWLOCATION => TRUE, // リダイレクト先に自動アクセス
306: CURLOPT_AUTOREFERER => TRUE, // リダイレクト先にReferer設定
307: CURLOPT_MAXREDIRS => 10, // リダイレクトを追跡する最大回数
308: CURLOPT_CONNECTTIMEOUT => 120, // 接続開始までの最大待ち時間
309: CURLOPT_TIMEOUT => 120, // リクエスト終了までの最大時間
310: );
311: $ch = curl_init($url);
312: curl_setopt_array($ch, $options);
313: curl_exec($ch);
314:
315: // cURLエラーチェック
316: if (curl_errno($ch)) {
317: curl_close($ch);
318: return array(NULL, NULL);
319: }
320: $header = curl_getinfo($ch);
321: curl_close($ch);
322:
323: // コンテンツ・サイズを求める
324: $size = $header['download_content_length'];
325: // HTTPステータスが200でサイズが求められなかった場合の処置
326: if (($header['http_code'] == 200) && ($size < 0)) {
327: $size = getContentsSize($url);
328: if ($size < 0) {
329: $options[CURLOPT_HEADER] = FALSE;
330: $ch = curl_init($url);
331: curl_setopt_array($ch, $options);
332: $res = curl_exec($ch);
333: curl_close($ch);
334: $size = strlen($res);
335: }
336: }
337:
338: return array($header['http_code'], $size);
339: }
cURL関数に設定するオプションは、コメントの通りである。
getContext 関数同様の偽装情報を変数 $header に格納し、HTTPヘッダとしてサーバに渡す。
アクセスに成功したら、 curl_getinfo 関数で得られる download_content_length をコンテンツ・サイズとして返す。
もし、HTTPステータスコードが正常(200)なのに、この値が取得できなかった場合は、後述するユーザー定義関数 getContentsSize を使ってサイズを取得する。
解説:http上のファイルサイズ取得
check404.php
267: /**
268: * http上のファイルサイズ取得
269: * @param string $url 対象コンテンツ
270: * @param long ファイルサイズ
271: */
272: function getContentsSize($url) {
273: $data = '';
274: $context = getContext();
275: $fp = @fopen($url, 'r', FALSE, $context);
276: if (! $fp) return (-1);
277: while (! feof($fp)) {
278: $data .= fread($fp, 1024);
279: }
280: fclose($fp);
281:
282: return strlen($data);
283: }
ユーザー関数 getContext で生成したストリームコンテキストを使って、ブラウザからアクセスしているように見せかけて fopen 関数を実行し、[fread:phph_function] 関数を使ってコンテンツを読み込み、そのサイズを返す。
解説:URLを正規化する
まず、httpではじまるアドレスであれば絶対アドレスなので、即リターンする。
次に、基準となる絶対アドレス $sour と正規化するURLを結合し、変数 $url に再代入する。このとき、パス分離記号のスラッシュ "/" が二重にならないよう調整している。
この段階では、変数 $url の中に、相対パス指定を意味するドット . または .. が含まれている。続く処理で、スラッシュ "/" を目印にパスを分離し、ドット標記を消し込んでいく。
check404.php
175: /**
176: * URLを正規化する
177: * 相対アドレスを絶対アドレスに変換する
178: * @param string $url 正規化するURL
179: * @param string $sour 読み込んだコンテンツのURL(絶対アドレス)
180: * @return string 正規化したURL / FALSE(正規化に失敗;URLではない)
181: */
182: function normalizeURLs($url, $sour) {
183: // 前後の空白を除く
184: $url = trim($url);
185: // httpで始まればそのまま返す
186: if (preg_match('/^http/', $url) != FALSE) {
187: return $url;
188: }
189:
190: // 末尾のスラッシュを除く
191: $sour = rtrim($sour, '/');
192:
193: // 基準となる絶対アドレスと正規化するURLを結合する
194: if (preg_match('/\:\/\//', $url) == FALSE) {
195: $regs = parse_url($sour);
196: // URLとして分離できなければFALSE
197: if (!isset($regs['scheme']) || !isset($regs['host'])) {
198: return FALSE;
199: }
200: $dirname = isset($regs['path']) ? dirname($regs['path']) : '';
201: if (preg_match('/^\//', $url) > 0) {
202: $url = ltrim($url, '/'); // 冒頭のスラッシュは除く
203: $dirname = '';
204: }
205: $url = $regs['scheme'] . '://' . $regs['host'] . $dirname . '/' . $url;
206: }
207:
208: // ドット . または .. を置換する
209: $regs = parse_url($url);
210: // URLとして分離できなければFALSE
211: if (!isset($regs['scheme']) || !isset($regs['host'])) {
212: return FALSE;
213: }
214: $aa = preg_split("/[\/\\\]/", $regs['path']);
215: $an = count($aa);
216: $bb = array();
217: $bn = 0;
218: for ($i = 1; $i < $an; $i++) {
219: switch ($aa[$i]) {
220: case '.':
221: break;
222: case '..':
223: $bn--;
224: if ($bn < 0) return FALSE;
225: break;
226: default:
227: $bb[$bn] = $aa[$i];
228: $bn++;
229: break;
230: }
231: }
232: $ss = '';
233: for ($i = 0; $i < $bn; $i++) {
234: $ss = $ss . '/' . $bb[$i];
235: }
236:
237: return $regs['scheme'] . '://' . $regs['host'] . $ss;
238: }
解説:HTMLバリーデーション
URL |
---|
https://validator.w3.org/nu/ |
check404.php
457: /**
458: * 指定したURLをHTMLバリーデーションする.
459: * W3C Markup Validator Web Service APIを利用する.
460: * @param string $url バリデーションするURL
461: * @return array(エラー数,警告数,情報数)/FALSE:APIコール失敗
462: */
463: function validateHTML($url) {
464: //W3C Markup Validator Web Service API
465: $webapi = 'https://validator.w3.org/nu/?';
466: //API引数
467: $params = array(
468: 'doc' => $url,
469: 'out' => 'json',
470: );
471: //HTTPヘッダ
472: $headers = array(
473: 'Content-Type: text/html; charset=UTF-8',
474: 'User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36',
475: );
476:
477: //W3C Markup Validator Web Service APIを呼び出す.
478: $options = array(
479: CURLOPT_URL => $webapi . http_build_query($params),
480: CURLOPT_RETURNTRANSFER => TRUE, // ホスト名の検証を無効化
481: CURLOPT_HTTPHEADER => $headers, // ヘッダ情報
482: CURLOPT_SSL_VERIFYPEER => FALSE, // SSL証明書の検証を無効化
483: CURLOPT_SSL_VERIFYHOST => FALSE, // ホスト名の検証を無効化
484: CURLOPT_FOLLOWLOCATION => TRUE, // リダイレクト先に自動アクセス
485: CURLOPT_AUTOREFERER => TRUE, // リダイレクト先にReferer設定
486: CURLOPT_MAXREDIRS => 10, // リダイレクトを追跡する最大回数
487: CURLOPT_CONNECTTIMEOUT => 120, // 接続開始までの最大待ち時間
488: CURLOPT_TIMEOUT => 120, // リクエスト終了までの最大時間
489: );
490: $ch = curl_init();
491: curl_setopt_array($ch, $options);
492: $response = curl_exec($ch);
493: curl_close($ch);
494:
495: // エラーチェック
496: if ($response == FALSE) {
497: return FALSE;
498: }
499: // JSONデコード
500: $json = json_decode($response);
501:
502: // エラーチェック
503: if (! isset($json->messages)) {
504: return FALSE;
505: } else if (count($json->messages) == 0) {
506: return array(0, 0, 0);
507: }
508:
509: // エラー、警告、情報をカウントする.
510: $error = $warning = $info = 0;
511: foreach ($json->messages as $message) {
512: // エラー
513: if (preg_match('/error/ui', (string)$message->type) > 0) {
514: $error++;
515: // 警告
516: } else if (preg_match('/warning/ui', (string)$message->type) > 0) {
517: $warning++;
518: // 情報
519: } else if (preg_match('/info/ui', (string)$message->type) > 0) {
520: $info++;
521: }
522: }
523:
524: return array($error, $info, $warning);
525: }
解説:メイン・プログラム
なお、パラメータ取得時に、対象URLがURL文字列であるかどうか、"pahooInputData.php" にある validURL関数を使ってバリデーションする。
また、除外パターンを行分割して配列 $excludes に格納し、各要素に対して正規表現であるかどうかのバリデーションを行う。バリデーションを行うのは、以下に示すユーザー定義関数 validRegexPattern である。
check404.php
648: // メイン・プログラム ======================================================
649: // パラメータを取得する
650: $items = array();
651: $errmsg = '';
652: $htmlerror = $htmlwarning = $htmlinfo = '?';
653:
654: $flag = getParam('flag', FALSE, '');
655: $flag = ($flag == '1') ? FALSE : TRUE;
656: $checkhtml = getParam('checkhtml', FALSE, '');
657: $checkhtml = ($checkhtml == '1') ? TRUE : FALSE;
658: // URLのバリデーション
659: $url = getParam('url', FALSE, '');
660: if (isButton('exec') && (validURL($url, $errmsg) == FALSE)) {
661: $errmsg = $url . ' はURLではありません';
662: }
663:
664: // 除外パターンのバリデーション
665: $excludeList = NULL;
666: $exclude = getParam('exclude', FALSE, DEF_EXCLUDE);
667: if (isButton('exec') && ($exclude != '')) {
668: $excludes = explode("\n", $exclude);
669: foreach ($excludes as $key=>$val) {
670: $val = trim($val);
671: if ($val != '') {
672: if (validRegexPattern($val)) {
673: $excludeList[$key] = $val;
674: } else {
675: $errmsg = $val . ' は正規表現ではありません';
676: break;
677: }
678: }
679: }
680: if (count($excludeList) == 0) {
681: $excludeList = NULL;
682: }
683: }
684:
685: // 404チェックを実行する
686: if ($errmsg == '') {
687: $errmsg= ($url != '') ? check404($url, $items, $errmsg, XPATH_LIST, $excludeList) : '';
688: // HTMLバリデーションを行う。
689: if (($url != '') && ($errmsg == '') && $checkhtml) {
690: $arr = validateHTML($url);
691: if ($arr != FALSE) {
692: $htmlerror = $arr[0];
693: $htmlinfo = $arr[1];
694: $htmlwarning = $arr[2];
695: }
696: }
697: }
698:
699: // 表示HTMLを作成する
700: $HtmlBody = makeCommonBody($url, $items, $htmlerror, $htmlwarning, $htmlinfo, $errmsg, $flag, $checkhtml, $exclude);
701:
702: // 画面に表示する
703: echo $HtmlHeader;
704: echo $HtmlBody;
705: echo $HtmlFooter;
706:
707: /*
pahooInputData.php
480: /**
481: * 正規表現のバリデーションを行う.
482: * スラッシュ /.../ で囲まれているかどうかを照合する
483: * @参考URL https://www.pahoo.org/e-soul/webtech/php02/php02-16-01.shtm
484: * @param string $reg 正規表現
485: * @return bool TRUE:正規表現/FALSE:正規表現ではない
486: */
487: function validRegexPattern($reg) {
488: $pattern = '/^\/.*[^\\\\]\/$/';
489: return preg_match($pattern, $reg);
490: }
参考サイト
- PHPでリンク切れを調べる(その2):ぱふぅ家のホームページ
この特徴を利用して、ホームページ内に書かれているリンク先が存在しているかどうか、存在する場合はコンテンツのサイズがゼロでないかどうかをチェックするプログラムを作ってみることにする。
あわせて、W3Cが提供するWebサービスを利用し、HTMLが文法的に正しいかをチェックする。
(2024年11月15日)プログラムを全面改訂:XPath式導入, アクセス回数低減