Help us understand the problem. What is going on with this article?

[PHP] WEBページ内に存在する外部へのリンクを全て取得する

More than 5 years have passed since last update.

前書き

あるWEBページから外部に飛ばされているリンクを全件取得して、プログラムからサイトマップ全体像を掴みたいとき、ありますよね。既にいいプログラムがあるかもしれないので、ひとまずググってみました。

ss (2014-01-19 at 04.16.54).png

[php] WEBページ内のリンク先URLをすべて取得する

ソースコードを覗いてみると・・・ 「汚い。」 (即答)
しかもこのコードを書いたブログ管理人さんも

コードについては、何をしてるのかさっぱりわかりません!
まー、とりあえず、これであれをそーすれば、自動でダウンロードするあれが作れるはず!

と説明を放棄されているし、このコードのもとになったサイトもドメインの有効期限が切れているという酷い有様。他にいいサンプルコードも見つからなかったので、自分で一から書いてみることにしました。

UrlScraper クラス

クロージャは使っていないので PHP5.2 ぐらいなら多分動きます。コメントはめんどくさいので入れてません

ライセンスは CC0 とします。バグ修正・改良して報告していただけるのもOK、パクって自分が書いたことにしてもOK!

コード

class UrlScraper {

    public static function parseLinks($html, $base_href = '') {
        $array = array();
        $p = self::parseUrl($base_href);
        $p['path'] = self::normalize($p['path']);
        $regex = '@<a[^>]*?(?<!\.)href="([^"]*+)"[^>]*+>(.*?)</a>@si';
        if (!preg_match_all($regex, $html, $matches, PREG_SET_ORDER)) {
            return $array;
        }
        foreach ($matches as $match) {
            try {
                $href = self::join($p, self::parseUrl($match[1]));
                $text = trim(strip_tags($match[2]));
                if (
                    $text !== '' &&
                    preg_match_all('@(?<!\.)alt="([^"]++)"@', $match[2], $matches)
                ) {
                    $text = implode(' - ', $matches[1]);
                }
                $set = array_map(array(__CLASS__, 'decode'), compact('href', 'text'));
                $array[serialize($set)] = $set;
            } catch (Exception $e) { }
        }
        return array_values($array);
    }

    private static function parseUrl($url) {
        if (!$p = parse_url($url) or self::isJavaScript($p['path'])) {
            throw new InvalidArgumentException('Invalid URL');
        }
        $host = '';
        if (isset($p['host'])) {
            $host .= $p['scheme'] . '://' . $p['host'];
            if (isset($p['port'])) {
                $host .= ':' . $p['port'];
            }
        }
        $path     = isset($p['path'])     ? $p['path']           : '';
        $query    = isset($p['query'])    ? '?' . $p['query']    : '';
        $fragment = isset($p['fragment']) ? '#' . $p['fragment'] : '';
        return compact('host', 'path', 'query', 'fragment');
    }

    private static function normalize($path) {
        $parts = array();
        $segments = explode('/', preg_replace('@/++@', '/', $path));
        foreach ($segments as $segment) {
            if ($segment === '.') {
                continue;
            }
            if (null === $tail = array_pop($parts)) {
                $parts[] = $segment;
            } elseif ($segment === '..') {
                if ($tail === '..') {
                    $parts[] = $tail;
                }
                if ($tail === '..' || $tail === '') {
                    $parts[] = $segment;
                }
            } else {
                $parts[] = $tail;
                $parts[] = $segment;
            }
        }
        $parts = implode('/', $parts);
        if (self::isAbsolute($path)) {
            $parts = preg_replace('@^(?:\.{2}/)++@', '/', $parts);
        }
        return $parts;
    }

    private static function join($a, $b) {
        if (!self::isAbsolute($b['path'])) {
            if ($b['path'] === '') {
                $b['path'] = $a['path'];
            } elseif ($a['path'] !== '') {
                $b['path'] = implode('/', array_merge(
                    array_slice(explode('/', $a['path']), 0, -1),
                    array('.', $b['path'])
                ));
            }
        }
        $b['path'] = self::normalize($b['path']);
        if (
            $a['host']  === $b['host'] || $b['host'] === '' and
            $a['path']  === $b['path']                      and
            $a['query'] === $b['query']
        ) {
            throw new RuntimeException;
        }
        $host = $b['host'] !== '' ? $b['host'] : $a['host'];
        return $host . $b['path'] . $b['query'] . $b['fragment'];
    }

    private static function isAbsolute($path) {
        return strpos($path, '/') === 0;
    }

    private static function isJavaScript($path) {
        return stripos($path, 'javascript:') === 0;
    }

    private static function decode($html) {
        return html_entity_decode($html, ENT_QUOTES, 'UTF-8');
    }

}

使い方

説明

HTML内に存在する外部へのURLリンクを抽出します。エンコーディングは UTF-8 前提なので、他のケースでは前に mb_convert_encoding 関数などを使って変換しておく必要があります。

public static array UrlScraper::parseLinks ( string $html [ , string $base_href = "" ] )

パラメータ

  • $html
    HTMLソース。
  • [$base_href]
    起点にするURLまたはパス。BASE要素・HREF属性の実装に従う。

返り値

href をURL、 text をURLが張られたテキストとする連想配列の配列を返します。

注意点

  • href text がユニークでないものは無視されます。
  • javascript: は無視されます。
  • ページ内リンクは無視されます。
  • 同一ページでもクエリが異なる場合は無視しません。
  • URLが張られた部分のHTMLタグ除去後にトリミングして空文字列となったときは、そこに存在する alt 属性の値を自動的に検知して割り当てます。複数あった場合は - で連結されます。

使用例

首相官邸のページを取得してみる

コード
$url = 'http://www.kantei.go.jp/sitemap.html';
print_r(UrlScraper::parseLinks(file_get_contents($url), $url));
結果
Array
(

    [0] => Array
        (
            [href] => http://www.kantei.go.jp/
            [text] => 首相官邸 Prime Minister of Japan and His Cabinet
        )

    [1] => Array
        (
            [href] => http://www.kantei.go.jp/foreign/index-e.html
            [text] => English
        )

    [2] => Array
        (
            [href] => http://www.kantei.go.jp/cn/index.html
            [text] => 中文
        )

    ...(中略)...

    [49] => Array
        (
            [href] => http://www.kantei.go.jp/rss.html
            [text] => RSS配信について
        )

    [50] => Array
        (
            [href] => http://www.kantei.go.jp/webaccessibility.html
            [text] => Webアクセシビリティ
        )

    [51] => Array
        (
            [href] => http://www.kantei.go.jp/jp/terms.html
            [text] => リンク・著作権等について
        )

)

Yahoo!JAPANトップページを取得してみる

コード
$html = file_get_contents('http://www.yahoo.co.jp');
preg_match('@<base href="([^"]*+)">@', $html, $matches);
print_r(UrlScraper::parseLinks($html, $matches[1]));
結果
Array
(

    ...

    [3] => Array
        (
            [href] => http://www.yahoo.co.jp/_ylh=XfarSdeAxqBF9TAzIwNzkxODE5OTkEdGlkAzEzBHRtcGwDdGFibGU-/r/c2
            [text] => ヤフオク!
        )

    [4] => Array
        (
            [href] => http://www.yahoo.co.jp/_ylh=XfarSdeAxqBF9TAzIwNzkxODE5OTkEdGlkAzEzBHRtcGwDdGFibGU-/r/c5
            [text] => 旅行、ホテル予約
        )

    [5] => Array
        (
            [href] => http://www.yahoo.co.jp/_ylh=XfarSdeAxqBF9TAzIwNzkxODE5OTkEdGlkAzEzBHRtcGwDdGFibGU-/r/c12
            [text] => ニュース
        )

    [6] => Array
        (
            [href] => http://www.yahoo.co.jp/_ylh=XfarSdeAxqBF9TAzIwNzkxODE5OTkEdGlkAzEzBHRtcGwDdGFibGU-/r/c13
            [text] => 天気
        )

    ...

)

相対パスに関するデモンストレーション

サンプル1: ベースに初期値を指定

コード
$html = <<<'EOD'
<a href="?q=php">?q=php</a>
<a href="./?q=php">./?q=php</a>
<a href="../?q=php">../?q=php</a>
<a href="/?q=php">/?q=php</a>
<a href="#php">#php</a>
<a href="./#php">./#php</a>
<a href="../#php">../#php</a>
<a href="/#php">/#php</a>
EOD;

print_r(UrlScraper::parseLinks($html, ''));
結果
Array
(
    [0] => Array
        (
            [href] => ?q=php
            [text] => ?q=php
        )

    [1] => Array
        (
            [href] => ?q=php
            [text] => ./?q=php
        )

    [2] => Array
        (
            [href] => ../?q=php
            [text] => ../?q=php
        )

    [3] => Array
        (
            [href] => /?q=php
            [text] => /?q=php
        )

    [4] => Array
        (
            [href] => ../#php
            [text] => ../#php
        )

    [5] => Array
        (
            [href] => /#php
            [text] => /#php
        )

)

サンプル2: ベースにファイルへの相対パスを指定

コード
$html = <<<'EOD'
<a href="?q=php">?q=php</a>
<a href="./?q=php">./?q=php</a>
<a href="../?q=php">../?q=php</a>
<a href="/?q=php">/?q=php</a>
<a href="#php">#php</a>
<a href="./#php">./#php</a>
<a href="../#php">../#php</a>
<a href="/#php">/#php</a>
EOD;

print_r(UrlScraper::parseLinks($html, 'foo/bar'));
結果
Array
(
    [0] => Array
        (
            [href] => foo/bar?q=php
            [text] => ?q=php
        )

    [1] => Array
        (
            [href] => foo/?q=php
            [text] => ./?q=php
        )

    [2] => Array
        (
            [href] => ?q=php
            [text] => ../?q=php
        )

    [3] => Array
        (
            [href] => /?q=php
            [text] => /?q=php
        )

    [4] => Array
        (
            [href] => foo/#php
            [text] => ./#php
        )

    [5] => Array
        (
            [href] => #php
            [text] => ../#php
        )

    [6] => Array
        (
            [href] => /#php
            [text] => /#php
        )

)

サンプル3: ベースにディレクトリへの相対パスを指定

コード
$html = <<<'EOD'
<a href="?q=php">?q=php</a>
<a href="./?q=php">./?q=php</a>
<a href="../?q=php">../?q=php</a>
<a href="/?q=php">/?q=php</a>
<a href="#php">#php</a>
<a href="./#php">./#php</a>
<a href="../#php">../#php</a>
<a href="/#php">/#php</a>
EOD;

print_r(UrlScraper::parseLinks($html, 'foo/bar/'));
結果
Array
(
    [0] => Array
        (
            [href] => foo/bar/?q=php
            [text] => ?q=php
        )

    [1] => Array
        (
            [href] => foo/bar/?q=php
            [text] => ./?q=php
        )

    [2] => Array
        (
            [href] => foo/?q=php
            [text] => ../?q=php
        )

    [3] => Array
        (
            [href] => /?q=php
            [text] => /?q=php
        )

    [4] => Array
        (
            [href] => foo/#php
            [text] => ../#php
        )

    [5] => Array
        (
            [href] => /#php
            [text] => /#php
        )

)

サンプル4: ベースにファイルへの絶対パスを指定

コード
$html = <<<'EOD'
<a href="?q=php">?q=php</a>
<a href="./?q=php">./?q=php</a>
<a href="../?q=php">../?q=php</a>
<a href="/?q=php">/?q=php</a>
<a href="#php">#php</a>
<a href="./#php">./#php</a>
<a href="../#php">../#php</a>
<a href="/#php">/#php</a>
EOD;

print_r(UrlScraper::parseLinks($html, '/foo/bar'));
結果
Array
(
    [0] => Array
        (
            [href] => /foo/bar?q=php
            [text] => ?q=php
        )

    [1] => Array
        (
            [href] => /foo/?q=php
            [text] => ./?q=php
        )

    [2] => Array
        (
            [href] => /?q=php
            [text] => ../?q=php
        )

    [3] => Array
        (
            [href] => /?q=php
            [text] => /?q=php
        )

    [4] => Array
        (
            [href] => /foo/#php
            [text] => ./#php
        )

    [5] => Array
        (
            [href] => /#php
            [text] => ../#php
        )

    [6] => Array
        (
            [href] => /#php
            [text] => /#php
        )

)

サンプル5: ベースにディレクトリへの絶対パスを指定

コード
$html = <<<'EOD'
<a href="?q=php">?q=php</a>
<a href="./?q=php">./?q=php</a>
<a href="../?q=php">../?q=php</a>
<a href="/?q=php">/?q=php</a>
<a href="#php">#php</a>
<a href="./#php">./#php</a>
<a href="../#php">../#php</a>
<a href="/#php">/#php</a>
EOD;

print_r(UrlScraper::parseLinks($html, '/foo/bar/'));
結果
Array
(
    [0] => Array
        (
            [href] => /foo/bar/?q=php
            [text] => ?q=php
        )

    [1] => Array
        (
            [href] => /foo/bar/?q=php
            [text] => ./?q=php
        )

    [2] => Array
        (
            [href] => /foo/?q=php
            [text] => ../?q=php
        )

    [3] => Array
        (
            [href] => /?q=php
            [text] => /?q=php
        )

    [4] => Array
        (
            [href] => /foo/#php
            [text] => ../#php
        )

    [5] => Array
        (
            [href] => /#php
            [text] => /#php
        )

)
Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away