Golang日志管理:使用log/slog实现高级功能和性能优化

Golang日志管理:使用log/slog实现高级功能和性能优化

    • 简介
    • 基础使用
      • 初始化和配置
      • 日志级别
    • 高级技巧
      • 自定义日志格式器
      • 条件日志处理
    • 实战案例
      • 场景一:API请求日志记录
      • 场景二:错误跟踪和用户通知
    • 性能优化
      • 优化日志记录的性能
        • 异步日志处理
        • 选择合适的日志级别
      • 使用高性能的日志框架
    • 错误处理和调试
      • 利用 `log/slog` 进行有效的错误处理
        • 记录错误信息
        • 使用日志级别区分错误严重性
      • 日志和调试技巧
        • 添加足够的上下文信息
        • 利用日志分析工具
    • 与其他库的集成
      • 集成 `log/slog` 与数据库操作库 `GORM`
        • 设置 `GORM` 的日志接口
      • 集成 `log/slog` 与网络框架 `Gin`

在这里插入图片描述

简介

在现代软件开发中,日志记录是一个不可或缺的部分,它帮助开发者追踪应用行为、调试问题并保持系统的健康。Golang,作为一种高效的编程语言,提供了多种日志记录工具,而 log/slog 库则是其中功能强大且灵活的选择之一。

log/slog 是一个标准的日志库,旨在提供一个简单、模块化且高效的日志解决方案。它支持不同级别的日志记录,如信息、警告和错误等,使得开发者可以根据不同的信息重要性进行适当的记录。此外,slog 的设计允许通过插件扩展其功能,如添加日志处理器、格式化器或是输出目标,从而满足更加复杂的日志管理需求。

使用 log/slog,开发者可以轻松实现日志的定制化处理,从而使得日志系统既能满足性能的需求,也兼顾到操作的便捷性。本文将通过几个章节,详细介绍如何有效地使用 log/slog 来进行日志记录和管理,包括基础的日志设置、高级技巧的应用、以及在实际开发中如何利用这些技巧来解决常见的问题。

接下来的部分,我们将从如何开始使用 log/slog 来进行基本的日志记录讲起,逐步深入到更为复杂的日志处理技巧和性能优化方法。

基础使用

初始化和配置

在 Golang 中使用 log/slog 进行日志记录前,首先需要进行适当的初始化和配置。以下是一个基本的示例,展示了如何快速开始使用 slog 进行日志记录:

package main

import (
    "log"
    "os"
)

func main() {
    // 创建一个日志文件
    file, err := os.OpenFile("app.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
    if err != nil {
        log.Fatal(err)
    }
    defer file.Close()

    // 设置日志输出到文件
    log.SetOutput(file)

    // 设置日志前缀
    log.SetPrefix("INFO: ")
    // 设置日志的格式
    log.SetFlags(log.Ldate | log.Ltime | log.Llongfile)

    log.Println("This is a test log entry")
}

在这个示例中,我们首先创建了一个日志文件 app.log,然后通过 SetOutput 方法将日志输出设置为该文件。同时,我们通过 SetPrefixSetFlags 方法来设置日志条目的前缀和格式,这样可以更清晰地了解日志的来源和上下文。

日志级别

log/slog 默认不区分日志级别,但你可以通过简单的封装来实现这一功能,以便在不同的场景中使用不同级别的日志记录:

package main

import (
    "log"
    "os"
)

const (
    LevelError = iota
    LevelWarning
    LevelInfo
)

func main() {
    file, err := os.OpenFile("app.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
    if err != nil {
        log.Fatal(err)
    }
    defer file.Close()

    logger := log.New(file, "PREFIX: ", log.Ldate|log.Ltime|log.Llongfile)

    // 使用不同的日志级别记录信息
    logError(logger, "This is an error message")
    logWarning(logger, "This is a warning message")
    logInfo(logger, "This is an info message")
}

func logError(logger *log.Logger, msg string) {
    logger.SetPrefix("ERROR: ")
    logger.Println(msg)
}

func logWarning(logger *log.Logger, msg string) {
    logger.SetPrefix("WARNING: ")
    logger.Println(msg)
}

func logInfo(logger *log.Logger, msg string) {
    logger.SetPrefix("INFO: ")
    logger.Println(msg)
}

在上面的代码中,我们定义了三个不同的函数 logErrorlogWarninglogInfo 来分别记录错误、警告和信息级别的日志。通过设置不同的前缀,可以在日志文件中清楚地看到每条日志的重要性级别。

高级技巧

自定义日志格式器

在许多复杂的应用场景中,开发者可能需要对日志的格式进行自定义,以适应特定的监控系统或日志分析工具的需求。log/slog 通过提供灵活的配置选项,允许开发者轻松定义自己的日志格式。下面是一个如何自定义日志格式器的示例:

package main

import (
    "fmt"
    "log"
    "os"
    "time"
)

type customLogger struct {
    logger *log.Logger
}

func newCustomLogger(out *os.File) *customLogger {
    return &customLogger{
        logger: log.New(out, "", 0),
    }
}

func (c *customLogger) Printf(format string, v ...interface{}) {
    c.logger.SetPrefix(fmt.Sprintf("CUSTOM LOG [%s]: ", time.Now().Format(time.RFC3339)))
    c.logger.Printf(format, v...)
}

func main() {
    file, err := os.OpenFile("custom_app.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
    if err != nil {
        log.Fatal(err)
    }
    defer file.Close()

    cLogger := newCustomLogger(file)
    cLogger.Printf("This is a custom log message with dynamic timestamp.")
}

在这个例子中,我们创建了一个 customLogger 类型,它包含一个内嵌的 log.Logger。我们重写了 Printf 方法,使其在每次记录日志时都会添加一个动态时间戳。这种方式使日志输出更加灵活和信息丰富。

条件日志处理

对于大型应用或在特定环境(如生产环境)中运行的应用,可能不需要记录所有日志。在这种情况下,条件日志处理变得尤为重要。以下是如何实现基于条件的日志记录的示例:

package main

import (
    "log"
    "os"
    "runtime"
)

func main() {
    file, err := os.OpenFile("conditional_app.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
    if err != nil {
        log.Fatal(err)
    }
    defer file.Close()

    logger := log.New(file, "INFO: ", log.Ldate|log.Ltime|log.Lshortfile)

    if runtime.GOOS == "linux" {
        logger.Println("This log is only written on Linux systems.")
    } else {
        logger.Println("This log is not written on Linux systems.")
    }
}

这段代码通过检查操作系统类型来决定是否记录特定的日志消息。这种方法在需要根据运行环境调整日志策略时非常有用。

实战案例

场景一:API请求日志记录

在开发Web服务时,记录每个API请求的详细信息对于后期分析和问题定位非常有用。使用 log/slog,我们可以轻松实现这一功能。以下是一个简单的示例,展示如何在一个基于HTTP的服务中记录每个请求的详细日志:

package main

import (
    "log"
    "net/http"
    "os"
)

func main() {
    file, err := os.OpenFile("api_requests.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
    if err != nil {
        log.Fatal(err)
    }
    defer file.Close()

    logger := log.New(file, "REQUEST: ", log.Ldate|log.Ltime|log.LUTC)

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        logger.Printf("Received request: %s %s from %s", r.Method, r.URL.Path, r.RemoteAddr)
        w.Write([]byte("Hello, world!"))
    })

    log.Println("Starting server on :8080")
    err = http.ListenAndServe(":8080", nil)
    if err != nil {
        log.Fatal("Error starting server: ", err)
    }
}

在这个例子中,我们为HTTP服务器的每个请求设置了一个日志记录点。这样,每当接收到一个请求时,都会在日志文件中记录请求的方法、路径和来源地址。

场景二:错误跟踪和用户通知

在复杂的应用中,当错误发生时,及时记录错误信息并通知相关人员是非常重要的。以下是如何使用 log/slog 来实现错误日志记录和邮件通知的示例:

package main

import (
    "log"
    "os"
    "net/smtp"
)

// 配置SMTP服务器信息
const (
    SMTPServer   = "smtp.example.com"
    SMTPPort     = "587"
    SMTPUser     = "your-email@example.com"
    SMTPPassword = "your-password"
)

func notifyByEmail(subject, message string) {
    auth := smtp.PlainAuth("", SMTPUser, SMTPPassword, SMTPServer)
    to := []string{"admin@example.com"}
    msg := []byte("To: admin@example.com\r\n" +
        "Subject: " + subject + "\r\n" +
        "\r\n" +
        message + "\r\n")
    err := smtp.SendMail(SMTPServer+":"+SMTPPort, auth, SMTPUser, to, msg)
    if err != nil {
        log.Println("Error sending email:", err)
    }
}

func main() {
    file, err := os.OpenFile("errors.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
    if err != nil {
        log.Fatal(err)
    }
    defer file.Close()

    logger := log.New(file, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile)

    // 示例错误情况
    testError := func() {
        defer func() {
            if r := recover(); r != nil {
                errMsg := "Recovered in testError: " + r.(string)
                logger.Println(errMsg)
                notifyByEmail("Error Notification", errMsg)
            }
        }()
        panic("something went wrong")
    }

    testError()
}

这段代码演示了如何在发生未捕获异常时记录详细的错误日志,并通过电子邮件发送通知。通过这种方式,开发者和运维团队可以迅速响应异常情况。

性能优化

优化日志记录的性能

在高并发的应用中,日志记录本身可能成为性能瓶颈。优化日志记录的性能是确保应用整体效率的关键一环。以下是几种提升 log/slog 日志处理性能的方法:

异步日志处理

为了减少日志记录对主应用性能的影响,可以实现异步日志处理。这意味着日志消息将被发送到一个独立的处理队列中,并由另一个线程或进程异步写入到存储系统。这样可以显著减少写日志操作对主业务逻辑的延迟影响。示例如下:

package main

import (
    "log"
    "os"
)

// 日志消息队列
var logQueue chan string

func init() {
    logQueue = make(chan string, 1000) // 创建一个有缓冲的通道
    go func() {
        file, err := os.OpenFile("async_app.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
        if err != nil {
            log.Fatal(err)
        }
        defer file.Close()

        logger := log.New(file, "ASYNC: ", log.Ldate|log.Ltime|log.Lshortfile)

        for msg := range logQueue {
            logger.Println(msg)
        }
    }()
}

func logMessage(message string) {
    logQueue <- message // 将消息发送到队列
}

func main() {
    for i := 0; i < 100; i++ {
        logMessage("Log entry number: " + string(i))
    }
}

在这个例子中,我们创建了一个日志消息队列,并通过一个独立的goroutine来处理这些消息。这样,主程序在发送日志消息时只需将消息放入队列,而不需等待日志写入操作完成。

选择合适的日志级别

在开发和测试环境中,记录详细的日志是有帮助的,但在生产环境中,过多的日志记录可能会导致性能下降和存储过载。合理设置日志级别,仅记录必要的信息,是优化日志性能的另一有效手段:

if debugMode {
    log.Println("Detailed debug information.")
}

使用上述条件语句,可以确保只有在调试模式(debugMode为真)下才记录详细的调试信息。

使用高性能的日志框架

虽然 log/slog 提供了基本的日志功能,但在处理极高性能需求时,可能需要考虑更专业的日志框架,如 zapzerolog。这些框架为性能优化提供了更多的配置选项和更高效的实现。

错误处理和调试

利用 log/slog 进行有效的错误处理

错误处理是软件开发中的一个重要方面,良好的错误处理策略不仅可以减少系统的不稳定性,还可以提供必要的信息以便快速定位问题。log/slog 通过提供清晰的日志记录,可以显著提升错误处理的效率。以下是几种使用 log/slog 进行错误处理的方法:

记录错误信息

当捕获到异常或错误时,应该记录详细的错误信息,包括错误发生的时间、位置和可能的原因。这不仅有助于开发者理解错误的性质,也便于后期的问题追踪和解决。

package main

import (
    "log"
    "os"
)

func main() {
    file, err := os.OpenFile("error_handling.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
    if err != nil {
        log.Fatal(err)
    }
    defer file.Close()

    logger := log.New(file, "ERROR: ", log.Ldate|log.Ltime|log.Llongfile)

    // 模拟一个错误情况
    result, err := someFunctionThatMightFail()
    if err != nil {
        logger.Printf("Error occurred: %v", err)
    }
}

func someFunctionThatMightFail() (int, error) {
    return 0, fmt.Errorf("something went wrong")
}

在这个例子中,someFunctionThatMightFail 函数可能会返回错误,我们通过日志记录详细的错误信息,这样有助于后续的问题分析和修复。

使用日志级别区分错误严重性

根据错误的严重性,可以使用不同的日志级别来记录错误。这样做可以使日志更加结构化,便于按严重性进行过滤和查找。

if err != nil {
    if criticalError(err) {
        logger.Fatalf("Critical error occurred: %v", err) // 记录严重错误,并停止程序
    } else {
        logger.Printf("Non-critical error: %v", err) // 记录非严重错误
    }
}

日志和调试技巧

为了更有效地使用日志进行调试,可以采用以下一些技巧:

添加足够的上下文信息

在记录日志时,添加足够的上下文信息是至关重要的。这包括不限于用户ID、操作类型、时间戳等,这些信息可以大大提升日志的用途。

利用日志分析工具

将日志记录到一些支持查询和分析的系统中,如ELK(Elasticsearch, Logstash, Kibana)堆栈,可以更有效地利用日志进行问题定位和性能监测。

与其他库的集成

集成 log/slog 与数据库操作库 GORM

在许多现代应用中,与数据库交互是不可避免的,而将日志库与数据库操作库集成可以帮助开发者更好地监控和调试数据库相关的操作。以下是如何将 log/slog 集成到使用 GORM 的项目中:

设置 GORM 的日志接口

GORM 是一个流行的 Golang ORM(对象关系映射)库,它自带一个灵活的日志接口,允许开发者插入自定义的日志处理逻辑。下面的示例展示了如何利用 log/slog 来记录 GORM 的日志:

package main

import (
    "gorm.io/gorm"
    "gorm.io/driver/sqlite"
    "log"
    "os"
)

func main() {
    // 设置日志文件
    file, err := os.OpenFile("gorm_integration.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
    if err != nil {
        log.Fatal(err)
    }
    defer file.Close()

    logger := log.New(file, "GORM: ", log.Ldate|log.Ltime|log.Lshortfile)

    // 初始化数据库连接
    db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{
        Logger: customLogger{logger},
    })
    if err != nil {
        logger.Println("Failed to connect to database:", err)
        return
    }

    // 数据库操作示例
    db.AutoMigrate(&Product{}) // 自动迁移模式
    logger.Println("Database migration completed.")
}

// 实现 GORM 的 logger 接口
type customLogger struct {
    *log.Logger
}

func (c customLogger) Printf(format string, args ...interface{}) {
    c.Printf(format, args...)
}

type Product struct {
    gorm.Model
    Code  string
    Price uint
}

在这个例子中,我们定义了一个 customLogger 结构,实现了 GORM 的日志接口。这样,所有 GORM 的日志都会通过我们自定义的 log/slog 记录器进行记录。

集成 log/slog 与网络框架 Gin

Gin 是一个高性能的 HTTP Web 框架,将其与 log/slog 集成可以有效地记录 HTTP 请求和响应数据。以下是集成的基本方法:

package main

import (
    "github.com/gin-gonic/gin"
    "log"
    "os"
)

func main() {
    // 配置日志
    file, err := os.OpenFile("gin_integration.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
    if err != nil {
        log.Fatal(err)
    }
    defer file.Close()

    logger := log.New(file, "GIN: ", log.Ldate|log.Ltime|log.Llongfile)

    // 创建 Gin 实例
    r := gin.New()
    r.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
        // 自定义 Gin 日志格式
        return logger.Printf("Method: %s, Path: %s, Status: %d, Latency: %s\n",
            param.Method, param.Path, param.StatusCode, param.Latency)
    }))
    r.Use(gin.Recovery())

    // 设置路由
    r.GET("/", func(c *gin.Context) {
        c.String(http.StatusOK, "Hello World")
    })

    // 启动服务器
    r.Run()
}

这段代码通过自定义 Gin 的日志格式器将日志记录到我们指定的文件中。这样做不仅增加了日志的可读性,也使得日志的存储更为灵活。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/593775.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

算法设计与分析——期末1h

目录 第一章 算法的定义 算法的三要素 算法的基本性质 算法的时间复杂度数量级&#xff1a; 第二章 兔子繁殖问题&#xff08;递推法&#xff09; 猴子吃桃问题&#xff08;递推法&#xff09; 穿越沙漠问题&#xff08;递推法&#xff08;倒推&#xff09;&#xff09; 百钱百…

C++进阶----多态

1.多态的概念 1.1 概念 多态的概念&#xff1a;通俗来说&#xff0c;就是多种形态&#xff0c;具体点就是去完成某个行为&#xff0c;当不同类型的对象去完成时会 产生出不同的状态。 举个例子&#xff1a;比如有一个基类Animal&#xff0c;它有两个子类Dog和Cat。每个…

SpringCloud知识点梳理

1. Spring Cloud 综述 1.1 Spring Cloud 是什么 [百度百科]Spring Cloud是⼀系列框架的有序集合。它利⽤Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中⼼、消息总线、负载均衡、断路器、数据监控等,都可以⽤ Spring Boot的开发⻛格…

eNSP-动态路由(ospf协议)

一、拓扑结构搭建 二、主机配置 pc1 pc2 三、路由器配置 1.AR2配置 <Huawei>sys #进入系统视图 [Huawei]int g0/0/0 #进入接口 [Huawei-GigabitEthernet0/0/0]ip address 192.168.0.2 24 #设置ip地址 [Huawei-GigabitEthernet0/0/0]q #返回上一级 [Huawei]int g0/0/1 …

长期找 AI 专家,邀请参加线上聊天直播

诚邀 AI 专家参加线上聊天&#xff0c;成为嘉宾。 分享前沿观点、探讨科技和生活 除节假日外&#xff0c;每周举办在线聊天直播 根据话题和自愿形式结合&#xff0c;每期 2~3 位嘉宾 成为嘉宾&#xff0c;见下&#xff1a;

== 和 equals()区别,equals()重写问题

对于引用类型&#xff1a;比较的是两个引用是否相同&#xff08;所指的是否为同一个对象&#xff09;&#xff0c;注&#xff1a;如果两个引用所指的对象内容一样&#xff0c;但是不是同一个对象&#xff08;hashcode不一样&#xff09;&#xff0c;依然返回false&#xff0c;随…

macOS DOSBox 汇编环境搭建

正文 一、安装DOSBox 首先前往DOSBox的官网下载并安装最新版本的DOSBox。 二、下载必备的工具包 在用户目录下新建一个文件夹&#xff0c;比如 dosbox: mkdir dosbox然后下载一些常用的工具。下载好了后&#xff0c;将这些工具解压&#xff0c;重新放在 dosbox 这个文件夹…

渗透之sql盲注(时间/boolean盲注)

sql盲注&#xff1a;sql盲注意思是我们并不能在web页面中看到具体的信息&#xff0c;我们只能通过输入的语句的真假来判断。从而拿到我们想要的信息。 我们通常使用ascii值来进行盲注。 目录 手动注入&#xff1a; 时间盲注&#xff1a; 布尔盲注&#xff1a; python脚本注…

2024阿里云ctf-web-chain17学习

agent jdk17依赖有h2思路清晰打jdbc attack <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- https://mvnrepository.com/artifact/com.aliba…

【Spark】Spark分布式环境安装(二)

Spark分布式环境安装 将spark-3.5.0-bin-hadoop3.tgz 文件上传到 Linux 并解压缩&#xff0c;放置在指定位置&#xff0c;路径中不要包含中文或空格&#xff0c;解压缩操作&#xff0c;不再强调。 基于Windows的环境体验 启动脚本 启动界面如图-1所示。 图-1 spark-shell启动…

Vue工程化开发和脚手架Vue CLI

目录 一、介绍 二、使用步骤 1. 全局安装&#xff08;一次&#xff09; 2.查看Vue版本 3.创建项目架子&#xff08;项目名不能使用中文&#xff09; 4.启动项目 一、介绍 Vue CLI是Vue官方提供的一个全局命令工具。可以帮助我们快速创建一个开发的Vue项目的标准化基础架子…

QT:小项目:登录界面 (下一个连接数据库)

一、效果图 登录后&#xff1a; 二、项目工程结构 三、登录界面UI设计 四主界面 四、源码设计 login.h #ifndef LOGIN_H #define LOGIN_H#include <QDialog>namespace Ui { class login; }class login : public QDialog {Q_OBJECTpublic:explicit login(QWidge…

架构每日一学 3:架构师六个生存法则之一:如何找到唯一且正确的架构目标?(二)

本文首发于公众号&#xff1a;腐烂的橘子 上一篇文章中&#xff0c;我们讨论了架构师第一个生存法则&#xff1a;必须有且仅有一个目标。今天我们主要讨论下如何找到这个目标。 确认一个正确目标且要试图逼近它 每一个企业的第一任务首先是活下来&#xff0c;然后再盈利。那么…

unity制作app(3)--gps定位

1.unity中定位Unity之GPS定位&#xff08;高德解析&#xff09;_unity gps定位-CSDN博客 代码需要稍微修改一下&#xff0c;先把脚本绑到一个button上试一试&#xff01; 2.先去高德地图认证&#xff08;app定位&#xff09; 创建应用和 Key-Web服务 API | 高德地图API (ama…

结构体介绍(2)

结构体介绍&#xff08;2&#xff09; 前言一、结构体的内存对齐之深入理解为什么存在内存对齐&#xff1f;修改默认对齐数 二、结构体传参2.1&#xff1a;该怎么传参呢&#xff1f; 三、结构体实现位段3.1什么是位段位段的内存分配位段的跨平台问题 总结 前言 根据之前讲了结…

CMakeLists.txt语法规则:改变行为的变量说明二

一. 简介 前面一篇文章学习了 CMakeLists.txt语法中的 部分常量变量&#xff0c;具体学习提供信息的变量&#xff0c;文章如下&#xff1a; CMakeLists.txt语法规则&#xff1a;提供信息的变量说明一-CSDN博客 CMakeLists.txt语法规则&#xff1a;提供信息的变量说明二-CSD…

【计算机网络原理】万字长文,持续更新...

文章目录&#x1f970; 计算机网络原理1.2 因特网概述1 网络、互联网&#xff08;互连网&#xff09;和因特网2 因特网发展的三个阶段ISP的概念基于ISP的三层结构的因特网 3 因特网的标准化工作4 因特网的组成 1.3 三种交换方式&#xff1a;电路交换、分组交换和报文交换电路交…

ctfshow 框架复现

文章目录 web 466web 467web 468web469web 470web 471web 472web 473web 474web 475web 476 web 466 Laravel5.4版本 &#xff0c;提交数据需要base64编码 代码审计学习—Laravel5.4 - 先知社区 (aliyun.com) 用第二条链子 反序列化格式 /admin/序列化串base64<?php na…

【多模态】29、OCRBench | 为大型多模态模型提供一个 OCR 任务测评基准

文章目录 一、背景二、实验2.1 测评标准和结果2.1.1 文本识别 Text Recognition2.1.2 场景文本中心的视觉问答 Scene Text-Centric VQA2.1.3 文档导向的视觉问答 Document-Oriented VQA2.1.4 关键信息提取 Key Information Extraction2.1.5 手写数学公式识别 Handwritten Mathe…

图片浏览软件-XnView

一、前言 XnView MP / Classic是一款免费的图像查看器&#xff0c;可轻松打开和编辑照片文件。图像查看器支持所有主要的图像格式&#xff08;JPEG&#xff0c;TIFF&#xff0c;PNG&#xff0c;GIF&#xff0c;WEBP&#xff0c;PSD&#xff0c;JPEG2000&#xff0c;OpenEXR&am…