揭秘如何用Monaco Editor打造功能强大的日志查看器

news/2025/1/5 16:57:10/文章来源:https://www.cnblogs.com/DTinsight/p/18650013

Monaco Editor 是一个基于浏览器的代码编辑器,由 Microsoft 开发,是 Visual Studio Code 的核心编辑器组件。为用户提供了一个功能丰富、性能优异的代码编辑环境,常用于 web 应用。
下面本文将从 Monaco Editor 的使用方法、使用逻辑作为切入点,讲述在网页中如何通过 Monaco Editor 实现日志查看器,包括实时日志和普通日志的展示、如何在 Monaco Editor 中支持a元素功能的相关内容。从理论到实践,涵盖了从基础概念到具体实现的全过程。

01 前言

在 Web IDE 中,控制台中展示日志是至关重要的功能。Monaco Editor 作为一个强大的代码编辑器,提供了丰富的功能和灵活的 API ,支持为内容进行“装饰”,非常适合用来构建日志展示器。如下图:
file
除了实时日志外,还有一些需要查看历史日志的场景。如下图:
file

02 什么是 Monarch

Monarch 是 Monaco Editor 自带的一个语法高亮库,通过它,我们可以用类似 JSON 的语法来实现自定义语言的语法高亮功能。这里不做过多的介绍,只介绍在本文中使用到的那部分内容。

一个语言定义基本上就是描述语言的各种属性的JSON值,部分通用属性如下:

  • tokenizer
    (必填项,带状态的对象)这个定义了tokenization的规则。Monaco Editor 中用于定义语言语法高亮和解析的一个核心组件。它的主要功能是将输入的代码文本分解成一个个的 token,以便于编辑器能够根据这些 token 进行语法高亮、错误检查和其他编辑功能。
  • ignoreCase
    (可选项=false,布尔值)语言是否大小写不敏感?tokenizer(分词器)中的正则表达式使用这个属性去进行大小写(不)敏感匹配,以及case场景中的测试。
  • brackets
    (可选项,括号定义的数组)tokenizer使用这个来轻松的定义大括号匹配,更多信息详见 @brackets和 bracket部分。每个方括号定义都是一个由3个元素或对象组成的数组,描述了open左大括号、close右大括号和token令牌类。默认定义如下:
[ ['{','}','delimiter.curly'],
['[',']','delimiter.square'],
['(',')','delimiter.parenthesis'],
['<','>','delimiter.angle'] ]

1. tokenizer

tokenizer 属性描述了如何进行词法分析,以及如何将输入转换成 token ,每个 token 都会被赋予一个 css 类名,用于在编辑器中渲染,内置的 css token 包括:

identifier         entity           constructor
operators          tag              namespace
keyword            info-token       type
string             warn-token       predefined
string.escape      error-token      invalid
comment            debug-token
comment.doc        regexp
constant           attribute
delimiter .[curly,square,parenthesis,angle,array,bracket]
number    .[hex,octal,binary,float]
variable  .[name,value]
meta      .[content]

当然也可以自定义 css token,通过以下方式将自定义的 css token 注入。

editor.defineTheme("vs", {
base: "vs",inherit: true,rules: [{token: "token-name",foreground: "#117700",}],
colors: {},
});

一个 tokenizer 由一个描述状态的对象组成。tokenizer 的初始状态由 tokenizer 定义的第一个状态决定。这句话什么意思呢?查看下方例子,root就是 tokenizer 定义的第一个状态,就是初始状态。同理,如果把 afterIf和 root两个状态调换位置,那么 afterIf就是初始状态。

monaco.languages.setMonarchTokensProvider('myLanguage', {
tokenizer: {
root: [
// 初始状态的规则[/\d+/, 'number'], // 识别数字[/\w+/, 'keyword'], // 识别关键字
// 转移到下一个状态[/^if$/, { token: 'keyword', next: 'afterIf' }],],
afterIf: [
// 处理 if 语句后的内容[/\s+/, ''], // 忽略空白[/[\w]+/, 'identifier'], // 识别标识符
// 返回初始状态[/;$/, { token: '', next: 'root' }],]}
});

如何获取 tokenizer 定义的第一个状态呢?

class MonarchTokenizer {...public getInitialState(): languages.IState {const rootState = MonarchStackElementFactory.create(null, this._lexer.start!);return MonarchLineStateFactory.create(rootState, null);}...
}

通过 getInitialState 获取初始的一个状态,通过代码可以看到 确认哪个是初始状态是通过 this._lexer.start 这个属性。这个属性又是怎么被赋值的呢?

function compile() {...for (const key in json.tokenizer) {if (json.tokenizer.hasOwnProperty(key)) {if (!lexer.start) {lexer.start = key;}const rules = json.tokenizer[key];lexer.tokenizer[key] = new Array();addRules('tokenizer.' + key, lexer.tokenizer[key], rules);}}...
}

在 compile 解析 setMonarchTokensProvider 传入的语言定义对象时,会将读取出来的第一个 key 作为初始状态。可能会有疑问,就一定能保证在定义对象时,写入的第一个属性,在读取时一定第一个被读出吗?
在 JavaScript 中,对象属性的顺序有一些特定的规则:

  • 整数键:如果属性名是一个整数(如 "1"、"2"等),这些属性会按照数值的升序排列。
  • 字符串键:对于非整数的字符串键,属性的顺序是按照它们被添加到对象中的顺序。
  • Symbol 键:如果属性的键是 Symbol 类型,这些属性会按照它们被添加到对象中的顺序。
    因此,当使用' for...in'循环遍历对象的属性时,属性的顺序如下:
  • 首先是所有整数键,按升序排列。
  • 然后是所有字符串键,按添加顺序排列。
  • 最后是所有 Symbol 键,按添加顺序排列。
    看个例子:
    file

上述例子可以看出,“1”、“2”虽然被写在了后面,但仍然会被排序优先输出,其后才是字符串键根据添加顺序输出。所以,尽可能不要使用整数键去定义状态名。
当 tokenizer 处于某种状态时,只有那个状态的规则才能匹配。所有规则是按顺序进行匹配的,当匹配到第一个规则时,它的 action 将被用来确定 token 的类型。不会再使用后面的规则进行尝试,因此,以一种最有效的方式排列规则是很重要的。比如空格和标识符优先。
如何定义一个状态?
每个状态定义为一个用于匹配输入的规则数组,规则可以有如下形式:

  • [regex, action]
    {regex: regex, action: action}形式的简写。
    [regex, action, next]
  • { regex: regex, action: action{ next: next} }形式的简写。
monaco.languages.setMonarchTokensProvider('myLanguage', {tokenizer: {root: [// [regex, action][/\d+/, 'number'], /** * [regex, action, next]* [/\w+/, { token: 'keyword', next: '@pop' }] 的简写*/[/\w+/, 'keyword', '@pop'], ]}
});

regex 是正则表达式,action 分为以下几种:

  • string
    { token: string } 的简写

  • [action, ..., actionN]
    多个 action 组成的数组。这仅在正则表达式恰好由 N 个组(即括号部分)组成时才允许。举个例子:
    '[/(\d)(\d)(\d)/, ['string', 'string', 'string']'

  • { token: tokenClass }
    这个 tokenClass 可以是内置的 css token,也可以是自定义的 token。同时,还规定了一些特殊的 token 类:

"@rematch"
备份输入并重新调用 tokenizer 。这只在状态发生变化时才有效(或者我们进入了无限的递归),所以这个通常和 next 属性一起使用。例如,当你处于特定的 tokenizer 状态,并想要在看到某些结束标记时退出,但是不想在处于该状态时使用它们,就可以使用这个。例如:

monaco.languages.setMonarchTokensProvider('myLanguage', {tokenizer: {root: [[/\d+/, 'number', 'word'],],word: [[/\d/, '@rematch', '@pop'],[/[^\d]+/, 'string']]}
});

这个 language 的状态流转图是怎么样的呢?

file

可以看出,在定义一个状态时,应保证状态存在出口即没有定义转移到其他状态的规则),否则可能会导致死循环,不断的使用状态内的规则去匹配。
"@pop"
弹出 tokenizer 栈以返回到之前的状态。
"@push"推入当前状态,并在当前状态中继续。

monaco.languages.setMonarchTokensProvider('myLanguage', {tokenizer: {root: [// 当匹配到开始标记时,推送新的状态[/^\s*function\b/, { token: 'keyword', next: '@function' }],],function: [// 在 function 状态下的匹配规则[/^\s*{/, { token: 'delimiter.bracket', next: '@push' }],[/[^}]+/, 'statement'],[/^\s*}/, { token: 'delimiter.bracket', next: '@pop' }],],}
});

$n
匹配输入的第n组,或者是$0代表这个匹配的输入。
$Sn状态的第 n 个部分,比如,状态 @tag.foo,用 $S0 代表整个状态名(即 tag.foo ),$S1 返回 tag,$S2 返回 foo 。

03 Monaco Editor 让日志实现不同主题

由于Monaco Editor 的使用不是本文的重点,后续就不再展开。接下来我们主要为大家介绍如何利用 Monaco Editor 实现日志查看器让不同的类型的日志有不同的高亮主题。

1、实时日志

实时日志中,存在不同的日志类型,如:info、error、warning等。

/*** 日志构造器* @param {string} log 日志内容* @param {string} type 日志类型*/
export function createLog(log: string, type = '') {let now = moment().format('HH:mm:ss');if (process.env.NODE_ENV == 'test') {now = 'test';}return `[${now}] <${type}> ${log}`;
}

根据日志方法可以看出,每条日志都是[xx:xx:xx]开头,紧跟着 <日志类型>,后面的是日志内容。(日志类型:info 、success、error、warning。)
注册一个自定义语言realTimeLog作为实时日志的一个language
这里规则也很简单,在 root 中设置了两条识别规则,分别是匹配日志日期和日志类型。在匹配到对应的日志类型后,给匹配到的内容打上token,然后通过next携带匹配的引用标识( $1 表示正则分组中的第1组)进入下一个状态 consoleLog,在状态consoleLog中,匹配日志内容,并打上token,直到遇见终止条件(日志日期)。

import { languages } from "monaco-editor/esm/vs/editor/editor.api";
import { LanguageIdEnum } from "./constants";languages.register({ id: LanguageIdEnum.REALTIMELOG });languages.setMonarchTokensProvider(LanguageIdEnum.REALTIMELOG, {keywords: ["error", "warning", "info", "success"],date: /\[[0-9]{2}:[0-9]{2}:[0-9]{2}\]/,tokenizer: {root: [[/@date/, "date-token"],[/<(\w+)>/,{cases: {"$1@keywords": { token: "$1-token", next: "@log.$1" },"@default": "string",},},],],log: [[/@date/, { token: "@rematch", next: "@pop" }],[/.*/, { token: "$S2-token" }],],},
});// ===== 日志样式 =====
export const realTimeLogTokenThemeRules = [{token: "date-token",foreground: "#117700",},{token: "error-token",foreground: "#ff0000",fontStyle: "bold",},{token: "info-token",foreground: "#999977",},{token: "warning-token",foreground: "#aa5500",},{token: "success-token",foreground: "#669600",},
];

状态流转图:
file

2、普通日志

普通日志与实时日志有些许不同,他的日志类型是不展示出来的,没有一个起始/结束标识符供Monarch高亮规则匹配。所以需要一个在文本中不展示,又能作为起始/结束的标识符。

也确实存在这么一个东西,不占宽度,又能被匹配——“零宽字符”。
零宽字符(Zero Width Characters)是指在文本中占用零宽度的字符,通常用于特定的文本处理或编码目的。它们在视觉上不可见,但在程序处理中可能会产生影响。
利用零宽字符创建不同日志类型的标识。

// 使用零宽字符作为不同类型的日志标识
// U+200B
const ZeroWidthSpace = '';
// U+200C
const ZeroWidthNonJoiner = '‌';
// U+200D
const ZeroWidthJoiner = '';
// 不同类型日志的起始 / 结束标识,用于 Monarch 语法文件的解析
const jobTag = {info: `${ZeroWidthSpace}${ZeroWidthNonJoiner}${ZeroWidthSpace}`,warning: `${ZeroWidthNonJoiner}${ZeroWidthSpace}${ZeroWidthNonJoiner}`,error: `${ZeroWidthJoiner}${ZeroWidthNonJoiner}${ZeroWidthJoiner}`,success: `${ZeroWidthSpace}${ZeroWidthNonJoiner}${ZeroWidthJoiner}`,
};

之后的编写语法高亮规则,与任务控制台相同。

import { languages } from "monaco-editor/esm/vs/editor/editor.api";
import { LanguageIdEnum } from "./constants";languages.register({ id: LanguageIdEnum.NORMALLOG });languages.setMonarchTokensProvider(LanguageIdEnum.NORMALLOG, {info: /\u200b\u200c\u200b/,warning: /\u200c\u200b\u200c/,error: /\u200d\u200c\u200d/,success: /\u200b\u200c\u200d/,tokenizer: {root: [[/@success/, { token: "success-token", next: "@log.success" }],[/@error/, { token: "error-token", next: "@log.error" }],[/@warning/, { token: "warning-token", next: "@log.warning" }],[/@info/, { token: "info-token", next: "@log.info" }],],log: [[/@info|@warning|@error|@success/,{ token: "$S2-token", next: "@pop" },],[/.*/, { token: "$S2-token" }],],},
});// ===== 日志样式 =====
export const normalLogTokenThemeRules = [{token: "error-token",foreground: "#BB0606",fontStyle: "bold",},{token: "info-token",foreground: "#333333",fontStyle: "bold",},{token: "warning-token",foreground: "#EE9900",},{token: "success-token",foreground: "#669600",},
];

状态流转图:

file

04 在 Monaco Editor 中支持a元素

Monaco Editor 本身是不支持在内容中插入 HTML 元素的,原生只支持对链接进行高亮,并且支持cmd + 点击打开链接。但仍可能会存在需要实现类似a元素的效果。
另辟蹊径,查找 Monaco Editor 的 API 后,linkProvider 也许可以大致满足,但仍有不足。
以下是介绍:
在 Monaco Editor 中,linkProvider 是一个用于提供链接功能的接口。它允许开发者为编辑器中的特定文本或代码片段提供链接,当用户悬停或点击这些链接时,可以执行特定的操作,比如打开文档、跳转到定义等。
具体用法:

const linkProvider = {provideLinks: function(model, position) {// 返回链接数组return [{range: new monaco.Range(1, 1, 1, 5), // 链接的范围url: 'https://example.com', // 链接的 URLtooltip: '点击访问示例' // 悬停提示}];}
};
monaco.languages.registerLinkProvider('javascript', linkProvider);

它是针对已注册的语言进行注册,不会影响到其他语言。在文本内容发生变化时就会触发provideLinks
根据这个 API 想到一个思路:

  • 在生成文本时,在需要展示为 a 元素的地方使用#link#${JSON.stringify(attrs)}#link#包裹,attrs 是一个对象,其中包含了 a 元素的attribute。
    *在文本内容传递给 Monaco Editor 之前,解析文本的内容,利用正则将a 元素标记匹配出来,使用attrs的链接文本替换标记文本,并记录替换后链接文本在文本内容中的索引位置。利用 Monaco Editor 的 getPositionAt获取链接文本在编辑器中的位置(起始/结束行列信息),生成Range。
    *使用一个容器收集对应的日志中的Link信息。在通过linkProvider将编辑器中对应的链接文本识别为链接高亮。
    *给 editor 实例绑定点击事件onMouseDown,如果点击的内容位置在收集的 Link 中时,触发对外提供的自定义链接点击函数。
    根据这一思路进行实现:
    *生成 a 元素标记。
interface IAttrs {attrs: Record<string, string>;props: {innerHTML: string;};
}
/**** @param attrs* @returns*/
export function createLinkMark(attrs: IAttrs) {return `#link#${JSON.stringify(attrs)}#link#`;
}
  • 解析文本内容
getLinkMark(value: string, key?: string) {if (!value) return value;const links: ILink[] = [];const logRegexp = /#link#/g;const splitPoints: any[] = [];let indexObj = logRegexp.exec(value);/*** 1. 正则匹配相应的起始 / 结束标签 #link# , 两两为一组*/while (indexObj) {splitPoints.push({index: indexObj.index,0: indexObj[0],1: indexObj[1],});indexObj = logRegexp.exec(value);}/*** 2. 根据步骤 1 获取的 link 标记范围,处理日志内容,并收集 link 信息*//** l为起始标签,r为结束标签 */let l = splitPoints.shift();let r = splitPoints.shift();/** 字符串替换中移除字符个数 */let cutLength = 0;let processedString = value;/** link 信息集合 */const collections:[number, number, string, ILink['attrs']][] = [];while (l && r) {const infoStr = value.slice(l.index + r[0].length, r.index);const info = JSON.parse(infoStr);/*** 手动补一个空格是由于后面没有内容,导致点击链接后面的空白处,光标也是在链接上的,* 导致当前的range也在link的range中,触发自定义点击事件*/const splitStr = info.props.innerHTML + ' ';/** 将 '#link#{"attrs":{"href":"xxx"},"props":{"innerHTML":"logDownload"}}#link#' 替换为 innerHTML 中的文本 */processedString =processedString.slice(0, l.index - cutLength) +splitStr +processedString.slice(r.index + r[0].length - cutLength);collections.push([/** 链接的开始位置 */l.index - cutLength,/** 链接的结束位置 */l.index + splitStr.length - cutLength - 1,/** 链接地址 */info.attrs.href,/** 工作流中应用,点击打开子任务tab */info.attrs,]);/** 记录文本替换过程中,替换文本和原文本的差值 */cutLength += infoStr.length - splitStr.length + r[0].length * 2;l = splitPoints.shift();r = splitPoints.shift();}/*** 3. 处理收集的 link 信息*/const model = editor.createModel(processedString, 'xxx');for (const [start, end, url, attrs] of collections) {const startPosition = model.getPositionAt(start);const endPosition = model.getPositionAt(end);links.push({range: new Range(startPosition.lineNumber,startPosition.column,endPosition.lineNumber,endPosition.column),url,attrs,});}model.dispose();return processedString;
}
  • 使用一个容器存储解析出来的 link
const value = `这是一串带链接的文本:${createLinkMark({props: {innerHTML: '链接a'},attrs: {href: 'http://www.abc.com'}
})}`
const links = getLinkMark(value)
  • 利用存储的 links 注册 LinkProvider
languages.registerLinkProvider('taskLog', {provideLinks() {return { links: links || [] };},
});
  • 绑定自定义事件

在点击 editor 中的内容时都会触发onMouseDown,在其中可以获取当前点击位置的Range信息,循环遍历收集的所有 Link,判断当前点击位置的Range是否在其中。containsRange方法可以判断一个Range是否在另一个Range中。

useEffect(() => {const disposable = logEditorInstance.current?.onMouseDown((e) => {const curRange = e.target.range;if (curRange) {const link = links.find((e) => {return (e.range as Range)?.containsRange(curRange);});if (link) {onLinkClick?.(link);}}});return () => {disposable?.dispose();};
}, [logEditorInstance.current]);

缺点:在日志实时打印时,出现链接不会立马高亮,需要等一会才会变成链接高亮。
file

欢迎大家关注“数栈研习社”公众号,我们会持续更新技术开发的相关学习内容,希望能与大家多多沟通交流。对袋鼠云产品和技术感兴趣的朋友,可以点击「阅读原文」了解更多企业的数字化与智能化内容。

《数据资产管理白皮书》下载地址:https://www.dtstack.com/resources/1073/?src=szsm

《行业指标体系白皮书》下载地址:https://www.dtstack.com/resources/1057/?src=szsm

《数据治理行业实践白皮书》下载地址:https://www.dtstack.com/resources/1001/?src=szsm

《数栈V6.0产品白皮书》下载地址:https://www.dtstack.com/resources/1004/?src=szsm

想了解或咨询更多有关袋鼠云大数据产品、行业解决方案、客户案例的朋友,浏览袋鼠云官网:https://www.dtstack.com/?src=szsm

同时,欢迎对大数据开源项目有兴趣的同学加入「袋鼠云开源框架钉钉技术群」,交流最新开源技术信息,群号码:30537511,项目地址:https://github.com/DTStack
《行业指标体系白皮书》下载地址:https://www.dtstack.com/resources/1057?src=szsm

《数栈产品白皮书》下载地址:https://www.dtstack.com/resources/1004?src=szsm

《数据治理行业实践白皮书》下载地址:https://www.dtstack.com/resources/1001?src=szsm

想了解或咨询更多有关大数据产品、行业解决方案、客户案例的朋友,浏览袋鼠云官网:https://www.dtstack.com/?src=szbky

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【新兴产业】人形机器人

产业链 人形机器人产业链上游为原材料、零部件以及软件平台,核心零部件包括伺服系统、执行器、减速器、控制系统、驱动器等,成本占比最高、技术难度大,具备较高的壁垒。 产业链中游为人形机器人的本体设计、制造及系统集成。 产业链下游为场景应用,目前较有潜力的人形机器人…

PVE 备份快照

PVE 备份快照 - 知乎 抄一个备份 vmware压缩整个虚拟机文件夹就可以连带快照一起打包。 PVE 自带的vzdump备份并不包含快照信息,备份之后再还原,会丢失快照信息。 备份时提示:snapshots found (not included into backup) 快照信息在循序渐进的开发模式中十分重要,遇到错误…

借助Atrribute扩展UnityEdior

借助Atrribute扩展UnityEdiorC# Attribute 简介 Attribute 是 C# 提供的一种强大的元数据机制,可以用来为代码的程序元素(如类、方法、属性等)附加额外的信息。这些附加信息可以在运行时通过反射机制读取,从而影响程序的行为。Attribute 的特性轻量级 Attribute 不会直接影…

C#实现LALR(1)解析器的生成器

Yet Another Compiler 参考lex和yacc的输入格式,参考虎书《现代编译原理-C语言描述》的算法,大力整合优化,实现了LALR(1)的C#生成器(暂命名为bitParser)。 词法分析器根据DFA和最小化DFA分别生成词法分析器代码(状态转换表、保留字、Token类型等)支持全Unicode字符。支持…

【新兴产业】量子信息

产业链 量子信息主要包括量子计算、量子通信和量子测量三大领域,在提升计算困难问题运算处理能力、加强信息安全保护能力、提高传感测量精度等方面,具备超越经典信息技术的潜力。 量子信息产业链从上游到下游主要包含基础光电元器件、量子通信 核心元器件、量子通信传输干线、…

【新兴产业】元宇宙

产业链 •顶层设计:政府从宏观角度为元宇宙的发展做出顶层设计,尤其在金融交易体系、社会治理制度方面给出明确定调和指明方向。 • 市场培育:地方政府因地制宜制定符合当地产业特色的元宇宙发展规划,并出台产业扶持政策、落地措施,制定未来执行计划。 • 产业联盟:建立与…

回首2024,展望2025,新年新“鲸”象~

回首 2024 年,数字孪生领域蓬勃发展,技术创新层出不穷,应用场景不断拓展。在这充满机遇与挑战的一年里,山海鲸可视化凭借国产自研的零代码数字孪生平台,为众多企业和政府机构提供了一站式的数字化解决方案,助力各行各业在数字化转型的道路上稳步前行。 1. 回首2024年 (一…

CICD Day2、基于jenkins Gitlab 的CICD web-demo配置

1、 代码提交 1.1 在服务器上创建一个web-demo的目录,用户web前端项目的开发目录 mkdir web-demo # 在web-demo目录下 编辑一个index.html的文件 cat index.html <!DOCTYPE html> <html> <head><meta charset="utf-8"><title>实例<…

CPU-Z处理器检测工具 v2.13.0中文绿色单文件

点击上方蓝字关注我 前言 CPU-Z是一个非常厉害的CPU检测小帮手。它能识别很多种类的CPU,而且打开和检测的速度都很快。这个工具能清楚地告诉我们关于CPU、主板、内存、显卡等硬件的详细信息,比如是哪个厂家生产的、处理器的名字、是怎么做出来的、封装技术怎么样,还有它们的…

uniapp(Hbuilderx)

目录微信开发者工具下载新建项目运行 微信开发者工具下载新建项目注意点 如果要打包安卓,需要勾上:运行微信小程序 1: 找到.exe路径,粘贴进去2:打开微信小程序“服务端口”

商品个性化推荐

商品个性化推荐 任务要求:二.原理讲解: 本项目的基于 用户的购买历史 和 商品之间的相似性 进行商品推荐。它主要包括两个核心部分:用户-商品矩阵 和 商品相似度计算。下面我会结合算法的原理和实际示例为你讲解这个推荐系统的工作原理。 2.1用户-商品矩阵(User-Product M…

探索新路径:金融行业如何借助内部知识库实现智能转型

引言 在数字化转型的浪潮中,金融行业正经历着前所未有的变革。随着大数据、人工智能等技术的飞速发展,金融机构不仅需要提升服务效率,更要优化内部管理,以创新驱动业务增长。内部知识库作为信息管理与知识分享的核心平台,正逐渐成为金融行业智能转型的关键驱动力。本文将探…