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

TinyWebServer学习笔记-log

为什么服务器要有一个日志系统?

  1. 故障排查和调试: 在服务器运行期间,可能会发生各种问题和故障,例如程序崩溃、性能下降、异常请求等。日志记录了服务器的运行状态、错误信息和各种操作,这些日志可以用来快速定位和排查问题,帮助开发人员更容易地找到问题的根本原因,从而更快地修复bug和提高服务器稳定性。

  2. 性能监测: 通过日志系统可以记录服务器的性能数据,包括请求处理时间、吞吐量、内存使用情况、CPU利用率等等。这些数据有助于监控服务器的性能,发现潜在的性能瓶颈,以便进行性能优化。

  3. 安全性: 日志可以记录系统的安全事件,如登录失败尝试、异常访问等。通过分析这些日志,可以检测和防止潜在的安全威胁,提高服务器的安全性。

  4. 法律合规性: 某些行业和法规要求服务器必须记录和保留特定类型的操作日志,以便进行审计和合规性检查。缺乏合规性的日志记录可能会导致法律责任。

  5. 运营和分析: 运营团队可以通过分析日志数据来了解用户行为、产品使用情况和趋势。这有助于改进产品、优化用户体验和制定业务策略。

  6. 历史记录: 日志还可以用作历史记录,以跟踪系统的状态和操作历史。这对于了解系统的演化和历史情况非常有用。

 实现过程

为了实现异步写入,利用循环数组实现阻塞队列。

template <class T>
class block_queue
{
public:block_queue(int max_size = 1000){if (max_size <= 0){exit(-1);}m_max_size = max_size;m_array = new T[max_size];m_size = 0;m_front = -1;m_back = -1;}void clear(){m_mutex.lock();m_size = 0;m_front = -1;m_back = -1;m_mutex.unlock();}~block_queue(){m_mutex.lock();if (m_array != NULL)delete [] m_array;m_mutex.unlock();}//判断队列是否满了bool full() {m_mutex.lock();if (m_size >= m_max_size){m_mutex.unlock();return true;}m_mutex.unlock();return false;}//判断队列是否为空bool empty() {m_mutex.lock();if (0 == m_size){m_mutex.unlock();return true;}m_mutex.unlock();return false;}//返回队首元素bool front(T &value) {m_mutex.lock();if (0 == m_size){m_mutex.unlock();return false;}value = m_array[m_front];m_mutex.unlock();return true;}//返回队尾元素bool back(T &value) {m_mutex.lock();if (0 == m_size){m_mutex.unlock();return false;}value = m_array[m_back];m_mutex.unlock();return true;}int size() {int tmp = 0;m_mutex.lock();tmp = m_size;m_mutex.unlock();return tmp;}int max_size(){int tmp = 0;m_mutex.lock();tmp = m_max_size;m_mutex.unlock();return tmp;}//往队列添加元素,需要将所有使用队列的线程先唤醒//当有元素push进队列,相当于生产者生产了一个元素//若当前没有线程等待条件变量,则唤醒无意义bool push(const T &item){m_mutex.lock();if (m_size >= m_max_size){m_cond.broadcast();m_mutex.unlock();return false;}m_back = (m_back + 1) % m_max_size;m_array[m_back] = item;m_size++;m_cond.broadcast();m_mutex.unlock();return true;}//pop时,如果当前队列没有元素,将会等待条件变量bool pop(T &item){m_mutex.lock();while (m_size <= 0){if (!m_cond.wait(m_mutex.get())){m_mutex.unlock();return false;}}/*m_front = (m_front + 1) % m_max_size;item = m_array[m_front];*///这里我认为应该先得到值,然后更新m_front下标item=m_array[m_front];m_front=(m_frong+1)% m_max_size;m_size--;m_mutex.unlock();return true;}//增加了超时处理bool pop(T &item, int ms_timeout){struct timespec t = {0, 0};struct timeval now = {0, 0};gettimeofday(&now, NULL);m_mutex.lock();if (m_size <= 0){t.tv_sec = now.tv_sec + ms_timeout / 1000;t.tv_nsec = (ms_timeout % 1000) * 1000;if (!m_cond.timewait(m_mutex.get(), t)){m_mutex.unlock();return false;}}if (m_size <= 0){m_mutex.unlock();return false;}m_front = (m_front + 1) % m_max_size;item = m_array[m_front];m_size--;m_mutex.unlock();return true;}private:locker m_mutex;        //互斥锁cond m_cond;           //条件变量T *m_array;            //存放日志的数组int m_size;            //已经用的空间int m_max_size;        //最大容量int m_front;           //数组头的下标int m_back;            //数组尾的下标
};

解释下timespec结构体和timeval结构体,timespec结构体,分为秒和微秒两个部分,timeval结构体now,分为秒和纳秒两个部分。在这个自定义队列中,当队列为空时,从队列中获取元素的线程将会被挂起;当队列是满时,往队列里添加元素的线程将会挂起。

class Log
{
public://C++11以后,使用局部变量懒汉不用加锁static Log *get_instance(){static Log instance;return &instance;}static void *flush_log_thread(void *args){Log::get_instance()->async_write_log();}//可选择的参数有日志文件、日志缓冲区大小、最大行数以及最长日志条队列bool init(const char *file_name, int close_log, int log_buf_size = 8192, int split_lines = 5000000, int max_queue_size = 0);void write_log(int level, const char *format, ...);void flush(void);private:Log();virtual ~Log();void *async_write_log(){string single_log;//从阻塞队列中取出一个日志string,写入文件while (m_log_queue->pop(single_log)){m_mutex.lock();fputs(single_log.c_str(), m_fp);m_mutex.unlock();}}private:char dir_name[128];                 //路径名char log_name[128];                 //log文件名int m_split_lines;                  //日志最大行数int m_log_buf_size;                 //日志缓冲区大小long long m_count;                  //日志行数记录int m_today;                        //因为按天分类,记录当前时间是那一天FILE *m_fp;                         //打开log的文件指针char *m_buf;                        //缓冲区block_queue<string> *m_log_queue;   //阻塞队列bool m_is_async;                    //是否同步标志位locker m_mutex;                     //互斥锁int m_close_log;                    //关闭日志
};#define LOG_DEBUG(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(0, format, ##__VA_ARGS__); Log::get_instance()->flush();}
#define LOG_INFO(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(1, format, ##__VA_ARGS__); Log::get_instance()->flush();}
#define LOG_WARN(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(2, format, ##__VA_ARGS__); Log::get_instance()->flush();}
#define LOG_ERROR(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(3, format, ##__VA_ARGS__); Log::get_instance()->flush();}

本项目中,使用单例模式创建日志系统,记录服务器的运行状态、错误信息和访问数据。能够按天分类,超行分类。可以选择同步和异步写入两种方式。

同步打开对应的文件写入日志;异步则采取生产者-消费者模型封装为阻塞队列,创建一个写线程,工作线程将要写的日志push进队列,写线程从队列中读取内容,写入日志。

在这个项目的日志系统中,如果设置了阻塞队列的长度,则代表选择了异步写入日志;如果没有设置,则代表同步写入日志。

为什么要日志分级?

  1. 信息过滤: 在大型应用程序中,产生的日志可能非常庞大。通过分级,可以根据需要选择性地查看日志。例如,开发人员可能只对错误和警告感兴趣,而不关心调试信息。

  2. 故障排查: 当应用程序出现故障或错误时,日志分级可以帮助开发人员快速定位问题。错误日志可以提供关于发生了什么错误的详细信息,而调试日志则可以提供更多上下文,帮助解决问题。

  3. 性能监测: 分级日志还可以用于监测应用程序的性能。通过记录某些操作的耗时信息,开发人员可以识别性能瓶颈,并进行优化。

  4. 审核和合规性: 在某些情况下,应用程序需要记录特定事件或行为,以满足合规性要求或进行审核。通过使用不同级别的日志,可以轻松地识别和检索这些信息。

  5. 容易维护: 通过使用分级日志,开发人员可以更容易地维护应用程序的日志记录。不同级别的日志通常被写入不同的文件或存储位置,这使得查找和清理日志变得更加简单。

常见的日志级别包括:

  • DEBUG(调试): 用于记录详细的调试信息,通常只在开发和测试阶段启用。

  • INFO(信息): 用于记录应用程序的重要事件和状态信息,例如启动、关闭、用户登录等。

  • WARNING(警告): 用于记录可能需要关注但不一定是错误的事件,例如配置警告或不寻常的操作。

  • ERROR(错误): 用于记录应用程序的错误事件,例如异常、未处理的异常、无法连接到数据库等。

  • CRITICAL(关键): 用于记录严重错误,可能导致应用程序无法正常运行的情况,例如关键服务崩溃。

本项目中,包括DEBUG,INFO,WARN,ERROR四个级别。

#define LOG_DEBUG(format, ...) Log::get_instance()->write_log(0, format, __VA_ARGS__)
#define LOG_INFO(format, ...) Log::get_instance()->write_log(1, format, __VA_ARGS__)
#define LOG_WARN(format, ...) Log::get_instance()->write_log(2, format, __VA_ARGS__)
#define LOG_ERROR(format, ...) Log::get_instance()->write_log(3, format, __VA_ARGS__)

 在init函数中完成路径名,文件名等私有数据成员的设置。

if (p == NULL)
{snprintf(log_full_name, 255, "%d_%02d_%02d_%s", my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday, file_name);
}
else
{strcpy(log_name, p + 1);strncpy(dir_name, file_name, p - file_name + 1);snprintf(log_full_name, 255, "%s%d_%02d_%02d_%s", dir_name, my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday, log_name);
}

这段代码判断file_name是是否包含'\',如果不包含,也就是没有包含路径信息,那么就用当前日期和file_name来构建日志文件名。p为空则代表file_name不包含路径信息,只包含文件名,那么生成最终文件名:年_月_日_文件名。如果p不为空,那么找到最后一个路径分隔符,计算出文件名所在文件夹的长度,并将路径复制到dir_name中,文件名复制到log_name中,最终构建的路径是文件路径+时间+文件名。也就是根据是否包含路径信息,生成不同格式的日志文件名。

接下来是分级和分片函数:

void Log::write_log(int level, const char *format, ...)
{struct timeval now = {0, 0};gettimeofday(&now, NULL);time_t t = now.tv_sec;struct tm *sys_tm = localtime(&t);struct tm my_tm = *sys_tm;char s[16] = {0};switch (level){case 0:strcpy(s, "[debug]:");break;case 1:strcpy(s, "[info]:");break;case 2:strcpy(s, "[warn]:");break;case 3:strcpy(s, "[erro]:");break;default:strcpy(s, "[info]:");break;}//写入一个log,对m_count++, m_split_lines最大行数m_mutex.lock();m_count++;if (m_today != my_tm.tm_mday || m_count % m_split_lines == 0) //everyday log{char new_log[256] = {0};fflush(m_fp);fclose(m_fp);char tail[16] = {0};snprintf(tail, 16, "%d_%02d_%02d_", my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday);if (m_today != my_tm.tm_mday){snprintf(new_log, 255, "%s%s%s", dir_name, tail, log_name);m_today = my_tm.tm_mday;m_count = 0;}else{snprintf(new_log, 255, "%s%s%s.%lld", dir_name, tail, log_name, m_count / m_split_lines);}m_fp = fopen(new_log, "a");}m_mutex.unlock();va_list valst;va_start(valst, format);string log_str;m_mutex.lock();//写入的具体时间内容格式int n = snprintf(m_buf, 48, "%d-%02d-%02d %02d:%02d:%02d.%06ld %s ",my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday,my_tm.tm_hour, my_tm.tm_min, my_tm.tm_sec, now.tv_usec, s);int m = vsnprintf(m_buf + n, m_log_buf_size - n - 1, format, valst);m_buf[n + m] = '\n';m_buf[n + m + 1] = '\0';log_str = m_buf;m_mutex.unlock();if (m_is_async && !m_log_queue->full()){m_log_queue->push(log_str);}else{m_mutex.lock();fputs(log_str.c_str(), m_fp);m_mutex.unlock();}va_end(valst);
}

思路如下:

        1、获得今天的日期时间,如果日志的日期不是今天则创建新的日志文件,然后向新的文件写入。

        2、如果是今天的日期时间,但是超过了文件的最大行,那么就进行分片,在文件结尾加入分片的ID,比如***_log_file1,***_log_file2这样。然后写入文件。

        3、如果是今天的日期,并且没有超过最大文件行,就写入对应的文件。

准备好要写入的文件后,格式化时间和内容,放到缓冲数组中,然后判断日志系统是异步写入还是同步写入,如果是异步则将需要写的日志内容加入到阻塞队列中,否则直接调用fputs函数将输入文件指针对应的文件中。

相关文章:

TinyWebServer学习笔记-log

为什么服务器要有一个日志系统&#xff1f; 故障排查和调试&#xff1a; 在服务器运行期间&#xff0c;可能会发生各种问题和故障&#xff0c;例如程序崩溃、性能下降、异常请求等。日志记录了服务器的运行状态、错误信息和各种操作&#xff0c;这些日志可以用来快速定位和排查…...

【kubernetes】CRI OCI

1 OCI OCI(Open Container Initiative)&#xff1a;由Linux基金会主导&#xff0c;主要包含容器镜像规范和容器运行时规范&#xff1a; Image Specification(image-spec)Runtime Specification(runtime-spec)runC image-spec定义了镜像的格式&#xff0c;镜像的格式有以下几…...

竞赛 机器视觉opencv答题卡识别系统

0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 答题卡识别系统 - opencv python 图像识别 该项目较为新颖&#xff0c;适合作为竞赛课题方向&#xff0c;学长非常推荐&#xff01; &#x1f947;学长这里给一个题目综合评分(每项满分5分…...

Youtube视频下载工具分享-油管视频,音乐,字幕下载方法汇总

YouTube视频下载方法简介 互联网上存在很多 YouTube 下载工具&#xff0c;但我们经常会发现自己收藏的工具没过多久就会失效&#xff0c;我们为大家整理的这几种方法&#xff0c;是存在时间较久并且亲测可用的。后续如果这些工具失效或者有更好的工具&#xff0c;我们也会分享…...

【算法练习Day11】滑动窗口最大值前 K 个高频元素

​&#x1f4dd;个人主页&#xff1a;Sherry的成长之路 &#x1f3e0;学习社区&#xff1a;Sherry的成长之路&#xff08;个人社区&#xff09; &#x1f4d6;专栏链接&#xff1a;练题 &#x1f3af;长路漫漫浩浩&#xff0c;万事皆有期待 文章目录 滑动窗口最大值前 K 个高频…...

华为云HECS云服务器docker环境下安装nginx

前提&#xff1a;有一台华为云服务器。 华为云HECS云服务器&#xff0c;安装docker环境&#xff0c;查看如下文章。 华为云HECS安装docker-CSDN博客 一、拉取镜像 下载最新版Nginx镜像 (其实此命令就等同于 : docker pull nginx:latest ) docker pull nginx查看镜像 dock…...

GET 和 POST的区别

GET 和 POST 是 HTTP 请求的两种基本方法&#xff0c;要说它们的区别&#xff0c;接触过 WEB 开发的人都能说出一二。 最直观的区别就是 GET 把参数包含在 URL 中&#xff0c;POST 通过 request body 传递参数。 你可能自己写过无数个 GET 和 POST 请求&#xff0c;或者已经看…...

机器学习(监督学习)笔记

目录 总览笔记内容线性回归梯度下降特征缩放多输出线性回归 逻辑回归二分类与逻辑回归分类任务的性能指标&#xff08;召回率&#xff0c;精度&#xff0c;F1分数等&#xff09;支持向量机SVMK近邻朴素贝叶斯分类器朴素贝叶斯分类器进阶多分类逻辑回归二分类神经网络多分类神经…...

科普rabbitmq,rocketmq,kafka三者的架构比较

对比 架构对比 从架构可以看出三者有些类似&#xff0c;但是在细节上有很多不同。下面我们就从它们的各个组件&#xff0c;介绍它们&#xff1a; RabbitMQ&#xff0c;是一种开源的消息队列中间件。下面是RabbitMQ中与其相关的几个概念&#xff1a; 1.生产者&#xff08;P…...

加密货币交易技巧——地利(二)

EMA指标 针对资金体量大的代币&#xff0c;做现货交易或低倍合约&#xff0c;可参考以下指标&#xff1a; 1.指标介绍&#xff1a;EMA&#xff0c;移动平均线指标&#xff0c;这里只分享中长线用法&#xff0c;非常实用且准确率超高 2.适用群体&#xff1a;适用于现货或低倍…...

服务网关Gateway_微服务中的应用

没有服务网关 问题&#xff1a; 地址太多安全性管理问题 为什么要使用服务网关 网关是微服务架构中不可或缺的部分。使用网关后&#xff0c;客户端和微服务之间的网络结构如下。 注意&#xff1a; 网关统一向外部系统&#xff08;如访问者、服务&#xff09;提供REST API。在Sp…...

2G大小的GPU对深度学习的加速效果如何?

训练数据情况 总共42776张224*224*3张图片 Found 42776 files belonging to 9 classes. Using 12833 files for training. 模型参数情况 Total params: 10,917,385 Trainable params: 10,860,745 Non-trainable params: 56,640 batch-size:12 GPU信息 NVIDIA GeForce GT 7…...

intel 一些偏门汇编指令总结

intel 汇编手册下载链接&#xff1a;https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html LDS指令&#xff1a; 手册中可以找到 位于 3-588 根据手册内容猜测&#xff1a;lds r16 m16:16 的作用&#xff0c;是把位于 [m16:16] 内存地址的数…...

python 多个proto文件import引用时出现ModuleNotFoundError错误

问题描述 my_proto文件夹里有两个proto文件&#xff0c;book.proto想要引用person.proto文件中的Person&#xff0c;如下 book.proto syntax "proto2";import "person.proto"; // 导入person.proto文件message Book {optional string name 1;optional …...

C语言图书管理系统

一、 系统概述 图书管理系统是一个用C语言编写的软件系统&#xff0c;旨在帮助图书馆或图书机构管理其图书馆藏书和读者信息。该系统提供了一套完整的功能&#xff0c;包括图书录入、借阅管理、归还管理、读者管理、图书查询、统计报表等。 二、 系统功能 2.1 图书录入 管理…...

归并排序及其非递归实现

个人主页&#xff1a;Lei宝啊 愿所有美好如期而遇 目录 归并排序递归实现 归并排序非递归实现 归并排序递归实现 图示&#xff1a; 代码&#xff1a; 先分再归并&#xff0c;像是后序一般。 //归并排序 void MergeSort(int* arr, int left, int right) {int* temp (int…...

【kubernetes】kubernetes中的Controller

1 什么是Controller&#xff1f; kubernetes采用了声明式API&#xff0c;与声明式API相对应的是命令式API&#xff1a; 声明式API&#xff1a;用户只需要告诉期望达到的结果&#xff0c;系统自动去完成用户的期望命令式API&#xff1a;用户需要关注过程&#xff0c;通过命令一…...

RabbitMQ-死信队列

接上文 RabbitMQ-java使用消息队列 1 死信队列简介 死信队列模式实际上本质是一个死信交换机绑定的死信队列&#xff0c;当正常队列的消息被判定为死信时&#xff0c;会被发送到对应的死信交换机&#xff0c;然后再通过交换机发送到死信队列中&#xff0c;死信队列也有对应的消…...

ElasticSearch - 基于 DSL 、JavaRestClient 实现数据聚合

目录 一、数据聚合 1.1、基本概念 1.1.1、聚合分类 1.1.2、特点 1.2、DSL 实现 Bucket 聚合 1.2.1、Bucket 聚合基础语法 1.2.2、Bucket 聚合结果排序 1.2.3、Bucket 聚合限定范围 1.3、DSL 实现 Metrics 聚合 1.4、基于 JavaRestClient 实现聚合 1.4.1、组装请求 …...

什么是数学建模(mooc笔记)

什么是数学建模 前提&#xff1a;我们数学建模国赛计划选择C题&#xff0c;故希望老师的教学中侧重与C题相关性大的模型及其思想进行培训。之后的学习内容中希望涉及以下知识点&#xff1a; logistic回归相关知识点。如&#xff1a;用法、适用、限制范围等。精学数学建模中常…...

【网络】每天掌握一个Linux命令 - iftop

在Linux系统中&#xff0c;iftop是网络管理的得力助手&#xff0c;能实时监控网络流量、连接情况等&#xff0c;帮助排查网络异常。接下来从多方面详细介绍它。 目录 【网络】每天掌握一个Linux命令 - iftop工具概述安装方式核心功能基础用法进阶操作实战案例面试题场景生产场景…...

【Oracle APEX开发小技巧12】

有如下需求&#xff1a; 有一个问题反馈页面&#xff0c;要实现在apex页面展示能直观看到反馈时间超过7天未处理的数据&#xff0c;方便管理员及时处理反馈。 我的方法&#xff1a;直接将逻辑写在SQL中&#xff0c;这样可以直接在页面展示 完整代码&#xff1a; SELECTSF.FE…...

苍穹外卖--缓存菜品

1.问题说明 用户端小程序展示的菜品数据都是通过查询数据库获得&#xff0c;如果用户端访问量比较大&#xff0c;数据库访问压力随之增大 2.实现思路 通过Redis来缓存菜品数据&#xff0c;减少数据库查询操作。 缓存逻辑分析&#xff1a; ①每个分类下的菜品保持一份缓存数据…...

关于 WASM:1. WASM 基础原理

一、WASM 简介 1.1 WebAssembly 是什么&#xff1f; WebAssembly&#xff08;WASM&#xff09; 是一种能在现代浏览器中高效运行的二进制指令格式&#xff0c;它不是传统的编程语言&#xff0c;而是一种 低级字节码格式&#xff0c;可由高级语言&#xff08;如 C、C、Rust&am…...

Maven 概述、安装、配置、仓库、私服详解

目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...

Xen Server服务器释放磁盘空间

disk.sh #!/bin/bashcd /run/sr-mount/e54f0646-ae11-0457-b64f-eba4673b824c # 全部虚拟机物理磁盘文件存储 a$(ls -l | awk {print $NF} | cut -d. -f1) # 使用中的虚拟机物理磁盘文件 b$(xe vm-disk-list --multiple | grep uuid | awk {print $NF})printf "%s\n"…...

安宝特方案丨船舶智造的“AR+AI+作业标准化管理解决方案”(装配)

船舶制造装配管理现状&#xff1a;装配工作依赖人工经验&#xff0c;装配工人凭借长期实践积累的操作技巧完成零部件组装。企业通常制定了装配作业指导书&#xff0c;但在实际执行中&#xff0c;工人对指导书的理解和遵循程度参差不齐。 船舶装配过程中的挑战与需求 挑战 (1…...

CVE-2020-17519源码分析与漏洞复现(Flink 任意文件读取)

漏洞概览 漏洞名称&#xff1a;Apache Flink REST API 任意文件读取漏洞CVE编号&#xff1a;CVE-2020-17519CVSS评分&#xff1a;7.5影响版本&#xff1a;Apache Flink 1.11.0、1.11.1、1.11.2修复版本&#xff1a;≥ 1.11.3 或 ≥ 1.12.0漏洞类型&#xff1a;路径遍历&#x…...

基于Java+VUE+MariaDB实现(Web)仿小米商城

仿小米商城 环境安装 nodejs maven JDK11 运行 mvn clean install -DskipTestscd adminmvn spring-boot:runcd ../webmvn spring-boot:runcd ../xiaomi-store-admin-vuenpm installnpm run servecd ../xiaomi-store-vuenpm installnpm run serve 注意&#xff1a;运行前…...

BLEU评分:机器翻译质量评估的黄金标准

BLEU评分&#xff1a;机器翻译质量评估的黄金标准 1. 引言 在自然语言处理(NLP)领域&#xff0c;衡量一个机器翻译模型的性能至关重要。BLEU (Bilingual Evaluation Understudy) 作为一种自动化评估指标&#xff0c;自2002年由IBM的Kishore Papineni等人提出以来&#xff0c;…...