当前位置: 首页 > news >正文

【黑马点评优化】2-Canel实现多级缓存(Redis+Caffeine)同步

【黑马点评优化】2-Canel实现多级缓存(Redis+Caffeine)同步

  • 0 背景
  • 1 配置MySQL
    • 1.1 开启MySQL的binlog功能
      • 1.1.1 找到mysql配置文件my.ini的位置
      • 1.1.2 开启binlog
    • 1.2 创建canal用户
  • 2 下载配置canal
    • 2.1 canal 1.1.5下载
    • 2.2 配置canal
    • 2.3 启动canal
    • 2.4 测试
  • 3 canal实现双写一致
    • 3.1 Redis操作封装
    • 3.2 编写监听器
    • 3.3 修改ShopServiceImpl.java
    • 3.4 测试
  • 参考资料

github地址如下:https://github.com/xianghua-2/hm-dianping

0 背景

【黑马点评优化】之使用Caffeine+Redis实现应用级二层缓存_caffeine redis二级缓存-CSDN博客

当时使用Redis+Caffeine实现对商铺信息的应用层两级缓存。文章提到了两级缓存Redis+Caffeine可以解决缓存雪等问题也可以提高接口的性能,但是可能会出现缓存一致性问题。如果数据频繁的变更,可能会导致Redis和Caffeine数据不一致的问题。

为此,使用Canel来解决这一问题。

MySQL工作原理如下:

一句话总结(详细工作原理,可以查看下面的介绍)

模拟MySQL从库读取binlog实现数据变更监听。它支持数据过滤、转换,并能将变更数据推送到不同的下游系统,如消息队列和其他数据库。

我的相关软件版本
mysql:8.0.36
redis:6.2.6
canal:1.1.5

1 配置MySQL

1.1 开启MySQL的binlog功能

1.1.1 找到mysql配置文件my.ini的位置

开始的时候找不到mysql配置文件(my.ini)的位置

通过下列方法找到:

  • 先连接mysql

mysql -h localhost -u root -p123456 (注意,有密码的话,才-p123456)

  • 输入以下命令

show variables like 'datadir';

这样就找到了配置文件所在的位置

1.1.2 开启binlog

my.ini的最后几行加上以下内容

[mysqld]
log-bin=mysql-bin
binlog-format=ROW
server-id=1

1.2 创建canal用户

DROP USER IF EXISTS 'canal'@'%';

CREATE USER 'canal'@'%' IDENTIFIED BY 'canal';

GRANT ALL PRIVILEGES ON *.* TO 'canal'@'%';

FLUSH PRIVILEGES;

但是由于mysql8.0之后 如果只设置了 % 的访问权限,
会导致localhost无法访问
所以 我们需要把当前权限更新为 localhost 再执行一遍
继续执行下述命令

select mysql;

update user set host = 'localhost' where user = 'canal' and host='%';

GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';

FLUSH PRIVILEGES;
查看所有用户访问权限结果如下:

-- 查看所有用户访问权限SELECT DISTINCT CONCAT('User: ''',user,'''@''',host,''';') AS query FROM mysql.user;

在这里插入图片描述

自MySQL 8.0.3开始,身份验证插件默认使用caching_sha2_password

解决:修改canal用户对应的身份验证插件为mysql_native_password

因此,接着执行下列命令

ALTER USER 'canal'@'%' IDENTIFIED WITH mysql_native_password BY 'password';

ALTER USER 'canal'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password';

之后我们再次查看canal用户对应的身份验证插件,如下即修改成功

在这里插入图片描述

2 下载配置canal

2.1 canal 1.1.5下载

在这里我们选择1.1.15版本的canal,因为canal1.1.15版本以后的canal不兼容Java 1.8

Release v1.1.5 · alibaba/canal (github.com)

选择canal.deployer-1.1.5.tar.gz下载,之后解压到自己的软件目录下。

在这里插入图片描述

2.2 配置canal

进入解压后的Canal目录,找到conf目录下的example实例,通常情况下,你可以通过修改conf/example/instance.properties文件来配置Canal连接到MySQL的参数,主要配置项包括:

canal.instance.master.address:MySQL服务器地址和端口。
canal.instance.dbUsername和canal.instance.dbPassword:用于连接MySQL的用户名和密码。
canal.instance.connectionCharset:数据库的字符集,通常为UTF-8。
canal.instance.tsdb.enable:是否启用表结构历史记录功能,建议开启。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

2.3 启动canal

当安装好canal的时候,在window中启动bat的时候有些问题,JDK17版本已经不持’PermSize=128m’

因此,删除start.bat中的这一行

在这里插入图片描述

之后,双击bin/start.bat运行即可。

结果如下:

在这里插入图片描述

2.4 测试

pom.xml中导入依赖

        <dependency><groupId>com.alibaba.otter</groupId><artifactId>canal.client</artifactId><version>1.1.7</version></dependency><dependency><groupId>com.alibaba.otter</groupId><artifactId>canal.protocol</artifactId><version>1.1.7</version></dependency>

之后,编写测试类如下:


import java.net.InetSocketAddress;
import java.util.List;import com.alibaba.otter.canal.client.CanalConnector;
import com.alibaba.otter.canal.client.CanalConnectors;
import com.alibaba.otter.canal.common.utils.AddressUtils;
import com.alibaba.otter.canal.protocol.CanalEntry.Column;
import com.alibaba.otter.canal.protocol.CanalEntry.Entry;
import com.alibaba.otter.canal.protocol.CanalEntry.EntryType;
import com.alibaba.otter.canal.protocol.CanalEntry.EventType;
import com.alibaba.otter.canal.protocol.CanalEntry.RowChange;
import com.alibaba.otter.canal.protocol.CanalEntry.RowData;
import com.alibaba.otter.canal.protocol.Message;
import org.jetbrains.annotations.NotNull;public class CanalTest {public static void main(String args[]) {// 创建链接CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress(AddressUtils.getHostIp(),11111), "example", "", "");int batchSize = 1000;int emptyCount = 0;try {connector.connect();connector.subscribe(".*\\..*");connector.rollback();int totalEmtryCount = 1200;while (emptyCount < totalEmtryCount) {Message message = connector.getWithoutAck(batchSize); // 获取指定数量的数据long batchId = message.getId();int size = message.getEntries().size();if (batchId == -1 || size == 0) {emptyCount++;System.out.println("empty count : " + emptyCount);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}} else {emptyCount = 0;// System.out.printf("message[batchId=%s,size=%s] \n", batchId, size);printEntry(message.getEntries());}connector.ack(batchId); // 提交确认// connector.rollback(batchId); // 处理失败, 回滚数据}System.out.println("empty too many times, exit");} finally {connector.disconnect();}}private static void printEntry(@NotNull List<Entry> entrys) {for (Entry entry : entrys) {if (entry.getEntryType() == EntryType.TRANSACTIONBEGIN || entry.getEntryType() == EntryType.TRANSACTIONEND) {continue;}RowChange rowChage = null;try {rowChage = RowChange.parseFrom(entry.getStoreValue());} catch (Exception e) {throw new RuntimeException("ERROR ## parser of eromanga-event has an error , data:" + entry.toString(),e);}EventType eventType = rowChage.getEventType();System.out.println(String.format("================> binlog[%s:%s] , name[%s,%s] , eventType : %s",entry.getHeader().getLogfileName(), entry.getHeader().getLogfileOffset(),entry.getHeader().getSchemaName(), entry.getHeader().getTableName(),eventType));for (RowData rowData : rowChage.getRowDatasList()) {if (eventType == EventType.DELETE) {printColumn(rowData.getBeforeColumnsList());} else if (eventType == EventType.INSERT) {printColumn(rowData.getAfterColumnsList());} else {System.out.println("-------> before");printColumn(rowData.getBeforeColumnsList());System.out.println("-------> after");printColumn(rowData.getAfterColumnsList());}}}}private static void printColumn(@NotNull List<Column> columns) {for (Column column : columns) {System.out.println(column.getName() + " : " + column.getValue() + "    update=" + column.getUpdated());}}
}

运行后,能看到,监听到了数据库的变化

在这里插入图片描述

3 canal实现双写一致

导入依赖

        <dependency><groupId>top.javatool</groupId><artifactId>canal-spring-boot-starter</artifactId><version>1.2.1-RELEASE</version></dependency>

新建com.hmdp.cache.handler包

3.1 Redis操作封装

新建ShopRedisHandler类

package com.hmdp.cache.handler;import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;import com.github.benmanes.caffeine.cache.Cache;
import com.hmdp.entity.Shop;
import com.hmdp.service.IShopService;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;import javax.annotation.Resource;
import java.util.List;import static com.hmdp.utils.RedisConstants.CACHE_SHOP_KEY;@Component
public class ShopRedisHandler implements InitializingBean {@Autowiredprivate StringRedisTemplate redisTemplate;@Autowiredprivate IShopService shopService;private static final ObjectMapper MAPPER = new ObjectMapper();@Resourceprivate Cache<String, Object> shopCache;// 缓存预热@Overridepublic void afterPropertiesSet() throws Exception {// 初始化缓存// 1.查询商品信息List<Shop> shopList = shopService.list();// 2.放入缓存for (Shop shop : shopList) {// 2.1.item序列化为JSONString json = MAPPER.writeValueAsString(shop);// 2.2 存入caffeindString key = CACHE_SHOP_KEY + shop.getId();shopCache.put(key, shop);// 2.2.存入redisredisTemplate.opsForValue().set(key, json);}//        // 3.查询商品库存信息
//        List<ItemStock> stockList = stockService.list();
//        // 4.放入缓存
//        for (ItemStock stock : stockList) {
//            // 2.1.item序列化为JSON
//            String json = MAPPER.writeValueAsString(stock);
//            // 2.2.存入redis
//            redisTemplate.opsForValue().set("item:stock:id:" + stock.getId(), json);
//        }}public void saveShop(Shop shop) {try {String json = MAPPER.writeValueAsString(shop);String key = CACHE_SHOP_KEY  + shop.getId();redisTemplate.opsForValue().set(key + shop.getId(), json);} catch (JsonProcessingException e) {throw new RuntimeException(e);}}public void deleteShopById(Long id) {String key = CACHE_SHOP_KEY  + id;redisTemplate.delete(key + id);}
}

3.2 编写监听器

新建ShopHandler类

通过实现EntryHandler<T>接口编写监听器,监听Canal消息。注意两点:

  • 实现类通过@CanalTable("tb_item")指定监听的表信息
  • EntryHandler的泛型是与表对应的实体类
package com.hmdp.cache.handler;import com.github.benmanes.caffeine.cache.Cache;
import com.hmdp.entity.Shop;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import top.javatool.canal.client.annotation.CanalTable;
import top.javatool.canal.client.handler.EntryHandler;import javax.annotation.Resource;import static com.hmdp.utils.RedisConstants.CACHE_SHOP_KEY;@CanalTable(value = "tb_shop")
@Component
public class ShopHandler implements EntryHandler<Shop>{@Autowiredprivate ShopRedisHandler redisHandler;@Resourceprivate Cache<String, Object> shopCache;@Overridepublic void insert(Shop shop) {// 写数据到JVM进程缓存String key = CACHE_SHOP_KEY + shop.getId();shopCache.put(key , shop);// 写数据到redisredisHandler.saveShop(shop);}@Overridepublic void update(Shop before, Shop after) {// 写数据到JVM进程缓存String key = CACHE_SHOP_KEY + after.getId();shopCache.put(key, after);// 写数据到redisredisHandler.saveShop(after);}@Overridepublic void delete(Shop shop) {// 删除数据到JVM进程缓存String key = CACHE_SHOP_KEY + shop.getId();shopCache.invalidate(key);// 删除数据到redisredisHandler.deleteShopById(shop.getId());}
}

3.3 修改ShopServiceImpl.java

修改ShopServiceImpl中的update方法。注释掉删除缓存的操作。

缓存更新由监听器完成。

    @Overridepublic Result update(Shop shop) {Long id = shop.getId();if(id == null){return Result.fail("店铺id不能为空");}//1.更新数据库updateById(shop);// @TODO 现在不再需要删除缓存了,由canal监听数据库的变化,然后更新缓存//2.删除缓存
//        stringRedisTemplate.delete(CACHE_SHOP_KEY + id);return Result.ok();}

3.4 测试

接下来可以自行调断点测试,也可以运行项目,然后更改数据库,查看终端输出。
在这里插入图片描述

参考资料

mysql 8.0找不到my.ini配置文件解决方案_mysql80没有my.ini-CSDN博客

Docker整合canal 踩坑实录_com.alibaba.otter.canal.parse.exception.canalparse-CSDN博客

Canal 1.1.5 启动报错:caching_sha2_password Auth failed_canal启动出现canal.deployer-1.1.5.jar:na-CSDN博客

Canal启动和运行出现的问题_canal启动闪退-CSDN博客

相关文章:

【黑马点评优化】2-Canel实现多级缓存(Redis+Caffeine)同步

【黑马点评优化】2-Canel实现多级缓存&#xff08;RedisCaffeine&#xff09;同步 0 背景1 配置MySQL1.1 开启MySQL的binlog功能1.1.1 找到mysql配置文件my.ini的位置1.1.2 开启binlog 1.2 创建canal用户 2 下载配置canal2.1 canal 1.1.5下载2.2 配置canal2.3 启动canal2.4 测试…...

php-fpm

摘要 php-fpm(fastcgi process manager)是PHP 的FastCGI管理器&#xff0c;管理PHP的FastCGI进程&#xff0c;提升PHP应用的性能和稳定性 php-fpm是一个高性能的php FastCGI管理器&#xff0c;提供了更好的php进程管理方式&#xff0c;可以有效的控制内存和进程&#xff0c;支…...

Python3测试开发面试题2

python的内存池机制 在Python中&#xff0c;内存管理是通过Python的内存管理器和C语言实现的&#xff0c;特别是依赖于CPython的实现。CPython使用一种名为“内存池”&#xff08;memory pool&#xff09;的技术来优化小对象&#xff08;如小整数、短字符串等&#xff09;的内…...

qt + opengl 给立方体增加阴影

在前几篇文章里面学会了通过opengl实现一个立方体&#xff0c;那么这篇我们来学习光照。 风氏光照模型的主要结构由3个分量组成&#xff1a;环境(Ambient)、漫反射(Diffuse)和镜面(Specular)光照。下面这张图展示了这些光照分量看起来的样子&#xff1a; 1 环境光照(Ambient …...

Webpack,Vite打包的理解

Webpack 和 Vite 都是现代前端开发中常用的构建工具&#xff0c;用于打包和优化项目代码。尽管它们的目标相似&#xff0c;但在设计理念、工作方式和适用场景上存在显著差异。 Webpack Webpack 是一个模块打包工具&#xff0c;主要用于将多个模块&#xff08;如 JavaScript、…...

Vue 3 30天精进之旅:Day 25 - PWA支持

一、引言 在前面的24天中&#xff0c;我们已经深入探讨了Vue 3的许多核心概念和高级特性。今天&#xff0c;我们将进入一个全新的领域——PWA&#xff08;Progressive Web App&#xff09;。PWA是一种现代Web应用程序的开发模式&#xff0c;它结合了Web和原生应用的优点&#…...

机器学习-生命周期

假如一个用户向银行申请贷款&#xff0c;银行该如何对这个用户进行评估?很明显&#xff0c;银行首先需要调查清楚该用户的资金储备情况和信用历史等&#xff0c;然后再决定是否向其放款。 整个机器学习生命周期如下图所示&#xff1a; 1、定义问题 在使用机器学习中的术语表…...

大道至简 少字全意 易经的方式看 缓存 mybatis缓存 rendis缓存场景 案例

目录 介绍 mybatis缓存 一级缓存 1.是什么 2.特点 3.场景 mybatis 二级缓存 1.是什么 2.特点 3.配置步骤 注意 一级缓存问题 二级缓存问题 扩展 1.MyBatis集成 Redis 2.直接使用Redis redis 缓存 一、String 字符串 二、Llst 列表 三、Hash 哈希 四、Set…...

如何使用 Flutter DevTools 和 PerformanceOverlay 监控性能瓶颈

使用 Flutter DevTools 和 PerformanceOverlay 监控性能瓶颈&#xff1a;详细分析与实战 在开发 Flutter 应用时&#xff0c;性能问题可能会导致用户体验下降&#xff0c;比如页面卡顿、掉帧、内存泄漏等。为了定位和解决这些问题&#xff0c;Flutter 提供了强大的性能监控工具…...

TS中Any和Unknown有什么区别

在 TypeScript 中&#xff0c;any 和 unknown 都是顶级类型&#xff08;top types&#xff09;&#xff0c;表示可以是任何类型的值。但它们在使用和行为上有显著区别&#xff0c;主要体现在类型安全性和使用方式上。 1. any 类型 特点&#xff1a; any 是 TypeScript 中最宽松…...

【Mpx】-环境搭建项目创建(一)

一.概述 官方文档&#xff1a;https://mpxjs.cn/guide/basic/start.html mpxjs/cli文档: https://github.com/mpx-ecology/mpx-cli 二.脚手架安装&创建项目 2.1项目创建 //脚手架安装 npm i -g mpxjs/cli //创建Mpx项目 mpx create mpx-demo(项目名称) //安装依赖 np…...

PyQt加载UI文件

1.动态加载 import sys from PySide6 import QtCore,QtWidgets from PySide6.QtWidgets import * from PySide6.QtUiTools import QUiLoaderclass readfile(QWidget):def __init__(self):super().__init__()self.uiQUiLoader().load("test.ui",self) self.__c…...

Java面试第二山!《计算机网络》!

在 Java 面试里&#xff0c;计算机网络知识是高频考点&#xff0c;今天就来盘点那些最容易被问到的计算机网络面试题&#xff0c;帮你轻松应对面试&#xff0c;也方便和朋友们一起探讨学习。 一、HTTP 和 HTTPS 的区别 1. 面试题呈现 HTTP 和 HTTPS 有什么区别&#xff1f;在…...

Mysql基础语句

一、 MySQL语句 在熟悉安装及访问 MySQL 数据库以后&#xff0c; 接下来将学习使用 MySQL 数据库的基本操作&#xff0c;这也是在服务器运维工作中不可或缺的知识。 本节中的所有数据库语句均在“MySQL>”操作环境中执行 MySQL 是一套数据库管理系统&#xff0c;在每台 MySQ…...

连接池Java导包

目录 一、Java导包 二、 数据库连接池 1. 概述 2. 常见参数 3. 常见连接池 4. Druid连接池&#xff08;重点&#xff09; 核心功能&#xff1a; 使用方法&#xff1a; 导入依赖 配置连接池&#xff1a; 代码配置&#xff1a; 配置文件&#xff1a; 获取连接&#…...

一些耳朵起茧子的名词解释

1 web应用 1.1 web应用的概念 Web应用&#xff08;Web Application&#xff09; 是一种通过浏览器访问的软件程序&#xff0c;它运行在服务器上&#xff0c;用户通过网络&#xff08;如互联网或内网&#xff09;与它进行交互。与传统网站&#xff08;主要提供静态内容&#x…...

HBuilderX中,VUE生成随机数字,vue调用随机数函数

Vue 中可以使用JavaScript的Math.random() 函数生成随机数&#xff0c;它会返回 0 到 1 之间的浮点数&#xff0c; 如果需要0到1000之前的随机数&#xff0c;可以对生成的随机数乘以1000&#xff0c;再用js的向下取整函数Math.floor() 。 let randNum Math.random(); // 生成…...

C#发送邮件

基础调用类&#xff1a; public class EmailHelper{/// <summary>/// 发件人名称/// </summary>public string MailName { get; set; }/// <summary>/// 收件人/// </summary>public string MailTo { get; set; }/// <summary>/// 密送/// <…...

2025-2-19学习笔记 : this关键字,constructor结构体,class类

1、This关键字 在 JavaScript 中&#xff0c;this 是一个关键字&#xff0c;其指向取决于函数的调用方式。理解 this 的指向对于编写正确的代码至关重要。以下是 this 在不同情况下的指向规则&#xff1a; 1. 全局函数调用 当函数在全局作用域中被调用时&#xff0c;this 指向…...

避坑:过早的文件结束符(EOF):解决“git clone龙蜥OS源码失败”的失败过程

避坑&#xff1a;过早的文件结束符&#xff08;EOF&#xff09;&#xff1a;解决“git clone龙蜥OS源码失败”的失败过程 安装Anolis OS 8.9 下载AnolisOS-8.9-x86_64-dvd.iso并安装。 使用uname -a查看内核版本为5.10.134-18.an8.x86_64。 [rootlocalhost cloud-kernel]# c…...

脑机新手指南(八):OpenBCI_GUI:从环境搭建到数据可视化(下)

一、数据处理与分析实战 &#xff08;一&#xff09;实时滤波与参数调整 基础滤波操作 60Hz 工频滤波&#xff1a;勾选界面右侧 “60Hz” 复选框&#xff0c;可有效抑制电网干扰&#xff08;适用于北美地区&#xff0c;欧洲用户可调整为 50Hz&#xff09;。 平滑处理&…...

YSYX学习记录(八)

C语言&#xff0c;练习0&#xff1a; 先创建一个文件夹&#xff0c;我用的是物理机&#xff1a; 安装build-essential 练习1&#xff1a; 我注释掉了 #include <stdio.h> 出现下面错误 在你的文本编辑器中打开ex1文件&#xff0c;随机修改或删除一部分&#xff0c;之后…...

【论文笔记】若干矿井粉尘检测算法概述

总的来说&#xff0c;传统机器学习、传统机器学习与深度学习的结合、LSTM等算法所需要的数据集来源于矿井传感器测量的粉尘浓度&#xff0c;通过建立回归模型来预测未来矿井的粉尘浓度。传统机器学习算法性能易受数据中极端值的影响。YOLO等计算机视觉算法所需要的数据集来源于…...

06 Deep learning神经网络编程基础 激活函数 --吴恩达

深度学习激活函数详解 一、核心作用 引入非线性:使神经网络可学习复杂模式控制输出范围:如Sigmoid将输出限制在(0,1)梯度传递:影响反向传播的稳定性二、常见类型及数学表达 Sigmoid σ ( x ) = 1 1 +...

Mobile ALOHA全身模仿学习

一、题目 Mobile ALOHA&#xff1a;通过低成本全身远程操作学习双手移动操作 传统模仿学习&#xff08;Imitation Learning&#xff09;缺点&#xff1a;聚焦与桌面操作&#xff0c;缺乏通用任务所需的移动性和灵活性 本论文优点&#xff1a;&#xff08;1&#xff09;在ALOHA…...

莫兰迪高级灰总结计划简约商务通用PPT模版

莫兰迪高级灰总结计划简约商务通用PPT模版&#xff0c;莫兰迪调色板清新简约工作汇报PPT模版&#xff0c;莫兰迪时尚风极简设计PPT模版&#xff0c;大学生毕业论文答辩PPT模版&#xff0c;莫兰迪配色总结计划简约商务通用PPT模版&#xff0c;莫兰迪商务汇报PPT模版&#xff0c;…...

Razor编程中@Html的方法使用大全

文章目录 1. 基础HTML辅助方法1.1 Html.ActionLink()1.2 Html.RouteLink()1.3 Html.Display() / Html.DisplayFor()1.4 Html.Editor() / Html.EditorFor()1.5 Html.Label() / Html.LabelFor()1.6 Html.TextBox() / Html.TextBoxFor() 2. 表单相关辅助方法2.1 Html.BeginForm() …...

倒装芯片凸点成型工艺

UBM&#xff08;Under Bump Metallization&#xff09;与Bump&#xff08;焊球&#xff09;形成工艺流程。我们可以将整张流程图分为三大阶段来理解&#xff1a; &#x1f527; 一、UBM&#xff08;Under Bump Metallization&#xff09;工艺流程&#xff08;黄色区域&#xff…...

Java并发编程实战 Day 11:并发设计模式

【Java并发编程实战 Day 11】并发设计模式 开篇 这是"Java并发编程实战"系列的第11天&#xff0c;今天我们聚焦于并发设计模式。并发设计模式是解决多线程环境下常见问题的经典解决方案&#xff0c;它们不仅提供了优雅的设计思路&#xff0c;还能显著提升系统的性能…...

Selenium 查找页面元素的方式

Selenium 查找页面元素的方式 Selenium 提供了多种方法来查找网页中的元素&#xff0c;以下是主要的定位方式&#xff1a; 基本定位方式 通过ID定位 driver.find_element(By.ID, "element_id")通过Name定位 driver.find_element(By.NAME, "element_name"…...