DrissionPage多线程实践
背景:项目中需要抓取部分平台的数据,因为涉及到登录,且暂未实现接口登录。所以采用selenium登录后获取cookie传给requests的方式来实现。后了解到
DrissionPage
国产开源库,等于是把selenium
和requests
结合起来(bushi,实际并不是selenium
而是这位大佬自己通过websorket
发送cdp
命令驱动浏览器)。看了下文档很快就上手了,实现了项目需求。过程中想要提高效率,就想到了多线程驱动。
需求
设定一个本次的需求:通过纯driver
的方式抓取gitee
开源项目https://gitee.com/explore/all的项目简介。(gitee
的项目数据是可以通过requests
获取的)
获取项目信息
先把待抓取的项目名称和url获取下来
from DrissionPage import ChromiumPage, ChromiumOptions
# 获取前2页的项目链接
all_proj_url = []
co = ChromiumOptions(read_file=False)
cp = ChromiumPage(addr_or_opts=co)
for i in range(1, 3):
cp.get(f'https://gitee.com/explore/all?page={i}')
proj_li = cp.s_eles('c:h3>a')
for proj in proj_li:
url_suffix = proj.attr('href')
title = proj.attr('title')
all_proj_url.append(f'{title} | {url_suffix}\n')
with open('target.txt', 'w', encoding='utf8') as f_w:
f_w.writelines(all_proj_url)
保存数据的结果如下:
for循环
先来看下直接for循环采集简介耗时多少
import time
from DrissionPage import ChromiumPage, ChromiumOptions
co = ChromiumOptions(read_file=False)
co.set_browser_path(r"D:\ProgramData\Twinkstar\twinkstar.exe")
cp = ChromiumPage(addr_or_opts=co)
def fun(url, index):
"""
根据项目url访问项目详情获取简介信息
"""
cp.get(url)
cp.wait.doc_loaded()
content = '暂无简介'
content_ele = cp.ele('.git-project-desc-text')
if content_ele:
content = content_ele.text
# else:
# Log.warning(f'index:{index} 未找到元素:{content_ele.args}')
Log.info(f'index:{index} url: {url} content: {content}')
return content
st = time.time()
futures = []
with open('target.txt', 'r', encoding='utf8') as f_r:
for index, line in enumerate(f_r):
url = line.strip('\n').split(' | ')[1]
fun(url, index)
cp.quit()
Log.info(f'total cost {time.time() - st}')
执行结果
D:\ProgramData\virtualenvs\all_tool\Scripts\python.exe D:\01work\02project\py_project\all_tool\Test\dp_learn.py
MindSpore/RingMo-Framework 中科院空天信息创新研究院与华为大模型研发团队联合打造的一款用于视觉领域的全国产化自监督预训练开发套件
...
total cost 128.63436770439148
可以看到耗时有128秒。
多线程1
修改代码为多线程执行,注意,这里是有坑的!这种方法看看就行
import time
from DrissionPage import ChromiumPage, ChromiumOptions
from concurrent.futures import ThreadPoolExecutor, as_completed
from common.operate_log import Log
co = ChromiumOptions(read_file=False)
co.set_browser_path(r"D:\ProgramData\Twinkstar\twinkstar.exe")
cp = ChromiumPage(addr_or_opts=co) # 注意这里有坑,在下面的执行结果中会体现
def fun(url):
"""
根据项目url访问项目详情获取简介信息
"""
cp.get(url) # 注意这里有坑,在下面的执行结果中会体现
content = '暂无简介'
content_ele = cp.ele('.git-project-desc-text')
if content_ele:
content = content_ele.text
else:
Log.info('未找到元素:', content_ele.args)
Log.info(content)
return content
st = time.time()
with ThreadPoolExecutor(max_workers=3,thread_name_prefix='thread') as tp:
with open('target.txt', 'r', encoding='utf8') as f_r:
futures = [tp.submit(fun, line.strip('\n').split(' | ')[1]) for line in f_r]
# 通过as_completed获取已完成的任务结果
for future in as_completed(futures):
res = future.result()
Log.info(f'total cost {time.time() - st}')
执行结果:
D:\ProgramData\virtualenvs\all_tool\Scripts\python.exe D:\01work\02project\py_project\all_tool\Test\dp_learn.py
2024-01-31 11:34:39.992 | thread_2 | INFO | - 用Rust实现仿nginx,力争实现一个可替代方案,http/https代理, socks5代理, 负载均衡, 反向代理, 静态文件服务器,四层TCP/UDP转发,websocket转发, 内网穿透nat
2024-01-31 11:34:41.827 | thread_1 | INFO | - MindSpore Pandas is a data analysis framework, which is compatible with Pandas interfaces and provides distributed processing capabilities.
2024-01-31 11:34:56.413 | thread_1 | INFO | - 👚 基于 Ant Design 设计语言的 Winform 界面库
2024-01-31 11:34:56.417 | thread_0 | INFO | - 👚 基于 Ant Design 设计语言的 Winform 界面库
2024-01-31 11:34:59.950 | thread_0 | INFO | - 中文对话0.2B小模型(ChatLM-Chinese-0.2B),开源所有数据集来源、数据清洗、tokenizer训练、模型预训练、SFT指令微调、RLHF优化等流程的全部代码。
2024-01-31 11:34:59.955 | thread_1 | INFO | - 中文对话0.2B小模型(ChatLM-Chinese-0.2B),开源所有数据集来源、数据清洗、tokenizer训练、模型预训练、SFT指令微调、RLHF优化等流程的全部代码。
...
个人计划安排保存在自己的服务器中,并在任意设备之间实时同步。同时还是一个个人博客。
2024-01-31 11:35:31.259 | thread_1 | INFO | - 本项目发布了Phytium系列CPU的Standalone BSP源码,Baremetal参考例程及其配置构建工具
2024-01-31 11:35:42.809 | thread_2 | INFO | - 未找到元素:
2024-01-31 11:35:42.810 | thread_2 | INFO | - 暂无简介
2024-01-31 11:35:42.811 | MainThread | INFO | - total cost 65.02382159233093
Process finished with exit code 0
从日志中看到如下几个问题:
- 不同的项目简介的内容相同
- 页面存在找不到元素的情况
原因分析:
dp
是通过websorket
的方式驱动浏览器的,每次访问页面、定位元素、操作元素等均是通过一个确定的ip:port
来发送的请求。
如果这个ws
连接已经存在,即复用。
所以会出现页面在操作上一个请求时,当前请求发起定位元素(获取元素属性)的请求,就会存在获取的结果乱了(或者上面出现的内容重复了)。同时,如果页面对象在定位元素准备操作元素时,如果此时另一个请求发过来页面更新了(访问了新的内容)当前要操作的元素不再存在于前一个获取到的页面,就会抛出异常。
下面我们修改下代码:
import time
from DrissionPage import ChromiumPage, ChromiumOptions
from concurrent.futures import ThreadPoolExecutor
from common.operate_log import Log
def fun(url, index):
"""
根据项目url访问项目详情获取简介信息
"""
# 每个线程单独创建自己的cp对象即使用单独的ws通信
co = ChromiumOptions(read_file=False)
co.set_browser_path(r"D:\ProgramData\Twinkstar\twinkstar.exe")
co.auto_port() # 每个线程单独使用一个ip:port通信
cp = ChromiumPage(addr_or_opts=co)
cp.get(url)
cp.wait.doc_loaded()
content = '暂无简介'
content_ele = cp.ele('.git-project-desc-text')
if content_ele:
content = content_ele.text
Log.info(f'index:{index} url: {url} content: {content}')
cp.quit(force=True)
return content
st = time.time()
with ThreadPoolExecutor(max_workers=3, thread_name_prefix='thread') as tp:
with open('target.txt', 'r', encoding='utf8') as f_r:
futures = [tp.submit(fun, line.strip('\n').split(' | ')[1], index) for index, line in enumerate(f_r)]
Log.info(f'total cost {time.time() - st}')
执行结果
D:\ProgramData\virtualenvs\all_tool\Scripts\python.exe D:\01work\02project\py_project\all_tool\Test\dp_learn.py
2024-02-01 10:11:27.391 | thread_2 | INFO | - index:2 url: https://gitee.com/tickbh/wmproxy content: 用Rust实现仿nginx,力争实现一个可替代方案,http/https代理, socks5代理, 负载均衡, 反向代理, 静态文件服务器,四层TCP/UDP转发,websocket转发, 内网穿透nat
2024-02-01 10:11:28.080 | thread_0 | INFO | - index:0 url: https://gitee.com/mindspore/ringmo-framework content: 中科院空天信息创新研究院与华为大模型研发团队联合打造的一款用于视觉领域的全国产化自监督预训练开发套件
2024-02-01 10:11:29.054 | thread_1 | INFO | - index:1 url: https://gitee.com/mindspore/mindpandas content: MindSpore Pandas is a data analysis framework, which is compatible with Pandas interfaces and provides distributed processing capabilities.
...
2024-02-01 10:14:07.356 | thread_2 | INFO | - index:25 url: https://gitee.com/yeytytytytyytyt/air-drop-plus content: 使用 Python 和快捷指令实现的 Windows 和 iOS 设备之间互传文件和剪贴板同步
2024-02-01 10:14:08.360 | thread_1 | INFO | - index:26 url: https://gitee.com/oi-contrib/VISLite content: 🎃 灵活、快速、简单的数据可视化交互式跨端前端库 💯
2024-02-01 10:14:17.849 | thread_0 | INFO | - index:27 url: https://gitee.com/phytium_embedded/phytium-standalone-sdk content: 本项目发布了Phytium系列CPU的Standalone BSP源码,Baremetal参考例程及其配置构建工具
2024-02-01 10:14:26.843 | thread_1 | INFO | - index:28 url: https://gitee.com/blossom-editor/blossom content: 一个支持私有部署的云端存储双链笔记软件,你可以将你所有的笔记,图片,个人计划安排保存在自己的服务器中,并在任意设备之间实时同步。同时还是一个个人博客。
2024-02-01 10:14:41.634 | thread_2 | INFO | - index:29 url: https://gitee.com/TencentOS/TencentOS-tiny content: 暂无简介
2024-02-01 10:14:44.896 | MainThread | INFO | - total cost 212.73357844352722
Process finished with exit code 0
可以看到上面的俩问题解决了:1. 多个线程的返回结果不同了;2. 页面找不到元素(丢失了页面对象)的情况不存在了
但是出现了新的问题:多线程执行的时间居然比for循环耗时更长,这里用了212秒!
下面修改程序解决这个问题。
多线程2
考虑到每个线程都会单独启动cp
对象,这里耗时应该是比较久的,所以尝试使用固定的cp
对象,不同的线程接管不同的对象执行任务。
- 方法1:在fun中定义启动
cp
对象
import time
from DrissionPage import ChromiumPage, ChromiumOptions
from concurrent.futures import ThreadPoolExecutor
from common.operate_log import Log
def fun(url, index):
"""
根据项目url访问项目详情获取简介信息
"""
co = ChromiumOptions(read_file=False)
co.set_browser_path(r"D:\ProgramData\Twinkstar\twinkstar.exe")
co.set_local_port(922 + (index % thread_num)) # 通过线程和任务的index确定使用哪个端口的cp对象,,,指定固定的端口通讯
cp = ChromiumPage(addr_or_opts=co) # 这里如果发现端口存在就不会再起新的浏览器
cp.get(url)
cp.wait.doc_loaded()
content = '暂无简介'
content_ele = cp.ele('.git-project-desc-text')
if content_ele:
content = content_ele.text
Log.info(f'index:{index} url: {url} content: {content}')
# cp.quit(force=True) # 因为复用了cp对象,所以执行完浏览器就不要关了,不然还会重新启动
return content
st = time.time()
thread_num = 3 # 定义了线程数
with ThreadPoolExecutor(max_workers=thread_num, thread_name_prefix='thread') as tp:
with open('target.txt', 'r', encoding='utf8') as f_r:
futures = [tp.submit(fun, line.strip('\n').split(' | ')[1], index) for index, line in enumerate(f_r)]
Log.info(f'total cost {time.time() - st}')
- 方法2:启动前就把浏览器对象初始化好
import time
from DrissionPage import ChromiumPage, ChromiumOptions
from concurrent.futures import ThreadPoolExecutor
from common.operate_log import Log
def fun(url, index):
"""
根据项目url访问项目详情获取简介信息
"""
cp = cp_li[index % thread_num]
cp.get(url)
cp.wait.doc_loaded()
content = '暂无简介'
content_ele = cp.ele('.git-project-desc-text')
if content_ele:
content = content_ele.text
Log.info(f'index:{index} url: {url} content: {content}')
return content
st = time.time()
thread_num = 3
cp_li = []
for port in range(thread_num): # 外部直接启动三个浏览器对象
co = ChromiumOptions(read_file=False)
co.set_browser_path(r"D:\ProgramData\Twinkstar\twinkstar.exe")
co.set_local_port(922 + port) # 指定固定的端口通讯
cp_li.append(ChromiumPage(addr_or_opts=co))
with ThreadPoolExecutor(max_workers=thread_num, thread_name_prefix='thread') as tp:
with open('target.txt', 'r', encoding='utf8') as f_r:
futures = [tp.submit(fun, line.strip('\n').split(' | ')[1], index) for index, line in enumerate(f_r)]
for cp in cp_li:
cp.quit()
Log.info(f'total cost {time.time() - st}')
方法2的好处是能够在执行完成后把浏览器关掉,而方法1,不太好处理。
执行结果
D:\ProgramData\virtualenvs\all_tool\Scripts\python.exe D:\01work\02project\py_project\all_tool\Test\dp_learn.py
2024-02-01 10:31:18.986 | thread_2 | INFO | - index:2 url: https://gitee.com/tickbh/wmproxy content: 用Rust实现仿nginx,力争实现一个可替代方案,http/https代理, socks5代理, 负载均衡, 反向代理, 静态文件服务器,四层TCP/UDP转发,websocket转发, 内网穿透nat
2024-02-01 10:31:19.415 | thread_1 | INFO | - index:1 url: https://gitee.com/mindspore/mindpandas content: MindSpore Pandas is a data analysis framework, which is compatible with Pandas interfaces and provides distributed processing capabilities.
2024-02-01 10:31:37.022 | thread_0 | INFO | - index:13 url: https://gitee.com/tsbrowser/xiangtian-workbench content: 一款集桌面管理、效率办公、游戏娱乐为一体的副屏“桌面系统”。 除副屏以外,主屏窗口化也可以正常使用。 基于Electron和Vue全家桶开发。 想天工作台客户端的开源项目库,可编译为客户端或者web网页(web版暂时体验糟糕,不推荐)。 如果不会开发的,也可以直接下载官网版本,安装在电脑上就可以使用。
2024-02-01 10:31:38.817 | thread_1 | INFO | - index:15 url: https://gitee.com/zhiming999/rpcf content: 这是一个基于C++的多语言,高可用,高并发,高安全的RPC和流服务框架。自带接口定义语言,快速生成分布式应用框架,有效节省学习和开发时间。本项目持续更新中。
...
2024-02-01 10:31:53.407 | thread_1 | INFO | - index:23 url: https://gitee.com/secretflow/secretpad content: 🎃 灵活、快速、简单的数据可视化交互式跨端前端库 💯
2024-02-01 10:31:55.329 | thread_2 | INFO | - index:28 url: https://gitee.com/blossom-editor/blossom content: 一个支持私有部署的云端存储双链笔记软件,你可以将你所有的笔记,图片,个人计划安排保存在自己的服务器中,并在任意设备之间实时同步。同时还是一个个人博客。
2024-02-01 10:31:55.738 | thread_0 | INFO | - index:27 url: https://gitee.com/phytium_embedded/phytium-standalone-sdk content: 本项目发布了Phytium系列CPU的Standalone BSP源码,Baremetal参考例程及其配置构建工具
2024-02-01 10:32:12.030 | thread_1 | INFO | - index:29 url: https://gitee.com/TencentOS/TencentOS-tiny content: 暂无简介
2024-02-01 10:32:12.031 | MainThread | INFO | - total cost 62.62155222892761
Process finished with exit code 0
通过执行日志可以看到前面提到的问题解决了,多线程执行后总耗时也降下来了,此处为62秒。
还有其他的方案吗?当然!
多线程3
前面是采用多个线程分别操作对应的浏览器对象。新的解决思路是,只启动一个浏览器对象,不同的线程操作不同的浏览器tab页。
import time
from DrissionPage import ChromiumPage, ChromiumOptions
from concurrent.futures import ThreadPoolExecutor
from common.operate_log import Log
def fun(url, index):
"""
根据项目url访问项目详情获取简介信息
"""
tabs = cp.tabs # 获取所有的tab页
if len(tabs) < thread_num: # 没到线程数量的tab页的话就新开tab页访问
cp.new_tab(url)
tab = cp.latest_tab
else:
tab = cp.get_tab(tabs[index % thread_num]) # 取对应的tab来访问
tab.get(url)
tab.wait.doc_loaded()
content = '暂无简介'
content_ele = tab.ele('.git-project-desc-text') # 注意这里要使用具体的tab来定位元素,而非cp对象
if content_ele:
content = content_ele.text
Log.info(f'index:{index} url: {url} content: {content}')
return content
st = time.time()
thread_num = 3
co = ChromiumOptions(read_file=False)
co.set_browser_path(r"D:\ProgramData\Twinkstar\twinkstar.exe")
cp = ChromiumPage(addr_or_opts=co)
with ThreadPoolExecutor(max_workers=thread_num, thread_name_prefix='thread') as tp:
with open('target.txt', 'r', encoding='utf8') as f_r:
futures = [tp.submit(fun, line.strip('\n').split(' | ')[1], index) for index, line in enumerate(f_r)]
cp.quit()
Log.info(f'total cost {time.time() - st}')
查看执行结果
D:\ProgramData\virtualenvs\all_tool\Scripts\python.exe D:\01work\02project\py_project\all_tool\Test\dp_learn.py
2024-02-01 16:56:00.731 | thread_1 | INFO | - index:3 url: https://gitee.com/aizuda/easy-retry content: 🔥🔥🔥 灵活,可靠和快速的分布式任务重试和分布式任务调度平台
2024-02-01 16:56:00.732 | thread_0 | INFO | - index:4 url: https://gitee.com/jeremyczhen/fdbus content: Fast Distributed Bus (FDBus)
2024-02-01 16:56:01.859 | thread_2 | INFO | - index:5 url: https://gitee.com/FredyXu/coffee content: 一个基于ESP32使用ESP-IDF开发的摩尔斯电码练习器。
...
2024-02-01 16:56:20.427 | thread_0 | INFO | - index:28 url: https://gitee.com/blossom-editor/blossom content: 一个支持私有部署的云端存储双链笔记软件,你可以将你所有的笔记,图片,个人计划安排保存在自己的服务器中,并在任意设备之间实时同步。同时还是一个个人博客。
2024-02-01 16:56:31.682 | thread_1 | INFO | - index:29 url: https://gitee.com/TencentOS/TencentOS-tiny content: 暂无简介
2024-02-01 16:56:37.540 | MainThread | INFO | - total cost 44.64077115058899
Process finished with exit code 0
通过执行日志开到,效率大幅度提升。总耗时现在降到了44秒。
总结
至此,多线程操作cp
就验证完毕了。可以看到有三种思路:
- 每个线程分别启动一个浏览器去执行任务,通过auto_port实现 – 每次启动关闭,会增加耗时
- 根据线程数提前启动浏览器进程后,在fun中使用不同浏览器执行,最后关闭进程。同时也介绍了set_local_port方法,本质和上面的auto_port一样,一个指定,一个随机
- 使用一个浏览器,启动多个tab页,多个线程分别操作不同的tab页即可。这里用到的是
new_tab
和get_tab
等方法
补充,本次验证使用的解释器和库版本:
Python 3.9.5
DrissionPage 4.0.4.3
由于dp
库的研发大佬还在不断迭代,后面可能有更方便的方法。后期项目中如有用到,再更新新版本实现方法。