PHP
HTML
URL
scraping

PHPで HTMLに埋め込まれた相対パスを絶対パス(URL)に「一括変換」する

More than 1 year has passed since last update.

HTMLに埋め込まれた相対パスを絶対パスに変換する
こちらの記事とコードをずっと以前から利用させていただいていて大変重宝しているのですが、一部加筆が必要だったのと、それをさらにHTMLソース全体に適用できるようにしたものを業務利用していたので、お礼も兼ねてそのコードをオープンにさせていただければと思います。

何がしたい?

<ul>
    <li><a href="../index.php">戻る</a></li>
    <li><a href="category">カテゴリ一覧</a></li>
    <li><a href="brand">ブランド一覧</a></li>
    <li><a href="shop">ショップ一覧</a></li>
    <li><a href="ranking">ランキング一覧</a></li>
    <li><a href="detail">こだわり検索</a></li>
</ul>

HTMLソースのリンクって、相対パスで書くことがありますよね。でもこんな相対パスのままだと、このHTMLソースからすべてのURLをスクレイピングして後から開こうと思っても開けません。リンク元URLを保存しておいて都度相対パス変換するのもめんどくさいので、元のHTMLソースのURLを一括で絶対パスに置き換えておきたい。つまり、こうしたい。

<ul>
    <li><a href="http://absolute.com/shop/index.php">戻る</a></li>
    <li><a href="http://absolute.com/shop/list/category">カテゴリ一覧</a></li>
    <li><a href="http://absolute.com/shop/list/brand">ブランド一覧</a></li>
    <li><a href="http://absolute.com/shop/list/shop">ショップ一覧</a></li>
    <li><a href="http://absolute.com/shop/list/ranking">ランキング一覧</a></li>
    <li><a href="http://absolute.com/shop/list/detail">こだわり検索</a></li>
</ul>

結論

全体のコードです。

Rel2Abs.php
class Rel2Abs {

  /**
   *  HTML に含まれるパスを、相対パスから絶対パスに置き換え
   */

  public static function rel2abs($url,$html) {
    $replacer = [];
    // <BASE>タグがあればそれを採用。なければ引数指定のURL(HTMLの元パス)
    if( preg_match( '/<base [^>]*href="(.*?)"/', $html, $match ) != false ){ $base = $match[1]; }
    else $base = $url;

    preg_match_all( '/(href|src)="(.+?)"/', $html, $urls, PREG_SET_ORDER);
    foreach( $urls as $url ) {
        $replacer[$url[2]] = static::convert_to_uri($url[2],$base);
    }
    foreach( $replacer as $key => $val ) $html = str_replace( '"'.$key.'"', '"'.$val.'"', $html );

    $replacer = [];
    preg_match_all( '/(href|src)=\'(.+?)\'/', $html, $urls, PREG_SET_ORDER);
    foreach( $urls as $url ) $replacer[$url[2]] = static::convert_to_uri($url[2],$base);
    foreach( $replacer as $key => $val ) $html = str_replace( "'$key'", "'$val'", $html );

    $replacer = [];
    preg_match_all( '/(href|src)=([^\'\"].+?)[>\s]/', $html, $urls, PREG_SET_ORDER);
    foreach( $urls as $url ) $replacer[$url[2]] = static::convert_to_uri($url[2],$base);
    foreach( $replacer as $key => $val ) $html = str_replace( "=$key", "=$val", $html );

    return $html;
  }

  /**
   * http://web-tsukuru.com/187
   * スクレイピングなどで画像URLを取得する時に使うために
   * ベースURLを元に相対パスから絶対パスに変換する関数
   *
   * @param string $target_path 変換する相対パス
   * @param string $base ベースとなるパス
   * @return $uri string 絶対パスに変換済みのパス
   */
  public static function convert_to_uri($target_path, $base) {
    $component = parse_url($base);

    $directory = preg_replace('!/[^/]*$!', '/', $component["path"]);

    switch (true) {

      // [0] 絶対パスのケース(簡易版)
      case preg_match("/^http/", $target_path):
        $uri =  $target_path;
        break;

      // [1]「//exmaple.jp/aa.jpg」のようなケース
      case preg_match("/^\/\/.+/", $target_path):
        $uri =  $component["scheme"].":".$target_path;
        break;

      // [2]「/aaa/aa.jpg」のようなケース
      case preg_match("/^\/[^\/].+/", $target_path):
        $uri =  $component["scheme"]."://".$component["host"].$target_path;
        break;

      // [2']「/」のケース
      case preg_match("/^\/$/", $target_path):
        $uri =  $component["scheme"]."://".$component["host"].$target_path;
        break;

      // [3]「./aa.jpg」のようなケース
      case preg_match("/^\.\/(.+)/", $target_path,$maches):
        $uri =  $component["scheme"]."://".$component["host"].$directory.$maches[1];
        break;

      // [4]「aa.jpg」のようなケース([3]と同じ)
      case preg_match("/^([^\.\/]+)(.*)/", $target_path,$maches):
        $uri =  $component["scheme"]."://".$component["host"].$directory.$maches[1].$maches[2];
        break;

      // [5]「../aa.jpg」のようなケース
      case preg_match("/^\.\.\/.+/", $target_path):
        //「../」をカウント
        preg_match_all("!\.\./!", $target_path, $matches);
        $nest =  count($matches[0]);

        //ベースURLのディレクトリを分解してカウント
        $dir = preg_replace('!/[^/]*$!', '/', $component["path"])."\n";
        $dir_array = explode("/",$dir);
        array_shift($dir_array);
        array_pop($dir_array);
        $dir_count = count($dir_array);

        $count = $dir_count - $nest;

        $pathto="";
        $i = 0;
        while ( $i < $count) {
          $pathto .= "/".$dir_array[$i];
          $i++;
        }
        $file = str_replace("../","",$target_path);
        $uri =  $component["scheme"]."://".$component["host"].$pathto."/".$file;

        break;

        default:
        $uri = $target_path;
    }
    return $uri;
  }

}

使い方

$html にHTMLソースを、$urlにはそれを取ってきたURLをそのまま入れます。

$url = "http://absolute.com/shop/list/index.php";

$html = '
<ul>
  <li><a href="../index.php">戻る</a></li>
  <li><a href="category">カテゴリ一覧</a></li>
  <li><a href="brand">ブランド一覧</a></li>
  <li><a href="shop">ショップ一覧</a></li>
  <li><a href="ranking">ランキング一覧</a></li>
  <li><a href="detail">こだわり検索</a></li>
</ul>
';

$result = Rel2Abs::rel2abs( $url, $html );

var_dump($result);

結果は…

<ul>
  <li><a href="http://absolute.com/shop/index.php">戻る</a></li>
  <li><a href="http://absolute.com/shop/list/category">カテゴリ一覧</a></li>
  <li><a href="http://absolute.com/shop/list/brand">ブランド一覧</a></li>
  <li><a href="http://absolute.com/shop/list/shop">ショップ一覧</a></li>
  <li><a href="http://absolute.com/shop/list/ranking">ランキング一覧</a></li>
  <li><a href="http://absolute.com/shop/list/detail">こだわり検索</a></li>
</ul>

免責😅

このコードを長いことスクレイパとして使っていて、それが対象としている限られたサイトでの実績は十分ですが、もしかしたらうまくいかないサイトもあるかもしれません……。何かあればご一報いただけるとありがたいです。
また今回公開する前に、その実績あるコードをクラスとして切り出しましたので、クラスとしては若いです。それに伴うバグがあったらスミマセン……。同じくご一報を(略)

感想とか

そもそも、相対パスを絶対パスにするっていう要件はWEBやってたらアタリマエに発生する要件だし、ブラウザやファイルシステムにはアタリマエに実装されているような機能なので、PHP標準関数になってたりしないものなのでしょうか??(見逃しているだけだったらホントスミマセン!)

もしくは、GuzzleやGoutteなどのWEBクライアントあたりにさらっと実装されているかもしれません(見逃しているだけだったら略)。少なくとも、「次のページに遷移する」といった要件はそれらが満たしてくれると思います。

おまけ phpunitテストコード

ユニットテストコードです。どういったことができるかもわかるかと思いますのでご利用いただければと思います。

Rel2AbsTest.php
class Rel2AbsTest extends \TestCase
{

    /**
     * @test
     * @covers Rel2Abs::rel2abs
     * @dataProvider rel2absdProvider
     * @return void
     */
     public function rel2abs( $exptected, $url, $html )
     {
       $result = Rel2Abs::rel2abs( $url, $html );
       $this->assertEquals( $exptected, $result  );
     }
     public function rel2absdProvider(){
       return [
         //   完成HTML, ← HTMLのURL,  元HTML
         [ 
           'href="https://absolute.com/" href=\'https://absolute.com/second.php\' '.
           'src="https://absolute.com/genus" src=\'https://absolute.com/dir/\' '.
           'href=https://absolute.com/third.php src=https://absolute.com/detail/third.php '.
           'change_image(\'itm-img\',\'https://absolute.com/forth.php\') ',

           'https://absolute.com/index.php',

           'href="/" href=\'/second.php\' '.
           'src="/genus" src=\'dir/\' '.
           'href=./third.php src=detail/third.php '.
           'change_image(\'itm-img\',\'forth.php\') ',
         ],
         [ 
           'href="https://test.com/" href=\'https://test.com/second.php\' '.
           'src="https://test.com/genus" src=\'https://test.com/dir/dir/\' '.
           'href=https://test.com/dir/third.php src=https://test.com/dir/detail/third.php '.
           'change_image(\'itm-img\',\'https://test.com/dir/forth.php\') ',

           'https://test.com/dir/index.php',

           'href="/" href=\'/second.php\' '.
           'src="/genus" src=\'dir/\' '.
           'href=./third.php src=detail/third.php '.
           'change_image(\'itm-img\',\'forth.php\') ',
         ],
     ];
     }

     /**
      * @test
      * @covers Rel2Abs::convert_to_uri
      * @dataProvider convert_to_uri_Provider
      * @return void
      */
      public function convert_to_uri( $exptected, $target_path, $base )
      {
          $result = Rel2Abs::convert_to_uri($target_path, $base);
          $this->assertEquals( $exptected, $result  );
      }
      public function convert_to_uri_Provider(){
        return [
          //  完成形              ← 元パス                 リンク元URL
          [ 'https://absolute.com/', 'https://absolute.com/', '' ],

          [ 'https://absolute.com/index.php', 'https://absolute.com/index.php', '' ],
          [ 'https://absolute.com/index.php', 'https://absolute.com/index.php', 'https://absolute.com/' ],
          [ 'https://absolute.com/index.php', '//absolute.jp/index.php',  'https://absolute.com/' ],
          [ 'https://absolute.com/index.php', '/index.php',  'https://absolute.com/' ],
          [ 'https://absolute.com/',          '/',           'https://absolute.com/' ],
          [ 'https://absolute.com/index.php', './index.php', 'https://absolute.com/' ],
          [ 'https://absolute.com/index.php', 'index.php',   'https://absolute.com/' ],

          [ 'https://absolute.com/',                '/',           'https://absolute.com/dir/' ],
          [ 'https://absolute.com/dir/index.php', './index.php', 'https://absolute.com/dir/' ],
          [ 'https://absolute.com/dir/index.php', 'index.php',   'https://absolute.com/dir/' ],
          [ 'https://absolute.com/index.php',       '../index.php','https://absolute.com/dir/' ],

          [ 'https://absolute.com/index.php',       '../index.php','https://absolute.com/dir/somewhat' ], // 末尾の / の有無で解釈が変わるがそういう仕様
          [ 'https://absolute.com/dir/index.php', '../index.php','https://absolute.com/dir/somewhat/' ],
          [ 'https://absolute.com/dir/index.php', '../index.php','https://absolute.com/dir/somewhat/index.php' ], // スラッシュがあるとこういう解釈
          [ 'https://absolute.com/index.php',       '../index.php','https://absolute.com/dir/somewhat.html' ], // スラッシュがないとこういう解釈

          [ 'https://absolute.com/',          '/',           'https://absolute.com/first.php' ],
          [ 'https://absolute.com/index.php', './index.php', 'https://absolute.com/first.php' ],
          [ 'https://absolute.com/index.php', 'index.php',   'https://absolute.com/first.php' ],
        ];
      }

}