【PHP】相対パスを絶対パス(URL)に変換する


スクレイピング系の処理を組んでいる人から「古い環境でも動くコードはあるか?」という質問を受けました。

内心「それぐらいググれよカス」と思いつつ、ググってみると意外とまともに動くコードが見つからなかったため、気分転換に書いてみました。


コード

都合上、PHP5.2辺りでも動くように書いてます。
未検証ですが、4系でもstripos()の代替となる関数を用意すれば動くかも?

function pathToUrl($pPath, $pUrl)
{
    $path = trim($pPath);    // 変換対象パス
    $url = trim($pUrl);      // 基準URL

    //-- 変換不要
    if ($path === '') { return $url; }

    if (stripos($path, 'http://') === 0 ||
        stripos($path, 'https://') === 0 ||
        stripos($path, 'mailto:') === 0 ||
        stripos($path, 'tel:') === 0) { return $path; }

    //-- #anchor
    if (strpos($path, '#') === 0) { return $url . $path; }

    //-- 基準URLを分解
    $urlAry = explode('/', $url);
    if (!isset($urlAry[2])) { return false; }

    //-- //path
    if (strpos($path, '//') === 0) { return $urlAry[0] . $path; }

    //-- 基準URLのHOME(scheme://host)
    $urlHome = $urlAry[0] . '//' . $urlAry[2];

    //-- 基準URLのパス
    if (!$pathBase = parse_url($url, PHP_URL_PATH)) { $pathBase = '/'; }

    //-- ?query
    if (strpos($path, '?') === 0) { return $urlHome . $pathBase . $path; }

    //-- /path
    if (strpos($path, '/') === 0) { return $urlHome . $path; }

    //-- ./path or ../path
    $pathBaseAry = array_filter(explode('/', $pathBase), 'strlen');
    if (strpos(end($pathBaseAry), '.') !== false) { array_pop($pathBaseAry); }

    foreach (explode('/', $path) as $pathElem) {
        if ($pathElem === '.') { continue; }
        if ($pathElem === '..') { array_pop($pathBaseAry); continue; }
        if ($pathElem !== '') { $pathBaseAry[] = $pathElem; }
    }

    return (substr($path, -1) === '/') ? $urlHome . '/' . implode('/', $pathBaseAry) . '/'
                                       : $urlHome . '/' . implode('/', $pathBaseAry);
}

動作テスト

$url = 'http://user:[email protected]/base/path/here/index.php?p=q';

$testAry = array(
    'URL' => 'https://www.google.com/gmail/',
    'mailto:' => 'mailto:[email protected]',
    'クエリのみ' => '?aaa=bbb&ccc=ddd',
    'パス+クエリ' => 'new.php?aaa=bbb',
    'アンカーのみ' => '#anchor',
    'パス+アンカー' => 'new.html#anchor',
    '絶対パス1' => '/new/path/abs.html',
    '絶対パス2' => '//new/path/abs.html',
    '相対パス1' => './',
    '相対パス2' => './new/path/rel2/',
    '相対パス3' => './new/path/rel3.html',
    '相対パス4' => '../../rel4.html',
    '相対パス5' => '.././/new/../././rel5.html',
);

foreach ($testAry as $title => $test) {
    echo "{$title}('{$test}') ⇒ ", pathToUrl($test, $url), "\n";
}

テスト結果

URL('https://www.google.com/gmail/') ⇒ https://www.google.com/gmail/
mailto:('mailto:[email protected]') ⇒ mailto:[email protected]

クエリのみ('?aaa=bbb&ccc=ddd') ⇒ http://user:[email protected]/base/path/here/index.php?aaa=bbb&ccc=ddd
パス+クエリ('new.php?aaa=bbb') ⇒ http://user:[email protected]/base/path/here/new.php?aaa=bbb

アンカーのみ('#anchor') ⇒ http://user:[email protected]/base/path/here/index.php?p=q#anchor
パス+アンカー('new.html#anchor') ⇒ http://user:[email protected]/base/path/here/new.html#anchor

絶対パス1('/new/path/abs.html') ⇒ http://user:[email protected]/new/path/abs.html
絶対パス2('//new/path/abs.html') ⇒ http://new/path/abs.html

相対パス1('./') ⇒ http://user:[email protected]/base/path/here/
相対パス2('./new/path/rel2/') ⇒ http://user:[email protected]/base/path/here/new/path/rel2/
相対パス3('./new/path/rel3.html') ⇒ http://user:[email protected]/base/path/here/new/path/rel3.html
相対パス4('../../rel4.html') ⇒ http://user:[email protected]/base/rel4.html
相対パス5('.././/new/../././rel5.html') ⇒ http://user:[email protected]/base/path/rel5.html

Webで見つけたコードは何故上手く動かない?

  1. そもそもPHPの古いバージョンに対応していない
    • これは仕方ないですね。私もPHP5.4以前の話とかはもう流石に忘れました(笑)

  2. user や pass の指定のあるURLを考慮できていない
    • user:pass@ の部分が欠落してしまうコードが多いようです。

  3. クエリパラメータを含むURLを考慮できていない
    • ?p=q の部分です。
      クエリパラメータを含むURLを元に変換すると、結果がめちゃくちゃになるコードが多いようです。

  4. mailto: や tel: を考慮できていない
    • 他にも色々ありますので、ここはもう少し丁寧に書いた方が良いと思います。
      今回はこれで十分との事で手を抜きました。

  5. クエリやアンカーのみを渡された時の事を考慮できていない。
    • '?name=value' や '#anchor' を渡された時です。

  6. 相対パスのちょっと変わった書き方に対応していない
    • '.././/new/../././rel5.html' というのは流石にお遊びですが、
      './' という書き方に対応できていないコードが多いようです。

4と5については、関数に渡す前にパラメータを先にチェックした方が良いでしょうね。
今回は使う人の用途に合わせたため、このような形になっています。


余談

少し気になったのは、正規表現を多用しているコードが多く見つかったという点です。

スクレイピングの性質上、関数の呼び出し回数がかなりの数になる事も多いと思いますので、できるだけ正規表現のようなコストのかかる処理は避けた方が良いのではないでしょうか。

また、パスにも様々な書き方がありますので、意外と見落としがありますね。
私も気分転換でサクっと書くつもりだったのですが、意外と時間がかかりました。

まだ見落としているところが色々とあるかもしれませんので、参考の際はご注意ください。
ご指摘などがございましたらどうぞお気軽に。