at backyard

Color my life with the chaos of trouble.

SeleniumでshadowRoot内の要素をJavaScript(execute_script)を利用しなくても取得できるようになった

f:id:shinshin86:20220114233129p:plain

はじめに

このポストは元々 PythonSelenium でshadowRootを取得するときにdictで取得されてしまう件 (chromedriver-binaryに関する問題) というタイトルで書かれたものだったが、2021年11月のSeleniumのshadowRootに関する対応内容に合わせて、タイトルを書き換えると同時に、大幅に加筆をしている。
(これは元々このポスト内で書かれていた内容と関連性が高いものであったと同時に、現在ではより良い書き方ができるようになっているため、そちらについての内容も記載した次第である)

以下、本文。

SeleniumのshadowRoot周りのアップデートについて

最近改めてSelenium周りのことを調べていたときにshadowRoot周りでいい感じのアップデートが入っていたことに気づいた。

そのためこの記事はそれを踏まえた上で、加筆している。

前半部分はもともとのこの記事の主題である PythonSelenium でshadowRootを取得するときにdictで取得されてしまう件 (chromedriver-binaryに関する問題) という内容と、AttributeError: 'dict' object has no attribute 'find_element_by_class_name'というエラーに関する内容を書いているが、後半はSeleniumでshadowRoot周りの処理を書く際のことなどを追記した。

具体的にはshadowRoot内の要素を取得する際にもJavaScript(execute_script)を使用せずに取得できるようになっている。

このshadowRoot周りのアップデートはSelnium利用者としては嬉しいアップデートである。

目次

AttributeError: 'dict' object has no attribute 'find_element_by_class_name'

SeleniumでShadow DOM配下(shadowRoot配下)の要素を取得するときは下記のようなコードを挟む必要がある。
(このコードを挟まずにそのまま取得しようとすると、失敗する)

shadow_root_element = driver.execute_script('return arguments[0].shadowRoot', hoge)

# shadowRoot配下の要素をclass名をもとに取得する場合
name = shadow_root_element.find_element_by_class_name("fuga")

ただ、今回 chromedriver-binary を下記のバージョンに上げたら、ここの処理で問題が発生した。

chromedriver-binary==96.0.4664.45.0

エラーの内容は下記の通りで、elementではなくdictで返ってきてしまうようになっていた。

AttributeError: 'dict' object has no attribute 'find_element_by_class_name'

この問題に対しては既にissueが上がっている。

github.com

chromedriver-binary==95.0.4638.69.0を利用することで解決

解決方法としてはバージョンを下げたものをひとまずは利用すれば良いようだ。
(私が利用していたChromeは96だったが、以下のバージョンでも問題なく動作した。)

pip install chromedriver-binary==95.0.4638.69.0

※ここまでが前半の内容(chromedriver_binaryに関する内容)となり、以降は2021年11月のアップデートでSeleniumのshadowRoot周りの処理が書きやすくなった件についての内容となる。現時点で最新のSeleniumやchromedriver-binaryを利用していれば、ここまでに書いた前半部分の内容については解消しているかと思われる。

2021年11月のアップデートでSeleniumのshadowRoot周りの処理が書きやすくなった

2021年11月のアップデートでSeleniumのshadowRoot周りに関するアップデートが入っていた。

アップデートの内容は下記のポストがとても参考になる。ここではPython以外にもJavaなどの書き方なども載っている他、Pythonseleniumで遭遇し、こちらのポストの前半部分にも書いていた下記のエラーに関する説明なども記載されている。

AttributeError: 'dict' object has no attribute 'find_element_by_css_selector'

そのためSeleniumを利用している方はぜひ一度目を通しておくと、色々と便利になっていて感動するかもしれない。
(私は感動した)

titusfortner.com

shadowRootの書き方が便利になったというが、実際どうやって取得できるようになったか?

具体的な内容は上のポストを見ていただくのが良いが、例えば下記のような形でshadowRootを取得することが可能。
なお、このコードは上のポストから引用しており、コメントは私の方でつけた。
このようにshadow_rootと直接指定取得できるのはかなり便利である。

# shadowRootを持っているelementを取得
shadow_host = driver.find_element(By.CSS_SELECTOR, '#shadow_root')

# shadow_rootと直接指定して取得できる!
shadow_root = shadow_host.shadow_root

# 直接取得したshadow_rootに対してfind_elementで取得
shadow_content = shadow_root.find_element(By.CSS_SELECTOR, '#shadow_content')

# 取得したelementのテキストを取得
print(shadow_content.text)

今まであれば、JavaScript( driver.execute_script )を使って下記のように書く必要があった。
(この書き方はこのポストの前半でも記述しているやり方である)

shadow_root = driver.execute_script("return arguments[0].shadowRoot", shadow_host)

これをやらずに行けるようになったのは嬉しい限りである。

上のコードで複数行に跨いで書いているが、勿論下記のように普通に shadow_root を跨いで処理が行えるのである。これはかなり便利!

shadow_host = driver.find_element(By.CSS_SELECTOR, '#shadow_root')

# 下記のような形で get_attributeなどももちろん使える。注) せっかくなので書いてみただけです
text_content = shadow_host.shadow_root.find_element(By.CSS_SELECTOR, "style").get_attribute("textContent")

shadow_rootのelementに対してBy.TAG_NAMEで取得しようとしてもうまく取得できない問題がある

これは便利だと思って色々と試していたらうまく動かない箇所があったので、そちらも併せて紹介したい。

例えば以下のような形で、shadow_rootに対してfind_elementでtag_nameを取得しようとしてうまく動かない。

print(elem.shadow_root.find_element(By.TAG_NAME, "style"))

この処理を行うと下記のようなエラーとなる。

selenium.common.exceptions.InvalidArgumentException: Message: invalid argument: invalid locator

これはなぜだろうと思って調べていたら、下記のようなコメントを見つけた。

github.com

詳細はリンク先のやり取りを読んでもらいたいが、現状は我々が普段利用している find_element(By.TAG_NAME, "tag_name") と同じような動作は行われないようだ。

そのため、上のような処理を行いたい場合は、下記のように By.CSS_SELECTOR を利用する。

print(elem.shadow_root.find_element(By.CSS_SELECTOR, "style"))

find_element_by_class_nameなどはdeprecated。今後はfind_elementメソッドを利用するようにしていく必要がある

またshadowRootは最新のfind_elementメソッドのみを利用するので、By を利用して指定する必要がある。

下記のようなエラーが出た場合、一度処理の内容を確認する必要がある。
(私も最初このエラーに出会ったが、どんなコードを書いていたか思い出せないので、ひとまず上に貼ったポスト内の説明だけを引用する)

AttributeError: 'ShadowRoot' object has no attribute 'find_element_by_css_selector'

またSeleniumでは find_element_by_class_name というような書き方はdeprecated となっており、現状最新のSeleniumでこれらのメソッドを利用するとwarningが出るようになっている。

書き換え方は以下のような形となる。
(詳しくは公式ドキュメントを参照)

find_element_by_class_name("foo")
↓
find_element(By.CLASS_NAME, "foo")

というわけで、Seleniumが便利になっていたという内容を追加させてもらった。