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

go 微服务框架 kratos 日志库使用方法及原理探究

一、Kratos 日志设计理念

kratos 日志库相关的官方文档:日志 | Kratos

Kratos的日志库主要有如下特性:

  • Logger用于对接各种日志库或日志平台,可以用现成的或者自己实现
  • Helper是在您的项目代码中实际需要调用的,用于在业务代码里打日志
  • Filter用于对输出日志进行过滤或魔改(通常用于日志脱敏)
  • Valuer用于绑定一些全局的固定值或动态值(比如时间戳、traceID或者实例id之类的东西)到输出日志中

Logger统一了日志的接入方式,Helper接口统一的日志库的调用方式。

二、Kratos 使用标准内置日志库的方法

1、打印日志到控制台的步骤 

①导入kratos日志包

import "github.com/go-kratos/kratos/v2/log"//若导入失败,则需先获取包
go get github.com/go-kratos/kratos/v2/log

②使用 kratos 内置标准输出创建日志对象 

logger := log.NewStdLogger(os.Stdout)

③用 Log 方法打印日志,需要传入日志级别 

logger.Log(log.LevelInfo, "msg", "logger.Log 打印的日志")

 ④为了简化写日志时需传入的参数,kratos 用 Helper 对 Logger 进行了包装,建议用新建 helper 对象写日志,方法如下:

//对 logger 进行包装,简化写日志时需传入的参数
h := log.NewHelper(logger)//写入信息级别的日志
h.Info("使用 kratos 内置标准输出记录的日志")//写入错误级别的日志
h.Errorf("用户名【%s】不存在", "张三")

 ⑤可通过 log.With() 方法绑定全局字段到 Vauler,用来打印全局信息到日志中

//通过 log.With 方法绑定全局字段到 Vauler,用来打印全局信息到日志中
logger = log.With(logger,"ts", log.DefaultTimestamp,"caller", log.DefaultCaller,"trace_id", tracing.TraceID(),"span_id", tracing.SpanID(),
)
h = log.NewHelper(logger)
h.Info("绑定了全局信息到日志中")

⑥用 log.NewFilter() 方法对日志输出进行过滤

// 日志过滤,显示特定级别的日志,或对敏感信息脱敏
h = log.NewHelper(log.NewFilter(logger,// 按等级过滤//log.FilterLevel(log.LevelError),// 按key遮蔽log.FilterKey("username"),// 按value遮蔽log.FilterValue("hello"),),
)
h.Warn("warn log")
h.Infow("password", "123456")
//日志中 kratos 会变为 ***
h.Infow("username", "kratos")
//日志中 hello 会变为 ***
h.Info("hello")

 效果演示:

 完整代码:

package mainimport ("os""github.com/go-kratos/kratos/v2""github.com/go-kratos/kratos/v2/log""github.com/go-kratos/kratos/v2/middleware/tracing""github.com/go-kratos/kratos/v2/transport/http"
)func main() {//使用 kratos 内置标准输出创建日志对象logger := log.NewStdLogger(os.Stdout)// 用 Log 方法打印日志,需要传入日志级别,不建议使用这种方式,而建议使用 helper 打印日志logger.Log(log.LevelInfo, "msg", "logger.Log 打印的日志")//对 logger 进行包装,简化写日志时需传入的参数h := log.NewHelper(logger)//写入信息级别的日志h.Info("使用 kratos 内置标准输出记录的日志")//写入错误级别的日志h.Errorf("用户名【%s】不存在", "张三")// 通过 log.With 方法绑定全局字段到 Vauler,用来打印全局信息到日志中logger = log.With(logger,"ts", log.DefaultTimestamp,"caller", log.DefaultCaller,"trace_id", tracing.TraceID(),"span_id", tracing.SpanID(),)h = log.NewHelper(logger)h.Info("绑定了全局信息到日志中")// 日志过滤,显示特定级别的日志,或对敏感信息脱敏h = log.NewHelper(log.NewFilter(logger,// 按等级过滤//log.FilterLevel(log.LevelError),// 按key遮蔽log.FilterKey("username"),// 按value遮蔽log.FilterValue("hello"),),)h.Warn("warn log")h.Infow("password", "123456")//日志中 kratos 会变为 ***h.Infow("username", "kratos")//日志中 hello 会变为 ***h.Info("hello")//创建 kratos http server 及 apphttpSrv := http.NewServer(http.Address(":8080"),)app := kratos.New(kratos.Name("测试log"),kratos.Server(httpSrv,),)if err := app.Run(); err != nil {log.Fatal(err)}
}

2、将日志写入到本地文件的方法

 ① 通过 os.OpenFile 新建文件,获取到 os.File 对象

f, err := os.OpenFile("../test.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {return
}

② 通过 log.NewStdLogger(w io.Writer) 新建 logger 时,传入上述 os.File 对象 f

//输出到控制台,传入os.Stdout;输出到文件,则传文件指针
logger := log.NewStdLogger(f)

注意:os.File 实现了  io.Writer 接口的 Write(p []byte) (n int, err error) 方法,所以能将 f 作为输入参数传入 log.NewStdLogger() 函数

③用 log.NewHelper 包装 logger,并打印日志

h1 := log.NewHelper(logger1)
h1.Info("输出到日志文件中的日志信息")

效果演示:

代码实现:

package mainimport ("os""github.com/go-kratos/kratos/v2""github.com/go-kratos/kratos/v2/log""github.com/go-kratos/kratos/v2/transport/http"
)func main() {// 通过 log.NewStdLogger(w io.Writer) 中的 io.Writer 设置日志输出方式// 将日志输出到 test.log 文件f, err := os.OpenFile("../test.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)if err != nil {return}logger1 := log.NewStdLogger(f)h1 := log.NewHelper(logger1)h1.Info("输出到日志文件中的日志信息")//创建 kratos http server 及 apphttpSrv := http.NewServer(http.Address(":8080"),)app := kratos.New(kratos.Name("测试log"),kratos.Server(httpSrv,),)if err := app.Run(); err != nil {log.Fatal(err)}
}

三、 zap 日志库的使用方法

1、zap简介

Zap 是 uber 用 go 语言实现的超快、结构化、分级的日志记录库,性能极高,比其他结构化日志记录包快4-10倍。日志记录结构化,以JSON等格式输出。

有关详细介绍见:zap package - go.uber.org/zap - Go Packages

2、使用方法

①安装 zap 包

go get -u go.uber.org/zap

②导入 zap 包

import "go.uber.org/zap"

③用 NewProduction 等方法创建 zap 日志对象

// 创建 zap 日志对象
zaplogger, _ := zap.NewProduction()
defer logger.Sync()

 ④调用 Info、Error 等方法打印日志

//使用 zap 写日志
zaplogger.Info("failed to fetch URL",zap.String("url", "http://dddd.com"),zap.Int("attempt", 3),zap.Duration("backoff", time.Second),
)

效果演示:

 完整代码:

package mainimport ("time"kzap "github.com/go-kratos/kratos/contrib/log/zap/v2""github.com/go-kratos/kratos/v2""github.com/go-kratos/kratos/v2/log""github.com/go-kratos/kratos/v2/transport/http""go.uber.org/zap"
)func main() {// 创建 zap 日志对象,并打印日志zaplogger, _ := zap.NewProduction()defer zaplogger.Sync()//使用 zap 写日志zaplogger.Info("failed to fetch URL",zap.String("url", "http://dddd.com"),zap.Int("attempt", 3),zap.Duration("backoff", time.Second))httpSrv := http.NewServer(http.Address(":8080"),)app := kratos.New(kratos.Name("测试log"),kratos.Server(httpSrv,),)if err := app.Run(); err != nil {log.Fatal(err)}
}

⑤如果需要将日志输出到文件,则使用如下方法:

package mainimport ("os"kzap "github.com/go-kratos/kratos/contrib/log/zap/v2""github.com/go-kratos/kratos/v2""github.com/go-kratos/kratos/v2/log""github.com/go-kratos/kratos/v2/transport/http""go.uber.org/zap""go.uber.org/zap/zapcore"
)func main() {// 配置Encoderencoder := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())// 配置 WriteSyncer,将日志写入文件f, err := os.OpenFile("zaptest.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)if err != nil {panic(err)}defer f.Close()writeSyncer := zapcore.AddSync(f)// 设置LogLevellevel := zap.NewAtomicLevelAt(zapcore.InfoLevel)// 创建Corecore := zapcore.NewCore(encoder, writeSyncer, level)// 构建Loggerzaplogger := zap.New(core)logger := kzap.NewLogger(zaplogger)// 将 zap 日志对象适配到 kratos 的日志库,以便直接使用 kratos 的日志方法h := log.NewHelper(logger)h.Info("msg", "kratos适配的zap, 写入日志到文件")httpSrv := http.NewServer(http.Address(":8080"),)app := kratos.New(kratos.Name("测试log"),kratos.Server(httpSrv,),)if err := app.Run(); err != nil {log.Fatal(err)}
}

 效果演示:

四、Kratos 适配 zap 日志库的方法

kratos 在 contrib/log  实现好了一些插件,用于适配目前常用的日志库。

  • std 标准输出,Kratos内置
  • fluent 输出到fluentd
  • zap 适配了uber的zap日志库
  • aliyun 输出到阿里云日志

使用方法:

①安装 kratos 的 zap 适配包

go get github.com/go-kratos/kratos/contrib/log/zap/v2

②导入 zap 适配包

import (kzap "github.com/go-kratos/kratos/contrib/log/zap/v2"
)

 ③创建 zap 日志对象

// 创建 zap 日志对象,并打印日志
zaplogger, _ := zap.NewProduction()
defer zaplogger.Sync()

④调用函数 NewLogger(zlog *zap.Logger) 将创建的 zap 日志对象适配到 kratos 的日志操作对象

// 将 zap 日志对象适配到 kratos 的日志库,以便直接使用 kratos 的日志方法
logger := kzap.NewLogger(zaplogger)

 ⑤调用函数 log.NewHelper 将 logger 进行包装,简化日志写入操作

h := log.NewHelper(logger)
h.Info("kratos适配的zap日志库")

效果演示:

代码实现:

package mainimport (kzap "github.com/go-kratos/kratos/contrib/log/zap/v2""github.com/go-kratos/kratos/v2""github.com/go-kratos/kratos/v2/log""github.com/go-kratos/kratos/v2/transport/http""go.uber.org/zap"
)func main() {// 创建 zap 日志对象,并打印日志zaplogger, _ := zap.NewProduction()defer zaplogger.Sync()// 将 zap 日志对象适配到 kratos 的日志库,以便直接使用 kratos 的日志方法logger := kzap.NewLogger(zaplogger)h := log.NewHelper(logger)h.Info("kratos适配的zap日志库")httpSrv := http.NewServer(http.Address(":8080"),)app := kratos.New(kratos.Name("测试log"),kratos.Server(httpSrv,),)if err := app.Run(); err != nil {log.Fatal(err)}
}

五、Kratos 适配第三方日志库的实现原理

kratos 底层日志接口 Logger,可用于快速适配各种日志库到框架中来,仅需要实现一个最简单的Log方法。

// Logger is a logger interface.
type Logger interface {Log(level Level, keyvals ...interface{}) error
}

 以适配 uber 的 zap 日志库为例,kratos 在 contrib/log/zap/zap.go 中定义了新的日志结构体 Logger,并实现了方法 Log(level log.Level, keyvals ...interface{}) error。其源码如下:

package zapimport ("fmt""go.uber.org/zap""github.com/go-kratos/kratos/v2/log"
)var _ log.Logger = (*Logger)(nil)type Logger struct {log    *zap.LoggermsgKey string
}type Option func(*Logger)// WithMessageKey with message key.
func WithMessageKey(key string) Option {return func(l *Logger) {l.msgKey = key}
}func NewLogger(zlog *zap.Logger) *Logger {return &Logger{log:    zlog,msgKey: log.DefaultMessageKey,}
}func (l *Logger) Log(level log.Level, keyvals ...interface{}) error {var (msg    = ""keylen = len(keyvals))if keylen == 0 || keylen%2 != 0 {l.log.Warn(fmt.Sprint("Keyvalues must appear in pairs: ", keyvals))return nil}data := make([]zap.Field, 0, (keylen/2)+1)for i := 0; i < keylen; i += 2 {if keyvals[i].(string) == l.msgKey {msg, _ = keyvals[i+1].(string)continue}data = append(data, zap.Any(fmt.Sprint(keyvals[i]), keyvals[i+1]))}switch level {case log.LevelDebug:l.log.Debug(msg, data...)case log.LevelInfo:l.log.Info(msg, data...)case log.LevelWarn:l.log.Warn(msg, data...)case log.LevelError:l.log.Error(msg, data...)case log.LevelFatal:l.log.Fatal(msg, data...)}return nil
}func (l *Logger) Sync() error {return l.log.Sync()
}func (l *Logger) Close() error {return l.Sync()
}

相关文章:

go 微服务框架 kratos 日志库使用方法及原理探究

一、Kratos 日志设计理念 kratos 日志库相关的官方文档&#xff1a;日志 | Kratos Kratos的日志库主要有如下特性&#xff1a; Logger用于对接各种日志库或日志平台&#xff0c;可以用现成的或者自己实现Helper是在您的项目代码中实际需要调用的&#xff0c;用于在业务代码里…...

VC++位移操作>>和<<以及逻辑驱动器插拔产生的掩码dbv.dbcv_unitmask进行分析的相关代码

VC位移操作>>和<<以及逻辑驱动器插拔产生的掩码dbv.dbcv_unitmask进行分析的相关代码 一、VC位移操作符<<和>>1、右位移操作符 >>&#xff1a;2、左位移操作符 <<&#xff1a; 二、逻辑驱动器插拔产生的掩码 dbv.dbcv_unitmask 进行分析的…...

查看gpu

## 查看gpu信息 if_cuda torch.cuda.is_available() print("if_cuda",if_cuda)gpu_count torch.cuda.device_count() print("gpu_count",gpu_count)...

CSS与表格设计

在网页设计中&#xff0c;表格是一种不可或缺的元素&#xff0c;用于展示和组织数据。虽然HTML提供了基本的表格结构&#xff0c;但通过CSS&#xff08;层叠样式表&#xff09;的应用&#xff0c;我们可以极大地提升表格的外观和用户体验。本文将探讨如何利用CSS来设计既美观又…...

阴影映射(线段树)

实时阴影是电子游戏中最为重要的画面效果之一。在计算机图形学中&#xff0c;通常使用阴影映射方法来实现实时阴影。 游戏开发部正在开发一款 2D 游戏&#xff0c;同时希望能够在 2D 游戏中模仿 3D 游戏的光影效果&#xff0c;请帮帮游戏开发部&#xff01; 给定 x-y 平面上的…...

Docker 容器间通讯

1、虚拟ip/访问 同一网络 安装docker时&#xff0c;docker会默认创建一个内部的桥接网络docker0&#xff0c;每创建一个容器分配一个虚拟网卡&#xff0c;容器之间(包括宿主机)可以根据分配的ip互相访问(ps:其他主机(包括其他主机的容器)无法ping通docker容器ip无法访问&#…...

C语言章节学习归纳--数据类型、运算符与表达式

3.1 C语言的数据类型&#xff08;理解&#xff09; 首先&#xff0c;对变量的定义可以包括三个方面&#xff1a; 数据类型 存储类型 作用域 所谓数据类型是按被定义变量的性质&#xff0c;表示形式&#xff0c;占据存储空间的多少&#xff0c;构造特点来划分的。在C语言中&…...

Centos 7.9 使用 iso 搭建本地 YUM 源

Centos 7.9 使用 iso 搭建本地 YUM 源 1 建立挂载点 [rootlocalhost ~]# mkdir -p /media/cdrom/ 2 创建光盘存储路径 [rootlocalhost ~]# mkdir -p /mnt/cdrom/ 3 上传 CentOS-7-x86_64-Everything-2207-02.iso 到 光盘存储路径 [rootlocalhost ~]# ls /mnt/cdrom/ CentOS-…...

NFT Insider #131:Mocaverse NFT市值破3.5万ETH,The Sandbox 参加NFCsummit

引言&#xff1a;NFT Insider由NFT收藏组织WHALE Members&#xff08;https://twitter.com/WHALEMembers&#xff09;、BeepCrypto &#xff08;https://twitter.com/beep_crypto&#xff09;联合出品&#xff0c;浓缩每周NFT新闻&#xff0c;为大家带来关于NFT最全面、最新鲜、…...

BatBot智慧能源管理平台,更加有效地管理能源

随着能源消耗的不断增加&#xff0c;能源管理已成为全球面临的重要问题。BatBot智慧能源管理作为一种的能源管理技术&#xff0c;促进企业在用能效率及管理有着巨大的提升。 BatBot智慧能源管理是一种基于人工智能技术的能源管理系统&#xff0c;通过智能分析和优化能源使用&…...

医院预约挂号系统微信小程序APP

医院预约挂号小程序&#xff0c;前端后台&#xff08;后台 java spring boot mysql&#xff09; 医院预约挂号系统具体功能介绍&#xff1a;展示医院信息、可以注册和登录&#xff0c; 预约挂号&#xff08;包含各个科室的预约&#xff0c;可以预约每个各个医生&#xff09;&…...

【代码随想录 二叉树】二叉树前序、中序、后序遍历的迭代遍历

文章目录 1. 二叉树前序遍历&#xff08;迭代法&#xff09;2. 二叉树后序遍历&#xff08;迭代法&#xff09;3. 二叉树中序遍历&#xff08;迭代法&#xff09; 1. 二叉树前序遍历&#xff08;迭代法&#xff09; 题目连接 &#x1f34e;因为处理顺序和访问顺序是一致的。所…...

Error:(6, 43) java: 程序包org.springframework.data.redis.core不存在

目录 一、在做SpringBoot整合Redis的项目时&#xff0c;报错&#xff1a; 二、尝试 三、解决办法 一、在做SpringBoot整合Redis的项目时&#xff0c;报错&#xff1a; 二、尝试 给依赖加版本号&#xff0c;并且把版本换了个遍&#xff0c;也不行&#xff0c;也去update过ma…...

Qt 科目一考试系统(有源码)

项目源码和资源&#xff1a;科目一考试系统: qt实现科目一考试系统 一.项目概述 该项目是一个基于Qt框架开发的在线考试系统&#xff0c;主要实现了考试题目的随机抽取、考试时间限制、成绩统计等功能。用户可以通过界面操作进行考试&#xff0c;并查看自己的考试成绩。 二.技…...

在 Visual Studio 2022 (VS2022) 中删除 Git 分支的步骤如下

git branch -r PS \MauiApp1> git push origin --delete “20240523备份” git push origin --delete “20240523备份”...

玩转OpenHarmony智能家居:如何实现开发版“碰一碰”设备控制

一、简介 “碰一碰”设备控制&#xff0c;依托NFC短距通信协议&#xff0c;通过碰一碰的交互方式&#xff0c;将OpenAtom OpenHarmony&#xff08;简称“OpenHarmony”&#xff09;标准系统设备和全场景设备连接起来&#xff0c;解决了应用与设备之间接续慢、传输难的问题&…...

订餐系统总结、

应用层&#xff1a; SpringBoot:快速构建Spring项目&#xff0c;采用“约定大于配置”的思想&#xff0c;简化Spring项目的配置开发。 SpringMvc&#xff1a;Spring框架的一个模块&#xff0c;springmvc和spring无需通过中间整合层进行整合&#xff0c;可以无缝集成。 Sprin…...

【因果推断从入门到精通二】随机实验3

目录 检验无因果效应假说 硬币投掷的特殊性何在&#xff1f; 检验无因果效应假说 无因果效应假说认为&#xff0c;有些人存活&#xff0c;有些人死亡&#xff0c;但接受mAb114治疗而不是ZMapp与此无关。在174例接受mAb14治疗的患者中&#xff0c;113/17464.9%存活了28天&…...

真实案例分享,终端pc直接telnet不到出口路由器。

1、背景信息 我终端pc的网卡地址获取的网关是在核心交换机上&#xff0c;在核心交换机上telnet出口路由器可以实现。 所有终端网段都不能telnet出口路由器&#xff0c;客户希望能用最小的影响方式进行解决。 2、现有配置信息 终端的无线和有线分别在两个网段中&#xff0c;…...

YOLOv8_seg的训练、验证、预测及导出[实例分割实践篇]

实例分割数据集链接,还是和目标检测篇一样,从coco2017val数据集中挑出来person和surfboard两类:链接:百度网盘 请输入提取码 提取码:3xmm 1.实例分割数据划分及配置 1.1实例分割数据划分 从上面得到的数据还不能够直接训练,需要按照一定的比例划分训练集和验证集,并按…...

web vue 项目 Docker化部署

Web 项目 Docker 化部署详细教程 目录 Web 项目 Docker 化部署概述Dockerfile 详解 构建阶段生产阶段 构建和运行 Docker 镜像 1. Web 项目 Docker 化部署概述 Docker 化部署的主要步骤分为以下几个阶段&#xff1a; 构建阶段&#xff08;Build Stage&#xff09;&#xff1a…...

深入剖析AI大模型:大模型时代的 Prompt 工程全解析

今天聊的内容&#xff0c;我认为是AI开发里面非常重要的内容。它在AI开发里无处不在&#xff0c;当你对 AI 助手说 "用李白的风格写一首关于人工智能的诗"&#xff0c;或者让翻译模型 "将这段合同翻译成商务日语" 时&#xff0c;输入的这句话就是 Prompt。…...

Rust 异步编程

Rust 异步编程 引言 Rust 是一种系统编程语言,以其高性能、安全性以及零成本抽象而著称。在多核处理器成为主流的今天,异步编程成为了一种提高应用性能、优化资源利用的有效手段。本文将深入探讨 Rust 异步编程的核心概念、常用库以及最佳实践。 异步编程基础 什么是异步…...

ArcGIS Pro制作水平横向图例+多级标注

今天介绍下载ArcGIS Pro中如何设置水平横向图例。 之前我们介绍了ArcGIS的横向图例制作&#xff1a;ArcGIS横向、多列图例、顺序重排、符号居中、批量更改图例符号等等&#xff08;ArcGIS出图图例8大技巧&#xff09;&#xff0c;那这次我们看看ArcGIS Pro如何更加快捷的操作。…...

【笔记】WSL 中 Rust 安装与测试完整记录

#工作记录 WSL 中 Rust 安装与测试完整记录 1. 运行环境 系统&#xff1a;Ubuntu 24.04 LTS (WSL2)架构&#xff1a;x86_64 (GNU/Linux)Rust 版本&#xff1a;rustc 1.87.0 (2025-05-09)Cargo 版本&#xff1a;cargo 1.87.0 (2025-05-06) 2. 安装 Rust 2.1 使用 Rust 官方安…...

RSS 2025|从说明书学习复杂机器人操作任务:NUS邵林团队提出全新机器人装配技能学习框架Manual2Skill

视觉语言模型&#xff08;Vision-Language Models, VLMs&#xff09;&#xff0c;为真实环境中的机器人操作任务提供了极具潜力的解决方案。 尽管 VLMs 取得了显著进展&#xff0c;机器人仍难以胜任复杂的长时程任务&#xff08;如家具装配&#xff09;&#xff0c;主要受限于人…...

Go语言多线程问题

打印零与奇偶数&#xff08;leetcode 1116&#xff09; 方法1&#xff1a;使用互斥锁和条件变量 package mainimport ("fmt""sync" )type ZeroEvenOdd struct {n intzeroMutex sync.MutexevenMutex sync.MutexoddMutex sync.Mutexcurrent int…...

Git 3天2K星标:Datawhale 的 Happy-LLM 项目介绍(附教程)

引言 在人工智能飞速发展的今天&#xff0c;大语言模型&#xff08;Large Language Models, LLMs&#xff09;已成为技术领域的焦点。从智能写作到代码生成&#xff0c;LLM 的应用场景不断扩展&#xff0c;深刻改变了我们的工作和生活方式。然而&#xff0c;理解这些模型的内部…...

HTML前端开发:JavaScript 获取元素方法详解

作为前端开发者&#xff0c;高效获取 DOM 元素是必备技能。以下是 JS 中核心的获取元素方法&#xff0c;分为两大系列&#xff1a; 一、getElementBy... 系列 传统方法&#xff0c;直接通过 DOM 接口访问&#xff0c;返回动态集合&#xff08;元素变化会实时更新&#xff09;。…...

OCR MLLM Evaluation

为什么需要评测体系&#xff1f;——背景与矛盾 ​​ 能干的事&#xff1a;​​ 看清楚发票、身份证上的字&#xff08;准确率>90%&#xff09;&#xff0c;速度飞快&#xff08;眨眼间完成&#xff09;。​​干不了的事&#xff1a;​​ 碰到复杂表格&#xff08;合并单元…...