解决selenium的sends_key发送文本的执行缓慢和卡顿的问题

在项目中使用selenium的send_keys中向body中添加大文本的数据时,会出现页面卡顿的现象。

源码解析, 问题原因

为此, 特地看了下send_keys的源码。先上源码, 再做分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
def send_keys(self, *value):
"""Simulates typing into the element.

:Args:
- value - A string for typing, or setting form fields. For setting
file inputs, this could be a local file path.

Use this to send simple key events or to fill out form fields::

form_textfield = driver.find_element_by_name('username')
form_textfield.send_keys("admin")

This can also be used to set file inputs.

::

file_input = driver.find_element_by_name('profilePic')
file_input.send_keys("path/to/profilepic.gif")
# Generally it's better to wrap the file path in one of the methods
# in os.path to return the actual path to support cross OS testing.
# file_input.send_keys(os.path.abspath("path/to/profilepic.gif"))

"""
# transfer file to another machine only if remote driver is used
# the same behaviour as for java binding
if self.parent._is_remote:
local_file = self.parent.file_detector.is_local_file(*value)
if local_file is not None:
value = self._upload(local_file)

self._execute(Command.SEND_KEYS_TO_ELEMENT,
{'text': "".join(keys_to_typing(value)),
'value': keys_to_typing(value)})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
def keys_to_typing(value):
"""Processes the values that will be typed in the element."""
typing = []
for val in value:
if isinstance(val, Keys):
typing.append(val)
elif isinstance(val, int):
val = str(val)
for i in range(len(val)):
typing.append(val[i])
else:
for i in range(len(val)):
typing.append(val[i])
return typing

如果你在程序中善于调试的话,应该会想看看_execute函数中传递进去的数据长什么样子,如果你没看过,没关系,通过分析keys_to_typing函数,一样可以知道为什么出现页面卡顿现象了。

keys_to_typing函数是执行的CPU密集行操作, 在数据量小的时候, 遍历的次数少, 可能体会不到明显的页面卡顿现象。但是如果数据量很大,例如解析后的带有多个iframe的页面源码, 想要将它输入到文本框时就会出现长时间的页面卡顿现象。

细看keys_to_typing,首先会创建一个列表, 然后将数据切片,切成单个字符,再调用append方法添加到typing中。这样会导致短暂性CPU飙升100%, 也会额外开辟更多的内存空间。导致出现卡顿现象。

注:
Python中的列表是具备动态扩容机制的,每次扩容所需要的时间复杂度为O(N), 每次都会在原来列表长度的所占用的内存的基础上申请额外的1/8数组长度+ newsize < 9 ? 3:6 的内存余量。因此频繁扩容带来不断的拷贝数据,导致CPU飙升。

解决方案

有两种解决方案。
1.不使用send_keys, 编写javascript代码,通过selenium驱动执行js。
2.重写send_keys源码, 不切分字符串为单个字符。

这里我使用第二种方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14

def send_keys(web_element: WebElement, *value) -> None:
"""
优化selenium的send_keys传输文本的性能
支持str, int, List, Tuple, Dict, Set X数据类型sa
"""
if web_element.parent._is_remote:
local_file = web_element.parent.file_detector.is_local_file(*value)
if local_file is not None:
value = web_element._upload(local_file)

web_element._execute(Command.SEND_KEYS_TO_ELEMENT,
{'text': " ".join(keys_to_typing(value)),
'value': keys_to_typing(value)})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def keys_to_typing(values: t.Tuple) -> t.List:
"""
类型转化
对非内置类型的可迭代对象,需实现__iter__内置方法,且返回可迭代类型
"""
res = []
for value in values:
if isinstance(value, str):
res.append(value)
elif isinstance(value, list):
res.extend(value)
elif isinstance(value, (set, tuple)):
res.extend(list(value))
elif isinstance(value, dict):
res.extend([f'{key}:{value}' for key, value in value.items()])
elif isinstance(value, Keys):
res.append(value)
elif isinstance(value, t.Iterable):
res.extend(iter(value))
return res

调用方式:

send_keys(input_box, text1, text2)

input_box为WebElement对象, text1、text2可以为int, str, 非嵌套的list, 非嵌套的tuple, 非嵌套的set, 非嵌套的dict以及非嵌套的自定义可迭代对象

这样一来,执行速度就提升很多了。