当前位置:AIGC资讯 > 数据采集 > 正文

爬虫 — 自动化爬虫 Selenium

目录
一、介绍 二、对比 三、安装 四、简单使用 五、定位元素 1、By.ID 2、By.CLASS_NAME 3、By.NAME 4、By.TAG_NAM 5、By.XPATH 六、操作元素 1、在输入框输入内容并搜索 2、打开网站搜索音乐并播放 七、Cookie 操作 1、获取所有的 Cookie 2、根据 Cookie 的 name 获取 Cookie 3、删除某个 Cookie 4、处理 Cookie 5、案例 八、Selenium 操作下拉菜单 九、Selenium 鼠标行为链 1、常用方法 2、案例 十、Selenium 切换页面与操作多窗口 1、切换页面 2、多窗口操作 十一、Selenium 高级操作 1、page_source:返回结构的源码 2、find():在源码当中查找某个字符的存在 3、By.LINK_TEXT:根据链接文本定位 4、get_attribute():获取属性值 5、.text:获取节点内容 十二、设置无界面 十三、页面等待 1、强制等待 2、隐式等待 3、显示等待 十四、案例

一、介绍

用 Selenium 爬取网页时,当前访问的 url 就是爬虫当中的目标 url,获取内容只要是页面上可见的,都可以爬取(可见即可爬)。

步骤

Selenium + 浏览器 + 浏览器驱动

1、导入

2、url(找动态 url,抓取到的数据是加密的)

3、获取内容,做解析

Selenium 是一个用于 Web 应用程序测试的工具,最初是为网站自动化测试而开发的,Selenium 可以直接运行在浏览器上,它支持所有主流的浏览器,可以接收指令,让浏览器自动加载页面,获取需要的数据,甚至页面截屏。

chromedriver 是一个驱动 Chrome 浏览器的驱动程序,使用它才可以驱动浏览器。

针对不同的浏览器有不同的 driver,但 Chrome 的兼容性最好。

二、对比

Ajax:可以使用网页实现异步更新,可以在不重新加载整个网页的情况下,对网页的某部分进行更新。

1、获取 Ajax 数据的方式

直接分析 Ajax 调用的接口,然后通过代码请求这个接口。

使用 Selenium + chromedriver 模拟浏览器行为获取数据。

2、获取 Ajax 数据方式对比

方式

优点

缺点

找数据接口

直接可以请求到数据,代码量少,性能高

分析接口比较复杂,尤其通过 js 混淆的接口,容易被发现是爬虫

Selenium

直接模拟浏览器行为,浏览器能请求到的,使用 Selenium 也能请求到

代码量多,性能低

三、安装

1、驱动下载地址

下载 Chrome 驱动

下载 Firefox 驱动

2、安装 Selenium 第三方库(建议指定版本安装)

pip install selenium==4.0.0a1

3、安装驱动(Chrome 的兼容性最好,建议安装 chromedriver)

先确定浏览器版本号 打开谷歌驱动下载地址 版本号前面三位对应上就可以,最后一位建议选择版本小的 按照电脑的系统来选择下载(如果电脑是 windows-64 位,选 win32 就行) 驱动下载完成之后,将文件进行解压,复制到 chromedriver.exe 到 Python 解释器目录下

终端输入命令:where python 可以查看 Python 的安装目录

四、简单使用

# 导入模块,加载驱动
from selenium import webdriver
# 内置库
import time

# 加载驱动
drive = webdriver.Chrome()

# 窗口最大化
drive.maximize_window()

# 加载网站
drive.get('https://www.baidu.com')

# 代码停3秒再运行
time.sleep(3)

# 关闭当前的窗口
# drive.close()  # 要打开多个窗口,关闭的是当前的窗口

# 退出驱动
drive.quit()  # 关闭所有窗口,退出浏览器

五、定位元素

# 导入模块,加载驱动
from selenium import webdriver
# 定位元素
from selenium.webdriver.common.by import By

# 加载驱动
driver = webdriver.Chrome()

# 窗口最大化
driver.maximize_window()

# 加载网站
driver.get('https://www.baidu.com')

1、By.ID

根据 id 来查找定位元素。

# find_element 找一个元素,find_elements 找多个
el = driver.find_element(By.ID, 'kw')
# 返回的是一个元素对象
print(el)

2、By.CLASS_NAME

通过 class 属性值定位某个元素。

el = driver.find_element(By.CLASS_NAME, 's_ipt')
# 返回的是一个元素对象
print(el)

3、By.NAME

通过 name 属性查找定位某个元素。

el = driver.find_element(By.NAME, 'wd')
# 返回的是一个元素对象
print(el)

4、By.TAG_NAM

通过标签定位元素。

input_tag = driver.find_elements(By.TAG_NAME, 'input')
# find_elements 定位元素,返回的数据类型是 list
print(input_tag)

5、By.XPATH

通过 xpath 语法定位元素。

el = driver.find_element(By.XPATH, '//input[@id="kw"]')
# 返回的是一个元素对象
print(el)

六、操作元素

1、在输入框输入内容并搜索

# 内置库
import time
# 导入模块,加载驱动
from selenium import webdriver
# 定位元素
from selenium.webdriver.common.by import By

# 加载驱动
driver = webdriver.Chrome()

# 窗口最大化
driver.maximize_window()

# 加载网站
driver.get('https://www.baidu.com')

# 定位到元素
el = driver.find_element(By.ID, 'kw')

# 元素输入框里面输入值
el.send_keys('唐僧')

# 等待2秒
time.sleep(2)

# 点击元素
driver.find_element(By.ID, 'su').click()

# 清空内容
el.clear()

2、打开网站搜索音乐并播放

# 内置库
import time
# 导入模块,加载驱动
from selenium import webdriver
# 定位元素
from selenium.webdriver.common.by import By
# 操作键盘
from selenium.webdriver.common.keys import Keys

# 加载驱动
driver = webdriver.Chrome()

# 窗口最大化
driver.maximize_window()

# 加载网站
driver.get('https://music.163.com/')

# 搜索内容
driver.find_element(By.ID, 'srch').send_keys('愿')

# 按回车键
driver.find_element(By.ID, 'srch').send_keys(Keys.ENTER)

# 等待2秒
time.sleep(2)

# 网页里面嵌套了一个网页,要进入该网页才会获取对应的数据
driver.switch_to.frame('g_iframe')

# 播放按钮
driver.find_element(By.ID, 'song_2010214999').click()

七、Cookie 操作

1、获取所有的 Cookie

cookies = driver.get_cookies()

2、根据 Cookie 的 name 获取 Cookie

value = driver.get_cookie(name)

3、删除某个 Cookie

driver.delete_cookie('key')

4、处理 Cookie

# 导入模块,加载驱动
from selenium import webdriver

# 加载驱动
driver = webdriver.Chrome()

# 窗口最大化
driver.maximize_window()

# 加载网站
driver.get('https://www.baidu.com')

# 获取百度的 cookie
cookies = driver.get_cookies()

# 返回的 list,列表里面嵌套的字典
print(cookies)

'''
通过程序拿到的 cookie 与页面的是不一致,还需要做处理
只需要里面的两个字段值,name 和 value
BAIDUID_BFESS=F875E52E04E65B3748D8FDDE2E25399E; ZFY=idY6KuND2T0e6ROY1txjofI8Nvn4lb6hXiw:BN2mgeaA:C
'''
for cookie in cookies:
    print(cookie['name']+'='+ cookie['value'])

5、案例

模拟登录 qq 空间

# 内置库
import time
# 导入模块,加载驱动
from selenium import webdriver
# 定位元素
from selenium.webdriver.common.by import By

# 加载驱动
driver = webdriver.Chrome()

# 最大化窗口
driver.maximize_window()

# 加载网站
driver.get('https://i.qq.com/')

# 切换 iframe
driver.switch_to.frame('login_frame')

# 等待2秒
time.sleep(2)

# 点击头像进行登录,获取登录之后的 cookie
driver.find_element(By.ID, 'img_out_1234567890').click()

# 等待2秒
time.sleep(2)

# 获取 cookie
cookies = driver.get_cookies()

# 打印出的数据为列表
# print(cookies)

# 获取的 cookie 需要处理
li = []
for cookie in cookies:
    # 只需要每一组字典里面的 name value
    li.append(cookie['name']+'='+cookie['value'])
    # print(cookie)

# 处理好的 cookie
cookie = '; '.join(li)
print(cookie)

# 导入库
import requests

# 确定 url, 静态加载的
url = 'https://user.qzone.qq.com/1234567890'

# 设置请求头参数
head = {
    'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36',
    'cookie': cookie
}

# 发请求,获取响应
res = requests.get(url, headers=head)

# 获取响应内容
# print(res.text)

# 写入文件
with open('qq.html', 'w', encoding='utf-8') as f:
    f.write(res.text)

八、Selenium 操作下拉菜单

select 元素不能直接点击,因为点击后还需要选中元素,这时候 selenium 就专门为 select 标签提供了一个类。

from selenium.webdriver.support.ui import Select

将获取到的元素当成参数传到这个类中,创建这个对象后,就可以使用这个对象进行选择了。

案例

目标网站:https://news.sina.com.cn/

需求:选择娱乐版块,日期8号相关新闻

分析:

1、打开对应网站

2、点击下拉菜单,选择对应的版块内容

3、点击日期图标,选择的日期是8号

# 内置库
import time
# 导入模块,加载驱动
from selenium import webdriver
# 专门针对 Select 标签使用
from selenium.webdriver.support.ui import Select
# 定位元素
from selenium.webdriver.common.by import By

# 加载驱动
driver = webdriver.Chrome()

# 最大化窗口
driver.maximize_window()

# 加载网站
driver.get('https://news.sina.com.cn/')

# 解析,直接定位到 select 标签
el = driver.find_element(By.NAME, 'channel')
# print(el)

# 定位到娱乐的板块,实例化 select 对象
select_tag = Select(el)

# 定位下拉框
# 方法一
# select_tag.select_by_index(4)  # 下标从0开始的
# 方法二
# select_tag.select_by_value('ent')  # 根据 option 里面 value 属性定位的
# 方法三
select_tag.select_by_visible_text('娱乐')  # text 指的是文本

# 等待1秒
time.sleep(1)

# 定位日期
driver.find_element(By.NAME, 'date').click()

# 选择8号
driver.find_element(By.XPATH, '//div[@id="dataView"]/div/table/tbody/tr[2]/td[2]').click()

九、Selenium 鼠标行为链

页面中需要借助鼠标操作元素,那么这时候可以使用鼠标行为链类 ActionChains 来完成,比如现在要将鼠标移动到某个元素上并执行点击事件。

学习文档

1、常用方法

actions = ActionChains(driver)  # 实例化⼀个⿏标⾏为链的对象
actions.move_to_element(inputTag)  # 将⿏标移动到元素的中间
actions.send_keys_to_element(inputTag,'python')  # 将键发送到元素
actions.move_to_element(submitTag)  # 将⿏标移动到元素的中间
actions.context_click()  # 对元素执⾏上下⽂单击(右键单击)
actions.click(submitTag)  # 单击⼀个元素
actions.perform()  # 执⾏所有存储的操作
actions.lick_and_hold(element)  # 点击但不松开⿏标
actions.double_click(element)  # 双击

更多方法可参考

2、案例

目标网站:https://passport.vip.com/login?src=https%3A%2F%2Fwww.vip.com%2F

需求:

1、加载网站

2、切换登录方式

3、输入账号密码,勾选协议,点击登录案例

4、鼠标移动到对应的验证码元素上面

# 内置库
import time
# 导入模块,加载驱动
from selenium import webdriver
# 定位元素
from selenium.webdriver.common.by import By
# 导入鼠标行为链
from selenium.webdriver import ActionChains

# 加载驱动
driver = webdriver.Chrome()

# 最大化窗口
driver.maximize_window()

# 加载网站
driver.get('https://passport.vip.com/login?src=https%3A%2F%2Fwww.vip.com%2F')

# 等待2秒
time.sleep(2)

# 切换登录方式 —— 1、定位元素 2、点击
driver.find_element(By.XPATH, '//div[@class="c-tab-nav "]/div[2]').click()

# 等待0.5秒
time.sleep(0.5)

# 输入用户名
driver.find_element(By.ID, 'J_login_name').send_keys('3424234234')

# 输入密码
driver.find_element(By.ID, 'J_login_pwd').send_keys('34234242')

# 等待0.5秒
time.sleep(0.5)

# 勾选协议
inputs = driver.find_element(By.ID, 'J_login_agree')  # 定位到这个元素

# 通过 click 无法点击成功,可以采用 js 点击
driver.execute_script('arguments[0].click();', inputs)

# 登录
driver.find_element(By.ID, 'J_login_submit').click()

# 等待1秒
time.sleep(1)

# 定位图片
img = driver.find_element(By.CLASS_NAME, 'vipsc_qimg')

# 鼠标移动
actions = ActionChains(driver)
actions.move_to_element(img)  # 移动到目标位置

# 提交行为链
actions.perform()

十、Selenium 切换页面与操作多窗口

1、切换页面

# 内置库
import time
# 导入模块,加载驱动
from selenium import webdriver

# 加载驱动
driver = webdriver.Chrome()

# 最大化窗口
driver.maximize_window()

# 先模拟打开多个网站
driver.get('https://www.baidu.com')  # 百度
time.sleep(3)
driver.execute_script('window.open("https://www.douban.com/")')  # 豆瓣
time.sleep(3)
driver.execute_script('window.open("https://juejin.cn/")')  # 掘金
time.sleep(3)
driver.execute_script('window.open("https://cloud.tencent.com/developer/ask/sof/1237007")')  # 腾讯
time.sleep(3)

# 打印鼠标聚焦的 url
# print(driver.current_url)

# 切换窗口
driver.switch_to.window(driver.window_handles[-3])

'''
0:https://www.baidu.com
1:https://cloud.tencent.com/developer/ask/sof/1237007
2:https://juejin.cn/
3:https://www.douban.com/

-1:https://www.douban.com/
-2:https://juejin.cn/
-3:https://cloud.tencent.com/developer/ask/sof/1237007
'''

# 打印鼠标聚焦的 url
print(driver.current_url)

2、多窗口操作

# 内置库
import time
# 导入模块,加载驱动
from selenium import webdriver
# 定位元素
from selenium.webdriver.common.by import By

# 加载驱动
driver = webdriver.Chrome()

# 最大化窗口
driver.maximize_window()

# 加载网站
driver.get('https://juejin.cn/')

# 等待2秒
time.sleep(2)

# 定位第二篇文章
driver.find_element(By.XPATH, '//div[@class="entry-list list"]/li[2]').click()

# 等待1秒
time.sleep(1)

# 切换窗口
driver.switch_to.window(driver.window_handles[1])

# 获取 elements 内容
te = driver.page_source
print(te)

十一、Selenium 高级操作

1、page_source:返回结构的源码

# 内置库
import time
# 导入模块,加载驱动
from selenium import webdriver

# 加载驱动
driver = webdriver.Chrome()

# 最大化窗口
driver.maximize_window()

# 加载网站
driver.get('https://music.163.com/#/discover/toplist')

# 等待2秒
time.sleep(2)

# 切换到 iframe
driver.switch_to.frame('g_iframe')

# 等待2秒
time.sleep(2)

# 拿到的是 elements 里面的内容,xpath 直接做解析就可以了
print(driver.page_source)

2、find():在源码当中查找某个字符的存在

# 内置库
import time
# 导入模块,加载驱动
from selenium import webdriver
# 定位元素
from selenium.webdriver.common.by import By

# 加载驱动
driver = webdriver.Chrome()

# 最大化窗口
driver.maximize_window()

# 加载网站
driver.get('https://tieba.baidu.com/f?kw=%E8%8F%9C%E8%B0%B1&ie=utf-8&pn=11400')

# 等待2秒
time.sleep(2)

# # 获取网页源码
# html = driver.page_source
# # 打印源码
# print(html)
# # 看网页源码里是否存在下一页
# print(driver.page_source.find('下一页>'))
'''
如果没有查找到对应的字符,返回的是-1
如果查找到了,返回的是数字
'''

while True:
    # 判断源码里存在下一页时
    if driver.page_source.find('下一页>') != -1:
        # 点击下一页的按钮
        driver.find_element(By.CLASS_NAME, 'next').click()
        # 等待1秒
        time.sleep(1)
    # 源码里不存在下一页时,跳出循环
    else:
        print('已经是最后一页了')
        break

3、By.LINK_TEXT:根据链接文本定位

# 内置库
import time
# 导入模块,加载驱动
from selenium import webdriver
# 定位元素
from selenium.webdriver.common.by import By

# 加载驱动
driver = webdriver.Chrome()

# 最大化窗口
driver.maximize_window()

# 加载网站
driver.get('https://movie.douban.com/top250')

# 等待2秒
time.sleep(2)

# 获取网页源码
html = driver.page_source

# 等待1秒
time.sleep(1)

# 根据链接文本定位,点击‘后页>’
driver.find_element(By.LINK_TEXT, '后页>').click()

# 打印点击‘后页>’后的文本
print(driver.page_source)

4、get_attribute():获取属性值

# 内置库
import time
# 导入模块,加载驱动
from selenium import webdriver
# 定位元素
from selenium.webdriver.common.by import By

# 加载驱动
driver = webdriver.Chrome()

# 最大化窗口
driver.maximize_window()

# 加载网站
driver.get('https://movie.douban.com/top250')

# 等待2秒
time.sleep(2)

# 获取图片标签
img_tag = driver.find_element(By.XPATH, '//div[@class="pic"]/a/img')

# 获取属性值 —— 通过方法
print(img_tag.get_attribute('src'))

5、.text:获取节点内容

# 内置库
import time
# 导入模块,加载驱动
from selenium import webdriver
# 定位元素
from selenium.webdriver.common.by import By

# 加载驱动
driver = webdriver.Chrome()

# 最大化窗口
driver.maximize_window()

# 加载网站
driver.get('https://movie.douban.com/top250')

# 等待2秒
time.sleep(2)

# 获取文本内容
div_tag = driver.find_element(By.XPATH, '//div[@class="hd"]').text

# 打印文本内容
print(div_tag)

十二、设置无界面

# 内置库
import time
# 导入模块,加载驱动
from selenium import webdriver
# 定位元素
from selenium.webdriver.common.by import By

# 设置参数
options = webdriver.ChromeOptions()

# 设置无界面模式
options.add_argument('--headless')

# 传递参数
driver = webdriver.Chrome(options=options)

# 加载网站
driver.get('https://movie.douban.com/top250')

# 等待3秒
time.sleep(3)

# 获取图片标签
img_tag = driver.find_element(By.XPATH, '//div[@class="pic"]/a/img')

# 打印属性值
print(img_tag.get_attribute('src'))

十三、页面等待

1、强制等待

time.sleep(时间)

2、隐式等待

调用 driver.implicitly_wait ,针对所有元素,设置等待时间,如果等待时间内加载出来,代码继续往下走,等待时间以内不断刷新看元素是否加载出来,超出则报出异常。

# 导入模块,加载驱动
from selenium import webdriver
# 定位元素
from selenium.webdriver.common.by import By

# 加载驱动
driver = webdriver.Chrome()

# 加载网站
driver.get('https://www.baidu.com')

# 显示等待,针对的全局元素
driver.implicitly_wait(10)

# 10秒内出现代码继续往下走
driver.find_element(By.ID, 'kw').send_keys('python')

# 等待10秒不出现就会报错
driver.find_element(By.ID, 'kw1').send_keys('python')

3、显示等待

表明某个条件成立后才执行获取元素的操作,也可以在等待的时候指定一个最大的时间,如果超过这个时间那么就抛出一个异常。

# 显示等待,需要导入模块
# 导入 WebDriverWait 类
from selenium.webdriver.support.wait import WebDriverWait
# 导入 expected_conditions 模块并使用别名 EC
from selenium.webdriver.support import expected_conditions as EC


# 导入模块,加载驱动
from selenium import webdriver
# 定位元素
from selenium.webdriver.common.by import By

# 显示等待,需要导入模块
# 导入 WebDriverWait 类
from selenium.webdriver.support.wait import WebDriverWait
# 导入 expected_conditions 模块并使用别名 EC
from selenium.webdriver.support import expected_conditions as EC

# 加载驱动
driver = webdriver.Chrome()

# 加载网站
driver.get('https://www.baidu.com')

# 默认是0.5秒刷新一次
# 等待时间总共是10秒,每隔1秒刷新一次
# until 具体的条件内容
element = WebDriverWait(driver, 10, 1).until(
    # 判断元素是否加载出来
    EC.presence_of_element_located((By.ID, 'kw'))
)
element.send_keys('hello')

隐式等待针对的是全局元素,全局生效,代码简单,只用于查找元素。

显示等待有很多的判断条件,适用范围更广,只针对某一个元素。

十四、案例

目标网站: https://juejin.cn/

需求:爬取文章内容,保存为 txt 格式,文件名以文章标题命名

分析:

翻页方式:滑动滚动条进行加载内容

1、设置先滚动5次,先把下面的内容加载出来,获取元素

2、先获取所有 li 元素,遍历 li 元素,进行点击操作

# 内置库
import time
# 加载驱动
from selenium import webdriver
# 定位元素
from selenium.webdriver.common.by import By
# 正则
import re

class JuJin(object):

    # 定义初始化方法
    def __init__(self):
        # 实例属性
        # 加载驱动
        self.driver = webdriver.Chrome()
        # 窗口最大化
        self.driver.maximize_window()
        # 加载网站
        self.driver.get('https://juejin.cn/')

    # 解析数据
    def parse_html(self):
        # 等待2秒
        time.sleep(2)
        # 获取 li 里的所有数据
        lis = self.driver.find_elements(By.XPATH, '//div[@class="entry-list list"]/li')
        # 循环处理数据
        for li in lis:
            # 捕获异常
            try:
                # 等待2秒
                time.sleep(2)
                # 获取文章标题标签
                a = li.find_element(By.CLASS_NAME, 'title')
                # 点击标题标签进入详情页
                self.driver.execute_script('arguments[0].click();', a)
                # 切换窗口到内容页
                self.driver.switch_to.window(self.driver.window_handles[1])
                # 等待2秒
                time.sleep(2)
                # 获取标题标签
                title = self.driver.find_elements(By.CLASS_NAME, 'article-title')
                # 获取文章内容标签
                contents = self.driver.find_elements(By.XPATH, '//div[@class="markdown-body cache"]/p')
                # 判断标题是否是空列表,如果是空列表,说明文章是广告文章
                if not title:
                    # 获取标题标签
                    title = self.driver.find_elements(By.XPATH, '//a[@class="title"]/span')
                    # 获取文章内容标签
                    contents = self.driver.find_elements(By.XPATH, '//div[@class="markdown-body"]/p')
                # 获取标题文本
                titles = title[0].text
                # 正则表达式替换标题特殊字符
                titles = re.sub(r'[,?/<>!: ()|"]', '', titles)
                # 定义组装数据的变量
                s = ''
                # 获取数据为列表,需循环取出
                for i in contents:
                    # 组装数据
                    s += i.text + '\n'
                # 打印文章标题,内容
                # print(titles)
                # 保存数据
                self.save_data(titles, s)
                # 关闭当前窗口
                self.driver.close()
                # 切换窗口到列表页
                self.driver.switch_to.window(self.driver.window_handles[0])
                # 等待1秒
                time.sleep(1)
            except Exception as e:
                # 打印异常
                print(e)
                print("没有文章内容")
                # 关闭当前窗口
                self.driver.close()
                # 切换窗口到列表页
                self.driver.switch_to.window(self.driver.window_handles[0])

    # 保存数据
    def save_data(self, title, contents):
        # 创建 txt 文本文档
        with open(f'掘金/{title}.txt', 'w', encoding='utf-8') as f:
            # 文件写入
            f.write(contents)

    # 滚动方法
    def slide(self, height):
        # 滑动滚动条
        self.driver.execute_script(
            f'window.scrollTo(0,{height})'
        )

    # 处理主逻辑
    def main(self):
        # 获取窗口的高度
        heights = self.driver.get_window_size()['height']
        # 滑动次数
        page = 1
        # 滑动
        while page <= 5:
            self.slide(heights)
            # 加高度
            heights = heights + heights
            # 滑动次数+1
            page += 1
            # 等待1秒
            time.sleep(1)
        # 解析数据
        self.parse_html()

# 创建对象
data = JuJin()
# 调用 main 方法,开始执行主程序
data.main()

?结尾

看到这里了还不给博主扣个:- ⛳️ 点赞☀️收藏 ⭐️ 关注!- ? ? ? ❤️ ?? ? ? ? ? ?- 拜托拜托这个真的很重要!- 你们的点赞就是博主更新最大的动力!- 有问题可以评论或者私信呢秒回哦。

更新时间 2024-07-22