Bookmarkletを作ろう（ウェブページのハック手順編）

# この記事は
2年前に書いた「[ブックマークレット準備編](https://qiita.com/kanaxx/items/63debe502aacd73c3cb8)」の記事へのアクセスが、じわじわと増えてきました。ありがとうございます。

さて、**ブックマークレットを作る手順**が分かっても、**上手にブックマークレット**は作れません。先人が作ったウェブアプリケーション（画面）をハックする折れない心が必要です。ブックマークレット自体に制約が多いので、制約の範囲の中でハックする必要があります。

ということで、今回は実用的なものを作るハック手順を紹介しようと思います。私がウェブアプリケーションをハックするときに、見ているポイントや気を付けるポイントを紹介しながら、ブックマークレットに仕上げを仕上げていきます。

# はじめに
## 一番大事なのは要件を決めること
いきなりストレートの直球を投げ込みました。別にブックマークレットに限ったことではないですね。プログラムを作る上でとても大事なことです。

ブックマークレットは単独で動くアプリケーションではないです。誰かが作ったウェブアプリケーション上で、自分のコードを動かすものです。ちょい足しの要素が強いです。完全な自動化まではできませんので、画面を操作をする上で面倒なところを削減するようなところがポイントです。

私が良く作るブックマークレットは、
:broken_heart:表示されている画面から1つ1つデータをコピペしないといけない
:arrow_right:HTMLからデータを抜き出して、コピペしやすい形でConsoleのログに書き出す

:broken_heart:社内の申請などでたくさんのテキストボックスや選択肢を埋めないといけないけど、いっつも決まった値で埋める必要がある
:arrow_right:自動で初期値を入力するか、入力補完の機能を付け足す

:broken_heart:ボタンやリンクを押すだけの操作だけど、何度も押さないといけないので面倒
:arrow_right:画面のボタンを押す作業を自動化する

などです。

## 別の手段も考えよう
本当にブックマークレットでいいですか？

 - 自分だけが使うならまだしも、誰かに使わせるにはインストール面倒ですよ。
 - バージョンアップも面倒ですよ。
 - データ登録系は苦手ですよ。ローカルホストで動くバッチプログラムのほうがよくないですか？
 - PythonでHTTPしてHTMLをほじくり返したほうがよくないですか？
 - なんなら、SeleniumやRPAのブラウザ自動操作でもよくないですか？

:sunflower: ブックマークレットの特性と限界を知った上で、実現できるものを上手に設計しましょう。


# ハック手順の紹介
あたりまえのことを言い終わったので本題へ入ります。
何はともあれ、まずは敵を知りましょう。
対象となるウェブアプリケーションの画面を決めたら、ページ内で起きていることを調べていきます。

## 1.まずは操作、操作
第一段階は、人間として画面操作をしてみて、何が起きているのかを感じましょう。といっても、適当に見ていてもしょうがないので、見るポイントはこの辺です。

1. 画面表示時にデータが全部あるのか、遅れて表示されるタイプか？
2. 入力に応じて画面が変わったり、モーダルが出たりするのか？
3. 入力に対しバリデーションのチェックがあるのか？
4. 1つの画面で完結するのか、画面遷移をするのか？

特に4の画面遷移するアプリケーションをハックするのは苦手です。

## 2. HTMLソースの調査
画面を見たあとは、HTMLのソースを確認します。この段階ではブラウザの「ページのソースの表示」機能で見える範囲でやります。DOMを見るというよりは、HTMLの癖を見抜く感じです。

ポイントは、

1. HTMLソース内にjavascriptの変数（配列やオブジェクト）がどれくらいあるか？
2. HTMLにidやclassがしっかり振ってあるか？
3. HTMLのdisplay:noneなどの隠し要素がどれくらいあるか？
4. HTML上に必要なデータが全部落ちているか？

1の部分は、Google Analytics用の変数として落ちていたり、`schema.org`の変数として落ちていたり、いろいろあります。結構、大事なデータがHTMLに埋まっていることがあります。
2の部分は、jQueryでの操作感にかかわってきます。idやclassが少なめだと、最悪は`body.innerHTML`からの正規表現でのぶっこ抜くのも考えます。


## 3. HTMLのDOM要素を調べる
2と似ていますが、見るポイントが異なります。

この部分では、画面の初期表示後に何かしらの操作をして、変更されたDOMを状態を確認します。例えば、最初は表示されていないけどクリックしたらリンクが表示されるような画面では、**表示された状態**のDOMを見ます。

ChromeのDev Toolの`Elements`パネルで調べます

見るポイントは、
1. 要素そのものか、上位の要素がブロック要素タブ（`<div>`など）に囲まれているか
2. 繰り返し要素に同じ`class`がついているのか
3. クリックしたあとにネットワーク通信をしているか？

何度か操作をして、`display:none`などのCSS要素が消えるイベントなども把握します。APIを呼んでそうな場合は、次の手順4で確認します。

## 4.画面内でAPIを呼び出しているか
SPA系で画面遷移が伴わない場合、裏側でAPI通信をしていることが多いです。**どのタイミング**で**どのAPI**を呼びだしているかを調べます。APIの呼び方が分かれば、ブックマークレットから勝手に呼び出すことが選択肢として入ってきます。

Chrome Dev Toolの`Network`パネルで調べています。
APIのエンドポイント、リクエストヘッダー、パラメータの詳細を知ることができます。

## 5. 使っているjavascriptのライブラリ
HTML表示完了時に読み込みされているjavascriptのライブラリは、ブックマークレットからでも使うことができます。どんなライブラリが使われているか、HTMLソースから`<script src="">`を検索して調べます。最近は避けられがちですが`jQuery`が読み込まれていれば、ブックマークレットを作るのはかなり簡単になります。

# 6. アウトプットをどうするか考える
ここは苦労するポイントかもしれません。
ページ内の要素を上手くつかって、何かしらの処理をするところまでたどりついても、それをどこにアウトプットするのかを考えないといけません。

今までやってきた例だと

- ページのHTMLに無理やりDOMを追加して、書き込む
- firebaseなどのAPIで書き込めるデータストアに投げ込む
- console.log()で結果を残す

最近の実例だと、1か月の勤務時間をWebの勤怠システムからExcelに書き写す必要があり:poop:、これがなかなかに骨の折れる作業でした。勤怠システム画面からjQueryで勤務時間を抜き出し、console.logでタブ区切り文字列で吐き出しておき、Consoleの文字列をExcelにコピペするだけにしました。
難しく考えすぎないのもポイントかもしれません。

:sunflower:　:sunflower:　:sunflower:
 というのが、ウェブページをハックするときに見ているポイントです。この手順とポイントに沿って、最近作ったブックマークレットを紹介します。

# 実際にブックマークレットを作ってみる

LOHACOのショッピングカートにたくさんの商品を入れて、「カート内の割引が効いているか、割引の組み合わせが間違えていないか」を確認するお仕事がありました。
100個くらいの商品を入れて確認をした後に、別の商品を入れて確認するためにカートを空っぽにする必要があります。しかし、一発で空にする機能は提供されていないので、1つずつ丁寧に心を込めて削除ボタンを押していく必要があります:frowning2:

困りました。削除を100回押すとか、目まいがする作業です。

- お金に関わる部分なので、できれば目視確認はしたい
- でも、全削除に時間がかかり、仕事に時間がかかりすぎる
- どうにか効率よくやりたい

こんな背景でした。

この作業を辞めてしまうというのは選択肢にないようで、そもそも論はできませんでした。
あなたの仕事は、カートの価格が正しいことを確認することであり、カートの商品を消すことではない！一刻も早く削除クリック作業から解放してあげなければ。。。

:warning: 2020年2月時点のHTMLで動作確認しています。将来にわたり動作するコードではないです。

# まずは対象の調査から
まずは、カートに商品を詰め込みます。詰め詰めしたら、カート画面を開いてみます。
URLはここっぽいですね。https://lohaco.jp/sf/cart/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/4ae6df76-4da9-3a65-9f23-3b3924124830.png)


## 動作を体感する
削除するのが大変ということなので、何も考えずに削除を押してみましょう。

![2020-02-02_13h19_33.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/1d1f1d68-1da1-2762-5a03-0302df02ab05.gif)

くるくる回って、消えるタイプですか。画面遷移が伴わないときは、DOMで非表示にするか、APIかなぁ。と想像だけしておきます。

くるくるが終わったら、次の削除を押す。1商品あたり3秒くらいかかりそうです。100商品だと、、、


## HTMLソースを見る

一覧系の画面は、`<div>`や`<table>`でデータを表示することが多いので、比較的データを特定しやすいです。Body周辺を眺めてみましょう。
おや、Bodyタグの中に商品名や価格の情報が見当たりません。その代わりに、途中に長～い`javascript`の変数があります。

この辺です
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/435210b7-8cd3-b714-874d-57fe8d4ec6d6.png)

`LOHACO.Env.PAGE`という変数が怪しいですね。
これを目で解読するのは大変だし、JSONっぽい変数なので、Chrome Dev ToolのConsoleで変数を覗いてみます。F12でConsoleを開いて、変数名を入力してENTER:arrow_down:するだけでよいです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/9d6a9714-5fb9-8c84-f685-32d499c6798e.png)
きれいなデータ構造だ。

マウスで▼を下っていきましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/59b6ef91-ff0f-8bc4-bfdd-d2b55b52b03a.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/a18be77f-de48-fbfa-0d34-813c632b50e2.png)

なるほど、この辺に商品があるっぽいですね。

Console開いたついでに、`$.fn.jquery`も入力して、jQueryが使えるか確認しておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/12d4a99d-77e2-b9aa-9c94-43ebe526d810.png)

jQueryの3.4系が入ってるらしい。ラッキーですね。

## DOMを確認
Bodyタグに表組（データ）が含まれていなかったので、完成した画面のHTMLを確認します。これはChrome Dev Toolの`Element`パネルで見ます。

`削除リンク`を右クリックして、検証
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/5183ef45-8b51-f041-2efc-8c8a9758e702.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/3641cb19-4382-74a2-fa2e-def1bb9302ff.png)

リンクはAでもButtonでもなく`p.delete`でしたね。
ついでに周辺のHTML構造も見ておきましょう。例えば、商品名は`p.title`にあるようです。

## APIの確認
画面遷移をせず、くるくるして商品が消えているので、裏でAPIを呼んでいる可能性が高いですね。このケースは、とりあえず、Chrome Dev Toolの`Network`パネルで調べてみます。

Networkを開いて、表示されている内容をクリアしてから、削除リンクを押す。
![2020-02-02_13h54_19.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/ee1834bc-c50b-8576-3c73-06f2f51d5ec2.gif)

ずらずら出てきました。
一番最初で`fetch`していますね、fetchの詳細を確認させていただきましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/bd46f212-661b-3683-4e0d-565887a885a2.png)

APIを呼び出してました。
`Request Method`が`DELETE`ですね、Bodyにパラメータは無くてURIでリソースを指定、ですか。Rest Fullな感じでかっこいいですね。

調査段階では、URLやHTTPメソッド、リクエストBodyやリクエストHeaderを確認し、可能であればPostManなどのHTTPクライアントで疑似的に投げて、受け取ってもらえるのか確認しておくのがいいです。


## ここまでの調査結果
ここまでの結果をまとめると

 - このページは、削除をクリックしたら画面遷移するタイプじゃないこと
 - データはHTML上から取るのではなく、scriptの変数で取れそうなこと
 - 削除リンクをクリックしたらAPIを呼んでいること
 - jQueryは使えること

ということが分かりました。ブックマークレットとして実現するものは、

1. カートにある商品のキーになりそうなコードを探してきて、
2. 削除リンクで呼び出しているAPIを呼んであげる

となります。言葉にすると簡単ですね。

たいていは、商品データを探すところや、集めるところに苦労しますが、javascriptの変数が落ちていたので、比較的簡単に終われそう。

# コーディング
javascriptでコードを書くだけです。書いたコードはこちらです。全部で100行くらいです。

```javascript:clear_cart.js

// ==ClosureCompiler==
// @output_file_name default.js
// @compilation_level SIMPLE_OPTIMIZATIONS
// @language_out ECMASCRIPT_2017
// ==/ClosureCompiler==
javascript:(
    async function(){

        //0.画面チェック
        if( !location.href.match(/lohaco\.jp\/sf\/cart\//)){
            alert("対象のページではないようです。カゴのページに移動します");
            window.location.href='https://lohaco.jp/sf/cart/';
            return;
        }
        
        var deletedCount = 0;
        var targetItems = [];

        //1. 普通の商品
        targetItems = targetItems.concat(LOHACO.Env.PAGE.lohacoItems);

        //2. discountItem
        for(var i=0; i<LOHACO.Env.PAGE.setDiscounts.length; i++){
            var setDiscount = LOHACO.Env.PAGE.setDiscounts[i];
            console.log('start %s-%s', setDiscount.name, setDiscount.lohacoItems.length );
            targetItems = targetItems.concat(setDiscount.lohacoItems);
        }

        //3. sellerItem
        for(var i=0; i<LOHACO.Env.PAGE.sellers.length; i++){
            var seller = LOHACO.Env.PAGE.sellers[i];
            console.log('start %s-%s', seller.name, seller.items.length );
            targetItems = targetItems.concat(seller.items);
        }

        //4. supplierItem
        targetItems = targetItems.concat(LOHACO.Env.PAGE.supplierItems);

        //5. 商品を消す
        console.log('target Items = %s', targetItems.length);
        deletedCount += await processItems(targetItems);

        //6. ここで終わり
        alert(deletedCount + '商品を消しました。F5でリロードしてね。');

    //======
        //Itemを繰り返してdelete呼ぶ関数
        async function processItems(items){
            var cnt = 0;
            for(var i=0; i<items.length; i++){
                //投げすぎないためのやさしさ。2回目以降はちょっと止まろう
                if(i>0){
                    await new Promise(resolve => setTimeout(resolve, 500)) // 0.5秒待つ
                }

                var code = items[i].catalogCode;
                var result = await deleteItem(code);
                cnt+=result;
                if(result == 1){
                    console.log('%s is OK', code);
                    deletedEffect(code);
                }else{
                    console.log('%s is NG', code);
                }
            }
            return cnt;
        }
        //単品Itemを削除するAPIを実行する関数
        async function deleteItem(itemcd){
            var url = 'https://lohaco.jp/sf/api/b/cart/items/' + itemcd;
            var response = await fetch(url, 
                {
                    method: "DELETE", 
                    headers: {
                        "Content-Type": "application/json", 
                        "x-csrf-token":LOHACO.Env.CSRF_TOKEN },
                    body:"{}"
                });
            var json = await response.text();
            // console.log(json);

            if(response.status=='200'){
                return 1;
            }else{
                return 0;
            }
        }
        //削除が終わったら表示を変える関数(動いている感じが出ないから)
        //商品名に取り消し線を入れて、自動スクロール
        function deletedEffect(itemcd){
            var selector = 'p.title > a[href^="/product/' + itemcd + '"]';
            var a = $(selector);
            if( a.length == 1){
                a.html( '
' + a.html() + '
' );
                var y = a.offset().top - 100;
                $(window).scrollTop(y);
            }
        }
    }
)();


```
https://github.com/kanaxx/bookmarklet/blob/master/lohaco/clear_cart.js


## プログラムの解説

### 商品を探す部分
コメントの1,2,3,4の部分です。
`LOHACO.Env.PAGE`の変数から商品情報から取り出しています。どうやら商品種類ごとに4種類のキーの中に、配列で入っているので丁寧に取り出します。

商品データ（商品コードっぽいやつ）は、`targetItems`に貯めこんでおきます。

サイトがバージョンアップすると、値の取り方が変わってしまうので、その時はあきらめて作り直しです。

### 削除のAPIを呼ぶ
`targetItems`の中を順番に、DELETEでAPIを実行します。fetchは非同期な関数なので普通に呼び出すと、結果を待たずにAPIにリクエストがバンバン飛んでしまいます。攻撃っぽく扱われると問題なので、awaitを入れて、商品1つが終わったら次の商品に行くようにしています。CSRF防止のトークンも必要っぽいので、一緒に送っておきます。

### 削除した感じを出す
無理やりAPIだけ呼ぶと画面に変化が起きないので、うまくいっているのか、いっていないのか利用者に伝わりません。（Console見ればわかるのだけど、エンジニア相手の仕事ではないので）
ということで、削除が終わったときに商品名部分に取り消し線を入れる処理を加えました。ついでに削除した商品が画面で見える位置にスクロールする処理も入れました。

この処理は、ほんのお気持ちです。

## 動かしてみる
では、実際に動かしてみます。

ブラウザを開き、カートに商品を入れ、`F12`キーを押し、Dev ToolのConsoleを開き、Consoleにソースを張り付けてENTER

動作の様子
![2020-02-02_19h29_10.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/c8052a7d-076a-0830-d4d8-065505ccda66.gif)

F5を押すと、空っぽになるはずですね。

全体的に上手くいったら、Closure Compilerで短縮化して、ブラウザにセットします。この手順は前の記事を読んでください。省略します。

## その他、補足
今回は上手くいきましたが、APIがうまく呼べないときは削除リンクを`$('a').click()`して押すケースもあります。とりあえず動くものが欲しいだけなので、その辺は臨機応変な感じで解決の引き出しを持っておくといいです。

# まとめ
今回は、HTMLの画面をどの観点でハックすれば実用的なブックマークレットが作れるか、僕なりの手順を紹介しました。Amazonのカートに自動投入したり、楽天の検索結果をハックしたり、他にもいろいろあるのですが、今日はここまでです。

何でそんなことを？みたいなエンジニア視点だと目まいがするような作業を、人間がやっていたりします。全てが不要な作業だとは言いませんが、効率よくやる方法はたくさんあるので、プログラムの力で助けてあげましょう。

今回のやつは、商品を消す作業が5分→1分くらいになったのではないでしょうか？4分の短縮も大きいですが、心が平和でいられるので、そっちのほうが大事です。

たった100行のスクリプトで、人助けができるなんて、素晴らしいことです。