日志系统项目(2)项目实现(实用工具类、日志等级类、日志消息类、日志格式化输出类)

前面的文章中我们讲述了日志系统项目的前置知识点,再本文中我们将开始日志项目的细节实现。

日志系统框架设计

本项目实现的是一个多日志器日志系统,主要实现的功能是让程序员能够轻松的将程序运行日志信息落地到指定的位置,且支持同步与异步两种方式的日志落地方式。
项目的框架设计将项目分为以下几个模块来实现。

模块划分

  • 日志等级模块:对输出日志的等级进行划分,以便于控制日志的输出,并提供等级枚举转字符串功能。

    • OFF:关闭
    • DEBUG:调试,调试时的关键信息输出。
    • INFO:提示,普通的提示型日志信息。
    • WARN:警告,不影响运行,但是需要注意一下的日志。
    • ERROR:错误,程序运行出现错误的日志。
    • FATAL:致命,一般是代码异常导致程序无法继续推进运行的日志。
  • 日志消息模块:中间存储日志输出所需的各项要素信息

    • 时间:描述本条日志的输出时间。
    • 线程ID:描述本条日志是哪个线程输出的。
    • 日志等级:描述本条日志的等级。
    • 日志数据:本条日志的有效载荷数据。
    • 日志文件名:描述本条日志在哪个源码文件中输出的。
    • 日志行号:描述本条日志在源码文件的哪一行输出的。
  • 日志消息格式化模块:设置日志输出格式,并提供对日志消息进行格式化功能。

    • 系统的默认日志输出格式:%d{%H:%M:%S}%T[9%t]%T[%p]%T[%c]%T%f:%1%T%m%no
      ->13:26:32 [2343223321] [FATAL] [root] main.c:76 套接字创建失败\n
    • %d{%H:%M:%S}:表示日期时间,花括号中的内容表示日期时间的格式。
    • %T:表示制表符缩进。
    • %t:表示线程ID。
    • %p:表示日志级别。
    • %c:表示日志器名称,不同的开发组可以创建自己的日志器进行日志输出,小组之间互不影响。
    • %f:表示日志输出时的源代码文件名。
    • %l:表示日志输出时的源代码行号。
    • %m:表示给与的日志有效载荷数据。
    • %n:表示换行。
    • 设计思想:设计不同的子类,不同的子类从日志消息中取出不同的数据进行处理。
  • 日志消息落地模块∶决定了日志的落地方向,可以是标准输出,也可以是日志文件,也可以滚动文件输出…

    • 标准输出:表示将日志进行标准输出的打印。
    • 日志文件输出:表示将日志写入指定的文件末尾。
    • 滚动文件输出:当前以文件大小进行控制,当一个日志文件大小达到指定大小,则切换下一个文件进行输出
    • 后期,也可以扩展远程日志输出,创建客户端,将日志消息发送给远程的日志分析服务器。
    • 设计思想:设计不同的子类,不同的子类控制不同的日志落地方向。
  • 日志器模块:

    • 此模块是对以上几个模块的整合模块,用户通过日志器进行日志的输出,有效降低用户的使用难度。
    • 包含有:日志消息落地模块对象,日志消息格式化模块对象,日志输出等级
  • 日志器管理模块:

    • 为了降低项目开发的日志耦合,不同的项目组可以有自己的日志器来控制输出格式以及落地方向,因此本项目是一个多日志器的日志系统。
    • 管理模块就是对创建的所有日志器进行统一管理。并提供一个默认日志器提供标准输出的日志输出。
  • 异步线程模块:

    • 实现对日志的异步输出功能,用户只需要将输出日志任务放入任务池,异步线程负责日志的落地输出功能,以此提供更加高效的非阻塞日志输出。

模块关系图

在这里插入图片描述

代码设计

实用类设计

完成一些零碎的功能接口,以便于后面会用到。

  • 获取系统时间信息
  • 判断文件是否存在
  • 获取文件所在路径
  • 创建目录
/*实用工具类的实现:
    1. 获取系统时间
    2. 判断文件是否存在
    3. 获取文件所在目录
    4. 创建目录
*/
namespace zyqlog
{
    namespace util
    {
        class Date 
        {
        public:
            static size_t now() // 获取当前的系统时间
            {
                return (size_t)time(nullptr);
            }
        };

        class File
        {
        public:
            static bool exists(const std::string &pathname) // 判断当前的文件是否存在
            {
                // return (access(pathname.c_str(), F_OK) == 0); // Linux下的接口

                struct stat st;
                if (stat(pathname.c_str(), &st) < 0)
                {
                    return false;
                }
                return true;
            }
            static std::string path(const std::string &pathname) // 获取当前的文件路径
            {
                // ./abc/a.txt
                size_t pos = pathname.find_last_of("/\\"); // 从文件路径最后的一个/或者\\开始获取文件路径
                if (pos == std::string::npos) return ".";
                return pathname.substr(0, pos + 1);
            }
            static void createDirectory(const std::string &pathname)
            {
                // ./abc/bcd/a.txt
                size_t pos = 0, idx = 0;
                while (idx < pathname.size())
                {
                    pos = pathname.find_first_of("/\\", idx); // 从文件路径开始处的/或者\\开始获取文件路径
                    if (pos == std::string::npos) // 获取到的结果如果已到文件末尾则说明传入的整个路径名都是需要创建的目录,直接进行创建
                    {
                        mkdir(pathname.c_str(), 0777);
                    }
                    std::string parent_dir = pathname.substr(0, pos + 1); // 将获取到的每级目录进行截取
                    if (parent_dir == "." || parent_dir == "..") // 如果截取的目录是当前目录或者是上一级目录则继续进行截取。
                    {
                        idx = pos + 1;
                        continue;
                    }
                    if (exists(parent_dir) == true) // 如果目录已经存在则继续进行截取
                    {
                        idx = pos + 1;
                        continue;
                    }
                    mkdir(parent_dir.c_str(), 0777); // 创建截取得到的未创建目录
                    idx = pos + 1;
                }
            }
        };
    }
}
/*test*/
std::cout << zyqlog::util::Date().now() << std::endl;
std::string pathname = "./abc/bcd/a.txt";
zyqlog::util::File().createDirectory(zyqlog::util::File::path(pathname));

日志等级类设计

日志等级共分为7个等级,分别为:

  • OFF 关闭所有日志输出
  • DRBUG 进行debug时候打印日志的等级
  • INFO 打印一些用户提示信息
  • WARN 打印警告信息
  • ERROR 打印错误信息
  • FATAL 打印致命信息-导致程序崩溃的信息
/*
    1. 定义枚举类,枚举出日志等级
    2. 提供转换接口,将枚举转换为对应的字符串
*/
namespace zyqlog
{
    class LogLevel
    {
    private:
    public:
        enum class value // 枚举类实现不同的日志等级
        {
            UNKNOW = 0,
            DEBUG,
            INFO,
            WARNING,
            ERROR,
            FATAL,
            OFF
        };

        static const char *toString(LogLevel::value level) // 将获取到的日志等级转换为字符串
        {
            switch (level)
            {
            case LogLevel::value::DEBUG:
                return "DEBUG";
            case LogLevel::value::INFO:
                return "INFO";
            case LogLevel::value::WARNING:
                return "WARNING";
            case LogLevel::value::ERROR:
                return "ERROR";
            case LogLevel::value::FATAL:
                return "FATAL";
            case LogLevel::value::OFF:
                return "OFF";
            }
            return "UNKNOW";
        }
    };
}

日志消息类设计

/*
    定义日志消息类,进行日志中间信息的存储:
    1. 日志的输出时间--用于过滤日志输出时间
    2. 日志等级--用于进行日志过滤分析
    3. 源文件名称
    4. 源代码行号--用于定位出现错误的代码的位置
    5. 线程ID--用于过滤出错的线程
    6. 日志主体消息
    7. 日志器名称--支持多日志器同时使用
*/
namespace zyqlog
{
    struct LogMsg
    {
        time_t _ctime; // 日志产生的时间戳
        LogLevel::value _level; // 日志等级
        std::thread::id _tid; // 线程ID
        size_t _line; // 行号
        std::string _file; // 源码文件名
        std::string _logger; // 日志器名称
        std::string _payload; // 有效消息数据
        LogMsg() {}
        LogMsg(LogLevel::value level, size_t line, const std::string file, const std::string logger, const std::string msg) 
            : _ctime(util::Date::now())
            , _level(level)
            , _line(line)
            , _tid(std::this_thread::get_id())
            , _file(file)
            , _logger(logger)
            , _payload(msg)
        {}
    };
}

日志格式化输出类

日志格式化(Formatter)类主要负责格式化日志消息。其主要包含以下内容

  • pattern成员:保存日志输出的格式字符串
    • %d 日期
    • %T 缩进
    • %t 线程id
    • %p 日志级别
    • %c 日志器名称
    • %f 文件名
    • %l 行号
    • %m 日志消息
    • %n 换行
  • std::vector< FormatItem::ptr > items成员:用于按序保存格式化字符串对应的子格式化对象
    FormatItem类主要负责日志消息子项的获取及格式化。其包含以下子类
  • MsgFormatItem:表示要从LogMsg中取出有效日志数据
  • LevelFormatItem:表示要从LogMsg中取出日志等级
  • ThreadFormatItem:表示要从LogMsg中取出线程ID
  • TimeFormatItem:表示要从LogMsg中取出时间戳并按照指定格式进行格式化
  • LineFormatItem:表示要从LogMsg中取出源码所在行号
  • TabFormatItem:表示⼀个制表符缩进
  • NLineFormatItem:表示⼀个换行
  • OtherFormatItem:表示非格式化的原始字符串

格式化的过程其实就是按次序从Msg中取出需要的数据进行字符串的连接的过程。

// 设计思想:
// 1. 抽象一个格式化子项基类
// 2. 基于基类, 派生出格式化子项子类
    // 在父类中定义父类指针数组,指向不同格式化子类对象
namespace zyqlog
{
    // 抽象格式化子项基类
    class FormatItem
    {
    public:
        using ptr = std::shared_ptr<FormatItem>;
        virtual void format(std::ostream &out, const LogMsg &msg) = 0;
    };
    
    // 派生类格式化子项子类--消息,等级,时间,文件名,行号,线程ID,日志器名,制表符,换行吗,其他
    class MsgFormatItem : public FormatItem
    {
    public:
        void format(std::ostream &out, const LogMsg &msg) override
        {
            out << msg._payload;
        }
    };

    class LevelFormatItem : public FormatItem
    {
    public:
        void format(std::ostream &out, const LogMsg &msg) override
        {
            out << LogLevel::toString(msg._level);
        }
    };

    class TimeFormatItem : public FormatItem
    {
    public:
        TimeFormatItem(const std::string &fmt = "%H:%M:%S") :_time_fmt(fmt) {}
        void format(std::ostream &out, const LogMsg &msg) override
        {
            struct tm t;
            localtime_r(&msg._ctime, &t);
            char tmp[32] = {0};
            strftime(tmp, 31, _time_fmt.c_str(), &t); // strftime()函数根据格式字符串将给定的日期和时间从给定的日历时间转换为以空结尾的多字节字符串。
            out << tmp;
        }
    private:
        std::string _time_fmt; // %H:%M:%S
    };

    class FileFormatItem : public FormatItem
    {
    public:
        void format(std::ostream &out, const LogMsg &msg) override
        {
            out << msg._file;
        }
    };

    class LineFormatItem : public FormatItem
    {
    public:
        void format(std::ostream &out, const LogMsg &msg) override
        {
            out << msg._line;
        }
    };

    class ThreadFormatItem : public FormatItem
    {
    public:
        void format(std::ostream &out, const LogMsg &msg) override
        {
            out << msg._tid;
        }
    };

    class LoggerFormatItem : public FormatItem
    {
    public:
        void format(std::ostream &out, const LogMsg &msg) override
        {
            out << msg._logger;
        }
    };

    class TabFormatItem : public FormatItem
    {
    public:
        void format(std::ostream &out, const LogMsg &msg) override
        {
            out << "\t";
        }
    };

    class NLineFormatItem : public FormatItem
    {
    public:
        void format(std::ostream &out, const LogMsg &msg) override
        {
            out << "\n";
        }
    };

    class OtherFormatItem : public FormatItem
    {
    public:
        OtherFormatItem(const std::string &str) :_str(str) {}
        void format(std::ostream &out, const LogMsg &msg) override
        {
            out << _str;
        }
    private:
        std::string _str;
    };

    /*
        %d 表示日期 包含子格式{%H:%M:%S}
        %t 表示线程ID 
        %c 表示日志器名称
        %f 表示源码文件名
        %l 表示源码行号
        %p 表示日志级别
        %T 表示制表符缩进
        %m 日志消息
        %n 表示换行
    */
    class ForMatter
    {
    public:
        using ptr = std::shared_ptr<ForMatter>;

        ForMatter(const std::string &pattern = "[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n") : _pattern(pattern) 
        {
            assert(parsePattern()); // 对格式化规则字符串进行解析
        }
        // 对msg进行格式化
        void format(std::ostream &out, LogMsg &msg)
        {
            for(auto &item : _items)
            {
                item->format(out, msg);
            }
        }
        std::string format(LogMsg &msg)
        {
            std::stringstream ss;
            format(ss, msg);
            return ss.str();
        }

    private:
        // 对格式化规则字符串进行解析
        bool parsePattern()
        {
            // 1. 对格式化规则字符串进行解析
            // abcd[ % d {%H:%M:%S} ][ %t][%c][%f:%l][%p]%T%m%n
            std::vector<std::pair<std::string, std::string>> fmt_order;
            size_t pos = 0;
            std::string key, val;
            while (pos < _pattern.size())
            {
                // 1. 处理原始字符串--判断是否是%,不是就是原始字符
                if (_pattern[pos] != '%')
                {
                    val.push_back(_pattern[pos++]);
                    continue;
                }
                // 能进行到此说明pos位置是%字符,%%处理称为一个%字符
                if (pos + 1 < _pattern.size() && _pattern[pos + 1] == '%')
                {
                    val.push_back('%');
                    pos += 2;
                    continue;
                }
                // 这时候原始字符串处理完毕
                if (!val.empty())
                {
                    fmt_order.push_back(std::make_pair("", val));
                    val.clear();
                }

                //代表%后面是一个格式化字符,格式化字符的处理
                pos += 1; // pos指向格式化字符的位置
                if (pos == _pattern.size()) 
                {
                    std::cerr << "%之后没有对应的字符!\n";
                    return false;
                }
                key = _pattern[pos]; // 确定key格式化字符的位置
                // 此时pos指向格式化字符后的位置
                pos += 1;
                if (pos < _pattern.size() && _pattern[pos] == '{') 
                {
                    pos += 1; // pos指向子规则的起始位置
                    while (pos < _pattern.size() && _pattern[pos] != '}')
                    {
                        val.push_back(_pattern[pos++]);
                    }
                    // 走到末尾跳出循环,则代表没有遇到},代表格式是错误的
                    if (pos == _pattern.size()) 
                    {
                        std::cerr << "子规则{}匹配出错!\n";
                        return false;
                    }
                    pos += 1; // pos指向}位置,向后走一步,到了下一步的位置
                }
                fmt_order.push_back(std::make_pair(key, val));
                key.clear();
                val.clear();
            }
            /*
				这个处理的过程以 abcd[%d{%H:%M:%S}][ %t][%c][%f:%l][%p]%T%m%n 为例子进行解析
				key = nullptr,val = abcd[
				key = d,val = %H:%M:%S
				key = nullptr,val = ][
				...
				得到数组内容之后,根据数组内容,创建格式化子项对象,添加到items成员数组中。
			*/
            // 2. 根据解析得到的数据初始化格式化子项数组成员
            for (auto &it : fmt_order)
            {
                _items.push_back(createItem(it.first, it.second));
            }

            return true;
        }

        // 根据不同格式化字符创建不同的格式化子项对象
        FormatItem::ptr createItem(const std::string &key, const std::string &val)
        {
            if (key == "d") return std::make_shared<TimeFormatItem>(val);
            if (key == "t") return std::make_shared<ThreadFormatItem>();
            if (key == "c") return std::make_shared<LoggerFormatItem>();
            if (key == "f") return std::make_shared<FileFormatItem>();
            if (key == "l") return std::make_shared<LineFormatItem>();
            if (key == "p") return std::make_shared<LevelFormatItem>();
            if (key == "T") return std::make_shared<TabFormatItem>();
            if (key == "m") return std::make_shared<MsgFormatItem>();
            if (key == "n") return std::make_shared<NLineFormatItem>();
            if (key.empty()) return std::make_shared<OtherFormatItem>(val);
            std::cerr << "没有对应的格式化字符: %" << key << std::endl;
            abort();
            return FormatItem::ptr();
        }
    private:
        std::string _pattern; // 格式化规则字符串
        std::vector<FormatItem::ptr> _items;
    };
}

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

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

相关文章

成都直播基地作为产业重要载体,引领直播行业健康、多元发展

近年来&#xff0c;我国网络直播行业呈现出井喷式的发展态势。众多直播平台如雨后春笋般涌现&#xff0c;直播内容丰富多样&#xff0c;涵盖游戏、电竞、美食、旅游、教育等多个领域。同时&#xff0c;成都直播产业园规模持续扩大&#xff0c;产业不断完善&#xff0c;整体呈现…

常见的音频与视频格式

本专栏是汇集了一些HTML常常被遗忘的知识&#xff0c;这里算是温故而知新&#xff0c;往往这些零碎的知识点&#xff0c;在你开发中能起到炸惊效果。我们每个人都没有过目不忘&#xff0c;过久不忘的本事&#xff0c;就让这一点点知识慢慢渗透你的脑海。 本专栏的风格是力求简洁…

蓝桥杯算法 一.

分析&#xff1a; 本题记录&#xff1a;m个数&#xff0c;异或运算和为0&#xff0c;则相加为偶数&#xff0c;后手获胜。 分析&#xff1a; 369*99<36500&#xff0c;369*100>36500。 注意&#xff1a;前缀和和后缀和问题

TABR: TABULAR DEEP LEARNING MEETS NEAREST NEIGHBORS IN 2023 阅读笔记

TABR: TABULAR DEEP LEARNING MEETS NEAREST NEIGHBORS IN 2023 论文地址&#xff1a;https://arxiv.org/abs/2307.14338 源代码&#xff1a;https://github.com/yandex-research/tabular-dl-tabr 摘要 针对表格数据问题&#xff08;例如分类、回归&#xff09;的深度学习&a…

利用项目管理软件规划的成功之路

项目开发对于任何类型的项目都是一个有用的过程。软件开发项目、建筑项目、运输项目和变更管理项目都可以从这种方法提供的结构、指导和策略中获益。 项目开发涉及规划项目时间表、投资资源以及安排团队成员的时间。与项目管理一样&#xff0c;项目开发贯穿项目始终&#xff0…

【前沿热点视觉算法】-用于RGB-D显著对象检测等领域的三维卷积神经网络

计算机视觉算法分享。问题或建议&#xff0c;请文章私信或者文章末尾扫码加微信留言。 1 论文题目 用于RGB-D显著对象检测等领域的三维卷积神经网络 2 论文摘要 RGB-deph&#xff08;RGB-D&#xff09;显著目标检测&#xff08;SOD&#xff09;近年来引起了越来越多的研究兴…

家装服务管理:Java技术的创新应用

✍✍计算机毕业编程指导师 ⭐⭐个人介绍&#xff1a;自己非常喜欢研究技术问题&#xff01;专业做Java、Python、微信小程序、安卓、大数据、爬虫、Golang、大屏等实战项目。 ⛽⛽实战项目&#xff1a;有源码或者技术上的问题欢迎在评论区一起讨论交流&#xff01; ⚡⚡ Java、…

Python 鼠标模拟

鼠标模拟即&#xff1a;通过python 进行模拟鼠标操作 引入类库 示例如下&#xff1a; import win32api import win32con import time 设置鼠标位置 设置鼠标位置为窗口中的回收站。 示例如下&#xff1a; # 设置鼠标的位置 win32api.SetCursorPos([30, 40]) 双击图标 设置…

揭秘工业以太网交换机的冗余与备份技术:如何保障网络稳定与数据安全

在工业自动化和智能制造领域&#xff0c;网络通信的稳定性和可靠性堪称业务连续性的命脉。网络一旦出现故障&#xff0c;将可能直接导致生产中断&#xff0c;甚至造成重大经济损失。鉴于此&#xff0c;工业以太网交换机——作为工业网络的核心组件&#xff0c;其冗余技术与备份…

从Unity到Three.js(outline 模型描边功能)

指定模型高亮功能&#xff0c;附带设置背景颜色&#xff0c;获取随机数方法。 百度查看说是gltf格式的模型可以携带PBR材质信息&#xff0c;如果可以这样&#xff0c;那就完全可以在blender中配置好材质导出了&#xff0c;也就不需要像在unity中调整参数了。 import * as THRE…

微信小程序02: 使用微信快速验证组件code获取手机号

全文目录,一步到位 1.前言简介1.1 专栏传送门1.1.1 上文小总结1.1.2 上文传送门 2. 微信小程序获取手机号2.1 业务场景(使用与充值)2.2 准备工作2.3 具体代码使用与注释如下2.3.1 代码解释(一)[无需复制]2.3.2 代码解释(二)[无需复制] 2.4 最后一步 获取手机号信息2.4.1 两行代…

Java设计模式 | 七大原则之依赖倒转原则

依赖倒转原则&#xff08;Dependence Inversion Principle&#xff09; 基本介绍 高层模块不应该依赖低层模块&#xff0c;二者都应该依赖其抽象&#xff08;接口/抽象类&#xff09;抽象不应该依赖细节&#xff0c;细节应该依赖抽象依赖倒转&#xff08;倒置&#xff09;的…

React基础-webpack+creact-react-app创建项目

学习视频&#xff1a;学习视频 2节&#xff1a;webpack工程化创建项目 2.1.webpack工程化工具&#xff1a;vite/rollup/turbopak; 实现组件的合并、压缩、打包等&#xff1b; 代码编译、兼容、校验等&#xff1b; 2.2.React工程化/组件开发 我们可以基于webpack自己去搭建…

React歌词滚动效果(跟随音乐播放时间滚动)

首先给audio绑定更新时间事件 const updateTime e > {console.log(e.target.currentTime)setCurrentTime(e.target.currentTime);};<audiosrc{currentSong.url}ref{audio}onCanPlay{ready}onEnded{end}onTimeUpdate{updateTime}></audio>当歌曲播放时间改变的时…

【力扣 - 有效的括号】

题目描述 给定一个只包括 (&#xff0c;)&#xff0c;{&#xff0c;}&#xff0c;[&#xff0c;] 的字符串 s &#xff0c;判断字符串是否有效。 有效字符串需满足&#xff1a; 左括号必须用相同类型的右括号闭合。左括号必须以正确的顺序闭合。每个右括号都有一个对应的相同…

免费享受企业级安全:雷池社区版WAF,高效专业的Web安全的方案

网站安全成为了每个企业及个人不可忽视的重要议题。 随着网络攻击手段日益狡猾和复杂&#xff0c;选择一个强大的安全防护平台变得尤为关键。 推荐的雷池社区版——一个为网站提供全面安全防护解决方案的平台&#xff0c;它不仅具备高效的安全防护能力&#xff0c;还让网站安…

Uniapp + VUE3.0 实现双向滑块视频裁剪效果

效果图 <template><view v-if"info" class"all"><video:src"info.videoUrl"class"video" id"video" :controls"true" object-fit"fill" :show-fullscreen-btn"false"play-btn…

linux服务器vi文件中文乱码

服务器vi编辑中文乱码 cat 文本是中文 可以编辑 vi /etc/environment 文件修改为utf8中文字符集 LANGzh_CN.UTF-8 LANGUAGEen_US:en LC_CTYPE"zh_CN.UTF-8" LC_NUMERIC"zh_CN.UTF-8" LC_TIME"zh_CN.UTF-8" LC_COLLATE"zh_CN.UTF-8"…

springboot219基于SpringBoot的网络海鲜市场系统的设计与实现

网络海鲜市场系统的设计与实现 摘 要 计算机网络发展到现在已经好几十年了&#xff0c;在理论上面已经有了很丰富的基础&#xff0c;并且在现实生活中也到处都在使用&#xff0c;可以说&#xff0c;经过几十年的发展&#xff0c;互联网技术已经把地域信息的隔阂给消除了&…

Python实现DAS单点登录

❇️ 流程 进入登录页面 &#xff08;DAS验证的登录页面&#xff09; 获取验证码图像&#xff0c;百度OCR识别 登录 &#x1f3de;️ 环境 Windows 11 Python 3.12 PyCharm 2023 &#x1f9f5; 准备工作 安装必要依赖库 bs4 Jupyter 推荐安装 Jupyter&#xff08;Anaco…