sbi証券のお知らせをためすぎた
SBI証券のお知らせを、200件近く溜め込んでしまった。
SBI証券はログイン後にリダイレクトが走る
リダイレクト画面でのログイン
ログイン時に、もとのリクエストアドレスにリダイレクトされる
たとえば、特定の株価を見ていて、ログインして詳細を続けてみる
たとえば、SBIラップ口座を開いて、ログインして、ラップ口座の詳細を見る。
これが、「リダイレクトされる」ログイン画面
「お知らせ」があるとリダイレクトされない。
お知らせがあると、元いたページにリダイレクトされずに、「お知らせ一覧」に遷移してしまう。
SBIラップ口座でログインしているのに、SBIのお知らせ一覧に行ってしまうのだ。
そもそも、多くの人は「お知らせ一覧」が出てくるのが「正常状態」とすら思ってる。そのように誤解している人も多い。
お知らせ一覧が強制表示される例
お知らせを全部確認しようとしたが、しんどすぎた
SBIのお知らせは数年(2年)分が溜め込めれるので、お知らせを全部確認するのは非常につらい仕事になる。
自動化するか!
さすがに2年前のものはもういらないし、パスワード変更のお知らせ、何回あるんだよ。
私は、書面交付に設定してるので、本当に重要なものは、郵送で送られてくる。
ということを踏まえて自動化するか!
iframe を使う。
金融機関のウェブサイトは、iframe でページを読み込むと、JSでフレーム検出が走る。なのでiframeで読み込めないと思っていた。*1
しかし、iframeは進化している。iframeにはいまや、安全なSANDBOX機能がある。
iframeは「Javascriptオフ」できるのだ。JSをOffにすると、JSによるフレーム検出は無視できるのだ。
もしフレーム化を拒否したければJSはもうだめなのだ。HTTPヘッダでやるべきなのだ。*2
iframe に埋め込む。
お知らせ一覧をiframe に埋め込む
埋め込みさえできれば、DOMアクセスして、全自動できる。
埋め込むためのスクリプト
class iframeLoader { constructor() { this.iframe = null; this.selector = '#MAINAREA01'; } async create_iframe(selector,href) { let iframe = null let iframe_id = 'my_worker_iframe' if (document.getElementById(iframe_id)) { iframe = document.getElementById(iframe_id) } else { // init iframe = document.createElement('iframe') iframe.setAttribute('sandbox', 'allow-forms allow-same-origin allow-top-navigation') iframe.setAttribute('id', 'my_worker_iframe'); iframe.setAttribute('width', '100%') iframe.setAttribute('height', '400px') let ele = document.querySelector(selector) let first = ele.firstElementChild ele.insertBefore(iframe, first) } return new Promise((resolve, reject) => { if (iframe) { resolve(iframe) } }) } async load_iframe(href) { this.iframe.src = href return new Promise((resolve, reject) => { this.iframe.onload = function () { resolve(this.iframe.contentWindow.document) } }) } async load_notice_iframe() { let ret = $x('//a[contains(./text() , "重要なお知らせ")]'); let a = ret[0] let b = a.onclick.toString().match(/openMsgBox\('(.+)'\)/)[1] let href = new URL(b, window.location.href).toString() this.iframe = await this.create_iframe(this.selector) let doc = await this.load_iframe(href) return new Promise((resolve, reject) => { resolve(this.iframe.contentWindow.document) }) } } class NoticeClicker { constructor() { this.iframe_id = 'my_worker_iframe'; } get iframe() { return document.getElementById(this.iframe_id); } get iframe_document() { return this.iframe.contentWindow.document } async iframe_send_button(button_name, doc) { let do_onclick = () => { function colSetSubmit(name, doc) { if ("ACT_backViewInfoList" == name || "ACT_cancel" == name) { doc.getElementById("ope_kbn_mb_impnotice_pop").value = "1"; } else if ("ACT_deleteInfoMessage" == name) { doc.getElementById("ope_kbn_mb_impnotice_pop").value = "4"; } else if ("ACT_estimate" == name) { doc.getElementById("ope_kbn_mb_impnotice_pop").value = "5"; } } }; let copy_submit_to_hidden = (button_name) => { let selector = `input[type='submit'][name^=${button_name}]` let form = doc.querySelector(selector).form let value = doc.querySelector(selector).value let input = document.createElement('input') input.value = value input.name = button_name input.type = 'hidden' form.appendChild(input) } do_onclick(button_name) copy_submit_to_hidden(button_name) doc.querySelector(`[type='submit'][name^=${button_name}]`).form.submit() } notice_exists() { let doc = this.iframe_document let link = doc.querySelector('td[class="mtext"][colspan="3"] a') return link != null } async click_notice() { let doc = this.iframe_document let link = doc.querySelector('td[class="mtext"][colspan="3"] a') link.click() return new Promise((resolve, reject) => { this.iframe.onload = function () { resolve(this.iframe_document) } }) } async click_confirm() { let doc = this.iframe_document this.iframe_send_button('ACT_estimate', doc) return new Promise((resolve, reject) => { this.iframe.onload = function () { resolve(this.iframe_document) } }) } async click_delete() { let doc = this.iframe_document this.iframe_send_button('ACT_deleteInfoMessage', doc) return new Promise((resolve, reject) => { this.iframe.onload = function () { resolve(this.iframe_document) } }) } async remove_notice() { let doc = this.iframe_document doc = await this.click_notice() doc = await this.click_confirm() doc = await this.click_delete() return doc } async remove_all_notice() { while (this.notice_exists()) { await this.remove_notice() } } }
起動用スクリプト
let loader = new iframeLoader() await loader.load_notice_iframe() let clicker = new NoticeClicker() clicker.remove_all_notice()
これをChrome Devで貼り付けて
ひたすら、自動でクリックさせる。
iframe は便利
ブラウザ操作とか。puppeeter や selenium ドライバやら、面倒は考えないで、サクッとページ遷移を伴う自動化を作ることができる。
スクレイピングとかもはかどるし、コンソールの変数が初期化されない。
テクニックとしては次の箇所の
constructor() { this.iframe_id = 'my_worker_iframe'; } get iframe(){ return document.getElementById(this.iframe_id); } get iframe_document(){ return this.iframe.contentWindow.document }
iframe 内部のDOMドキュメントを取り出す部分。これを覚えておく。
iframe = document.getElementById(this.iframe_id) iframe.contentWindow.document
iframeを動的に作る部分、ここが大事。
sandboxでフォームは使えるようにする。
が、フレーム検出のJSは動かしてやんない(allow-scriptsを未指定)
iframe = document.createElement('iframe') iframe.setAttribute('sandbox', 'allow-forms allow-same-origin allow-top-navigation') iframe.setAttribute('id', 'my_worker_iframe'); iframe.setAttribute('width', '100%') iframe.setAttribute('height', '400px') let ele = document.querySelector(selector) let first = ele.firstElementChild ele.insertBefore(iframe, first)
最後に、iframeのロードをawait / promiseで待ち受ける箇所
同じコードが大量にあるのは、resolveがスコープで全部違うため。
async load_iframe(href) { this.iframe.src = href return new Promise((resolve, reject) => { this.iframe.onload = function () { resolve(this.iframe.contentWindow.document) } }) }
この繰り返しで、iframeに閉じ込めたら、フォーム送信のJavascriptが簡単にかけて、スクレイピングするより手軽に使える
ブックマークレットで呼び出したり、コンソールを叩くだけで、自動化しちゃえる。iframe神がかってた。
ただし、ちゃんとContent-Security-Policyが設定されるサイトでは使えないので注意。また、異なるドメインのiframeは操作できません。*3
今回のサイトはCSP非対応だったと、ドメインが同じだったのでJS コンソールからズルができた。
もうズルはしません。
今回は数年分を溜め込んだのでチート行為をしましたが、今後はこまめにチェックして削除するので許してください。さすがに180件をポチポチは嫌だし無理だよ。