Contents
execute_scriptに引数をわたす
第2以降にわたすことができる。要素もわたすことができる。たとえばクリックしたいボタンが画面外にあったり、ほかの要素に隠れていてクリックできない場合はつぎのようにするといい。
1 2 3 |
span = driver.find_element_by_css_selector('span') # span.click()だと例外になる driver.execute_script('arguments[0].click();', span) |
また、execute_script内でreturnすると要素などを受け取ることもできる。
1 2 3 |
spans = driver.execute_script(''' return document.querySelectorAll("span"); ''') |
もちろん、引数にわたしたりreturnしたりできるのは要素だけではなく文字列とかもOK。
個人的にはpythonで要素を探して、execute_script内でクリックなどの操作をすることが多い。javascriptではリスト内包表記が使えないので。といえjavascriptもmap/filterがあるのでぜんぶjavascriptでやることもできるのだが。
どの要素がキー入力を受け付けているかわからない場合
どの要素がキー入力を受け付けているかわからない場合は、強引ではあるが基点となる要素から上へひとつずつ試していく。
1 2 3 4 5 6 7 8 9 10 11 12 |
elem = driver.find_element_by_css_selector('.input') for i in range(10): try: elem.send_keys(msg) break except Exception as e: print('メッセージ送信できませんでした:', elem.tag_name, elem.text, elem.get_attribute('class')) elem = elem.find_element_by_xpath('..') # elem.parentではだめ if i >= range(10)[-1]: print('10個さかのぼってもだめでした') raise e |
大量のchromedriver.exeをまとめて終了する方法
試行錯誤しながらSeleniumを使ったプログラムを書いていると、chromedriver.exeのゴミが大量に残っていることがある。タスクマネージャーからひとつひとつ消すのが面倒なときは、コマンドプロンプトから以下のコマンドを実行するといい。
1 |
taskkill /F /im chromedriver.exe |
closeメソッド
Seleniumを使ったPythonプログラムをWindows10のタスクスケジューラから起動していた。
もう1年近くそれでうまくいっていたのに、ある日突然実行しっぱなしになり終了しなくなる。タスクマネージャをみるとchromedriver.exeの死骸がならんでいる。
「ユーザーがログオンしているかどうかにかかわらず実行する」で実行していたが、「ユーザーがログオンしているときのみ実行する」にするとうまくいく。実行権限の問題かなと思い、「最上位の特権で実行する」にチェックを入れて「ユーザーがログオンしているかどうかにかかわらず実行する」にもどして実行すると、やはり実行しっぱなし。なにが問題かわからず、しかしchromedriver.exeが残った状態で実行しっぱなしなので、seleniumに問題がありそうだ。
ということで範囲をせばめていくと、なんと犯人はcloseメソッドだった。closeメソッドを呼んでいる行でストップしている。
いままでseleniumの終了は
1 2 |
driver.close() driver.quit() |
としていたが、driver.quit()だけにしたら直った。しかし解せぬ。なぜいきなり?Seleniumをバージョンアップとか、Chromeをバージョンアップとか、Windows Updateとか、なんにもしていないこのタイミングで。謎だ…。
javascriptでクリック
1 2 |
elem = driver.find_element_by_id("button") elem.click() |
とすると、ほかの要素に隠れていたり(モーダル使ってるサイトでありがち)、スクロールして画面内にないとクリックできない。隠している要素をdisplay:none;とかにしてもいいが、ボタンまでnoneになるケースもあったりして面倒なので、以下のようにjavascriptでクリックしてしまうのが簡単。
1 |
driver.execute_script("document.getElementById('button').click();") |
これからはクリックはぜんぶjavascriptでやってしまおうか。seleniumでクリックするメリットってなんかあるのか。面倒しかない。
seleniumでクリックする場合、pycharmから起動するとクリックできるけど、タスクスケジューラから「ユーザーがログオンしているかどうかにかかわらず実行する」で起動するとクリックできなくなったりする。もういやだ。
待機する
execute_scriptを使わない場合
基本的にはdriverを取得した直後にimplicitly_waitで長めの時間をセットしておくといい(セットした時間をこえると例外が投げられる)。
1 2 |
driver = webdriver.Chrome(chrome_options=options) driver.implicitly_wait(30) |
implicitly_waitによりdriver.find_element~~するたび、指定した要素が現れるまで待つ。
execute_scriptを使う場合
execute_scriptを使う場合はimplicitly_waitでは待ってくれないので、set_page_load_timeoutによりページがロードされるのを待つ。
1 2 |
driver = webdriver.Chrome(chrome_options=options) driver.set_page_load_timeout(60) |
execute_scriptを使わない場合はset_page_load_timeoutは必要ない。
implicitly_wait、set_page_load_timeoutはdriverを取得した直後にいちどだけ呼べばOK。
動的に生成される要素をexecute_scriptで扱う場合
クリックしたあと新しいページに飛ぶ場合はset_page_load_timeoutによりロードされるまで待ってくれるが、動的(javascript的)に新しい要素が生成される場合は待ってくれない。たとえば動的に生成された要素を(execute_scriptを使ってjavascriptで)クリックしたい場合は、WebDriverWaitクラスのuntilメソッドとEC.element_to_be_clickableメソッドを使う。
1 2 3 4 5 6 7 8 9 |
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By driver.get('url') WebDriverWait(driver, 30).until(EC.element_to_be_clickable((By.CSS_SELECTOR, 'CSSセレクタ'))) # クリックできるかチェック # WebDriverWait(driver, 30).until(EC.presence_of_element_located((By.CSS_SELECTOR, 'CSSセレクタ'))) # 要素の存在をチェックしたい場合 # WebDriverWait(driver, 30).until(EC.presence_of_all_elements_located((By.CSS_SELECTOR, 'CSSセレクタ'))) # 複数の要素の存在をチェックしたい場合 driver.execute_script("document.querySelector('CSSセレクタ').click();") |
EC.element_to_be_clickableはselenium的にクリック可能かをチェックしている(elem.click()できるか)。ただ、selenium的にクリックできるならjavascript的にもクリックできるので使える。
いちおうの頭に入れておきたいのは、仮にEC.element_to_be_clickableでクリック不可と判定されてもjavascriptではクリックできる場合もあるということ(たとえばほかの要素に隠れている場合)。
time.sleep()はできるだけ使わない。最終手段。
send_keysが失敗してしまう件
implicitly_wait、set_page_load_timeoutでタイムアウトを設定し、time.sleepをすべてなくしたところ、たびたび(いつもではないが)send_keysによる入力が失敗していることに気づいた。send_keysはエラーもはかないので特定にだいぶ時間がかかった。。。
send_keysのまえにtime.sleepをおかないと正しく入力されない。そうやってもいいけど、execute_scriptでjavascript的に直接書き込むと確実かつ速い。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# textのなかに「'」が入っているとjavascriptの実行に失敗するので事前にエスケープしておく。 text = re.sub("'", "\\'", text) # value属性を持つ要素の場合 driver.execute_script("document.getElementById('ID').value = '{}';".format(text)) # div要素の中に書き込む driver.execute_script( """ elem = document.querySelector('div'); elem.textContent = '{}'; """.format(text) ) |
とりあえず
elem.send_keys()・elem.click()とexecute_scriptは一緒に使わないほうがいい。time.sleep()を逐次使いながらならいけるけど、time.sleep()排除してimplicitly_wait・set_page_load_timeoutで済まそうとするとおかしなことになる。
WebDriverWait(明示的待機)は使いものにならない場合あり
実験してみると、expected_conditions(as EC)モジュールのvisibility_of_element_locatedメソッドは、おそらくvisibility属性がvisibleかhiddenか、あるいはdisplay属性がnoneかどうかだけみている感じ。実際にブラウザでみたときに見えるかどうかを判定してくれるわけではない。
ソースをみてみても、最終的にis_displayedメソッドのなかでexecute_scriptを使ってJavascript的に判定しているっぽい。
1 2 3 4 5 6 7 8 9 |
def is_displayed(self): """Whether the element is visible to a user.""" # Only go into this conditional for browsers that don't use the atom themselves if self._w3c and self.parent.capabilities['browserName'] == 'safari': return self.parent.execute_script( "return (%s).apply(null, arguments);" % isDisplayed_js, self) else: return self._execute(Command.IS_ELEMENT_DISPLAYED)['value'] |
position: absoluteにして要素を重ねるても、下にあって見えない要素はEC.visibility_of_element_locatedで見えると判定されるし、EC.element_to_be_clickableでクリッカブルであると判定される。実際にクリックすると、「・・・ is not clickable at point ・・・」といわれ例外となる。
その要素が最前面にあるかどうか判定してくれるメソッドはないっぽい。
execute_scriptを使ってJavascript的にクリックはできる。ただ、その結果メニューを出すなど狙った結果を引き出せない場合もある。そのページのスクリプトによる。たぶん要素が全面に出てないときはクリックイベントを拾っていないと思われる。
某サイトをスクレイプしようと思ったら、Seleniumを使っていることがバレバレで拒否されてしまった。
最初は要素のど真ん中をクリックしているからばれるのだろうと思い、
1 2 3 |
button = driver.find_element_by_css_selector('button[~~~="~~~"]') actions = ActionChains(driver) actions.move_to_element(button).move_by_offset(random.randint(5, 55), random.randint(3, 8)).click().perform() |
こんな感じで座標をずらしてクリックしてみたがだめだった。
Seleniumであること自体がばれているのかとあたりをつけて、「selenium 判別」でググったところずばりの情報がヒットした。Seleniumで自動操作されているブラウザーの場合、navigator.webdriverがtrueになっているのだと。つまりJavascriptで
1 2 3 |
if (navigator.webdriver) { location.href = 'error_page.html'; } |
みたいにすれば拒否することができる。navigator.webdriverをundefinedのままにするオプションはないようだ。navigator.webdriverにundefinedを設定してから目的のページに遷移してもtrueにセットされ直されるため、回避策はないようだ。
どうしても突破したいなら、ブラウザをWinAppDriverやPyAutoGUIから操作する手があるが、かなり面倒だろう。
ちなみに某サイトが利用しているボット判定サービスのページをみてみたらAIで判定していると豪語しているが、navigator.webdriverで判定してるだけじゃん。それが証拠にSeleniumでChromeを起動したのちinput関数で止めておいて、手動でクリックしてもばれたもの。
puppeteer
puppeteerというのを使ったらうまいこと潜り抜けられた。PythonではなくNode.jsなのでややコードを書くのが覚束ないが。
注意点としては、付属してくるChromeではなく元から存在するChromeをlaunchメソッドのオプションに指定しないとばれる。
途中までPyAutoGUIでやろうとしていたが、手間がかかりすぎる。