selenium常用操作汇总

本文总结使用selenium进行web/UI自动化时,会用到的一些常用操作。

定位元素

driver.find_element_by_xpath()#1、绝对路径 2、元素属性 3、层级和属性结合 4、使用逻辑运算符
driver.find_element_by_id()#根据id定位,HTML规定id属性在HTML文档中必须是唯一的
driver.find_element_by_name()#name属性定位
driver.find_element_by_class_name()#class属性定位
driver.find_element_by_tag_name()#标签名字定位 因为标签基本都不是唯一的,所以不用这个
driver.find_element_by_link_text()#定位文本链接,需要对应的元素有href属性
driver.find_element_by_partial_link_text()#模糊定位文本链接
driver.find_element_by_css_selector()#CSS选择器

By定位元素

是元素定位另外一种方式,跟上面的8种底层一样。

1

2

from selenium.webdriver.common import By

driver.find_element(By.ID,'kw')

定位一组元素

1

2

driver.find_elements_by_xpath()#其他方法同理

driver.find_elements(By.XPATH,'')

浏览器操作

driver.set_window_size()#设置浏览器大小,可以设置成移动端大小
driver.back()#浏览器后退
driver.forward()#浏览器前进
driver.set_window_position()
driver.set_window_rect()
driver.refresh()#模拟浏览器刷新
driver.maximize_window()#最大化浏览器

设置浏览器参数

在定义driver的时候设置chrome_options参数,该参数是一个Options类所实例化的对象。其中,常用的参数是设置浏览器是否可视化和浏览器的请求头等信息,前者可以加快代码的运行速度,后者可以有效地防止网站的反爬虫检测

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

from selenium.webdriver.chrome.options import Options

url='https://movie.douban.com/'

#Options类实例化

chrome_options=Options()

#设置浏览器参数

#--headless是不 显示浏览器启动和执行过程c

chrome_options.add_argument('--headless')

#设置lang和User-Agent信息,防止反爬虫检测

chrome_options.add_argument('lang=zh_CN.UTF-8')

UserAgent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.101 Safari/537.36'

chrome_options.add_argument('User-Agent='+UserAgent)

#启动浏览器并设置chrome_options参数

driver=webdriver.Chrome(options=chrome_options)

driver.get(url)

driver.maximize_window()

print(driver.title)

# print(driver.page_source) #获取网页的html代码

元素操作

driver.find_element_by_xpath().click()#点击元素
driver.find_element_by_xpath().clear()#清空文本
driver.find_element_by_xpath().send_keys()#模拟键盘输入

页面交互

webElement接口提供了一些可以与页面交互的方法

submit和click在某些情况下可以互换,submit只用于表单的提交按钮。

driver.find_element_by_xpath().submit()#提交输入框的内容 类似于回车提交搜索框内容
driver.find_element_by_xpath().size#返回元素尺寸
driver.find_element_by_xpath().text#获取元素文本
driver.find_element_by_xpath().get_attribute()#获取元素属性值
driver.find_element_by_xpath().is_selected()#是否被选中
driver.find_element_by_xpath().is_enabled()#判断元素是否可以使用
driver.find_element_by_xpath().is_displayed()#返回元素是否用户可见

鼠标事件

关于鼠标操作的方法都封装在Actionchains中

from selenium.webdriver.common.action_chains import ActionChains
rigt_check=driver.find_element_by_id('kw')
ActionChains(driver).context_click(rigt_check).perform()#context_click对元素右击操作
#perform()执行所有ActionChains中存储的行为
ActionChains(driver).move_to_element(rigt_check).perform()#鼠标悬停
ActionChains(driver).double_click(rigt_check).perform()#鼠标双击
ActionChains(driver).drag_and_drop(source,target).perform()#鼠标由源元素拖动至目标元素

1

2

3

4

5

6

7

8

9

10

11

url='https://passport.bilibili.com/login'

  

driver=webdriver.Chrome()

driver.get(url)

#双击登录按钮

ele=driver.find_element_by_class_name('tit')

ActionChains(driver).double_click(ele).perform()

sleep(2)

#拖拽滑块

ele=driver.find_element_by_class_name('gt_slider_knob,gt_show')

ActionChains(driver).drag_and_drop_by_offset(ele,100,0).perform()

键盘事件

模拟键盘输入,可以是按键,组合键。

from selenium.webdriver.common.keys import Keys
driver.find_element_by_xpath().send_keys(Keys.BACK_SPACE)#对该元素操作一次backspace
driver.find_element_by_xpath().send_keys(Keys.SPACE)#对该元素输入空格
driver.find_element_by_xpath().send_keys(Keys.CONTROL,'a')#ctrl+A全选输入框内容
driver.find_element_by_xpath().send_keys(Keys.CONTROL,'x')#ctrl+x剪切输入框内容
driver.find_element_by_xpath().send_keys(Keys.CONTROL,'v')#ctrl+v粘贴内容到输入框
driver.find_element_by_xpath().send_keys(Keys.ENTER)#通过回车键代替单击操作
driver.find_element_by_xpath().send_keys(Keys.F1)#键盘F1

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

34

35

36

from selenium import webdriver

from selenium.webdriver.common.action_chains import ActionChains

from selenium.webdriver.common.keys import Keys

from selenium.webdriver.common.by import By

from time import sleep

url='https://www.baidu.com'

  

driver=webdriver.Chrome()

driver.get(url)

ele=driver.find_element_by_id('kw')

ele.send_keys("selenium")

sleep(2)

#删除最后一个文字

ele.send_keys(Keys.BACK_SPACE)

sleep(2)

  

#添加空格键+教程

ele.send_keys(Keys.SPACE)

ele.send_keys("教程")

sleep(2)

  

#ctrl+a全选输入框内容

ele.send_keys(Keys.CONTROL,'a')

sleep(2)

  

#ctrl+x 剪切输入框内容

ele.send_keys(Keys.CONTROL,'x')

sleep(2)

  

  

#ctrl+v 粘贴内容到输入框

ele.send_keys(Keys.CONTROL,'v')

sleep(2)

  

#回车键代替单击

driver.find_element_by_id('su').send_keys(Keys.ENTER)

获得验证信息

自动化用例执行完成后,可以从页面上获取一些信息来证明用例执行成功或者失败

driver.title #获得页面title
driver.current_url#获得页面curl
driver.find_element_by_xpath().text#获取元素文本

元素等待

由于web页面多数使用AJAX技术,浏览器在加载页面时,页面上的元素可能不是同时被加载完成的,需要加入等待。

显式等待:显性等待能够根据判断条件而灵活地等待,程序每隔一段时间检测一次,如果检测结果与条件相符,就执行下一步,否则继续等待,直到超过设置的最长时间为止

from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
element=WebDriverWait(driver,5,0.5).until(EC.presence_of_element_located(By.ID,'kw'))
#每0.5秒检测一次这个元素能否被定位到,5秒后超时
#expected_conditions提供了很多预期条件判断方法
#也可以用is_displayed()判断元素是否可见

隐式等待

隐性等待是指在一个设定的时间内检测网页是否加载完成,也就是一般情况下用户看到浏览器标签栏那个小圈不再转,才会执行下一步。比如代码中设置30秒等待时间,网页只要在30秒内完成加载就会自动执行下一步,如果超出30秒就会抛出异常。值得注意的是,隐性等待对整个driver的周期都起作用,所以只要设置一次即可。

通过一定的时长等待页面上某元素加载完成,如果超出了设置的时长元素,还没有被加载,则抛出没有元素异常。

通常设置在driver后面。

driver.implicitly_wait(10)

sleep休眠方法

在需要的地方添加。

1

2

from time import sleep

sleep(3)

隐性等待和显性等待相比于time.sleep这种强制等待更为灵活和智能,可解决各种网络延误的问题,隐性等待和显性等待可以同时使用,但最长的等待时间取决于两者之间的最大数,如上述代码的隐性等待时间为30秒,显性等待时间为20秒,则该代码的最长等待时间为隐性等待时间。 

多框架切换

frame是一个框架页面,在HTML 5中已经不支持使用框架,但在一些网站中依然会看到它的身影。frame的作用是在HTML代码中嵌套一个或多个不同的HTML代码,每嵌套一个HTML代码都需要由frame来实现

iframe和frame实现的功能是相同的,只不过使用方式和灵活性有所不同,无论是iframe还是frame,Selenium的定位和操作方式都是一样的

由于一个HTML可以嵌套一个或多个iframe,因此Selenium在操作不同的iframe时需要通过switch_to.frame()来切换到指定的iframe,再执行相应的操作 

driver.switch_to.frame('kw')#可以直接取表单的id或者name属性,如果表单没有这两个属性,可以先定位表单元素,再切换
driver.switch_to.parent_frame('kw')#跳出当前一级表单
driver.switch_to.default_content('kw')#跳回最外层页面
#根据索引定位
driver.switch_to_frame(0)
#根据id或者name属性
driver.switch_to_frame('framea')
新版本 driver.switch_to.frame(0)

如百度知道的文本框

 如下代码实现打开百度知道题目页面,点击我要回答,定位至iframe,输入内容,跳出iframe,并点击提交回答按钮

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

url='https://zhidao.baidu.com/question/1903337615241287780.html'

  

driver=webdriver.Chrome()

driver.get(url)

driver.maximize_window()

#点击我要回答

driver.find_element_by_id('answer-bar').click()

#切换到frame内部的html

driver.switch_to.frame('ueditor_0')

#定位frame内部的元素

driver.find_element_by_xpath('/html/body').send_keys('美国')

#跳回网页HTML 如果不切回网页,则找不到提交回答按钮,且js代码也执行不了

# driver.switch_to.default_content()

js='window.scrollTo(0,100)'

driver.execute_script(js)

sleep(3)

#点击网页提交回答按钮

driver.find_element_by_xpath("//div[@class='addons line']/a").click()

多窗口切换

search_windows=driver.current_window_handle #获得当前窗口句柄
all_handles=driver.window_handles #获得所有窗口的句柄
driver.switch_to.window(handle)#切换窗口
handles=driver.window_handles
driver.switch_to_window(handles[0])
driver.switch_to_window(handles[1])

应用

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

from selenium import webdriver

from selenium.webdriver.common.by import By

url='https://www.baidu.com/'

driver=webdriver.Chrome()

driver.get(url)

driver.implicitly_wait(10)

search_windows=driver.current_window_handle #获得窗口句柄

driver.find_element_by_link_text('登录').click()

driver.find_element_by_link_text('立即注册').click()

import time

#获得所有窗口的句柄

all_handles=driver.window_handles

#切换到注册窗口

for handle in all_handles:

    if handle!=search_windows:

        driver.switch_to.window(handle)

        print("当前是注册窗口")

        time.sleep(2)

#回到搜索窗口

for handle in all_handles:

    if handle==search_windows:

        driver.switch_to.window(handle)

        print("当前是搜索窗口")

        driver.find_element_by_id('TANGRAM__PSP_4__closeBtn').click()#关闭注册窗口

        driver.find_element_by_id('kw').send_keys("selenium")

        driver.find_element_by_id('su').click()

        time.sleep(2)

driver.quit()

警告框处理

webdriver中处理JavaScript生成的alert,confirm,prompt十分简单,具体做法是使用switch_to_alert方法定位到alert/confirm/prompt,然后使用text/accept/dismiss/send_keys等方法进行操作。

  • text:返回alert/confirm/prompt中的文字信息
  • accept():接受现有警告框
  • dismiss():解散现有警告框
  • send_keys:发送文本至警告框

应用,如图切换至alert并接受

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

from selenium import webdriver

from selenium.webdriver.common.action_chains import ActionChains

from selenium.webdriver.common.by import By

from time import sleep

url='https://www.baidu.com/'

driver=webdriver.Chrome()

driver.get(url)

driver.maximize_window()

driver.implicitly_wait(10)

#鼠标悬停至设置链接

link=driver.find_element_by_id("s-usersetting-top")

ActionChains(driver).move_to_element(link).perform()

  

driver.find_element_by_link_text('搜索设置').click()

  

#保存设置

driver.find_element_by_class_name('prefpanelgo').click()

sleep(2)

#接受警告框

# driver.switch_to_alert().accept()

  

driver.quit()

上传文件

一般文件上传,是打开windows窗口从窗口中选择本地文件,但webdriver无法操作windows控件。

1、普通上传 将本地文件路径作为值传给input标签,注意必须是input标签

2、插件上传,基于flash,js,Ajax等技术实现上传

#定位上传按钮,添加本地文件
driver.find_element_by_name('file').send_keys("D:\\upload_file.txt")

另一种方法为基于AutoIt软件来实现上传。但是不推荐,因为不在Python范围。

下载文件

webdriver允许设置默认的文件下载路径,文件会被自动下载并存放到设置的目录中

操作cookie

什么情况下回用到cookie操作?当开发人员开发一个功能,当用户登录后,会将用户的用户名写入浏览器cookie,指定key为username,就可以通过get_cookies()找到username,打印value。

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

url='https://www.youdao.com/'

  

driver=webdriver.Chrome()

driver.get(url)

driver.maximize_window()

driver.implicitly_wait(10)

  

# #获得cookie

  

driver.add_cookie({'name':'Login_User','value':'Password'})

cookie=driver.get_cookies()

print("所有Cookie为",cookie)

#获取name为Login_User的cookie

one_cookie=driver.get_cookie('Login_User')

print("Login_User的Cookie为",one_cookie)

# "删除name为Login_User的cookie

driver.delete_cookie('Login_User')

  

surplus_cookies=driver.get_cookies()

print("剩余的cookie为:",surplus_cookies)

  

driver.delete_all_cookies()

surplus_cookies=driver.get_cookies()

print("剩余的cookie为:",surplus_cookies)

driver.quit()

返回结果为

所有Cookie为[{
    'domain': 'www.youdao.com',
    'httpOnly': False,
    'name': '___rl__test__cookies',
    'path': '/',
    'secure': False,
    'value': '1662293521877'
}, {
    'domain': '.youdao.com',
    'expiry': 1696853521,
    'httpOnly': False,
    'name': 'OUTFOX_SEARCH_USER_ID',
    'path': '/',
    'secure': False,
    'value': '"555592633@10.108.162.135"'
}, {
    'domain': 'www.youdao.com',
    'httpOnly': False,
    'name': 'Login_User',
    'path': '/',
    'secure': True,
    'value': 'Password'
}, {
    'domain': '.youdao.com',
    'expiry': 1696853520,
    'httpOnly': False,
    'name': 'OUTFOX_SEARCH_USER_ID_NCOO',
    'path': '/',
    'secure': False,
    'value': '1883563674.2397'
}]
Login_User的Cookie为 {
    'domain': 'www.youdao.com',
    'httpOnly': False,
    'name': 'Login_User',
    'path': '/',
    'secure': True,
    'value': 'Password'
}
剩余的cookie为: [{
    'domain': 'www.youdao.com',
    'httpOnly': False,
    'name': '___rl__test__cookies',
    'path': '/',
    'secure': False,
    'value': '1662293521877'
}, {
    'domain': '.youdao.com',
    'expiry': 1696853521,
    'httpOnly': False,
    'name': 'OUTFOX_SEARCH_USER_ID',
    'path': '/',
    'secure': False,
    'value': '"555592633@10.108.162.135"'
}, {
    'domain': '.youdao.com',
    'expiry': 1696853520,
    'httpOnly': False,
    'name': 'OUTFOX_SEARCH_USER_ID_NCOO',
    'path': '/',
    'secure': False,
    'value': '1883563674.2397'
}]
剩余的cookie为: []

调用Javascript

可以用来控制浏览器滚动条

1

2

3

4

5

6

7

8

9

10

11

url='https://www.baidu.com/'

  

driver=webdriver.Chrome()

driver.get(url)

  

driver.find_element_by_id('kw').send_keys("selenium")

driver.find_element_by_id('su').click()

driver.set_window_size(600,600)

#通过js设置浏览器窗口的滚动条位置

js="window.scrollTo(500,500);"

driver.execute_script(js)

窗口截图

1

2

3

4

5

6

7

8

9

10

url='https://www.baidu.com/'

  

driver=webdriver.Chrome()

driver.get(url)

  

driver.find_element_by_id('kw').send_keys("selenium")

driver.find_element_by_id('su').click()

sleep(2)

driver.get_screenshot_as_file("C:\\Users\\yangyl13\\Desktop\\3.png")

driver.quit()

关闭窗口

driver.quit() #退出相关驱动程序,关闭所有窗口

driver.close()#关闭单个窗口。

验证码处理

1、测试环境去掉验证码

2、万能验证码

3、验证码识别技术 如python-tesseract

4、记录cookie

下拉框选择

from selenium.webdriver.support.select import Select
Select(driver.find_element_by_id('')).select_by_index('2')#根据下拉框索引选择
Select(driver.find_element_by_id('')).select_by_value('Python')#根据下拉框value属性选择
Select(driver.find_element_by_id('')).select_by_visible_text('Python')#根据下拉框可见文本选择

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/731686.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

勾股定理(点赞支持!谢谢)

股定理是一个基本的几何定理,在中国,《周髀算经》记载了勾股定理的公式与证明,相传是在商代由商高发现,故又有称之为商高定理;三国时代的蒋铭祖对《蒋铭祖算经》内的勾股定理作出了详细注释,又给出了另外一…

Python hashlib statistics pyecharts模块

文章目录 Python hashlib 模块Python statistics 模块Python pyecharts 模块 Python hashlib 模块 Python 的 hashlib 模块是一个提供不同哈希算法实现的库,比如 MD5、SHA1、SHA256 等。这个模块允许你快速计算给定数据的哈希值,这在需要确保数据完整性…

App拉起微信小程序参考文章

App拉起微信小程序参考文章h5页面跳转小程序-----明文URL Scheme_weixin://dl/business/?appid*appid*&path*path*&qu-CSDN博客文章浏览阅读561次,点赞16次,收藏5次。仅需两步,就能实现h5跳转小程序,明文 URL Scheme&…

vue2 vue-cli vue-router vuex

Vue2 插值表达式 利用表达式进行插值渲染,将数据渲染到页面中。 语法:{{ 表达式 }} PS: 使用的数据要存在支持的是表达式,不是语句 if、for不能在标签属性中使用{{ }} v-show和v-if v-show底层原理:切换css的dis…

第三方应用软件提权方法

1、Serv-u 安全性测试(分为有配置文件有修改权限与 servUDaemon.exe 默认管理员帐号和密码没修改进行提权) 2、FlashFXP 安全性测试(攻击者只需通过 webshell 下载 quick.dat、sites.dat、stats.dat这三个文件进行本地替换,就可以…

SRC学习-成为赏金猎人

你是否对漏洞挖掘充满好奇?零基础或有基础但想更进一步?想赚取可观的漏洞赏金让自己有更大的自由度? 那么,不妨了解下土拨鼠的安全屋 这或许也是你成为漏洞赏金猎人的第一课。 逻辑漏洞挖掘手法与创新思路,带你突破…

@EnableWebMvc介绍和使用详细demo

EnableWebMvc是什么 EnableWebMvc 是 Spring MVC 中的一个注解,它用于启用 Spring MVC 框架的基本功能,以便你可以使用 Spring MVC 提供的特性来处理 Web 请求。 通常情况下,在基于 Spring Boot 的应用中,并不需要显式地使用 Ena…

GC--垃圾回收

目录 垃圾回收概念 什么是垃圾? 垃圾回收机制什么时候会进行GC?? 应该关心垃圾回收那些哪些区域的回收 垃圾回收相关算法 垃圾回收算法:[标记阶段、回收阶段] 垃圾标记阶段 标记阶段的目的 引用计数算法(目前没有在使用&…

实现类似 Word 协同编辑--Canvas-Editor

前言 对于word的协同编辑,已经构思很久了,但是没有找到合适的插件。今天推荐基于canvas/svg 的富文本编辑器 canvas-editor,能实现类似word的基础功能,如果后续有更好的,也会及时更新。 Canvas-Editor效果图: 官方文…

mysql笔记:9. 数据查询

文章目录 一、SELECT语句二、简单查询1. 查询表所有数据2. 查询部分数据3. 计算结果4. 列别名5. 去除重复项6. 表别名7. LIMIT限制数据 三、WHERE子句1. 比较查询条件2. BETWEEN AND范围查询3. IN查询4. LIKE匹配5. 空数据查询6. AND多条件查询7. OR多条件查询 四、操作查询结果…

FFmpeg--AAC音频解码流程

文章目录 AAC 组成函数分析读aac帧写aac帧aac的head参数设置 运行结果 AAC 组成 AAC音频格式:是⼀种由MPEG-4标准定义的有损⾳频压缩格式 ADTS:是AAC音频的传输流格式 AAC音频文件的每一帧由ADTS Header和AAC Audio Data组成 每⼀帧的ADTS的头⽂件都包含了⾳频的采…

llama-index调用qwen大模型实现RAG

背景 llama-index在实现RAG方案的时候多是用的llama等英文大模型,对于国内的诸多模型案例较少,本次将使用qwen大模型实现llama-index的RAG方案。 环境配置 (1)pip包 llamaindex需要预装很多包,这里先把我成功的案例…

计算机网络——概述

计算机网络——概述 计算机网络的定义互连网(internet)互联网(Internet)互联网基础结构发展的三个阶段第一个阶段——APPANET第二阶段——商业化和三级架构第三阶段——全球范围多层次的ISP结构 ISP的作用终端互联网的组成边缘部分…

Python 映射函数map()详解

一、映射函数定义 它用于对容器中的元素进行映射(或变换) 二、映射函数语法 map(function, iterable) function:一个提供变换规则的函数,返回变换之后的元素iterable:一个或多个序列(可迭代对象&#xff09…

新零售SaaS架构:订单履约系统架构设计(万字图文总结)

什么是订单履约系统? 订单履约系统用来管理从接收客户订单到将商品送达客户手中的全过程。 它连接了上游交易(客户在销售平台下单环)和下游仓储配送(如库存管理、物流配送),确保信息流顺畅、操作协同&…

【ElasticSearch】es索引、映射、文档基本操作复杂查询

各位小伙伴们大家好,欢迎来到这个小扎扎的ElasticSearch专栏,本篇博客由B战尚硅谷的ElasticSearch视频总结而来,鉴于 看到就是学到、学到就是赚到 精神,这波依然是血赚 ┗|`O′|┛ 🌆 内容速览 1 es数据格…

PDE小题

给出二阶偏微分方程 u t t − u x x − u x y x 2 t u_{tt}-u_{xx}-u_{xy}x^2t utt​−uxx​−uxy​x2t 的三维系数矩阵 对于二阶偏微分方程 u t t − u x x − u x y x 2 t u_{tt} - u_{xx} - u_{xy} x^2t utt​−uxx​−uxy​x2t,我们可以通过分析每个项的系…

浏览器本地存储localstorage,sessionStorage

浏览器本地存储localstorage,sessionStorage 文章目录 一、localstoragelocalstorage存储localstorage读取localstorage删除localstorage清空 二、sessionStoragelocalstorage读取localstorage删除localstorage清空 总结 一、localstorage 即使浏览器关闭&#xf…

替换模板变量生成测试用例

需求: 针对指标的查值有固定的文法例如 ${org_name}的业务量是多少,需要替换变量来生成测试用例 package net.yto.com.testplatform;import com.alibaba.fastjson.JSON;import java.util.ArrayList; import java.util.List;public class TestMain {publi…

【node版本问题】运行项目报错 PostCSS received undefined instead of CSS string

最近该项目没有做任何修改,今天运行突然跑不起来报错了 PostCSS received undefined instead of CSS string 【原因】突然想起来期间有换过 node 版本为 16.17.1 【解决】将 node 版本换回之前的 14.18.0 就可以了