使用Python Selenium 实现拖拽动作时动作不流畅,问题排查和解决

848 阅读4分钟

出现了标题问题,没有达到预期的效果。上网也查找一些有没有相关的内容的说明和解决方案,但是大多数解决方案与我这个情况好像不相关。特此将我自己的解决方案记录下来,供人参考。

着急可以直接看 结果。或者 升级到 selenium 4.0 版本,就能解决这个问题。

看文章前,先确认下自己的环境与我的是否一致,因为这个文章的时间比较久了。如果用的新版本可能已经解决了这个问题的发生。

环境

  • macOS 10.14.4
  • python 3.7.2
  • selenium 3.141.0
  • chrome 76.0.3809.132(64位)
  • webdriver是和chrome相匹配的版本

问题

最近由于工作原因,需要自动登录一些平台的账户。但是最近发现有时登录时,经常弹出滑块让你进行拖拽验证。理论上直接破解js的加密函数是可以做到的,但是太费时间了。出于简单的目的,于是使用 python 的 selenium 库进行用户模拟操作处理。

在一切就绪后,发现了一个问题,就是拖拽的动作很不流畅,感觉是一顿一顿的移动,根本无法达到“拟人”的操作效果。

查看主要实现拖拽部分的代码,感觉也没有什么问题。主要代码如下:

# 选中要拖拽的元素
slider_el = self.wait.until(EC.element_to_be_clickable((By.CLASS_NAME, 'xxx')))
# 然后‘点击并按住’。
ActionChains(self.driver).click_and_hold(slider_el).perform()

# tracks 是生成一些列的X轴位移距离数组。例如:[0,0,0,1,1,1,2,5,7, ...]
# 实现正向滑动
for track in tracks:
    yoffset_random = random.uniform(-2, 4)
    # xoffset、yoffset在函数内部都会被转为整数的。
    ActionChains(self.driver).move_by_offset(xoffset=track, yoffset=yoffset_random).perform()

PS:这里有一些网站可能可能因为元素设计的原因,导致你获取到元素后,直接click_and_hold拖拽失效。可以尝试让让焦点稍微位移几个元素,然后再尝试后续动作。

效果如下: 拖拽(抖动)示例 拖拽(抖动)示例2 虽然在示例中,第一次验证通过了(真的是碰巧)。但是可以发现在整个的运动过程中,每次移动后都会出现一个短暂的停顿效果,尤其在速度比较快的时候感觉特别明显。

解决

由于没能找到别人的解决方法,只能自己动手找找了。看文档没发现什么还能传递的特殊函数可以控制。那么就先从拖动方法的源码看起。

1. 外部调用的移动鼠标的函数

方法 ActionChains(self.driver).move_by_offset(x, y) 文件 /selenium/webdriver/common/action_chains.py

def move_by_offset(self, xoffset, yoffset):
    """
    Moving the mouse to an offset from current mouse position.

    :Args:
     - xoffset: X offset to move to, as a positive or negative integer.
     - yoffset: Y offset to move to, as a positive or negative integer.
    """
    if self._driver.w3c:
        self.w3c_actions.pointer_action.move_by(xoffset, yoffset)
        self.w3c_actions.key_action.pause()
    else:
        self._actions.append(lambda: self._driver.execute(
            Command.MOVE_TO, {
                'xoffset': int(xoffset),
                'yoffset': int(yoffset)}))
    return self

2. 第一步就是调用移动鼠标的函数的实现

方法 self.w3c_actions.pointer_action.move_by(xoffset, yoffset) 文件 /selenium/webdriver/common/actions/pointer_actions.py

def move_by(self, x, y):
    self.source.create_pointer_move(origin=interaction.POINTER, x=int(x), y=int(y))
    return self

3. 上一步它初始化了一个类,并调用这个的类的具体实现函数

方法 self.source.create_pointer_move(origin=interaction.POINTER, x=int(x), y=int(y)) 文件 /selenium/webdriver/common/actions/pointer_input.py

class PointerInput(InputDevice):
	
    DEFAULT_MOVE_DURATION = 250

    def __init__(self, kind, name):
        super(PointerInput, self).__init__()
        if (kind not in POINTER_KINDS):
            raise InvalidArgumentException("Invalid PointerInput kind '%s'" % kind)
        self.type = POINTER
        self.kind = kind
        self.name = name

    def create_pointer_move(self, duration=DEFAULT_MOVE_DURATION, x=None, y=None, origin=None):
        action = dict(type="pointerMove", duration=duration)
        action["x"] = x
        action["y"] = y
        if isinstance(origin, WebElement):
            action["origin"] = {"element-6066-11e4-a52e-4f735466cecf": origin.id}
        elif origin is not None:
            action["origin"] = origin

        self.add_action(action)
    ...

到这里我们找到拖拽这个动作的根源了。 def create_pointer_move(self, duration=DEFAULT_MOVE_DURATION, x=None, y=None, origin=None) 该方法有4个参数,其中x,y是我们传递的,origin是在上一层方法传入的参数,而 duration是一个默认值,且没有在class初始化时传入,也没有通过方法传入。DEFAULT_MOVE_DURATION = 250 根据名字我猜到这个参数应该就是用来控制每次执行移动的消耗实现。根据测试其值应该是毫秒值。

那么我们只需要修改它应该就可以减少每次执行的时间,来增加拖动的流畅度。我是直接在源码里修改这个值的大小测试的。最后结果如下图:

调整后示例 调整后示例2

我调整到50左右,反正从我这里看,这个拖动可以算作是流畅了,我的目的也达到了。测试的时候,通过率基本能达到9成左右。超过5成测试都是一次性通过的。

结果

修改 selenium 源码 /selenium/webdriver/common/actions/pointer_input.py 文件中的 DEFAULT_MOVE_DURATION = 250 的值,到50左右,这个值应该是用于控制动作的执行时间。

最后附上项目地址: github.com/RysisLiang/…

本内容仅供学习交流讨论,没有任何针对性。如果有其它的解决方案,欢迎留言。