Python+Selenium 如何使用execute_async_script的callback

Selenium如何使用execute_async_script的callback

前言

在我百度的时候,貌似关于execute_script的讲解不少,但是关于execute_async_script的讲解不是很多,还看到有文章在问callback为什么不能用(写在python里是肯定不能用的……)所以自己也写一篇关于execute_async_script的用法。

基础

很多用法上的错误是源于对概念、模型等问题理解有偏差造成的,这里必须先阐述一下关于Python、JS的一些基础概念或者相关理论,不然只有代码,可能永远不能理解问题本质。
如果你对JS的异步机制(例如Promise等)已经很了解,可以跳过基础部分
如果你只是不想看废话,可以直接跳到完整样例和其他几个例子,相信聪明的你肯定一看就懂

1. JS异步

JS是一个单线程语言,这是针对浏览器设计的,JS本身不支持任何延迟、卡顿、锁死线程的操作(谁都不想上网的时候画面卡住吧?),取而代之的是使用 回调技术 来完成延时或者很耗时的任务。
例如,如果在JS中想要调用funcA,然后1秒钟后调用funcB,那么必须这么写:

funcA()
setTimeout(funcB, 1000)

仔细观察funcA后面是有括号的,funcB后面没有括号,这是将funcB作为参数传入setTimeout函数的意思,也就是通知主线程,1s后调用funcB

这一行代码并不会造成线程卡顿,它只是将funcB加入了JS的任务队列中,等待执行,如果后面有代码,后面的代码会立刻执行。例如下面的代码,执行顺序是A -> C -> B

funcA()
setTimeout(funcB, 1000)
funcC()

更具体的内容,可以百度查阅有关js 线程js 异步js Promisejs 异步编程相关话题,这里只需对JS的异步编程思想做一些了解即可。

2. Python同步

Python是一个多线程语言,没有复杂难懂的回调机制,如果你使用了定时,那么 当前线程会被“锁死”,直到这一行结束

funcA()
time.sleep(1)
funcB()

例如上面的代码,将会是A -> B这样的顺序,A和B之间时长为1秒,当程序执行到time.sleep时线程会等待,不会执行其他代码。
这与JS的异步区别很大,在刚才的A -> C -> B的例子中,JS主线程执行时间可能只有几微妙(A和C执行就一瞬间,B执行也一瞬间),中间1000毫秒的时间不会执行任何代码。

3. Selenium

Selenium是一个测试框架,主要语言是Python,那么同步和异步的写法,哪一种简单呢?显然是Python的同步写法简单,符合人类思维。
事实上,execute_async_script也是这样设计的,他被设计为执行异步代码,但不可能为了这个问题让你在python里面写异步代码。
所以事实上,这个函数是个同步的函数,程序执行到execute_async_script时,会一直等待代码执行完毕,然后继续执行下一行。
例如:

funcA()
result = browser.execute_async_script('// some script')
funcB()

这样的代码,会先执行funcA,然后线程卡死在execute_async_script,一直死等,等到execute_async_script执行完毕,再执行funcB。这里的result是代码执行结果,定义方式比较特别,熟悉JS的Promise机制的话会比较好懂,不熟悉的话也没关系,接下来慢慢讲解。

4. 异步和回调

显然,我们可以这样简单的理解,你把一段代码交给selenium,selenium就会把这段代码交给chrome执行,然后等待执行完毕,selenium再把结果还给你(还给python)。
但是对于JS的异步编程这其中是存在问题的, 异步的代码即使执行完毕,结果也没有返回 ,这是异步编程的一个主要特性。(如果没理解,可以翻回去再看看JS异步那一块)
那么你可能要问了,代码都执行完了,也没有运算结果,那怎么获得执行结果?
答案是:回调
假设我现在有一个异步过程,它需要计算很久,这个计算是在其他线程完成的,那么我将这个过程封装以后, 如何让别人拿到这个结果?
在JS中,你只要提前给定回调,就可以实现这个功能。
下面是一个假设的例子,请仔细观察主调方和被调方的不同,仔细理解:

// 主要代码部分,执行计算,然后拿到结果
someComplicatedCalculation(1, 1, function (result) {
    console.log(result)
})

// 给定参数,异步计算,并拿到结果
function someComplicatedCalculation(a, b, callback) {
    const result = a + b // 一段很复杂的计算过程

    // 调用callback函数,传入参数是计算结果
    callback(result)
}

我们先看someComplicatedCalculation的用法,传入a和b都是计算参数,也就是两个加数,但是结果并不是这个函数的返回值,或者说这个函数没有返回值。那么结果在哪里呢?
这个函数的第三个参数就是点睛之笔,你需要传入一个函数,这个函数的第一个参数,就是计算的结果。
那么,你只需要将所有计算完毕以后的代码,全部复制粘贴到这个函数里面,就可以拿到计算结果了。(当然这也引来问题,有兴趣可以百度js 回调地狱
也就是说,第三个参数是一个函数,你需要把一个函数当作参数传入。

接下来我们再看看someComplicatedCalculation是如何实现的呢?重点就在callback(result)这里,也就是说,你只要简单的在这个参数后面加括号,就会调用这个函数了。
显然,负责编写计算过程的人(被调方),是不知道调用这个函数的人(主调方)之后要干嘛的,所以就干脆把结果往函数里面一扔,之后想干嘛,都由调用函数的人决定(主调方决定如何使用计算结果)。

5. Selenium

再让我们回到Selenium,和第四个问题相似,这里,selenium是主调方,你想要让浏览器执行的代码是被调方,所以你应该执行一个叫做callback的函数,把你的计算结果传入这个函数,这样selenium就能拿到你给的结果。并且拿到结果的时候,selenium也就知道,异步过程已经执行完毕了,不需要继续等了。
因此,正确的写法,回调自然是在被调方中调用的,也就是在JS中调用。(这就是为什么回调肯定不在python里面写的原因)
例如:

result = browser.execute_async_script(js_script)
// js_script的内容
setTimeout(function () {
  callback('这是我的计算结果')
}, 1000)

像这样,python主线程会停在execute_async_script这一行,等待大约1秒,最后拿到js的执行结果,最终result是字符串"这是我的计算结果"。

Selenium中execute_async_script的使用

1. 如何在JS中执行callback

最后还剩一个问题,在js中的callback到底在哪?
这就需要原文档来解释了……不过我运气好偶然看到了。
这里给出参考链接 https://www.e-learn.cn/content/wangluowenzhang/374155

This callback is always injected into the executed function as the last argument.

这跟Selenium的运行原理有关,最后一个参数的获取方法如下:

const callback = arguments[arguments.length - 1]

修饰符const、var都无所谓,arguments不是变量,是一个关键字,表示当前函数栈的所有参数,是一个类似数组的结果。总之这样就能拿到callback了。

2. 完整样例

// python代码
from selenium import webdriver
import time

# 读取JS代码,这里保存在test.js文件中
def get_script():
    with open('./test.js', encoding='utf-8') as f:
        js_script = f.read()
        return js_script

# 主函数
def main():
	# 启动chrome,这都是selenium标准用法,不清楚请百度
    chrome_options = webdriver.ChromeOptions()
    chrome_options.add_argument('--headless')
    chrome_options.add_argument('--disable-gpu')
    browser = webdriver.Chrome(options=chrome_options, executable_path='chromedriver.exe')
    mainUrl = 'http://www.baidu.com'
    browser.get(mainUrl)

	# 异步执行JS代码并获取结果,同时记录前后时间
    start_time = time.time()
    result = browser.execute_async_script(get_script())
    end_time = time.time()

	# 打印结果,并显示结果的类型
	print('执行结果:', result)
    print('结果类型:', type(result))

	# 打印开始和结束时间,以及总时长
    print('运行时长:', end_time - start_time)
    browser.quit()

if __name__ == '__main__':
    main()

// test.js的内容
const callback = arguments[arguments.length - 1]
setTimeout(function() {
    callback('Hello world!')
}, 1000)

运行结果

DevTools listening on ws://127.0.0.1:57796/devtools/browser/69eeec93-a5a8-48e1-a143-cc0aeea73600
执行结果: Hello world!
结果类型: 
运行时长: 1.0462801456451416

可以看出,程序成功地获取了JS中的执行结果,并且Python的主线程也等待了1秒左右,也就是一直在等待程序执行结束。

3. 返回数组的例子

Python代码保持不变

// test.js的内容
const callback = arguments[arguments.length - 1]
setTimeout(function() {
    callback([1, 2, 3])
}, 1000)

运行结果

DevTools listening on ws://127.0.0.1:57870/devtools/browser/cc3543c7-c8ca-4ae7-85a5-61bebb13684a
执行结果: [1, 2, 3]
结果类型: 
运行时长: 1.0500686168670654

3. 返回对象的例子

Python代码保持不变

// test.js的内容
const callback = arguments[arguments.length - 1]
setTimeout(function() {
    callback({
        id: 12,
        book_name: '颈椎病康复指南',
        author: 'CSDN'
    })
}, 1000)

运行结果

DevTools listening on ws://127.0.0.1:57959/devtools/browser/3ad1d611-66cb-40f2-ad2b-1fa27dab822c
执行结果: {'author': 'CSDN', 'book_name': '颈椎病康复指南', 'id': 12}
结果类型: 
运行时长: 1.0560016632080078

你可能感兴趣的:(经验)