はてだBlog(仮称)

私的なブログど真ん中のつもりでしたが、気づけばWebサイト系のアプリケーション開発周りで感じたこと寄りの自分メモなどをつれづれ述べています。2020年6月現在、Elasticsearch、pandas、CMSなどに関する話題が多めです。...ですが、だんだんとより私的なプログラムのスニペット置き場になりつつあります。ブログで述べている内容は所属組織で販売している製品などに関するものではなく、また所属する組織の見解を代表するものではありません。

Python os.urllib.parse.urljoinでHTML内の相対パスをルート相対パスにお手軽に変換 と 昔話

はじめに

Pythonの標準ライブラリの

  • os.urllib.parse.urljoin
  • os.path.join
  • os.path.normpath

スニペット事例紹介です。

特に、os.urllib.parse.urljoinについては、

「/a/b/c/foo.html」と「../../common/img/z.jpg」を引数に与えると、「/a/common/img/z.jpg」を直接戻してくれる

という、相対パスからのルート相対パス変換@静的HTMLの分析・パッチ/変換あるあるをダイレクトに実装してくれるので、アドホックに手軽にこのようなことをやりたい場合は非常にありがたいライブラリでした。

昔話(静的HTML内の相対パスリンクをルート相対パスに変換)

実際の紹介の前に昔話をひとつ。

よくある話ですが、Bシェルのシェルスクリプトしか使えない(perlも使えない)環境で、静的html内のリンクを相対パスからルート相対パスに変換したりということがしばしばありました。

やりたいことはこんなことです。↓

f:id:azotar:20200202133801p:plain

この変換ですが、シェル芸のエキスパートならまだしも、そうではない自分には結構手間な話でした。

というのも次の図のように、親ディレクトリを示す「ドット2つ(..)」のパスの変換も必要になるからです。

f:id:azotar:20200202140906p:plain

ひとつのアイディアとして、シェルスクリプトの中で実際に「『cd』して『pwd』」して得られた値を使ってしのいでいましたが、これはこれでいろいろ手間です。

まあ、Bシェルと言っても、bashは使えたので、バッククォート版ではなくて、「$と()」によるコマンド置換*1は利用できましたのでその分は気軽でしたが...

ディレクトリやファイル名のパス結合

前述の例ぐらいなら言語によっては標準ライブラリがあるんじゃないということで、いまさらながら公式Rをみたりやググってみたところ、「結合」という意味では次の関数が使えそうです。

os.path.join docs.python.org

os.urllib.parse.urljoin docs.python.org

なお、似た観点のライブラリとして、ある相対パスに関して、実行環境での実際の絶対パスを得るという、Path.resolve()という関数もあります。ここではリンクするに留めておきます。

Path.resolve https://docs.python.org/ja/3/library/pathlib.html#pathlib.Path.resolve

やってみる

やってみた方が早そうですので、説明無用で次のスクリプトを起動してみます。

from urllib.parse import urljoin

d = [
'/a/b/c/',
'/a/b/c/index.html',
'/a/b/c/foo.html',
'/a/b/c/bar',
'/a/b/c'
]

for i in ['d/e/f.jgp', '../../y/z.png']:
    print('-----')
    print(i)
    for j in d:
        print('--')
        j_i = j.ljust(20,' ') + i.ljust(20,' ') 
        print('ospath:',os.path.join(j, i).ljust(40,' '), j_i)
        print('osnorm:',os.path.normpath(os.path.join(j, i)).ljust(40,' '), j_i)
        print('urllib:',urljoin(j, i).ljust(40,' '), j_i)

↓ 実行結果

-----
d/e/f.jgp
--
ospath: /a/b/c/d/e/f.jgp                         /a/b/c/             d/e/f.jgp           
osnorm: /a/b/c/d/e/f.jgp                         /a/b/c/             d/e/f.jgp           
urllib: /a/b/c/d/e/f.jgp                         /a/b/c/             d/e/f.jgp           
--
ospath: /a/b/c/index.html/d/e/f.jgp              /a/b/c/index.html   d/e/f.jgp           
osnorm: /a/b/c/index.html/d/e/f.jgp              /a/b/c/index.html   d/e/f.jgp           
urllib: /a/b/c/d/e/f.jgp                         /a/b/c/index.html   d/e/f.jgp           
--
ospath: /a/b/c/foo.html/d/e/f.jgp                /a/b/c/foo.html     d/e/f.jgp           
osnorm: /a/b/c/foo.html/d/e/f.jgp                /a/b/c/foo.html     d/e/f.jgp           
urllib: /a/b/c/d/e/f.jgp                         /a/b/c/foo.html     d/e/f.jgp           
--
ospath: /a/b/c/bar/d/e/f.jgp                     /a/b/c/bar          d/e/f.jgp           
osnorm: /a/b/c/bar/d/e/f.jgp                     /a/b/c/bar          d/e/f.jgp           
urllib: /a/b/c/d/e/f.jgp                         /a/b/c/bar          d/e/f.jgp           
--
ospath: /a/b/c/d/e/f.jgp                         /a/b/c              d/e/f.jgp           
osnorm: /a/b/c/d/e/f.jgp                         /a/b/c              d/e/f.jgp           
urllib: /a/b/d/e/f.jgp                           /a/b/c              d/e/f.jgp           
-----
../../y/z.png
--
ospath: /a/b/c/../../y/z.png                     /a/b/c/             ../../y/z.png       
osnorm: /a/y/z.png                               /a/b/c/             ../../y/z.png       
urllib: /a/y/z.png                               /a/b/c/             ../../y/z.png       
--
ospath: /a/b/c/index.html/../../y/z.png          /a/b/c/index.html   ../../y/z.png       
osnorm: /a/b/y/z.png                             /a/b/c/index.html   ../../y/z.png       
urllib: /a/y/z.png                               /a/b/c/index.html   ../../y/z.png       
--
ospath: /a/b/c/foo.html/../../y/z.png            /a/b/c/foo.html     ../../y/z.png       
osnorm: /a/b/y/z.png                             /a/b/c/foo.html     ../../y/z.png       
urllib: /a/y/z.png                               /a/b/c/foo.html     ../../y/z.png       
--
ospath: /a/b/c/bar/../../y/z.png                 /a/b/c/bar          ../../y/z.png       
osnorm: /a/b/y/z.png                             /a/b/c/bar          ../../y/z.png       
urllib: /a/y/z.png                               /a/b/c/bar          ../../y/z.png       
--
ospath: /a/b/c/../../y/z.png                     /a/b/c              ../../y/z.png       
osnorm: /a/y/z.png                               /a/b/c              ../../y/z.png       
urllib: /y/z.png                                 /a/b/c              ../../y/z.png       

まとめ

上記の結果をみやすいように次の表にしました。

f:id:azotar:20200202141117p:plain

各関数の位置付け・生い立ちからすると当たり前ですが、html内の相対パスを変換するという用途については、やはりurljoinが都合が良さそうです。

ただし、os.path.joinとnormpathからすると、スラッシュ有無を起点にディレクトリかどうかを判断しているようですので、2,3,7,8あたりの結果にケチをつかられるのはある意味いいがかりかもしれません。

また、「/a/b/c」の例の動作を例にとって、os.path.joinの方がurljoinよりも用途にあっているという場合もしばしばあるかもしれません。

以上、「昔話」を成仏させるために筆をとってみました。

おまけ

ターゲットとなるファイルのルート相対パスと起点となるディレクトリ・ファイルが分かっていて、ターゲットとなるファイルの相対パスが欲しい場合もあります。

次の図のような今回の本題と反対の演算です。

f:id:azotar:20200202133900p:plain

こちらは、次のように、os.path.relpathを使うと、あっさり対応できそうです。

In [1]: import os                                                                                                                                       

In [2]: os.path.relpath('/a/y/z.png',start='/a/b/c/')                                                                                           
Out[2]: '../../y/z.png'

※ startに渡すディレクトリの形式には注意!

os.path.relpath

docs.python.org

その他参考リンク

記事本文ではos.pathを例に挙げましたが、pathlibにシフトしていくべきなのかもしれません。

docs.python.org

※ ページの最後に、os.pathと pathlibの同等のものの対応表があります。

https://google.github.io/styleguide/shell.xml

linuxjm.osdn.jp