需要从Selenium服务器导出整个DOM树及元素ID
我一直在用Python的Selenium做网页自动化测试。自动化的关键部分就是找到网页上用户能看到的对象对应的元素。下面这个API大部分情况下都能用,但并不是每次都有效。
find_element_by_xxx, xxx can be id, name, xpath, tag_name etc.
当网页的HTML结构太复杂时,我想要搜索DOM树。我在想,能不能让Selenium服务器把整个DOM序列化(也就是把所有元素的ID都列出来,这样就可以通过webdriver服务器进行操作)。客户端(也就是Python脚本)可以用自己的搜索算法来找到合适的元素。
需要注意的是,Python的Selenium可以通过下面的方式获取整个HTML页面:
drv.page_source
但是,解析这个内容并不能从Selenium服务器的角度获取内部元素的ID,所以这并没有什么用。
编辑1:为了让内容更清楚(感谢@alecxe):这里需要的是Selenium服务器中所有DOM元素的序列化表示(保持它们的DOM结构),这个序列化的表示可以发送到客户端(一个Python的Selenium测试应用),然后客户端可以自己进行搜索。
6 个回答
我知道的有两种方法是:
get_source = driver.page_source
第二种是使用JavaScript:
pageSource = driver.execute_script("return document.documentElement.outerHTML;")
你可以试试使用页面对象模式。这种方法听起来更符合你目前的需求。虽然你可能不需要把所有的东西都改成这种方式,但至少在这一部分你可以考虑一下。
http://selenium-python.readthedocs.org/en/latest/test-design.html?highlight=page%20object
你也可以遍历页面上的所有元素,然后一个一个地保存它们,但应该有一些库可以做到这一点。我知道在.Net中有htmlAgility,但我不太确定在Python中有没有类似的东西。
更新
我找到了这个……也许对你有帮助。Html Agility Pack for python
请查看我的其他回答,了解关于获取Selenium标识符时遇到的问题。
问题在于我们需要减少很多find_element
的调用,以避免因此产生的多次请求。
我提供的另一种方法是使用execute_script
来在浏览器中进行搜索,然后返回所有需要的元素。例如,下面这段代码原本需要三次请求,但可以减少到只需要一次请求:
el, parent, text = driver.execute_script("""
var el = document.querySelector(arguments[0]);
return [el, el.parentNode, el.textContent];
""", selector)
这段代码会根据我传入的CSS选择器返回一个元素、该元素的父元素以及该元素的文本内容。如果页面加载了jQuery,我可以用jQuery来进行搜索。逻辑可以根据需要变得复杂。
这种方法可以解决大多数希望减少请求次数的情况,但对于我在其他回答中举的例子,它并不能解决所有问题。
问题
有时候,你可能需要在客户端(Python)处理网页,而不是在服务器(浏览器)上处理。例如,如果你有一个已经用Python写好的机器学习系统,它需要在执行操作之前分析整个网页。虽然可以通过多次调用find_element
来实现,但这样做会很耗费资源,因为每次调用都需要在客户端和服务器之间来回传输。而把它改写成在浏览器中运行可能成本也太高了。
为什么Selenium的标识符不行
不过,我觉得用Selenium的标识符来获取DOM的序列化信息并没有一个高效的方法。Selenium会在需要的时候创建这些标识符,比如你调用find_element
或者从execute_script
调用中返回DOM节点时。但如果你对每个元素都调用find_element
来获取标识符,那你就又回到了原点。我可以想象在浏览器中给DOM加上所需的信息,但没有公开的API可以请求某种形式的WebElement
ID的预分配。实际上,这些标识符是设计得不透明的,即使有解决方案能获取所需的信息,我也会担心它在不同浏览器中的兼容性和后续支持。
解决方案
不过,有一种方法可以在客户端和服务器端都能用的地址系统:XPath。这个想法是将DOM序列化解析成树状结构,然后获取你感兴趣的节点的XPath,并用这个XPath来获取对应的WebElement。所以,如果你需要进行多次客户端和服务器之间的往返来确定需要点击的单个元素,你可以将这个过程简化为一次查询页面源代码加上一次带有所需XPath的find_element
调用。
这里有一个超级简单的概念验证示例。它获取了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()
注意:
上面的代码没有使用
driver.page_source
,因为Selenium的文档说明它返回的内容不一定是最新的。它可能是当前DOM的状态,也可能是页面首次加载时的DOM状态。这个解决方案和
find_element
在处理动态内容时遇到的问题是一样的。如果在分析过程中DOM发生变化,那么你得到的就是一个过时的DOM表示。如果在分析过程中需要生成JavaScript事件,而这些事件又改变了DOM,那么你需要重新获取DOM。(这和前一点类似,但使用
find_element
的解决方案可以通过仔细安排调用顺序来避免我在这一点中提到的问题。)lxml
的树结构可能和DOM树在结构上有所不同,以至于从lxml
获得的XPath无法正确指向DOM中的对应元素。lxml
处理的是浏览器传递给它的HTML的清理后的序列化视图。因此,只要代码编写得能防止我在第2和第3点提到的问题,我认为这种情况不太可能发生,但也不是不可能。
试试这个:
find_elements_by_xpath("//*")
这样应该能匹配文档中的所有元素。
更新(为了更好地符合问题的细节):
使用JavaScript,并将DOM返回为字符串:
execute_script("return document.documentElement.outerHTML")