40
38

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2014-01-18

前書き

ある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
        )

)
40
38
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
40
38

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?