AST简介

在平时的开发中,经常会遇到对JavaScript代码进行检查或改动的工具,例如ESLint会检查代码中的语法错误;Prettier会修改代码的格式;打包工具会将不同文件中的代码打包在一起等等。这些工具都对JavaScript代码本身进行了解析和修改。这些工具是如何实现对代码本身的解析呢?这就要用到一种叫做AST抽象语法树的技术。

抽象语法树(Abstract Syntax Tree, 缩写为AST),是将代码抽象成一种树状结构,树上的每个节点表示代码中的一种语法,包括标识符,字面量,表达式,语句,模块等等。将代码形成抽象语法树AST之后,还可以通过这棵树还原成代码。在AST的形成过程中会丢弃注释和空白符等无意义的内容。

将代码抽象成AST需要两个步骤:1.词法分析,是扫描代码并将其中的内容标识为token数组。2.语法分析,是将词法分析的token数组转化为树状的形式,也就形成了抽象语法树。下面我们分别看一下每个步骤。

在这里插入图片描述

词法分析

词法分析是生成AST的第一个步骤,它将代码的内容转换为一个一个的标识token,忽略空白符号,识别标识符的类型,最终形成一个tokens数组。这里使用Esprima,一个ECMAScript解析器来生成tokens。本来想用babel,但是babel没有抛出独立生成tokens的方法。

const esprima = require("esprima");
const code = "const a = 2 + 1;";
const tokens = esprima.tokenize(code);
console.log(tokens);/* 生成的tokens
[{ type: 'Keyword', value: 'const' },{ type: 'Identifier', value: 'a' },{ type: 'Punctuator', value: '=' },{ type: 'Numeric', value: '2' },{ type: 'Punctuator', value: '+' },{ type: 'Numeric', value: '1' },{ type: 'Punctuator', value: ';' }
]
*/

在Node.js中执行这段代码,命令行中就打印出了生成的tokens,放在上面的注释中了。可以看到,代码中除了空白之外,全都被生成token了,value为实际的内容,type为这个内容的类型,其中Keyword为JavaScript的关键字,Numeric为数字,Punctuator是符号,Identifier指的是标识符,例如变量名等。

我们再看一个代码中包含多行与函数的场景,与上面的代码相比,只有code不一样,因此其它代码就省略了:

// 准备解析的代码
const fun = function (a) {const b = 2;return a + b;
}/* 生成的tokens
[{ type: 'Keyword', value: 'const' },{ type: 'Identifier', value: 'fun' },{ type: 'Punctuator', value: '=' },{ type: 'Keyword', value: 'function' },{ type: 'Punctuator', value: '(' },{ type: 'Identifier', value: 'a' },{ type: 'Punctuator', value: ')' },{ type: 'Punctuator', value: '{' },{ type: 'Keyword', value: 'const' },{ type: 'Identifier', value: 'b' },{ type: 'Punctuator', value: '=' },{ type: 'Numeric', value: '2' },{ type: 'Punctuator', value: ';' },{ type: 'Keyword', value: 'return' },{ type: 'Identifier', value: 'a' },{ type: 'Punctuator', value: '+' },{ type: 'Identifier', value: 'b' },{ type: 'Punctuator', value: ';' },{ type: 'Punctuator', value: '}' }
]
*/

这个例子复杂很多,通过结果可以看到函数虽然作为一个整体,但是被拆分成了多个token。而且这个例子中虽然有多行,但生成的tokens是没有换行标志的。事实上对于Esprima,可以配置保留注释和以及展示每个token在源码中的位置(从而可以判断是否有换行)。更多配置可以看Esprima文档。

const esprima = require("esprima");
// 准备解析的代码
const code = `// 打印输出
console.log(value);`;
const tokens = esprima.tokenize(code, { loc: true, comment: true });
console.log(JSON.stringify(tokens));/* 生成的tokens
[{type: "LineComment",value: " 打印输出",loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 7 } },},{type: "Identifier",value: "console",loc: { start: { line: 2, column: 0 }, end: { line: 2, column: 7 } },},{type: "Punctuator",value: ".",loc: { start: { line: 2, column: 7 }, end: { line: 2, column: 8 } },},{type: "Identifier",value: "log",loc: { start: { line: 2, column: 8 }, end: { line: 2, column: 11 } },},{type: "Punctuator",value: "(",loc: { start: { line: 2, column: 11 }, end: { line: 2, column: 12 } },},{type: "Identifier",value: "value",loc: { start: { line: 2, column: 12 }, end: { line: 2, column: 17 } },},{type: "Punctuator",value: ")",loc: { start: { line: 2, column: 17 }, end: { line: 2, column: 18 } },},{type: "Punctuator",value: ";",loc: { start: { line: 2, column: 18 }, end: { line: 2, column: 19 } },},
];
*/

可以看到结果中打印了每一个token的起始位置和最终位置,注释也保留了,使用LineComment类型。另外注意看console.log这个对象方法,在数组中被分解为了标识符+符号+标识符三个。另外这个被解析的代码我故意放了一个错误(value未定义就被使用值),但这里仅仅是识别token,代码是否可以被执行与生成tokens无关。

语法分析

词法分析结束之后,对生成tokens进一步分析,最后形成一个语法树,就是抽象语法树AST了。这里我们还是使用Esprima生成。(本想尝试利用词法分析结果来生成AST,但暂时没有找到相关的API,因此只能从代码直接生成AST了)。

const esprima = require("esprima");
// 准备生成的代码
const code = "const a = 2 + 1;";
const ast = esprima.parse(code);
console.log(JSON.stringify(ast));/* 生成的AST
{type: "Program",body: [{type: "VariableDeclaration",declarations: [{type: "VariableDeclarator",id: { type: "Identifier", name: "a" },init: {type: "BinaryExpression",operator: "+",left: { type: "Literal", value: 2, raw: "2" },right: { type: "Literal", value: 1, raw: "1" },},},],kind: "const",},],sourceType: "script",
}
*/

可以看到代码被解析成了一个JSON结构,这个JSON结构即描述了代码本身。我们从外向内试着分析一下这个结构:

  • Program 表示一个程序
  • VariableDeclaration 表示变量声明,kind表示了是const
  • VariableDeclarator 表示变量声明的标识符,是名称为a的Identifier标识符
  • BinaryExpression 二元运算符表达式,其中操作符是+号
  • Literal 加号的左右都是字面量,值为数字1和2

因此,一段代码就被分析成了这样一颗AST树。这里我们再看一个例子:

// 准备解析的代码
const fun = function (a) {const b = 2;return a + b;
}/* 生成的AST
{type: "Program",body: [{type: "VariableDeclaration",declarations: [{type: "VariableDeclarator",id: { type: "Identifier", name: "fun" },init: {type: "FunctionExpression",id: null,params: [{ type: "Identifier", name: "a" }],body: {type: "BlockStatement",body: [{type: "VariableDeclaration",declarations: [{type: "VariableDeclarator",id: { type: "Identifier", name: "b" },init: { type: "Literal", value: 2, raw: "2" },},],kind: "const",},{type: "ReturnStatement",argument: {type: "BinaryExpression",operator: "+",left: { type: "Identifier", name: "a" },right: { type: "Identifier", name: "b" },},},],},generator: false,expression: false,async: false,},},],kind: "const",},],sourceType: "script",
}
*/

这个例子增加了FunctionExpression,表示函数;BlockStatement表示块级语句;ReturnStatement表示return语句等。与前面词法分析将函数拆分成多个token不一样,这次函数是一个整体,且内部包含了params,BlockStatement和ReturnStatement等,确实是做到了对语法本身的分析。

与tokenize方法一样,语法分析的parse方法也有很多选项,例如解析注释,列出在代码中的位置等等,这里就不描述了。除了通过代码之外,还有AST explorer网站可以将代码在线转换为AST,并且可以在线标黄AST结点对应的代码。

抽象语法树规范ESTree

前面生成的AST树的结构中,我们看到了很多AST结点,这些节点有type表示结点类型,以及其他属性。Javascript有这么多语法规则,而且在不停的更新,如何知道这些语法规则的类型和属性定义呢?

ESTree就提供了Javascript语法到AST结点的属性定义的规则。ESTree并不是AST转换工具,它是一份文档,提供了ECMAScript标准中的语法到AST结点定义的规则。生成AST的工具,需要遵守这个规则,这样不同工具生成的抽象语法树就是通用的,我们也只需要写一套代码来解析即可。ESTree维护在GitHub,通过查看ESTree的目录结构和规则示例,可以看到不仅包含了每年的最新语法,甚至还包含了一些还在stage中的语法。

// 目录结构(部分)
├── experimental/
├── extensions/
├── stage3/
├── deprecated.md
├── es2015.md
├── es2016.md
├── es2017.md
├── es2018.md
├── es2019.md
├── es2020.md
├── es2021.md
├── es2022.md
├── es2025.md
├── es2026.md
├── es5.md
└── ...// 规则实例
interface Function <: Node {id: Identifier | null;params: [ Pattern ];body: FunctionBody;
}
A function declaration or expression.

ESTree最早是由Mozilla工程师在开发Javascript引擎SpiderMonkey时定义的,后来被大家接受并形成了事实的标准,现在由Babel,ESlint和Acorn维护。ESTree并不是一个强制规则,因此也有部分AST转换工具没有使用这个标准。下面的工具介绍中会提到这一点。

AST工具列表

首先列举一下目前社区中流行的AST生成工具,以及它们是否支持ESTree和输出Tokens。

工具名称支持ESTree支持输出Tokens简介
Esprima很早的AST工具,支持最新语法的进度较慢
Acorn流行的AST工具,支持插件
EspreeESLint(JS语法检查工具)的AST工具,最初基于Esprima开发,后来基于Acorn
@babel/parserBabel(JS编译器)的AST工具,fork于Acorn
UglifyJSUglifyJS(JS压缩工具)中提供的AST工具,独立格式不支持ESTree

其中Esprima是非常早的AST工具,也很好用,但由于ES6开始,ECMAScript标准中语法的数量增多速度变快,导致Esprima无法即使更新,因此出现了更多AST生成工具。其中Acorn是流行的工具之一,具有插件机制,现在很多JavaScript工具都采用了Acorn或者在它的基础上继续开发。下面我们简单列举下这些工具的代码使用方式示例:

/*code    被解析的代码tokens  词法分析结果ast     生成的抽象语法树
*/
const code = "const a = 2 + 1;";// --- Esprima ---
const esprima = require("esprima");
const tokens = esprima.tokenize(code);
const ast = esprima.parse(code);// --- Acorn ---
const acorn = require("acorn");
// it是个js遍历器
const it = acorn.tokenizer(code);
const tokens = [...it]const ast = acorn.parse(code, { ecmaVersion: "latest" });// --- Espree ---
const espree = require("espree");
const tokens = espree.tokenize(code);
const ast = espree.parse(code, { ecmaVersion: "latest" });// --- @babel/parser ---
const babelParser = require("@babel/parser");
const ast = babelParser.parse(code);// --- UglifyJS ---
const UglifyJS = require("uglify-js");
const ast = UglifyJS.minify(code, {parse: {}, compress: false, mangle: false,output: { ast: true, code: false }
});

通过代码示例可以看到,虽然API细节个别有区别,但使用方式是基本一致的。还有一些社区工具虽然有解析AST的能力,但却没有抛出API,比如Terser;还有一些工具是直接集成了上面的工具作为解析AST的方法,例如recast。这些工具我们就不在表格中列出了。

AST的应用Demo

AST在Javascript的工具中应用非常广泛:代码编译构建,混淆压缩,语法高亮,错误检查,格式修改,ES版本兼容等等,应用实在是太多了。可以说在前端工程化领域中绝大部分工具都需要AST的能力。但其中大部分工具都不仅只是生成语法树,它们还会对AST进行修改,最后再生成代码。我们再用一个流程图来表示:

在这里插入图片描述
从图中可以看到,大部分工具利用AST的方式是,先把代码生成AST,然后再对AST进行遍历和修改,生成一颗新的AST。最后将AST生成代码作为输出结果。这里我们尝试一个最简单的例子,其中遍历AST使用的是Estraverse,从AST生成代码使用的是Escodegen。

const esprima = require("esprima");
const estraverse = require("estraverse");
const escodegen = require("escodegen");const code = `
let fun = function (letter) {let b = 2;console.log(" let letter");return b + letter;
};
`;
// 生成AST
const ast = esprima.parse(code);
// 遍历
estraverse.traverse(ast, {enter: (node) => {if (node.type === "VariableDeclaration" && node.kind === 'let') {// 修改ASTnode.kind = "const";}},
});
// 生成新代码
const newCode = escodegen.generate(ast);
console.log(newCode);/* 生成结果
const fun = function (letter) {const b = 2;console.log(' let letter');return b + letter;
};
*/

上面代码的作用是,将输入代码中的let变量声明修改为const。这仅仅是个最简单的Demo,没有考虑变量存在修改不能使用const的场景。有些人可能对于AST的作用有疑惑,认为对代码为用字符串替换或者正则的方式也能实现。但当代码内容复杂时,例如变量名和字符串常量中都可能包含let,这时使用常规的正则方式难度是比较高的。

CST具体语法树

前面我们介绍的都是AST抽象语法树,它是忽略注释,分号等无意义内容之后的组成的一颗树。那么有没有一种语法树可以保留这些内容呢?有的,它就是具体语法树CST(Concrete Syntax Tree)。具体语法树是代码的完整表示,在代码高亮,代码格式化等方面非常有用。这里我们使用Tree-sitter工具,尝试对代码生成具体语法树。

const Parser = require('tree-sitter');
const JavaScript = require('tree-sitter-javascript');const parser = new Parser();
parser.setLanguage(JavaScript);
const sourceCode = 'let x = 2;';
const tree = parser.parse(sourceCode);console.log(tree.rootNode.toString());
console.log(tree.rootNode.children[0].toString());
console.log(tree.rootNode.children[0].children);
console.log(tree.rootNode.children[0].children[1].toString());
console.log(tree.rootNode.children[0].children[1]);/*  输出
(program (lexical_declaration (variable_declarator name: (identifier) value: (number))))
(lexical_declaration (variable_declarator name: (identifier) value: (number)))
[SyntaxNode {type: let,startPosition: {row: 0, column: 0},endPosition: {row: 0, column: 3},childCount: 0,},VariableDeclaratorNode {type: variable_declarator,startPosition: {row: 0, column: 4},endPosition: {row: 0, column: 9},childCount: 3,},SyntaxNode {type: ;,startPosition: {row: 0, column: 9},endPosition: {row: 0, column: 10},childCount: 0,}
]
(variable_declarator name: (identifier) value: (number))
VariableDeclaratorNode {type: variable_declarator,startPosition: {row: 0, column: 4},endPosition: {row: 0, column: 9},childCount: 3,
}
*/

Tree-sitter本身是用C语言编写的,但提供了JavaScript语言的npm包供使用。同时它也支持解析多种语言,不同的语言引入不同的解析器即可。Tree-sitter并不使用ESTree作为解析格式,而是使用S表达式,如我们输出的第一行tree.rootNode.toString()。S表达式(S-Expression)类似于前缀表达式,在Lisp语言中使用较多。这里举一个简单的例子:

  • 原表达式: 1 * (2 + 3)
  • S表达式: (* 1 (+ 2 3))

从例子中可以简单理解为,在中间的操作符需要提到最前面。然后我们再来看输出的第一行,其中每个节点的含义如下:

  • program 程序根节点
  • lexical_declaration 声明语句
  • variable_declarator 声明器
  • name(identifier) 变量名
  • value:(number) 数字值

然后将它们组合起来:

  • (program (lexical_declaration (variable_declarator name: (identifier) value: (number))))
  • (程序根节点 (声明语句 (声明器 变量名 数字值)))

详细含义和解析方法可以查看Tree-sitter文档。不过细心看虽然用了S表达式的形式,但这依然是一颗AST而不是CST,因为树中并没有分号等无意义符号。我们仔细看输出的子节点中,发现了分号的存在,这一类无意义结点被称作匿名节点,在S表达式中并不会展示,但是遍历子节点时,是可以遍历到的。另外Tree-sitter也能解析存在错误的语法:

const sourceCode = 'let x = 2qwe;';
/*
(program (lexical_declaration (variable_declarator name: (identifier) (ERROR (number)) value: (identifier))))
*/

在代码有错误的情况下,依然生成了S表达式,且表达式中可以看到ERROR结点。这种能力对语法错误提示等功能有帮助。

总结

这篇文章仅仅是对语法树(AST CST)做了一个简单的介绍,并没有对原理和细节做深入的了解。事实上,解析与生成代码是计算机学科中一个专门的领域,也是一门大学的课程,叫做编译原理。像是龙书《编译原理》中就有讲到词法分析,语法分析,语法树生成等等,其中的复杂程度远远超过了一篇博客的内容。

对于语法树的生成工具来说,AST和CST的界限也并不是完全区分的(一般默认是AST模式),很多工具就可以在语法树中保留注释等无意义内容,有些也可以解析包含错误的代码。具体用AST还是CST还是自定义的树,要看使用场景。

前端除了JavaScript之外,还有JSX代码,TS代码,CSS代码等等,都有工具可以做到解析语法树。

有一个前端非常常用的编译器,叫做babel,包含了很多编译相关的工具。关于babel相关的内容,会在后面单独文章介绍。同时也将会尝试关于AST更复杂一点的应用。

参考

  • AST 抽象语法树知识点
    https://mp.weixin.qq.com/s/KaIaCjRGC55UB6px15M1kw
  • CST vs AST 以及 biome 和 Oxc 各自的选择理由
    https://juejin.cn/post/7504168956594683943
  • Babel文档
    https://babeljs.io/
  • 前端工程化基石 – AST(抽象语法树)以及AST的广泛应用
    https://juejin.cn/post/7155151377013047304
  • Esprima 文档
    https://esprima.org/
  • 快来享受AST转换的乐趣
    https://zhuanlan.zhihu.com/p/617125984
  • ESTree Github
    https://github.com/estree/estree
  • AST explorer JavaScript代码在线转换为AST语法树
    https://astexplorer.net/
  • 【转译器原理 parser 篇】实现 js 新语法并编译到 css
    https://juejin.cn/post/6959502530745204772
  • JS AST 原理揭秘
    https://zhaomenghuan.js.org/blog/js-ast-principle-reveals.html
  • Acorn Github
    https://github.com/acornjs/acorn
  • Espree Github
    https://github.com/eslint/js/tree/main/packages/espree
  • UglifyJS Github
    https://github.com/mishoo/UglifyJS
  • UglifyJS — why not switching to SpiderMonkey AST
    https://lisperator.net/blog/uglifyjs-why-not-switching-to-spidermonkey-ast/
  • Terser Github
    https://github.com/terser/terser
  • Estraverse Github
    https://github.com/estools/estraverse
  • Escodegen Github
    https://github.com/estools/escodegen
  • Tree-sitter 文档
    https://tree-sitter.github.io/tree-sitter/
  • Node Tree-sitter 文档
    https://tree-sitter.github.io/node-tree-sitter/
  • 利用 Tree-sitter 进行语法树分析
    https://juejin.cn/post/7407278157449052186

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

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

相关文章

Java函数式编程之【基本数据类型流】

一、基本数据类型与基本数据的包装类 在Java编程语言中&#xff0c;int、long和double等基本数据类型都各有它们的包装类型Integer、Long和Double。 基本数据类型是Java程序语言内置的数据类型&#xff0c;可直接使用。 而包装类型则归属于普通的Java类&#xff0c;是对基本数据…

.NET Core部署服务器

1、以.NET Core5.0为例&#xff0c;在官网下载 下载 .NET 5.0 (Linux、macOS 和 Windows) | .NET 根据自己需求选择x64还是x86&#xff0c;记住关键下载完成还需要下载 Hosting Bundel &#xff0c;否则不成功 2、部署https将ssl证书放在服务器上&#xff0c;双击导入&#…

YOLO---04YOLOv3

YOLOV3 论文地址&#xff1a;&#xff1a;【https://arxiv.org/pdf/1804.02767】 YOLOV3 论文中文翻译地址&#xff1a;&#xff1a;【YOLO3论文中文版_yolo v3论文 中文版-CSDN博客】 YOLOv3 在实时性和精确性在当时都是做的比较好的&#xff0c;并在工业界得到了广泛应用 …

Qt知识点3『自定义属性的样式表失败问题』

问题1&#xff1a;自定义类中的自定义属性&#xff0c;如何通过样式表来赋值除了QT自带的属性&#xff0c;我们自定义的类中如果有自定义的静态属性&#xff0c;也可以支持样式表&#xff0c;如下 &#xff1a; Q_PROPERTY(QColor myBorderColor READ getMyBorderColor WRITE s…

RDQS_c和RDQS_t的作用及区别

&#x1f501; LPDDR5 中的 RDQS_t 和 RDQS_c — 复用机制详解 &#x1f4cc; 基本角色 引脚名 读操作&#xff08;READ&#xff09;作用 写操作&#xff08;WRITE&#xff09;作用&#xff08;当启用Link ECC&#xff09; RDQS_t Read DQS True&#xff1a;与 RDQS_c…

测试分类:详解各类测试方式与方法

前言&#xff1a;为什么要将测试进行分类呢&#xff1f;软件测试是软件生命周期中的⼀个重要环节&#xff0c;具有较高的复杂性&#xff0c;对于软件测试&#xff0c;可以从不同的角度加以分类&#xff0c;使开发者在软件开发过程中的不同层次、不同阶段对测试工作进行更好的执…

新手docker安装踩坑记录

最近在学习docker&#xff0c;安装和使用折腾了好久&#xff0c;在这里记录一下。下载# 依赖安装 sudo apt update sudo apt install -y \ca-certificates \curl \gnupg \lsb-release# 使用清华镜像源&#xff08;Ubuntu 24.04 noble&#xff09; echo \"deb [arch$(dpkg …

TOGAF指南1

1.TOGAF标准简介 TOGAF&#xff08;The Open Group Architecture Framework&#xff09;就像是一个企业架构的“操作手册”。它帮助企业设计、搭建和维护自己的“系统地图”&#xff0c;确保不同部门、技术、业务目标能像齿轮一样协调运转。 它的核心是&#xff1a; 用迭代的方…

[Linux入门] Linux 防火墙技术入门:从 iptables 到 nftables

目录 一、防火墙基础&#xff1a;netfilter 与 iptables 的关系 1️⃣什么是 netfilter&#xff1f; 2️⃣什么是 iptables&#xff1f; 二、iptables 核心&#xff1a;五链四表与规则体系 1️⃣什么是 “链”&#xff08;Chain&#xff09;&#xff1f; 2️⃣ 什么是 “…

函数fdopendir的用法

以下是关于 fdopendir 函数的详细解析&#xff0c;结合其核心功能、参数说明及典型应用场景&#xff1a;&#x1f50d; ‌一、函数功能与原型‌‌核心作用‌将已打开的目录文件描述符&#xff08;fd&#xff09;转换为目录流指针&#xff08;DIR*&#xff09;&#xff0c;用于后…

[源力觉醒 创作者计划]_文心4.5开源测评:国产大模型的技术突破与多维度能力解析

声明&#xff1a;文章为本人真实测评博客&#xff0c;非广告&#xff0c;并没有推广该平台 &#xff0c;为用户体验文章 一起来轻松玩转文心大模型吧&#x1f449; 文心大模型免费下载地址 一、引言&#xff1a;文心4.5开源——开启多模态大模型新时代 2025年6月30日&#x…

微信小程序无法构建npm,可能是如下几个原因

安装位置的问题&#xff0c;【npm安装在cd指定位置】小程序缓存的问题退出小程序&#xff0c;重新构建即可

从 MyBatis 到 MyBatis - Plus:@Options 注解的那些事儿

在 MyBatis 以及 MyBatis - Plus 的开发过程中&#xff0c;注解的使用是提升开发效率和实现特定功能的关键。今天我们就来聊聊 Options 注解&#xff0c;以及在 MyBatis - Plus 中它的使用场景和替代方案。 一、MyBatis 中的 Options 注解 在 MyBatis 框架中&#xff0c;Option…

转换图(State Transition Diagram)和时序图(Sequence Diagram)画图流程图工具

针对程序员绘制状态转换图&#xff08;State Transition Diagram&#xff09;和时序图&#xff08;Sequence Diagram&#xff09;的需求&#xff0c;以下是一些好用的工具推荐&#xff0c;涵盖在线工具、桌面软件和基于文本的工具&#xff0c;适合不同场景和偏好。这些工具在易…

基于php的在线酒店管理系统(源代码+文档+PPT+调试+讲解)

课题摘要在旅游住宿行业数字化转型的背景下&#xff0c;传统酒店管理存在房态更新滞后、预订渠道分散等问题。基于 PHP 的在线酒店管理系统&#xff0c;凭借其开发高效、兼容性强的特点&#xff0c;构建集客房管理、预订处理、客户服务于一体的综合性管理平台。 系统核心功能包…

视频质量检测中卡顿识别准确率↑32%:陌讯多模态评估框架实战解析

原创声明本文为原创技术解析&#xff0c;核心技术参数与架构设计引用自《陌讯技术白皮书》&#xff0c;禁止未经授权的转载与改编。一、行业痛点&#xff1a;视频质量检测的现实挑战在实时流媒体、在线教育、安防监控等领域&#xff0c;视频质量直接影响用户体验与业务可信度。…

流式输出阻塞原因及解决办法

流式输出不懂可看这篇文章&#xff1a;流式输出&#xff1a;概念、技巧与常见问题 正常情况&#xff0c;如下代码所示&#xff1a; async def event_generator():# 先输出数字1yield "data: 1\n\n"# 然后每隔2秒输出数字2&#xff0c;共输出10次for i in range(10):…

linux系统----Ansible中的playbook简单应用

目录 Playbooks中tasks语法使用 1、file 创建文件&#xff1a;touch 创建目录&#xff1a;directory 2、lineinfile 修改文件某一行文本 3、replace 根据正则表达式替换文件内容&#xff08;指定换字符串&#xff09; 5、template/copy 模板作用类似于copy&#xff0…

bmcweb工作流程

在openbmc中,bmcweb是一个web服务程序,类似于lighttpd,提供web服务。本文将简单介绍这个服务进程的执行流程。 bmcweb的入口函数main(). main() -> run() run()先注册routes,最后调用app.run(). 第一个注册的route为crow::webassets:requestRoutes(). crow::webasse…

伞状Meta分析重构癌症幸存者照护指南:从矛盾证据到精准决策

还记得你第一次做出Meta分析时的成就感吗&#xff1f;那种从海量文献中抽丝剥茧&#xff0c;最终得出可靠结论的感觉&#xff0c;简直不要太爽&#xff01;但是&#xff0c;时代在进步&#xff0c;科研在卷动&#xff0c;Meta分析也有它的"升级版"——伞状Meta分析&a…