Python的DrissionPage网页自动化工具库笔记

2/21/2025 Pandas

[toc]

# Python的DrissionPage网页自动化工具库笔记

DrissionPage 是一个基于 Python 的网页自动化工具。既能控制浏览器,也能收发数据包,还能把两者合而为一。类似Selenium自动化测试工具。

DrissionPage官网 (opens new window) 目前最新版本为DrissionPage 4.1.0.17

python_20250221101442.png

# 安装

支持的运行环境

  • 操作系统:Windows、Linux 和 Mac。
  • python 版本:3.6 及以上
  • 支持浏览器:Chromium 内核(如 Chrome 和 Edge)
# 使用 pip 安装 DrissionPage
pip install DrissionPage

# 升级命令
pip install DrissionPage --upgrade

# 升级指定版本
pip install DrissionPage==4.0.0b17
1
2
3
4
5
6
7
8

# 示范例子

控制浏览器,打开网页。

代码逻辑如下

  1. 创建浏览器对象,用于启动或接管浏览器
  2. 获取一个 Tab 标签页对象
  3. 使用 Tab 标签页对象访问网址
  4. 使用 Tab 标签页对象获取标签页内需要的元素对象
  5. 使用元素对象进行交互操作。
# 导入
from DrissionPage import Chromium,ChromiumOptions

# 浏览器设置
def getBrowser():
    # 浏览器的可执行文件路径
    path = r'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe'  
    # 浏览器启动设置对象
    co = ChromiumOptions()
    # 设置浏览器路径
    co.set_browser_path(path)
    # 自动获取空闲端口,从而启动一个全新浏览器
    co.auto_port()
    # 开启无头模式
    # co.headless(True)
    # 来宾模式
    co.set_argument('--guest')
    # 无痕模式
    co.incognito()
    # 设置浏览器的窗口大小
    co.set_argument('--window-size', '390,844')
    # 阻止“自动保存密码”的提示气泡
    co.set_pref('credentials_enable_service', False)
    # 阻止“要恢复页面吗?Chrome未正确关闭”的提示气泡
    co.set_argument('--hide-crash-restore-bubble')
    # 设置浏览器的UA,UA可选择设置为手机或者电脑的UA。
    co.set_user_agent(user_agent='Mozilla/5.0 (iPhone; CPU iPhone OS 10_2_1 like Mac OS X) AppleWebKit/602.4.6 (KHTML, like Gecko) Mobile/14D27 MicroMessenger/6.5.5 NetType/WIFI Language/zh_CN')
    # 浏览器静音
    co.mute(True)
    # 传入浏览器配置,创建浏览器对象
    browser = Chromium(addr_or_opts=co)
    return browser

if __name__ == '__main__':
    # 连接浏览器
    browser = getBrowser()
    # 获取最后激活的标签页对象
    tab = browser.latest_tab
    # 标签页访问一个网址
    tab.get('https://www.baidu.com')
    # 获取文本框元素对象
    ele = tab.ele('#kw')
    # 向文本框元素对象输入文本
    ele.input('DrissionPage')  
    # 点击按钮,上两行的代码可以缩写成这样
    tab('#su').click()  
    # 获取所有<h3>元素
    links = tab.eles('tag:h3')  
    # 遍历并打印结果
    for link in links:  
        print(link.text)

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

# 创建浏览器

Chromium对象,用于管理浏览器整体相关的操作。如标签页管理、获取浏览器信息、设置浏览器的运行参数等。

注意:每个浏览器只能有一个Chromium对象(同一进程中)。对同一个浏览器重复使用Chromium()获取的都是同一个对象。

默认情况下,程序会使用 9222 端口来启动一个浏览器。

创建浏览器对象的方式

from DrissionPage import Chromium, ChromiumOptions

# 方式1 默认创建
browser = Chromium()

# 方式2 指定端口或地址
browser = Chromium(9333)
browser = Chromium('127.0.0.1:9333')

# 方式3 通过配置信息
# 浏览器启动设置对象
co = ChromiumOptions()
# 设置浏览器路径
co.set_browser_path(r'D:\chrome.exe')
# 传入浏览器配置,创建浏览器对象
browser = Chromium(addr_or_opts=co)

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

更多的用法请自行在官网中查询。

# 浏览器启动设置类 ChromiumOptions

ChromiumOptions类是浏览器的启动配置。

from DrissionPage import Chromium,ChromiumOptions
# 创建浏览器启动配置对象
co = ChromiumOptions()

# 设置浏览器路径
co.set_browser_path("XXXXX")
# 自动获取空闲端口,从而启动一个全新浏览器
co.auto_port()
# 开启无头模式
co.headless(True)
# 来宾模式
co.set_argument('--guest')
# 无痕模式
co.incognito()
# 设置浏览器的窗口大小
co.set_argument('--window-size', '390,844')
# 阻止“自动保存密码”的提示气泡
co.set_pref('credentials_enable_service', False)
# 阻止“要恢复页面吗?Chrome未正确关闭”的提示气泡
co.set_argument('--hide-crash-restore-bubble')
# 设置浏览器的UA
co.set_user_agent(user_agent='Mozilla/5.0 (iPhone; CPU iPhone OS 10_2_1 like Mac OS X) AppleWebKit/602.4.6 (KHTML, like Gecko) Mobile/14D27 MicroMessenger/6.5.5 NetType/WIFI Language/zh_CN')
# 浏览器静音
co.mute(True)
# 传入浏览器配置,创建浏览器对象
browser = Chromium(addr_or_opts=co)

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

更多的用法请自行在官网中查询。

多浏览器共存

如果想要同时操作多个浏览器,或者自己在使用其中一个上网,同时控制另外几个跑自动化,就需要给这些被程序控制的浏览器设置单独的 端口 和 用户文件夹,否则会造成冲突。

使用ChromiumOptions对象的auto_port()方法,可以指定程序每次使用空闲的端口和临时用户文件夹创建浏览器。并且使用auto_port()的配置对象可由多个Chromium对象共用,不会出现冲突。

这种方式创建的浏览器是全新不带任何数据的,并且运行完毕后数据会自动清除。

注意:auto_port()支持多线程,多进程使用时由小概率出现端口冲突。多进程使用时,可用scope参数指定每个进程使用的端口范围,以免发生冲突。

# 导入 
from DrissionPage import Chromium, ChromiumOptions

# ChromiumOptions对象的auto_port()方法
co = ChromiumOptions().auto_port()

# 创建多个浏览器对象
tab1 = Chromium(addr_or_opts=co).latest_tab
tab2 = Chromium(addr_or_opts=co).latest_tab

tab2.get('https://DrissionPage.cn')
tab1.get('https://www.baidu.com')
1
2
3
4
5
6
7
8
9
10
11
12

# 浏览器对象

更多的用法请自行在官网中查询。

# 导入
from DrissionPage import Chromium
# 创建浏览器对象
browser = Chromium()  
1
2
3
4

标签页操作

# get_tab() 获取一个标签页对象或它的 id
tab = browser.get_tab()
# get_tabs() 获取多个符合条件的标签页对象
tabs = browser.get_tabs()
# latest_tab获取最新的标签页对象
l_tab = browser.latest_tab
# new_tab() 新建一个标签页对象
a_tab = browser.new_tab()
# .....
1
2
3
4
5
6
7
8
9

浏览器运行参数

# 设置一个或多个 cookie
browser.set.cookies()
# 用于清除浏览器所有 cookies
browser.set.cookies.clear()
# 设置是否启用自动处理 alert 弹窗
browser.set.auto_handle_alert()

# 返回浏览器所有域名的 cookies
browser.cookies()
# 清除浏览器的缓存
browser.clear_cache()
# 关闭浏览器
browser.quit()
1
2
3
4
5
6
7
8
9
10
11
12
13

# 标签页对象Tab

  • Tab 对象从浏览器对象获取,每个 Tab 对象对应浏览器上一个实际的标签页。
  • 大部分操作都使用 Tab 对象进行,如访问网页、调整窗口大小、监听网络等。
  • 默认情况下每个标签页只有一个 Tab 对象,关闭单例模式后可用多个 Tab 对象同时控制一个标签页。
from DrissionPage import Chromium
# 创建标签页对象
tab = Chromium().latest_tab
1
2
3

# 网页交互

标签页对象控制浏览器的标签页,是页面控制的主要单位。

更多的用法请自行在官网中查询。

页面跳转

# get()方法用于跳转到一个网址。可指定本地文件路径。
tab.get('https://DrissionPage.cn')

# back() 此方法用于在浏览历史中后退若干步
tab.back(2)  # 后退两个网页

# refresh() 用于刷新当前页面
tab.refresh() 
1
2
3
4
5
6
7
8

设置cookies及缓存

# 给页面设置一个或多个 cookie
tab.set.cookies()
# 用于清除页面所有 cookie
tab.set.cookies.clear()
# 删除页面中的一个cookie
tab.set.cookies.remove(name="xxxx")

# 设置某项 sessionStorage
tab.set.session_storage(item='abc', value='123')
# 设置某项 localStorage 信息
tab.set.local_storage(item='abc', value='123')
1
2
3
4
5
6
7
8
9
10
11

页面窗口管理

# 窗口最大化
tab.set.window.max()
# 窗口最小化
tab.set.window.mini()
# 设置窗口大小
tab.set.window.size(500, 500)
1
2
3
4
5
6

关闭及重连

# 关闭标签页
tab.close()
# 用于页面对象断开与页面的连接,但不关闭标签页。断开后,对象不能对标签页进行操作。
tab.disconnect()
# 用于关闭与页面连接,然后重建一个新连接。wait是等待重连时间
tab.reconnect(wait=10)
1
2
3
4
5
6
  • reconnect() 主要用于应付长期运行导致内存占用过高,断开连接可释放内存,然后重连继续控制浏览器。

# 获取网页信息

成功访问网页后,可使用 Tab 对象属性和方法获取页面信息。

from DrissionPage import Chromium

tab = Chromium().latest_tab
tab.get('https://www.baidu.com')

# 返回当前页面 html 文本
tab.html
# 返回当前页面title文本
tab.title
# 返回当前页面 user agent 信息
tab.user_agent
# 返回当前访问的 url
tab.url
# 返回cookies
tab.cookies()
# 返回sessionStorage 信息
tab.session_storage()
# 返回localStorage 信息
tab.local_storage()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 定位元素

当我们获取到页面的html时,如何准确的定位元素是自动化重中之重的技能。

更多的用法请自行在官网中查询。

假设有这样一个页面

<html>
<body>
<div id="one">
    <p class="p_cls" name="row1">第一行</p>
    <p class="p_cls" name="row2">第二行</p>
    <p class="p_cls">第三行</p>
</div>
<div id="two">
    第二个div
</div>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12

# 定位语法

基本定位语法

元素属性包括以下三种

  • 标签名 @tag() 即<div id="one">中的div
  • 标签体中的属性 @**** 如<div id="one">中的id,写作'@id'
  • 元素文本 @text() 即<p class="p_cls">第三行</p>中的第三行

例子

tab.ele('@id=one')  # 获取第一个id为one的元素
tab.ele('@tag()=div')  # 获取第一个div元素
tab.ele('@text()=第一行')  # 获取第一个文本为“第一行”的元素
1
2
3

单个@在只以一个属性作为匹配条件时使用,以'@'开头,后面跟属性名称。当需要多个条件同时确定一个元素时,每个属性需要用'@@'开头。

# 查找class为p_cls且文本为“第三行”的元素
ele = tab.ele('@@class=p_cls@@text()=第三行') 
1
2

匹配模式

匹配模式指某个查询中匹配条件的方式,有精确匹配、模糊匹配、匹配开头、匹配结尾四种。

  • 精确匹配 =
  • 模糊匹配 :
  • 匹配开头 ^
  • 匹配结尾 $

例子

ele = tab.ele('@id=row1')  # 获取id属性为'row1'的元素
ele = tab.ele('@id:ow')  # 获取id属性包含'ow'的元素
ele = tab.ele('@id^row')  # 获取id属性以'row'开头的元素
ele = tab.ele('@id$w1')  # 获取id属性以'w1'结尾的元素
1
2
3
4

# 页面内或元素内查找

页面对象和元素对象都拥有ele()和eles()方法,用于获取其内部指定子元素。

ele()

用于查找其内部第一个符合条件的元素。

from DrissionPage import Chromium
tab = Chromium().latest_tab
tab.get('https://www.baidu.com')

# 在页面内查找元素
ele = tab.ele('#one')
# 在元素内查找后代元素
ele = tab.ele('第二行')
1
2
3
4
5
6
7
8

eles()

用于查找其内部符合条件的所有元素。返回的是匹配到的所有元素组成的列表。

from DrissionPage import Chromium
tab = Chromium().latest_tab
tab.get('https://www.baidu.com')

# 获取页面内的所有p元素
p_eles = tab.eles('tag:p')
# 获取ele1元素内的所有p元素
p_eles = ele1.eles('tag:p')
# 打印第一个p元素的文本
print(p_eles[0])
1
2
3
4
5
6
7
8
9
10

# 获取元素信息

当我们定位到元素后,可用获取到元素的各种各样的信息。更多的用法请自行在官网中查询。

# 元素内容和元素属性

from DrissionPage import Chromium
tab = Chromium().latest_tab
tab.get('https://www.baidu.com')

# 在页面内查找元素
ele = tab.ele('#one')

# 返回元素的标签名
ele.tag
# 返回元素的outerHTML文本
ele.html
# 返回元素的innerHTML文本
ele.inner_html
# 返回元素内所有文本组合成的字符串
ele.text
# 返回元素内未经处理的原始文本
ele.raw_text
# 以字典形式返回元素所有属性及值
ele.attrs
# 返回元素的href属性或src属性,没有这两个属性则返回None
ele.link
# 返回元素 css 样式属性值
ele.style()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 元素状态信息

from DrissionPage import Chromium
tab = Chromium().latest_tab
tab.get('https://www.baidu.com')

# 在页面内查找元素
ele = tab.ele('#one')

# 此属性返回获取内部或相对定位元素的超时时间,实际上是元素所在页面的超时设置。
ele.timeout
# 此属性以布尔值方式返回元素是否在视口中,以元素可以接受点击的点为判断。
ele.states.is_in_viewport
# 此属性以布尔值方式返回元素是否整个在视口中。
ele.states.is_whole_in_viewport
# 以布尔值返回表单单选或多选元素是否选中
ele.states.is_checked
# 以布尔值返回元素是否可见
ele.states.is_displayed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 元素交互操作

当我们在页面内获取到元素信息后,可能需要与页面中的元素进行交互操作。如点击,输入,修改等。

更多的用法请自行在官网中查询。

# 点击元素

from DrissionPage import Chromium
tab = Chromium().latest_tab
tab.get('https://www.baidu.com')

# 在页面内查找元素
ele = tab.ele('#one')

# 对ele元素进行模拟点击,如判断被遮挡也会点击
ele.click()
# 用js方式点击ele元素,无视遮罩层
ele.click(by_js=True)
# 如元素不被遮挡,用模拟点击,否则用js点击
ele.click(by_js=None)

# 点击元素,触发文件选择框,并把指定的文件路径添加到网页
ele.click.to_upload(file_paths="文件路径")

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

# 输入内容

from DrissionPage import Chromium
tab = Chromium().latest_tab
tab.get('https://www.baidu.com')

# 在页面内查找元素
ele = tab.ele('#one')

# 清空元素文本
ele.clear() 
# 向元素输入文本
ele.input('Hello world!')
# 向元素输入文本并回车
ele.input('Hello world!\n')
1
2
3
4
5
6
7
8
9
10
11
12
13

# 修改元素

from DrissionPage import Chromium
tab = Chromium().latest_tab
tab.get('https://www.baidu.com')

# 在页面内查找元素
ele = tab.ele('#one')

# 设置元素的 innerHTML 内容
ele.set.innerHTML(html="html文本")
# 设置元素property属性
ele.set.property('value', 'Hello world!')
# 设置元素 attribute 属性
ele.set.attr(name='href', value='https://DrissionPage.cn')
# 删除元素 attribute 属性
ele.remove_attr(name='href')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 监听网络数据包

DrissionPage 在每个页面对象(包括 Tab 和 Frame 对象)内置了一个监听器,专门用于抓取浏览器数据包。

我们可用通过获取网络数据包,从而读取数据包中的内容。下面是两个示例。

更多的用法请自行在官网中查询。

示例1 等待并获取数据包

from DrissionPage import Chromium

tab = Chromium().latest_tab
tab.get('https://gitee.com/explore/all')  # 访问网址,这行产生的数据包不监听

# 开启监听器,获取包含该文本的数据包
tab.listen.start('gitee.com/explore') 

# 循环5次
for _ in range(5):
    # 点击下一页,这个操作会产生数据包
    tab('@rel=next').click()  
    # 等待并获取一个数据包
    res = tab.listen.wait()  
    # 打印数据包url
    print(res.url)  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

示例2 实时获取数据包

from DrissionPage import Chromium

tab = Chromium().latest_tab
tab.listen.start('gitee.com/explore')  # 开启监听器,获取包含该文本的数据包
tab.get('https://gitee.com/explore/all')  # 访问网址

i = 0
for packet in tab.listen.steps():
    # 打印数据包url
    print(packet.url)
    # 点击下一页
    tab('@rel=next').click()  
    i += 1
    if i == 5:
        break
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 上传文件

更多的用法请自行在官网中查询。

上传文件有两种方式:

  • 方式1 拦截文件输入框,自动填入路径。
  • 方式2 找到<input>元素,填入文件路径。

方式1

from DrissionPage import Chromium
tab = Chromium().latest_tab
# 获取上传按钮元素
ele = tab('#uploadButton')
# 点击上传按钮,并上传文件
ele.click.to_upload(r'C:\\text.txt')
1
2
3
4
5
6

方式2

如果<input>元素很好找,这种方式是很简便的。

# 获取input元素
upload = tab('tag:input@type=file')

# 传入一个文件路径
upload.input('D:\\test1.txt')

# 传入多个文件路径,方式 1
paths = 'D:\\test1.txt\nD:\\test2.txt'
upload.input(paths)

# 传入多个文件路径,方式 2
paths = ['D:\\test1.txt', 'D:\\test2.txt']
upload.input(paths)s

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

# 写过的刷视频demo

这个demo是用来在某个视频网站中给用户刷视频的。完全是通过DrissionPage库模拟浏览器来刷视频的。与其他通过调用API接口来刷视频的方式不太一样,因此将代码记录在这里。

import os
import glob
from DrissionPage import ChromiumPage, ChromiumOptions
from log.log_config import logger
import time
import threading
from datetime import datetime,timedelta
import cv2
import random
import re

# 浏览器页面对象设置
def getPage(user):
    path = r'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe'  # 浏览器的可执行文件路径
    # 设置无头模式
    # co = ChromiumOptions().headless()
    co = ChromiumOptions()
    # 设置浏览器路径
    co.set_browser_path(path)
    # 来宾模式
    co.set_argument('--guest')
    # 无痕模式
    co.incognito()
    # 设置初始窗口大小
    co.set_argument('--window-size', '390,844')
    # 阻止“自动保存密码”的提示气泡
    co.set_pref('credentials_enable_service', False)
    # 阻止“要恢复页面吗?Chrome未正确关闭”的提示气泡
    co.set_argument('--hide-crash-restore-bubble')
    # 设置浏览器的UA
    co.set_user_agent(user_agent=user['ua'])
    # 自动获取空闲端口,从而启动一个全新浏览器
    co.auto_port()
    # 浏览器静音
    co.mute(True)
    # 创建页面对象,并启动或接管浏览器,传入浏览器配置
    page = ChromiumPage(co)
    # 之后出现的弹窗都会自动确认
    page.set.auto_handle_alert()
    return page

# 将视频时长字符串和视频进度转换为对应的秒数
def convertTime(video_time,video_status):
    # 次数代码省略

# 登录页面
def toLoginPage(page,user):
    # 跳转到登录页面
    page.get('https://gd.aqscwlxy.com/h5/pages/login/login')
    # 定位到账号文本框和密码文本框,输入账号密码
    a_eles = page.eles('@class=uni-input-input')
    logger.info(f"{user['name']} {len(a_eles)}")
    if a_eles:
        a_eles[0].input(user['idCard'])
        a_eles[1].input(user['pwd'])
    else:
        raise Exception(f"{user["name"]} {len(a_eles)} 。没找到登录页面的元素,等待下次重启")

    # 点击我已阅读
    page.ele('@class=uni-checkbox-input').click()
    # 点击登录按钮,找到class为login_btn的元素节点
    page.ele('@class=login-btn').click()

# 新的视频截图方法,从多个视频中随机获得一个视频文件进行截图。
def new_video_capture(user):
    # 指定要查找文件的目录
    directory = user['face_path']
    # 使用glob模块查找文件名包含'名字+身份证'的文件
    files = glob.glob(os.path.join(directory, f'*{user['name']+user['idCard']}*'))
    if files:
        random_user_video_path = random.choice(files)
    else:
        raise Exception(f"{user["name"]} 。没有找到视频文件")

    logger.info(f"{user["name"]} 开始执行 new_video_capture 方法 进行人脸视频截图。人脸视频文件为 {random_user_video_path} ====================== ")
    # 打开视频文件
    video = cv2.VideoCapture(random_user_video_path)
    # 获取视频的总帧数
    frame_count = int(video.get(cv2.CAP_PROP_FRAME_COUNT))
    # 获取视频的帧率
    fps = int(video.get(cv2.CAP_PROP_FPS))
    # 计算出视频时长
    video_time = int(frame_count / fps)
    is_success = False
    while is_success is False:
        # 随机生成一个秒数,范围在1-video_time之间
        rand_int = random.randint(1, video_time)
        # 计算要截取的帧数
        frame_to_capture = int(rand_int * fps)
        # 跳到视频文件的指定帧数
        video.set(cv2.CAP_PROP_POS_FRAMES, frame_to_capture)
        # 读取该帧数
        ret, frame = video.read()
        # 截图路径
        imgPath = directory + "\\img\\" + user['idCard'] + '-' + str(random.randint(1,1000)) + '.jpeg'
        # 如果成功读取到帧,则保存为图片
        if ret:
            # 把视频截图保持到指定路径中,截图成功返回Ture
            is_success = cv2.imwrite(imgPath, frame)
            logger.info(f'{user["name"]} 截图成功。is_success 为 {is_success}')
        else:
            logger.error(f'{user["name"]} 截图失败,开始重新截图。is_success 为 {is_success}')

    # 释放视频文件
    video.release()
    newimgPath = directory + "\\img\\"  + user['idCard'] + '-' + str(random.randint(1,1000)) + '.jpeg'
    # 对图片进行处理
    resize_and_compress_image(imgPath,newimgPath)
    # 删除旧图片
    os.remove(imgPath)
    return newimgPath

# 图片大小压缩到90kb之内 和 重新设置图片尺寸
def resize_and_compress_image(input_path, output_path, target_size=90 * 1024):
    # 读取图片
    image = cv2.imread(input_path)
    # 调整图片分辨率尺寸
    # 获取原始图像的高度和宽度
    height,width = image.shape[:2]
    # 如果图片宽大于高,将设置图片宽为1170,高为对应的比例。如果图片高大于宽,将设置图片高为1170,宽为对应的比例。
    if width > height:
        # 定义目标宽度
        target_width = 1170
        # 根据原始宽高比计算目标高度
        target_height = int((target_width / width) * height)
    elif height > width:
        # 定义目标宽度
        target_height = 1170
        # 根据原始宽高比计算目标宽度
        target_width = int((target_height / height) * width)

    # 使用cv2.resize进行缩放,保持宽高比
    resized_img = cv2.resize(image, (target_width, target_height))

    # 重新赋值
    image = resized_img

    # 图片大小压缩
    # 设置图片初始质量
    quality = 95
    encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), quality]
    while True:
        result, encimg = cv2.imencode('.jpeg', image, encode_param)
        if result:
            size = len(encimg)
            if size <= target_size:
                break
            quality -= 5
            encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), quality]

    with open(output_path, 'wb') as f:
        f.write(encimg)

# 主方法
def main(user):
    try:
        # 获取浏览器对象
        logger.info(f"{threading.current_thread().name} 线程开始运行。。。。")
        # 设置ua
        user['ua'] = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
        page = getPage(user)

        # 开启监听器,监听数据包
        page.listen.start(targets=['gd.aqscwlxy.com/gd_api/study/studyrecord.php', 'gd.aqscwlxy.com/gd_api/face/study_face.php'])

        # 先检查是否有对应视频文件,使用glob模块查找文件名包含'名字+身份证'的文件
        files = glob.glob(os.path.join(user['face_path'], f'*{user['name'] + user['idCard']}*'))
        if files == []:
            raise Exception(f"{user["name"]} {user['idCard']} 没有找到视频文件")

        # 登录页面
        toLoginPage(page, user)

        # 此时进入到课程列表,查询有几个课程
        course_count = len(page.eles("@class=uni-list-item"))
        if course_count == 0:
            raise Exception(f"{user["name"]} {user['idCard']} course_count = {course_count} 没有找到对应课程。等待下次重启。")

        # 根据课程数量遍历几次
        for index in range(course_count):
            # 根据index获取第几个课程。
            course = page.eles("@class=uni-list-item")[index]
            # 课程名称
            course_name = course.ele("@class=uni-title").text
            # 查询课程是否完成
            course_progress = course.ele("@class=text-totalhour").text
            # 使用正则表达式提取两个数字
            numbers = re.findall(r"\d+\.\d+", course_progress)
            if numbers[0] < numbers[1]:
                logger.warning(f"{user["name"]} {course_name} {course_progress}  没学完。")

            # 点击课程
            course.click()

            # 查询是否有身份确认弹窗,若存在直接报异常
            identity_check = page.ele("@text()=系统已查询到您的培训班报名记录,请身份确认后开始学习。点击下方【身份确认】按钮进行身份确认!",timeout=2)
            if identity_check:
                # 若存在身份确认弹窗,则点击身份确认按钮。
                logger.info(f"{user['name']} 开始身份确认成功。")
                page.ele("@class=uni-modal__btn uni-modal__btn_primary").click()
                # 获取人脸文件
                imgPath = new_video_capture(user)
                logger.info(f"{user['name']} 人脸图片为 {imgPath}")
                # 触发文件上传按钮,并上传文件
                time.sleep(1)
                page.ele("@id=bt_selectphoto").click.to_upload(imgPath)
                # 找到提交按钮并点击
                time.sleep(1)
                page.ele("@id=bt_sendblog").click(by_js=True)
                # 找到马上开始学习按钮并点击
                time.sleep(3)
                page.ele("@id=gostudy").click()
                logger.success(f"{user['name']} 身份确认成功。开始学习。")
                # 后面会回到课程列表页面。此处会报一次异常。等重新运行后就正常了。

            # # 查询是否有确认提示弹窗,若存在则直接点击确认按钮
            check_model = page.ele("@text():学员无法在手机端和电脑端同时登录学习",timeout=2)
            if check_model:
                # 找到弹窗的确认按钮,有就点击,没有就不点击。
                c_ele = page.ele("@class=uni-modal__btn uni-modal__btn_primary",timeout=2)
                if c_ele:
                    c_ele.click()

            # 查询课程中的各个章节并遍历
            chapters = page.eles('@class=tab-item-box')[1].children()
            for chapter in chapters:
                # 查询章节中的各个视频并遍历
                videos = chapter.ele("@class=chapter-box").children()
                for video in videos:
                    a = video.text
                    video_str = a.split("\n")
                    video_time = video_str[0]
                    video_title = video_str[1]
                    video_status = video_str[3]

                    if video_status == "已观看100.00%":
                        continue
                    else:
                        # 将视频时长转换为秒数
                        sleep_time = convertTime(video_time,video_status)
                        logger.info(f"{user['name']} 开始看视频 {video_title}。视频总时长为 {video_time} 视频状态 {video_status} 。因此还需要看 {sleep_time} 秒")
                        # 点击视频标签
                        video.click(by_js=True)
                        # 点击是否播放弹窗的确认按钮
                        page.ele("@class=uni-modal__btn uni-modal__btn_primary").click()
                        # 等待检测是否有"前一次认证操作未完成"弹窗。如果有则点击确定
                        auth_tag = page.wait.ele_displayed("@@class=uni-modal__bd@@text():前一次认证操作未完成", raise_err=False,timeout=2)
                        if auth_tag:
                            page.ele("@class=uni-modal__btn uni-modal__btn_primary").click()

                        # 视频播放剩余的结束时间,额外加上120-180秒
                        target_time = datetime.now() + timedelta(seconds=sleep_time + random.randint(120,180))

                        # 若当前时间大于目标时间,则表示视频已经放完了
                        while datetime.now() < target_time:
                            logger.info(f"{user['name']} 视频 {video_title}。休眠60秒")
                            # 休眠60秒
                            time.sleep(60)
                            # 等待take-photo-box元素标签是否可用 (如果可用表示当前页面是上传照片页面)
                            photo_tag = page.wait.ele_displayed("@class=take-photo-box", raise_err=False)
                            if photo_tag:
                                logger.warning(f"{user['name']} 视频 {video_title}。检测到当前页面是人脸验证页面。开始上传人脸照片")
                                # 获取人脸文件
                                imgPath = new_video_capture(user)
                                logger.info(f"{user['name']} 人脸图片为 {imgPath}")
                                # 触发文件上传按钮,并上传文件
                                time.sleep(1)
                                page.ele("@class=take-photo-box").click.to_upload(imgPath)
                                # 找到提交按钮(标签为uni-button,标签文本包含'提交验证')
                                time.sleep(1)
                                b_tag = page.ele("@@tag()=uni-button@@text():提交验证")
                                if b_tag:
                                    b_tag.click(by_js=True)

                            # 监听数据包并打印
                            for packet in page.listen.steps(count=2,timeout=5):
                                response_info = packet.response
                                if response_info.url == "https://gd.aqscwlxy.com/gd_api/study/studyrecord.php" and response_info.body['msg'] == '更新成功':
                                    logger.success(f"{user['name']} 视频 {video_title}。打卡成功")
                                elif response_info.url == "https://gd.aqscwlxy.com/gd_api/face/study_face.php" and response_info.body['msg'] == '认证成功,请继续学习!':
                                    logger.success(f"{user['name']} 视频 {video_title}。人脸认证成功")
                                else:
                                    raise Exception(f"{user['name']} 视频 {video_title}。其他情况。response_info = {response_info.body} , {response_info.url} , {response_info.status} ")

                        # 当视频播放完毕后会出现是否做测试的弹框。点击取消
                        exam_tag = page.wait.ele_displayed("@@class=uni-modal__bd@@text():当前视频已播放完成,是否进行在线测试",raise_err=False)
                        if exam_tag:
                            page.ele("@class=uni-modal__btn uni-modal__btn_default").click()

                        logger.success(f"{user['name']} 视频 {video_title}。已看完")

            logger.success(f"{user['name']} 已看完课程 {course_name}。")
            # 返回上一个页面
            page.back(1)

        # 终止监听器
        page.listen.stop()
        logger.success(f"{user['name']} 已看完所有视频。")
        # 关闭当前页面
        page.close()
        # 关闭浏览器
        page.quit()
    except Exception as e:
        # 终止监听器
        page.listen.stop()
        # 关闭当前页面
        page.close()
        # 关闭浏览器
        page.quit()
        # 打印完整异常信息
        logger.exception(f"发生异常。异常信息为 {e}")
        # 如果脚本作为单文件运行时,想要循环刷的话,则直接休眠后重新开启线程即可。
        time.sleep(1200)
        main(user)

if __name__ == '__main__':
    """
        刷时程序。该脚本是使用DrissionPage库模拟浏览器,来刷时
        手机端:https://gd.aqscwlxy.com/h5/pages/login/login?f=%252F     注意:可以在微信开发者工具中打开,也可以在浏览器中打开。F12设置为手机尺寸
        PC端网址 https://gd.aqscwlxy.com/pc/login                          注意:F12设置为电脑尺寸
    """

    user_list = [
        {'name': '小明', 'idCard': 'XXXXXXXXXXXXXX', 'pwd': 'XXXXXX', 'org': 'XXX', 'face_path': 'C:\\Users\\18271\\Desktop\\face_video'},
    ]

    # 给这些未进行刷题并且未刷完视频的用户创建子线程
    for user in user_list:
        # 休眠
        time.sleep(3)
        # 创建一个子线程
        t = threading.Thread(target=main, name=f"{user['name']}", args=(user,))
        # 运行该子线程
        t.start()

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
Last Updated: 2/24/2025, 5:57:41 PM