はてだBlog(仮称)

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

Webサイトのコンテンツ移行のテスト(デジタルだけどアナログな世界) pyquery、requests

 はじめに

CMSを変更したり、おおよそ保持するテキストは変えないもののデザイン(htmlマーキングを含む意味)を変更した際に、現行の内容を移行漏れしていないことをざっくりテストしたいということはないだろうか。

この手のざっくり移行テストをするというのはなかなか悩ましい。

  1. ヒトの感覚で見ると内容の9割方は同じだが、新になって読み替えしたり、項目が増えていたり、そもそもマーキングが変わっているので、diffコマンドによる比較は難しい。
  2. ログイン必要な場合がある。例えば、新の方は新しいCMS上にデータ登録されているものの試験中のため、未公開である。
  3. 繰り返しやあらかじめ用意したリストに従って数百、数千単位でチェックしたい。なので自動化とまでは行かずとも流し逃げできるぐらいの仕掛け・ツールにしたい。
  4. 重厚なツールである必要はない。それ自体のメンテのコストはかけられない。使い捨てで良い。

とした場合に、程よいものを見つけられなかったので、 日曜プログラミングでざっくりツールを作ることにした(というストーリー設定の)その記録。

Python自体の練習と、pyqueryとrequestsの習作になっております。 随分アナログな世界だと思うが、自分の住む世界ではこれで割り切るのも手立てだと思っている。

 アプローチ

<着目点のおさらい>

仕様が異なる箇所があるので出現順どおりのdiffは無理だが、本質的には、同じ内容になるような移行なので、規定の仕様に伴い差異が出る部分に固定で含まれる文字(「【」や「】」、「≪」や「≫」など)をおよびhtmlの装飾タグを双方から取り除き、残った文字を1文字ずつ(ソートして)ユニークにしたものを比較したものは常に一致する。

逆に言うと、要件の取り込み漏れでデータを正しく移行できていないのに、偶然上記が一致する確率は低いので、上記の条件で新旧比較することで、問題があるものがあれば、ある程度の精度で検出できると思われる。

f:id:azotar:20181125130537p:plain

ツールの仕様・考え方

  1. ツールの引数として、新旧のURLを受け取る。
  2. pyqueryで、新旧のこことここのブロックを比較対象とする箇所を「CSSセレクタ」で指定して比較用文字列にストックしていく。この時、HTMLのタグは取り除く。
  3. 新旧の文字列を比較する。違いがあれば、NGとして出力する。
  4. 2の際に、仕様変更になっており、比較対象外とすべき文字列に含めたくないものがある。今回はストイックな比較は求められないので、あらかじめストックする比較用文字列に比較対象外ワードとしてつっこんでおく。

ざっくり全件走行させて、アンマッチワードが発生している新旧ページをピックアップして問題箇所を対処する、比較してみて初めて気づいた比較対象外ワードを比較用文字列に追加してノイズを減らす... という手順を繰り返して、アンマッチワードが0文字になるように繰り返していく。

新旧比較ツール

# -*- coding: utf-8 -*-
# 公開中の現ページと対応する新レイアウトのページの新CMSプレビュー画面から、テキスト内容が同じになるべき箇所を
# スクレイピングして比較し、OK、NGのリストを出力する。
#
# 起動方法(例)
# echo "現ページのURL 新ページのURL" | python3 本スクリプト
#   ※標準出力で1行ずつ2つのURLのリストを渡すと、それらを比較してOK、NGを出力する。
#      
# 出力イメージ:標準出力
#   旧URL NG 新URL  (),./CDHILMNOPRST[]acdfghimnoprtwx−
#       
#       ※4項目目が、差異が出た文字のリスト
#
import pyquery,requests,sys

# メイン処理
def main():
    #デバッグモード(コマンド起動時にダミーの引数が設定されている)でない場合、新CMSにログインしてセッションを取得しておく
    s = login(len(sys.argv) == 1)

    #標準入力から与えられたリストのURLをクロールしコンテンツを比較
    for line in sys.stdin.readlines():
        urls = line.rstrip("\n").split(" ")
        if len(urls) == 2 :
        #「^」演算子で対象差(集合のどちらか一方に含まれる)の文字を取得
            unmatch_words =  signature(s,urls[0],"OLD") ^ signature(s,urls[1],"NEW")
            #比較結果を標準出力に出力する
            print(urls[0] + "\t" 
                  + "OK" if len(unmatch_words) == 0 else "NG" + "\t" 
                  + urls[1] + "\t"
                  + "".join(list(sorted(unmatch_words))))

#原則内容が一致するブロックのテキストを取得
def signature(requests_s,url,type):
    #新と旧で仕様上の差異が発生する文字列を設定する。
    common_hosei_words =  ""  
    c = common_hosei_words
    #クロールする
    if type == "OLD":   #旧の該当項目を取得する(ここでは、1、2、3の項目が対象というストーリー)
        pqd = pq_data(requests_s,url)
        # 1. XXX情報
        c += pqd("#XXXBlock div .detailText").text()  
        #  2. お役立ちXXX情報の表部分
        c += pqd("#XXXBox table.sFirst").text() 
        c += pqd("#XXXBox table.sSecond").text() 
        c += pqd("#XXXBox table.sThird").text() 
        #  3. 個別ボックス
        c += pqd("#XXXBox table").text() 
    if type == "NEW":  #新の該当項目を取得する
        pqd = pq_data(requests_s,url)
        #省略:実際は、OLDの例と同様に取得
    print(url + "\n" + c + "\n",file=sys.stderr)   #標準エラーにデバッグ出力     
    return set(list(c)) #文字列中の文字(1文字ずつ)をユニークした集合を返す  あいうえおかいあ → あ,い,う,え,お,か
    #return ''.join(sorted(set(list(c)))) #文字列中の文字(1文字ずつ)をユニークにしてソートした文字列を返す  あいうえおかいあ → あいうえおか

# pyqueryで解析したオブジェクトを返す
def pq_data(requests_s,url):
    res = requests_s.get(url)
    res.encoding = res.apparent_encoding
    return pyquery.PyQuery(res.text)

# ログインする (もちろん新のCMSの仕様による)
def login(flg):
    login_url = "http://new-cms.example.com/signin/"
    s = requests.Session()         
    res = s.get(login_url)
    res.encoding = res.apparent_encoding
    post = {
        "loginId":"ユーザー名", #ユーザ名
        "password":"パスワード", #パスワード
        }
    s.post(login_url,data=post) if flg else "" #flgがTrueならログインする。
    return s

main()


参考文献

note.nkmk.me

python.zombie-hunting-club.com