在Python中捕获`KeyboardInterrupt`而不关闭Selenium Webdriver会话

26
一个Python程序通过Selenium WebDriver驱动Firefox。代码嵌入了一个类似于以下的try/except块中:
session = selenium.webdriver.Firefox(firefox_profile)
try:
    # do stuff
except (Exception, KeyboardInterrupt) as exception:
    logging.info("Caught exception.")
    traceback.print_exc(file=sys.stdout)

如果程序因为错误而中止,WebDriver会话不会关闭,因此Firefox窗口仍然保持打开状态。但是,如果程序因为KeyboardInterrupt异常而中止,则Firefox窗口会关闭(我想这是因为WebDriver会话也被释放了),我想避免这种情况。
我知道这两个异常都通过同一个处理程序,因为在两种情况下都可以看到"Caught exception"消息。
如何避免使用KeyboardInterrupt时关闭Firefox窗口?

1
因为您在except语句中包含了非常通用和广泛的异常类Exception。尝试将自己限制在KeyboardInterrupt上,看看是否有效。 - Abhinav
我无法在Windows 7上使用Firefox 52.1,geckodriver 0.16.1和Selenium 3.9.0重现此问题。您能否请告知您正在使用的操作系统以及Firefox、geckodriver和Selenium的版本? - James
我也无法重现这个问题。在我的测试中,你已经编写的代码在 Windows 7 上使用最新版本的 Firefox、Geckodriver 和 Selenium 已经按照预期运行。但是,Chrome 的行为不同,因为它在两种情况下都被关闭了。 - sytech
3个回答

7

我有一个解决方案,但它相当丑陋。

当按下Ctrl+C时,Python会接收到一个中断信号(SIGINT),该信号会在您的进程树中传播。Python还会生成一个KeyboardInterrupt,因此您可以尝试处理与您的进程逻辑绑定的内容,但与子进程耦合的逻辑无法受到影响。

要影响传递给子进程的信号,您必须在通过subprocess.Popen生成进程之前指定如何处理信号。

有各种选项,这个选项来自另一个答案

import subprocess
import signal

def preexec_function():
    # Ignore the SIGINT signal by setting the handler to the standard
    # signal handler SIG_IGN.
    signal.signal(signal.SIGINT, signal.SIG_IGN)

my_process = subprocess.Popen(
    ["my_executable"],
    preexec_fn = preexec_function
)

问题在于,你没有调用Popen,而是委托给selenium。关于这个问题,有各种讨论。据我所知,其他试图影响信号屏蔽的解决方案在屏蔽操作没有在调用Popen之前执行时容易失败。
此外,请记住,Python文档中有一个关于使用preexec_fn的重要警告,因此请自行决定是否使用它。
“幸运的是”,Python允许在运行时覆盖函数,因此我们可以这样做:
>>> import monkey
>>> import selenium.webdriver
>>> selenium.webdriver.common.service.Service.start = monkey.start
>>> ffx = selenium.webdriver.Firefox()
>>> # pressed Ctrl+C, window stays open.
KeyboardInterrupt
>>> ffx.service.assert_process_still_running()
>>> ffx.quit()
>>> ffx.service.assert_process_still_running()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python3.6/site-packages/selenium/webdriver/common/service.py", line 107, in assert_process_still_running
    return_code = self.process.poll()
AttributeError: 'NoneType' object has no attribute 'poll'

如下所示,使用 monkey.py

import errno
import os
import platform
import subprocess
from subprocess import PIPE
import signal
import time
from selenium.common.exceptions import WebDriverException
from selenium.webdriver.common import utils

def preexec_function():
    signal.signal(signal.SIGINT, signal.SIG_IGN)

def start(self):
  """
        Starts the Service.
        :Exceptions:
         - WebDriverException : Raised either when it can't start the service
           or when it can't connect to the service
        """
  try:
    cmd = [self.path]
    cmd.extend(self.command_line_args())
    self.process = subprocess.Popen(cmd, env=self.env,
                                    close_fds=platform.system() != 'Windows',
                                    stdout=self.log_file,
                                    stderr=self.log_file,
                                    stdin=PIPE,
                                    preexec_fn=preexec_function)
#                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  except TypeError:
    raise
  except OSError as err:
    if err.errno == errno.ENOENT:
      raise WebDriverException(
        "'%s' executable needs to be in PATH. %s" % (
          os.path.basename(self.path), self.start_error_message)
      )
    elif err.errno == errno.EACCES:
      raise WebDriverException(
        "'%s' executable may have wrong permissions. %s" % (
          os.path.basename(self.path), self.start_error_message)
      )
    else:
      raise
  except Exception as e:
    raise WebDriverException(
      "The executable %s needs to be available in the path. %s\n%s" %
      (os.path.basename(self.path), self.start_error_message, str(e)))
  count = 0
  while True:
    self.assert_process_still_running()
    if self.is_connectable():
      break
    count += 1
    time.sleep(1)
    if count == 30:
      raise WebDriverException("Can not connect to the Service %s" % self.path)

这段代码是从Selenium开始的(链接),高亮显示的部分是添加的一行代码。 这是一个简陋的hack,可能会有问题。祝好运:D


1
你在导入中缺少了这个:from selenium.common.exceptions import WebDriverException - xApple

4

我受到了@einsweniger答案的启发,非常感谢!这段代码对我起了作用:

import subprocess, functools, os
import selenium.webdriver

def new_start(*args, **kwargs):
    def preexec_function():
        # signal.signal(signal.SIGINT, signal.SIG_IGN) # this one didn't worked for me
        os.setpgrp()
    default_Popen = subprocess.Popen
    subprocess.Popen = functools.partial(subprocess.Popen, preexec_fn=preexec_function)
    try:
        new_start.default_start(*args, **kwargs)
    finally:
        subprocess.Popen = default_Popen
new_start.default_start = selenium.webdriver.common.service.Service.start
selenium.webdriver.common.service.Service.start = new_start

相较于之前的答案,它更不会干扰代码的完整函数,因为没有重写代码。但是另一方面,它修改了subprocess.Popen函数本身,这可能被某些人称为相当丑陋的做法。

总之,这样做能够完成工作,并且当源代码的 Service.start发生变化时,你不必更新代码。


我在new_start.default_start(*args, **kwargs)通过subprocess.Popen = functools.partial(subprocess.Popen, preexec_fn=preexec_function)遇到了"maximum recursion depth"错误。 - undefined
@philipkd 你应该只运行这段代码一次。这段代码将原始的 start 方法存储在 new_start.default_start 中,如果你执行两次,它将存储 new_start,这就解释了递归错误。 - undefined
为了让代码只运行一次,你可以将它放在if not hasattr(selenium.webdriver.common.service.Service.start, 'default_start'): ...的内部。 - undefined

0
我受到@User9123的回答的启发,并稍作修改:
import functools
import subprocess
from selenium.webdriver import Firefox

subprocess_Popen = subprocess.Popen
subprocess.Popen = functools.partial(subprocess_Popen, process_group=0)
driver = Firefox()
subprocess.Popen = subprocess_Popen  # Undo the monkey patch

我目前只在Ubuntu上尝试过这个,但它完全符合我的期望!

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接