はてだBlog(仮称)

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

静的HTMLサイトの画像が規約にしたがっているかチェック(Python / os.path.commonpath) と昔話

はじめに

Pythonの次のライブラリ/便利メソッドの簡単な紹介です。

紹介といいながらも、上記を個人的な昔話にからめながら、スニペット事例をペタペタとはってみたというところになります。

昔話

CMSの移行時などにコンテンツデータを整備したいといったケースではあるあるではないかと思うのですが、現行のコンテンツのHTML(特に一点もののページ)において、共有画像がただしく共有ルールにしたがって共有されているかということをチェックしたいことがあります。

また、逆の話として、本来はあるページでのみ利用するべき個別画像が必要以上に他のページから直リンクされており、掲載期限との兼ね合いの運用をいたずらに難しくしているというものがないかチェックしたいというのもあるでしょう。

この辺の話が難しいのは、大半はマークアップ規約やCMS製品の仕組みでうまく取り計らっているものの、いかんせん「コンテンツ」というのは主観によるところが大きく、せいぜい半構造化の範囲である程度ゆるやかな運用にせざるを得ないため、例外がつきものというところです。

具体的には、下の図のように(図の例は多少デフォルメ・単純化されていますが)、理想どおりになっているOKパターンとそうでない例外となってしまったNGパターンを調査するといったことが考えられます。

f:id:azotar:20200202233438p:plain

以前は、結構この手のチェックにいろいろバッドノウハウ含め手間を要していた用に思いますが、最近はそもそもこういうことが起きにくくなってきたこともありましょうが、BeautifulSoupやらのスクレイピング系のライブラリなどを活用すれば、ワンライナーに毛が生えたぐらいのスクリプトでさくっと調査できてしまうなぁと感じる次第です。

スニペット

ということで事例です。

(1)あるディレクトリ配下の全てのHTMLを読み込みimgタグのsrc属性を抜き出す

import glob
from bs4 import BeautifulSoup as sp
import bs4 
from urllib.parse import urljoin 
import sys

# ルート相対ディレクトリで走行したとする。配下の拡張子がhtmlのファイルを読み込みする。
# マシンスペックに見合わないようなファイル数だと破綻するので注意。最低限、iglobにするとかは必要かも。
files =  [{ 'fn': i, 'file':open(i).read()} for i in glob.glob('**',recursive=True) if i.endswith('.html')] # エラー処理は省略
chklist = []

for f in files:
    bsobj = sp(f['file'], 'html.parser')
    fname = '/' + f['fn'] # ルート相対ディレクトリで走行した扱いのため「/」を補う。
    imgsrc = [ [fname, urljoin(fname,i.get('src')) ] for i in bsobj.find_all('img') if i.get('src')] 
    chklist.extend(imgsrc) 

print('page,img')
print( '\n'.join([ i[0] + ',' + i[1] for i in sorted(chklist,key=lambda x:x[1]) ] ) )



(2)ファイル名→そのファイル名のimgタグのリンク先の関連テーブル風データを元に、共通ルールにしたがって画像管理されているかチェックする

前述の②-OK/NGパターンを判定しています。

本来は、(1)の続きですが、ここでは、コピペでも動作例が確認できるように、インプットデータはプログラム内のCSV風文字列にしています。

import pandas as pd
import io
import sys
import os

csv = """
page,img
/xxx/a/b/foo.html,/xxx/a/img_ng/ng.jpg
/xxx/a/b/bar.html,/xxx/a/img_ng/ng.jpg
/xxx/boo.html,/xxx/a/img_ng/ng.jpg
/xxx/a/b/foo.html,/xxx/a/img/i.jpg
/xxx/a/b/bar.html,/xxx/a/img/i.jpg
"""

# 実際はpandasを介すほどではないかもしれない。
# しかし、手軽なCSVの読み込みと、ループ処理を宣言的に実装できることと、今回はそこまでやっていないが他に何かやりたい時のshape変更の伸び代を期待して、pandasを利用。

df = pd.read_csv(io.StringIO(csv))

a = df.groupby('img')['page'].apply(list).reset_index()

def 期待通り共有されているか判定(s):
    IMGDIR_PREFIX = ('/img','/images/') # ここでは「imgで始まるディレクトリ」「imagesディレクトリ」は共通フォルダ用の画像フォルダの命名ルールであるとした。
    pages = s['page']
    imgpath = s['img']
    commonpath = os.path.commonpath(pages)
    commonpath = os.path.commonpath([imgpath,commonpath])
    path = imgpath.replace(commonpath, '')
    return [pages,imgpath,'共通パス:'+path,'判定' + str(path.startswith(IMGDIR_PREFIX)) ]

a.apply(期待通り共有されているか判定,axis=1).to_csv(sys.stdout)

↓実行結果

0,"[['/xxx/a/b/foo.html', '/xxx/a/b/bar.html'], '/xxx/a/img/i.jpg', '共通パス:/img/i.jpg', '判定True']"
1,"[['/xxx/a/b/foo.html', '/xxx/a/b/bar.html', '/xxx/boo.html'], '/xxx/a/img_ng/ng.jpg', '共通パス:/a/img_ng/ng.jpg', '判定False']"

おわりに(ひとりごと)

昔話をおさらいしたくて記事にしてしまった。

標準ライブラリを使って良いのであれば、もっとシンプルかつ堅牢にかけるかもですが、それはデキる人にまかせよう。

あと、記事を書きながら、この類は最近は不要かもと思ったけど、リソースをCDNに置いたりするから逆に必要かもとおも思ったりもした。

参考リンク

os.path.commonpath

commonpathの公式R。

docs.python.org

ちなみに、os.pathの上等版のpathlibにはぱっと見、commonpathに対応するものはない模様。

例↓

In [5]: os.path.commonpath(['/xxx/a/b/foo.html', '/xxx/a/b/bar.html', '/xxx/boo.html'])                                                                 
Out[5]: '/xxx'

os.path.commonprefix

...というものもある。名前のとおり。

https://docs.python.org/ja/3/library/os.path.html#os.path.commonprefix

目的外使用ながら、文字列のリストの最初の部分の共通項を抜き出すのにも使える。

しっかり調べたりする時間がが無い場合にはその用途で使っちゃうかも。

In [21]: os.path.commonprefix(['ああ/xxx/a/b/foo.html', 'ああ/xxx/a/b/bar.html', 'ああ/xxx/boo.html'])                                                  
Out[21]: 'ああ/xxx/'