SeleniumのStaleElementReferenceExceptionに対応する

概要

Selenium (javaAPI) を用いてスクレイピングを実施しするような仕組みを作っていたところタイトルにもあるエラーに遭遇した
(そうでなくてもSeleniumはなれるまでなかなかピーキーな動きをするときもあるが)

厄介なことに毎回必ず同じタイミングで発生するわけでもなく、その発生はかなり不定期である。
実施しているスクレピング処理においては、条件を引き起こすページすら一致せずにどのページでもまんべんなく発生する可能性が存在するようである。

今回はその解決方法について調査して実装までを検討する。

調査

まずはスタックトレースを見てみる、関連部分だけを抽出すると概ね下記のようなメッセージが出ている。

Caused by: org.openqa.selenium.StaleElementReferenceException: Element not found in the cache - perhaps the page has changed since it was looked up
Command duration or timeout: 7.64 seconds
For documentation on this error, please visit: http://seleniumhq.org/exceptions/stale_element_reference.html
Build info: version: 'unknown', revision: 'unknown', time: 'unknown'
System info: host: 'something host', ip: 'xxx.xxx.xxx.xxx', os.name: 'Linux', os.arch: 'amd64', os.version: 'something os', java.version: '1.8.0_91'
Driver info: org.openqa.selenium.firefox.FirefoxDriver
Capabilities [{applicationCacheEnabled=true, rotatable=false, pageLoadStrategy=unstable, handlesAlerts=true, databaseEnabled=true, version=42.0, platform=LINUX, nativeEvents=false, acceptSslCerts=true, webStorageEnabled=true, locationContextEnabled=true, browserName=firefox, takesScreenshot=true, javascriptEnabled=true, pageLoadingStrategy=unstable, cssSelectorsEnabled=true}]
Session ID: xxxxxxxxxx

ご丁寧にドキュメントのURLがエラーメッセージに含まれているので、公式ページへアクセスしてみる。

読んでみるとこの例外が発生するパターンは大きく分けて2つ存在するようだ。


参照しているエレメントが削除された場合

大概はこちらのケースに該当するようである。
例えばエレメントの参照を取得した時点から、別ページに移動、その後さらに保持していた参照を引き続き利用しようとした場合や、はたまた何らかのjsライブラリによって保持していたエレメントの参照が別の参照に置き換えられてしまった場合などがある。

この場合例えばid属性などは同じものを保持しているのにもかかわらず、内部的には別のDOMインスタンスとして管理されているため、例えば同じ属性を用いてセレクタを作成し、参照を引っ張ってきたとしても以前参照していたものはなくなってしまったためこのような例外を引き起こす。

エレメントがDOMにアタッチされていない場合

例えばtabを表現するテクニックにDivを予め用意しておいて、実際に表示されるDomは一つだけで他のDomは単純に値を保持するためだけに使用されているような場合、場合によっては他のDivはDOMから参照が保持されていない場合があるようである。
こんなケースは非常にまれでしょう。


その他jsによってエレメントタイプが変更された場合などもこの例外を引き起こす可能性があるようです。
が、どれにしても今回のケースに当てはまっていないように思う。

そのためweb上を検索しほかのサイトを調査していたところ、ページが読み込まれる際に間髪入れずにDOMを検索する際にうまくいかないことがあるようである。
またリトライすることでうまくDOMを検索できるケースもあるようであった。

これらを踏まえて対応を考える。

対応

はっきりとした原因はわからなかったが、おそらく今回のケースはページを移動した直後にDOMを検索することが原因だと予想する。

そのためページを移動した後にwait処理を導入する。またそれと同時にリトライ機構を導入する。

実装

下記の用に実装を行った。概念的なものしか示していないがなんとなくイメージは掴めると思います。
もともとの処理としてはループで色々なページを探索していたので、forの概形だけ残してあります。

変更前

    for( someCondition ) {
        driver.get(url);

        // スクレイピング処理
        someScrape();
    }

変更後

    for ( someCondition ) {
        driver.get(url);

        // StaleElementReferenceException を回避するため、ここで明示的にDOMの読み込みを待つ
        _sleepSec(5);

        for(int count=1; count<=RETRY_COUNT; count++) {
            try {

                // スクレイピング処理
                someScrape();

                break;
            } catch (StaleElementReferenceException e) {
                // StaleElementReferenceException が発生した場合は規定回数内であればリトライを行う
                if (count >= RETRY_COUNT) {
                    throw e;
                }
                _sleepSec(5);
            }
        }
    }

対応したところ、今のところ問題なさそうである。

参考ページ

http://www.software-testing-tutorials-automation.com/2015/02/how-to-handle-stale-element-reference.html
http://blog.afnf.net/blog/69


MVCというデザインパターンはもはや十分ではない

概要

今さら私が提示するような話でもないほど同じような話はネット上に腐るほど存在しているだろう。
にも関わらず不思議とシステム開発の現場レベルで見てみると、昔と変わらない手法をずっと続けていたり、また優秀なアーキテクトに恵まれていない現場などはひどく絡みあったコードと格闘していることは多い。
改めて近代的なデザインパターン・アーキテクチャとは何かということについて自分なりに詰めてみようと思う。

初めてに言ってしまうと、ずばりこの記事の着地は ドメイン駆動設計(DDD)への招待である。
同じような疑問を感じている人の何かヒントになればと思う。

人々はオブジェクト指向を手にした

昨今のソフトウェア製作の設計パターンは変化しているのだろうか、ということをまず考えてみよう。
「コンピュータの登場から今まで」のような大きなくくりで見た時には、ソフトウェアの設計パターンは大きく変化していると断言していいだろう。(といってもそんな昔に私は生まれていないが)

時代ごとに変化する大きな要因の一つに、偉大な先人たちが開発してきたコンピュータ言語がある。
昔はアセンブラ・C言語などの、手続き型言語が大部分を占めていた時代があるが、この頃開発においてドメインに着目するというよりかは手続きそれ自体に着目している。
これは言語のパラダイム的にやむを得ない部分が大きいが、いずれにしてもプログラミングされたその内容はそれぞれが意味をあまり持たない。個別には意味がわからない処理の連続が、結果として業務ロジックとして存在している。

その後時代は流れてついに人類はオブジェクト指向言語を手にする。このオブジェクト指向というのはプログラミングパラダイムの革命の一つと言っていいだろう。
オブジェクト指向によってドメインモデルと実装がかなり親和性を持って結合できるようになったのである。
そしてコンピュータ資源も豊富になってきた今、高級言語を使っていればあまりメモリなどの資源を期にすることもない。
実装都合でねじ曲げてきた手続き型への束縛からも開放されつつある。

これが爆発的となり、様々なツールなどが世にでる。sbtなどのマルチプロジェクトを取り替えるような機構ができたりして、その実装性能よりも、実装とドメインモデルをどれだけ近づけて、人間の脳に理解しやすいようにプログラミングを行う時代になっている。

MVCの誕生

Web業界ではそれまで多くのシステムは cgi と呼ばれる仕組みを利用してシステムを実装していた。
これは先程の話で言うとオブジェクト指向の誕生前で、割りかし手続き型の側面が強かった。

これがオブジェクト指向の出現によってMVCというデザインパターンの実装が用意になり、それをサポートする多くのフレームワークが誕生した。
多くの(初級)開発者たちはとりあえずMVCすればそれなりに構造化された実装を手にすることができた。

MVCの功として、まず多くの初級開発者たちにとって開発の目印になってくれたことである。
それまでとくらべてエンジニアたちの一種のヒントのようなトピックを投下して議論の対象となった。

また実際そのデザインパターンはシステムがあまり大きくないうちは非常に効率的に開発できる(ように見える)ためそれほど規模の大きくないシステムにとっては問題がそれほど露呈しない場合も多い。

MVCは銀の弾丸ではない

実際MVCのすべてが否定されるべきものではない、一種のアンチパターンとして利口なUIパターンなどというものがあるがそういった多くの場合はとらないでおくべき選択肢を序盤に排除してくれるような働きをしている。

ただ単純にMVCという一言で対応するにはシステム開発はそれほどシンプルではなかったという話である。

個人的に思うところは MVC の表す Model の責務が大きすぎるのである。
そのため人によって解釈が異なったり、またそもそもそこの設計を怠ったりということで大きなシステムに対応できなくなっているパターンをよく目にするように思う。

具体的によく目にするのが Model 内部にDBレイヤへのアクセス処理などが記述されていて、どんどんモデルが膨らんでいくなどのようなことである。

本来のクラスの責務をしっかり検討する必要があり、MVC を拡張するようなよりドメインとマッチした設計思想が必要なのである。

DDDへの招待

そこで DDD である。昔からあったのかもしれないが、最近特に多く目にするようになってきた。

DDD とはドメイン駆動設計の略称であるが、これはドメイン(業務ロジック)に特に着目して、それをいかに齟齬なく実装に落としこむかということに着目した設計手法である。

実際かなり具体的な設計思想を(先人たちの経験の蓄積として)もっており、これらを盛り込むことでシステムが大きくなってきた際にも拡張性がある実装を担保できる。

たとえば具体的には先程の話で一緒くたに Model と称していたものを Entity や ValueObject など(他にもいろいろあるが)いうようなより詳細なドメインモデルに落としこむ。
これによりMVCで取り扱っていたものを、より適切な責務へと分割することができる、、、などなど

その内容については長くなるのでまたの機会に割愛するが、DDDのバイブルである下記の書籍に目を通すと良いと思う。一度目を通しておいて損はないです。


osxでsshにて公開鍵認証を行っていてkeychainにパスワードを毎回聞かれる際の対応

なんてことはない小ネタですが、備忘録として

対応としては秘密鍵に対応する公開鍵が存在していないことが原因のようです。

keychainの仕様と言ってしまえばそれまでですが、公開鍵を ~/.ssh ディレクトリ内部に保存することで対応できました。

~/.ssh/configを確認したりssh-addを明示的に実行したりといろいろ試しましたがやはり公開鍵がなければ駄目なようです。

公開鍵がないという場合も対向サーバから scp などで持ってきましょう。置くだけで対応できました。