web-dev-qa-db-ja.com

Seleniumサーバーから要素IDを含むDOMツリー全体をダンプする必要があります

私はWeb自動化テストにpython Seleniumを使用しています。自動化の重要な部分は、HTMLページでユーザーに表示されるオブジェクトに適切な要素を見つけることです。次のAPIは、ほとんどの場合に機能します。時間ですが、常にではありません。

find_element_by_xxx,  xxx can be id, name, xpath, tag_name etc. 

HTMLページが複雑すぎる場合は、DOMツリーを検索したいと思います。 SeleniumサーバーにDOM全体をシリアル化するように依頼することが可能かどうか疑問に思います(Webドライバーサーバーを介してアクションを実行するために使用できる要素IDを使用)。クライアント側(Pythonスクリプト)は、独自の検索アルゴリズムを実行して適切な要素を見つけることができます。

python Seleniumは次の方法でhtmlページ全体を取得できることに注意してください

drv.page_source

ただし、これを解析しても、Seleniumサーバーの観点からは内部要素IDが得られないため、役に立ちません。

EDIT1:より明確にするために言い換えます(@alecxeに感謝):ここで必要なのは、Seleniumサーバー内のすべてのDOM要素(DOM構造が保持されている)のシリアル化された表現です。このシリアル化された表現は次のことができます。独自の検索を実行できるクライアント側(python Seleniumテストアプリ))に送信されます。

9
packetie

試してください:

find_elements_by_xpath("//*")

これは、ドキュメント内のすべての要素と一致する必要があります。

更新(質問の改良に一致するため):

Javascriptを使用して、DOMを文字列として返します。

execute_script("return document.documentElement.outerHTML")
5
David K. Hess

問題

わかりました。サーバー(ブラウザ)側ではなく、クライアント(Python)側でページの実質的な処理を実行する必要がある場合があります。たとえば、ある種の機械学習システムがすでにPythonで記述されていて、アクションを実行する前にページ全体を分析する必要がある場合、それを大量に実行することは可能ですがfind_element呼び出しの場合、各呼び出しはクライアントとサーバー間のラウンドトリップであるため、これは非常にコストがかかります。また、ブラウザーで機能するように書き換えると、コストがかかりすぎる可能性があります。

Seleniumの識別子がそれを行わない理由

ただし、DOMのシリアル化を一緒に Selenium独自の識別子で取得する効率的な方法がわかりません。 Seleniumは、find_elementを呼び出すとき、またはDOMノードがexecute_script呼び出しから返されるとき(またはexecute_async_scriptがスクリプトに与えるコールバックに渡されるとき)に、必要に応じてこれらの識別子を作成します。しかし、find_elementを呼び出して各要素の識別子を取得すると、正方形に戻ります。ブラウザのDOMを必要な情報で装飾することを想像できますが、WebElementidのある種の事前割り当てを要求するパブリックAPIはありません。実際のところ、これらの識別子は不透明になるように設計されているため、ソリューションが何らかの方法で必要な情報を取得できたとしても、クロスブラウザの実行可能性と継続的なサポートが心配になります。

解決策

ただし、両側で機能するアドレス指定システムを取得する方法があります:XPath。アイデアは、DOMシリアル化をクライアント側のツリーに解析してから、関心のあるノードのXPathを取得し、これを使用して対応するWebElementを取得することです。したがって、クリックを実行する必要がある単一の要素を決定するために数十のクライアントサーバーラウンドトリップを実行する必要がある場合は、これをページソースの最初のクエリと1つのfind_element呼び出しに減らすことができます。必要なXPath。

これは、非常に単純な概念実証です。 Googleのフロントページのメイン入力フィールドを取得します。

from StringIO import StringIO

from Selenium import webdriver
import lxml.etree

#
# Make sure that your chromedriver is in your PATH, and use the following line...
#
driver = webdriver.Chrome()
#
# ... or, you can put the path inside the call like this:
# driver = webdriver.Chrome("/path/to/chromedriver")
#

parser = lxml.etree.HTMLParser()

driver.get("http://google.com")

# We get this element only for the sake of illustration, for the tests later.
input_from_find = driver.find_element_by_id("gbqfq")
input_from_find.send_keys("foo")

html = driver.execute_script("return document.documentElement.outerHTML")
tree = lxml.etree.parse(StringIO(html), parser)

# Find our element in the tree.
field = tree.find("//*[@id='gbqfq']")
# Get the XPath that will uniquely select it.
path = tree.getpath(field)

# Use the XPath to get the element from the browser.
input_from_xpath = driver.find_element_by_xpath(path)

print "Equal?", input_from_xpath == input_from_find
# In JavaScript we would not call ``getAttribute`` but Selenium treats
# a query on the ``value`` attribute as special, so this works.
print "Value:", input_from_xpath.get_attribute("value")

driver.quit()

ノート:

  1. 上記のコードはdriver.page_sourceを使用していません。これは、Seleniumのドキュメントに、返されるものの鮮度について保証がないと記載されているためです。現在のDOMの状態、またはページが最初に読み込まれたときのDOMの状態である可能性があります。

  2. このソリューションには、動的コンテンツに関してfind_elementが抱える問題とまったく同じ問題があります。分析の実行中にDOMが変更された場合は、DOMの古い表現に取り組んでいます。

  3. 分析の実行中にJavaScriptイベントを生成する必要があり、これらのイベントによってDOMが変更される場合は、DOMを再度フェッチする必要があります。 (これは前のポイントと似ていますが、find_element呼び出しを使用するソリューションは、呼び出しのシーケンスを注意深く順序付けることにより、 this ポイントで話している問題を回避できると考えられます。)

  4. lxmlのツリーは、lxmlから取得したXPathがDOM内の対応する要素をアドレス指定しないように、DOMツリーと構造的に異なる可能性があります。 。 lxmlが処理するのは、ブラウザが渡すHTMLのクリーンアップされたシリアル化されたビューです。したがって、ポイント2と3で述べた問題を防ぐためにコードが記述されている限り、これはありそうなシナリオとは思われませんが、不可能ではありません。

13
Louis

Seleniumの識別子を取得する試みに関する問題については、私の その他の回答 を参照してください。

繰り返しますが、問題は、それらに関連付けられたラウンドトリップを回避するために、find_element呼び出しの束を減らすことです。

私の他の答えとは異なる方法は、execute_scriptを使用してブラウザで検索を実行し、必要なすべての要素を返すことです。たとえば、このコードでは3回のラウンドトリップが必要ですが、1回のラウンドトリップに減らすことができます。

el, parent, text = driver.execute_script("""
var el = document.querySelector(arguments[0]);
return [el, el.parentNode, el.textContent];
""", selector)

これにより、渡したいCSSセレクターに基づいて、要素、要素の親、および要素のテキストコンテンツが返されます。ページにjQueryが読み込まれている場合は、jQueryを使用して検索を実行できます。また、ロジックは必要に応じて複雑になる可能性があります。

この方法は、ラウンドトリップを減らすことが望ましい場合の大部分を処理しますが、他の回答の図で示したようなシナリオは処理しません。

1
Louis

ページオブジェクトパターンの活用を試みることができます。この場合、それはあなたが探しているものに近いように聞こえます。すべてをそれに変更するわけではないかもしれませんが、少なくともこの部分については、それを検討することをお勧めします。

http://Selenium-python.readthedocs.org/en/latest/test-design.html?highlight=page%20object

ページのすべての要素をループして、一度に1つずつ保存することもできますが、それを実行できるライブラリが必要です。 .NetにはhtmlAgilityがあることを私は知っています。 Pythonについてはよくわかりません。

更新私はこれを見つけました...多分それはあなたを助けるでしょう。 Python用のHTMLアジリティパック

0
mutt