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

# 前書き

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

![ss (2014-01-19 at 04.16.54).png](https://qiita-image-store.s3.amazonaws.com/0/25060/b606316c-32bc-2fb5-047f-d36766d45517.png)

### [\[php\] WEBページ内のリンク先URLをすべて取得する](http://blog.livedoor.jp/qoozy/archives/52488858.html)

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

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

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

# **UrlScraper** クラス

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

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


## コード

```php
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** 関数などを使って変換しておく必要があります。

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

### パラメータ

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

### 返り値

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

### 注意点

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

# 使用例

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

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

```text:結果
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トップページを取得してみる

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

```text:結果
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: ベースに初期値を指定

```php:コード
$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, ''));
```

```text:結果
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: ベースにファイルへの相対パスを指定

```php:コード
$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'));
```

```text:結果
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: ベースにディレクトリへの相対パスを指定

```php:コード
$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/'));
```

```text:結果
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: ベースにファイルへの絶対パスを指定

```php:コード
$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'));
```

```text:結果

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: ベースにディレクトリへの絶対パスを指定

```php:コード
$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/'));
```

```text:結果
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
        )

)
```