使用 selenium 的 Python 并行执行
Posted
技术标签:
【中文标题】使用 selenium 的 Python 并行执行【英文标题】:Python parallel execution with selenium 【发布时间】:2017-08-01 15:48:14 【问题描述】:我对使用 selenium 在 python 中并行执行感到困惑。似乎有几种方法可以解决它,但有些方法似乎已经过时了。
有一个名为python-wd-parallel
的python 模块,它似乎有一些功能可以做到这一点,但它是从2013 年开始的,现在这还有用吗?我还找到了this example。
有concurrent.futures
,这似乎更新了很多,但实现起来并不容易。 谁有在 selenium 中并行执行的工作示例?
也有只使用线程和执行器来完成工作,但我觉得这会慢一些,因为它没有使用所有的内核并且仍然以串行形式运行。
使用 selenium 进行并行执行的最新方法是什么?
【问题讨论】:
【参考方案1】:使用joblib's Parallel 模块来做到这一点,它是一个很好的并行执行库。
假设我们有一个名为 urls
的 url 列表,我们希望并行截取每个 URL 的屏幕截图
首先让我们导入必要的库
from selenium import webdriver
from joblib import Parallel, delayed
现在让我们定义一个将屏幕截图作为base64的函数
def take_screenshot(url):
phantom = webdriver.PhantomJS('/path/to/phantomjs')
phantom.get(url)
screenshot = phantom.get_screenshot_as_base64()
phantom.close()
return screenshot
现在要并行执行,您要做的是
screenshots = Parallel(n_jobs=-1)(delayed(take_screenshot)(url) for url in urls)
当这一行完成执行时,您将在screenshots
中拥有来自所有已运行进程的所有数据。
关于并行的解释
Parallel(n_jobs=-1)
表示使用所有可以使用的资源
delayed(function)(input)
是 joblib
为您尝试并行运行的函数创建输入的方式
更多信息可以在joblib
文档中找到
【讨论】:
是否有任何直接的方法可以为一个 n_jobs 重用一个 webdriver.PhantomJS 实例,而不是每次迭代都关闭和打开? 您为什么要这样做?尝试从多个进程访问一个 webdriver 实例似乎是个坏主意——我相信这会损害并行化。无论如何,如果您决定继续这样做,则必须使 webdriver 可序列化 非常感谢。我的理由是为每个进程设置一个驱动程序实例(而不是一个驱动程序实例用于多个进程),因为在“如何加速硒”列表中,“重用驱动程序实例”这一行项几乎排在首位 为了不重新创建实例,我会将urls
列表切割成大小均匀的子列表,然后将它们发送到进程,这样就可以生成进程(以及创建 webdriver 实例)每个进程只会发生一次
我认为这取决于。这完全是生成进程和在它们之间传输数据的开销的函数,这与并行化的好处相悖 - 但大多数情况下,如果操作不是很短,您将从使用并行实现中受益【参考方案2】:
-
Python Parallel Wd 接缝从它的 github 上消失了(最后一次提交是 9 年前)。它还为硒实现了obsolete protocol。我还没有测试过,我不推荐。
Selenium 性能提升 (concurrent.futures)
简答
threads
和 processes
都会为您的硒代码提供相当大的加速。
下面给出了简短的例子。 selenium 工作由返回页面标题的selenium_title
函数完成。这不处理每个线程/进程执行期间发生的异常。对于那种外观长答案 - 处理异常。
-
线程池
concurrent.futures.ThreadPoolExecutor
。
from selenium import webdriver
from concurrent import futures
def selenium_title(url):
wdriver = webdriver.Chrome() # chrome webdriver
wdriver.get(url)
title = wdriver.title
wdriver.quit()
return title
links = ["https://www.amazon.com", "https://www.google.com"]
with futures.ThreadPoolExecutor() as executor: # default/optimized number of threads
titles = list(executor.map(selenium_title, links))
-
进程池工人
concurrent.futures.ProcessPoolExecutor
。只需要将上面代码中的ThreadPoolExecuter
替换为ProcessPoolExecutor
。它们都派生自Executor
基类。此外,您必须保护 main,如下所示。
if __name__ == '__main__':
with futures.ProcessPoolExecutor() as executor: # default/optimized number of processes
titles = list(executor.map(selenium_title, links))
长答案
为什么Threads
与 Python GIL 一起工作?
由于 Python GIL,即使是严格的 Python 也对线程有限制,即使线程将被上下文切换。 Selenium 的实现细节将带来性能提升。 Selenium 通过发送诸如POST
、GET
(HTTP requests
) 之类的命令来工作。这些被发送到浏览器驱动程序服务器。因此,您可能已经知道 I/O 绑定任务 (HTTP requests
) 会释放 GIL,从而提高性能。
处理异常
我们可以对上面的例子做一些小的修改来处理产生的线程上的Exceptions
。我们不使用executor.map
,而是使用executor.submit
。这将返回包装在 Future
实例上的标题。
要访问返回的标题,我们可以使用future_titles[index].result
,其中索引大小为len(links)
,或者简单地使用for
,如下所示。
with futures.ThreadPoolExecutor() as executor:
future_titles = [ executor.submit(selenium_title, link) for link in links ]
for future_title, link in zip(future_titles, links):
try:
title = future_title.result() # can use `timeout` to wait max seconds for each thread
except Exception as exc: # this thread migh have had an exception
print('url :0 generated an exception: :1'.format(link, exc))
请注意,除了迭代 future_titles
之外,我们还会迭代 links
,所以如果某个线程中的 Exception
我们知道哪个 url(link)
对此负责。
futures.Future
类很酷,因为它们让您可以控制从每个线程接收到的结果。比如它是否正确完成或有异常等等,更多关于here。
另外值得一提的是futures.as_completed
更好,如果您不关心线程返回项目的顺序。但由于控制异常的语法有点难看,我在这里省略了它。
性能增益和线程
首先为什么我一直使用线程来加速我的 selenium 代码:
在 I/O 绑定任务上,我使用 selenium 的经验表明,在使用进程池 (Process
) 或线程 (Threads
) 之间存在minimal or no diference。 Here 对于 I/O 绑定任务上的 Python 线程与进程也得出了类似的结论。
我们也知道进程使用自己的内存空间。这意味着更多的内存消耗。此外,进程的生成速度也比线程慢一些。
【讨论】:
以上是关于使用 selenium 的 Python 并行执行的主要内容,如果未能解决你的问题,请参考以下文章
使用 selenium TestNG 和 selenium 进行并行测试
selenium+python自动化90-unittest多线程执行用例