Vite: Bundler实现JavaScript的AST解析器—词法分析、语义分析

news2024/11/18 16:53:22

概述

基于前文,我们写了一个迷你版的 no-bundle 开发服务,也就是 Vite 开发阶段的 Dev Server,而在生产环境下面,处于页面性能的考虑,Vite 还是选择进行打包(bundle),并且在底层使用 Rollup 来完成打包的过程。接下来,我们实现一个 JavaScript Bundler,理解生产环境下 Vite/Rollup 的模块打包究竟是如何实现的。

不过,需要提前声明的是,Bundler 的实现非常依赖于 AST 的实现,有相当多的地方需要解析模块 AST 并且操作 AST 节点,因此,我们有必要先完成 AST 解析的方案。目前在业界有诸多的 JavaScript AST 解析方案,如 acorn 、 @babel/parser 、 swc 等,可以实现开箱即用,为了深入理解原理,我们来开发 AST 的解析器,实现 tokenize 和 parse 的底层逻辑,而这本身也是一件非常有意思的事情

搭建开发测试环境

首先通过 $ pnpm init -y 新建项目,安装测试工具 vitest :

$ pnpm i vitest -D

新建 src/test 目录,之后所有的测试代码都会放到这个目录中。我们不妨先尝试编写一个测试文件:

// src/__test__/example.test.ts
import { describe, test, expect } from "vitest";
describe("example test", () => {
 test("should return correct result", () => {
   expect(2 + 2).toBe(4);
 });
});

然后在 package.json 中增加如下的 scripts : "test": "vitest"

接着在命令行执行 pnpm test ,如果你可以看到如下的终端界面,说明测试环境已经搭建成功:

在这里插入图片描述

词法分析器开发

接下来,我们正式进入 AST 解析器的开发,主要分为两个部分来进行: 词法分析器 和 语法分析器 。

首先是 词法分析器 ,也叫分词器(Tokenizer),它的作用是将代码划分为一个个词法单元,便于进行后续的语法分析。比如下面的这段代码

let foo = function() {}

在经过分词之后,代码会被切分为如下的 token 数组:

['let', 'foo', '=', 'function', '(', ')', '{', '}']

从中你可以看到,原本一行普通的代码字符串被拆分成了拥有语法属性的 token 列表,
不同的 token 之间也存在千丝万缕的联系,而后面所要介绍的 语法分析器 ,就是来梳理
各个 token 之间的联系,整理出 AST 数据结构。

当下我们所要实现的词法分析器,本质上是对代码字符串进行逐个字符的扫描,然后根据
一定的语法规则进行分组。其中,涉及到几个关键的步骤:

  • 确定语法规则,包括语言内置的关键词、单字符、分隔符等
  • 逐个代码字符扫描,根据语法规则进行 token 分组

接下来我们以一个简单的语法为例,来初步实现如上的关键流程。需要解析的示例代码如下:

let foo = function() {}

1 ) 确定语法规则

新建 src/Tokenizer.ts ,首先声明一些必要的类型:

export enum TokenType {
  // let
  Let = "Let",
  // =
  Assign = "Assign",
  // function
  Function = "Function",
  // 变量名
  Identifier = "Identifier",
  // (
  LeftParen = "LeftParen",
  // )
  RightParen = "RightParen",
  // {
  LeftCurly = "LeftCurly",
  // }
  RightCurly = "RightCurly",
}

export type Token = {
  type: TokenType;
  value?: string;
  start: number;
  end: number;
  raw?: string;
};

然后定义 Token 的生成器对象:

const TOKENS_GENERATOR: Record < string, (...args: any[]) => Token > = {
  let (start: number) {
    return {
      type: TokenType.Let,
      value: "let",
      start,
      end: start + 3
    };
  },
  assign(start: number) {
    return {
      type: TokenType.Assign,
      value: "=",
      start,
      end: start + 1
    };
  },
  function(start: number) {
    return {
      type: TokenType.Function,
      value: "function",
      start,
      end: start + 8,
    };
  },
  leftParen(start: number) {
    return {
      type: TokenType.LeftParen,
      value: "(",
      start,
      end: start + 1
    };
  },
  rightParen(start: number) {
    return {
      type: TokenType.RightParen,
      value: ")",
      start,
      end: start + 1
    };
  },
  leftCurly(start: number) {
    return {
      type: TokenType.LeftCurly,
      value: "{",
      start,
      end: start + 1
    };
  },
  rightCurly(start: number) {
    return {
      type: TokenType.RightCurly,
      value: "}",
      start,
      end: start + 1
    };
  },
  identifier(start: number, value: string) {
    return {
      type: TokenType.Identifier,
      value,
      start,
      end: start + value.length,
    };
  },
}
type SingleCharTokens = "(" | ")" | "{" | "}" | "=";
// 单字符到 Token 生成器的映射
const KNOWN_SINGLE_CHAR_TOKENS = new Map <
  SingleCharTokens,
  typeof TOKENS_GENERATOR[keyof typeof TOKENS_GENERATOR] >
  ([
    ["(", TOKENS_GENERATOR.leftParen],
    [")", TOKENS_GENERATOR.rightParen],
    ["{", TOKENS_GENERATOR.leftCurly],
    ["}", TOKENS_GENERATOR.rightCurly],
    ["=", TOKENS_GENERATOR.assign],
  ]);

2 ) 代码字符扫描、分组

现在我们开始实现 Tokenizer 对象

export class Tokenizer {
  private _tokens: Token[] = [];
  private _currentIndex: number = 0;
  private _source: string;
  constructor(input: string) {
    this._source = input;
  }
  tokenize(): Token[] {
    while (this._currentIndex < this._source.length) {
      let currentChar = this._source[this._currentIndex];
      const startIndex = this._currentIndex;

      // 根据语法规则进行 token 分组
    }
    return this._tokens;
  }
}

在扫描字符的过程,我们需要对不同的字符各自进行不同的处理,具体的策略如下:

  • 当前字符为分隔符,如 空格 ,直接跳过,不处理;
  • 当前字符为字母,需要继续扫描,获取完整的单词:
    • 如果单词为语法关键字,则新建相应关键字的 Token
    • 否则视为普通的变量名
  • 当前字符为单字符,如 { 、 } 、 ( 、 ) ,则新建单字符对应的 Token

接着我们在代码中实现:

// while 循环内部
let currentChar = this._source[this._currentIndex];
const startIndex = this._currentIndex;
const isAlpha = (char: string): boolean => {
  return (char >= "a" && char <= "z") || (char >= "A" && char <= "Z");
}

// 1. 处理空格
if (currentChar === ' ') {
  this._currentIndex++;
  continue;
}

// 2. 处理字母
else if (isAlpha(currentChar)) {
  let identifier = '';
  while (isAlpha(currentChar)) {
    identifier += currentChar;
    this._currentIndex++;
    currentChar = this._source[this._currentIndex];
  }
}
let token: Token;
if (identifier in TOKENS_GENERATOR) {
  // 如果是关键字
  token =
    TOKENS_GENERATOR[identifier as keyof typeof TOKENS_GENERATOR](
      startIndex
    );
} else {
  // 如果是普通标识符
  token = TOKENS_GENERATOR["identifier"](startIndex, identifier);
}
this._tokens.push(token);
continue;
}

// 3. 处理单字符
else if (KNOWN_SINGLE_CHAR_TOKENS.has(currentChar as SingleCharTokens)) {
  const token = KNOWN_SINGLE_CHAR_TOKENS.get(
    currentChar as SingleCharTokens
  ) !(startIndex);
  this._tokens.push(token);
  this._currentIndex++;
  continue;
}

OK,接下来我们来增加测试用例,新建 src/test/tokenizer.test.ts ,内容如下:

describe("testTokenizerFunction", () => {
  test("test example", () => {
    const result = [
      { type: "Let", value: "let", start: 0, end: 3 },
      { type: "Identifier", value: "a", start: 4, end: 5 },
      { type: "Assign", value: "=", start: 6, end: 7 },
      { type: "Function", value: "function", start: 8, end: 16 },
      { type: "LeftParen", value: "(", start: 16, end: 17 },
      { type: "RightParen", value: ")", start: 17, end: 18 },
      { type: "LeftCurly", value: "{", start: 19, end: 20 },
      { type: "RightCurly", value: "}", start: 20, end: 21 },
    ];
    const tokenizer = new Tokenizer("let a = function() {}");
    expect(tokenizer.tokenize()).toEqual(result);
  });
});

然后在终端执行 pnpm test ,可以发现如下的测试结果:

在这里插入图片描述
说明此时一个简易版本的分词器已经被我们开发出来了,不过目前的分词器还比较简陋,仅仅支持有限的语法,不过在明确了核心的开发步骤之后,后面继续完善的过程就比较简单了。

语法分析器开发

在解析出词法 token 之后,我们就可以进入语法分析阶段了。在这个阶段,我们会依次遍历 token,对代码进行语法结构层面的分析,最后的目标是生成 AST 数据结构。至于代码的 AST 结构到底是什么样子,你可以去 AST Explorer 网站进行在线预览:

在这里插入图片描述
接下来,我们要做的就是将 token 数组转换为上图所示的 AST 数据。

首先新建 src/Parser.ts ,添加如下的类型声明代码及 Parser 类的初始化代码:

export enum NodeType {
  Program = "Program",
    VariableDeclaration = "VariableDeclaration",
    VariableDeclarator = "VariableDeclarator",
    Identifier = "Identifier",
    FunctionExpression = "FunctionExpression",
    BlockStatement = "BlockStatement",
}
export interface Identifier extends Node {
  type: NodeType.Identifier;
  name: string;
}
interface Expression extends Node {}
interface Statement extends Node {}
export interface Program extends Node {
  type: NodeType.Program;
  body: Statement[];
}
export interface VariableDeclarator extends Node {
  type: NodeType.VariableDeclarator;
  id: Identifier;
  init: Expression;
}
export interface VariableDeclaration extends Node {
  type: NodeType.VariableDeclaration;
  kind: "var" | "let" | "const";
  declarations: VariableDeclarator[];
}
export interface FunctionExpression extends Node {
  type: NodeType.FunctionExpression;
  id: Identifier | null;
  params: Expression[] | Identifier[];
  body: BlockStatement;
}
export interface BlockStatement extends Node {
  type: NodeType.BlockStatement;
  body: Statement[];
}
export type VariableKind = "let";
export class Parser {
  private _tokens: Token[] = [];
  private _currentIndex = 0;
  constructor(token: Token[]) {
    this._tokens = [...token];
  }

  parse(): Program {
    const program = this._parseProgram();
    return program;
  }

  private _parseProgram(): Program {
    const program: Program = {
      type: NodeType.Program,
      body: [],
      start: 0,
      end: Infinity,
    };
    // 解析 token 数组
    return program;
  }
}

从中你可以看出,解析 AST 的核心逻辑就集中在 _parseProgram 方法中,接下来让我们一步步完善一个方法:

export class Parser {
  private _parseProgram {
    // 省略已有代码
    while (!this._isEnd()) {
      const node = this._parseStatement();
      program.body.push(node);
      if (this._isEnd()) {
        program.end = node.end;
      }
    }
    return program;
  }
  // token 是否已经扫描完
  private _isEnd(): boolean {
    return this._currentIndex >= this._tokens.length;
  }
  // 工具方法,表示消费当前 Token,扫描位置移动到下一个 token
  private _goNext(type: TokenType | TokenType[]): Token {
    const currentToken = this._tokens[this._currentIndex];
    // 断言当前 Token 的类型,如果不能匹配,则抛出错误
    if (Array.isArray(type)) {
      if (!type.includes(currentToken.type)) {
        throw new Error(
          `Expect ${type.join(",")}, but got ${currentToken.type}`
        );
      }
    } else {
      if (currentToken.type !== type) {
        throw new Error(`Expect ${type}, but got ${currentToken.type}`);
      }
    }
    this._currentIndex++;
    return currentToken;
  }

  private _checkCurrentTokenType(type: TokenType | TokenType[]): boolean {
    if (this._isEnd()) {
      return false;
    }
    const currentToken = this._tokens[this._currentIndex];
    if (Array.isArray(type)) {
      return type.includes(currentToken.type);
    } else {
      return currentToken.type === type;
    }
  }
  private _getCurrentToken(): Token {
    return this._tokens[this._currentIndex];
  }

  private _getPreviousToken(): Token {
    return this._tokens[this._currentIndex - 1];
  }
}

一个程序(Program)实际上由各个语句(Statement)来构成,因此在 _parseProgram 逻辑中,我们主要做的就是扫描一个个语句,然后放到 Program 对象的 body 中。那么,接下来,我们将关注点放到语句的扫描逻辑上面。从之前的示例代码:

let a = function() {}

我们可以知道这是一个变量声明语句,那么现在我们就在 _parseStatement 中实现这类语句的解析:

export enum NodeType {
  Program = "Program",
    VariableDeclarator = "VariableDeclarator",
}
export class Parser {
  private _parseStatement(): Statement {
    // TokenType 来自 Tokenizer 的实现中
    if (this._checkCurrentTokenType(TokenType.Let)) {
      return this._parseVariableDeclaration();
    }
    throw new Error("Unexpected token");
  }

  private _parseVariableDeclaration(): VariableDeclaration {
    // 获取语句开始位置
    const {
      start
    } = this._getCurrentToken();
    // 拿到 let
    const kind = this._getCurrentToken()
      .value;
    this._goNext(TokenType.Let);
    // 解析变量名 foo
    const id = this._parseIdentifier();
    // 解析函数表达式
    const init = this._parseFunctionExpression();
    const declarator: VariableDeclarator = {
      type: NodeType.VariableDeclarator,
      id,
      init,
      start: id.start,
      end: init ? init.end : id.end,
    };
    // 构造 Declaration 节点
    const node: VariableDeclaration = {
      type: NodeType.VariableDeclaration,
      kind: kind as VariableKind,
      declarations: [declarator],
      start,
      end: this._getPreviousToken()
        .end,
    };
    return node;
  }
}

接下来主要的代码解析逻辑可以梳理如下:

  • 发现 let 关键词对应的 token,进入 _parseVariableDeclaration
  • 解析变量名,如示例代码中的 foo
  • 解析函数表达式,如示例代码中的 function() {}

其中,解析变量名的过程我们通过 _parseIdentifier 方法实现,解析函数表达式的过程由 _parseFunctionExpression 来实现,代码如下:

// 1. 解析变量名
private _parseIdentifier(): Identifier {
  const token = this._getCurrentToken();
  const identifier: Identifier = {
    type: NodeType.Identifier,
    name: token.value!,
    start: token.start,
    end: token.end,
  };
  this._goNext(TokenType.Identifier);
  return identifier;
}

// 2. 解析函数表达式
private _parseFunctionExpression(): FunctionExpression {
  const {
    start
  } = this._getCurrentToken();
  this._goNext(TokenType.Function);
  let id = null;
  let id = null;
  if (this._checkCurrentTokenType(TokenType.Identifier)) {
    id = this._parseIdentifier();
  }
  const node: FunctionExpression = {
    type: NodeType.FunctionExpression,
    id,
    params: [],
    body: {
      type: NodeType.BlockStatement,
      body: [],
      start: start,
      end: Infinity,
    },
    start,
    end: 0,
  };
  return node;
}
// 用于解析函数参数
private _parseParams(): Identifier[] | Expression[] {
  // 消费 "("
  this._goNext(TokenType.LeftParen);
  const params = [];
  // 逐个解析括号中的参数
  while (!this._checkCurrentTokenType(TokenType.RightParen)) {
    let param = this._parseIdentifier();
    params.push(param);
  }
  // 消费 ")"
  this._goNext(TokenType.RightParen);
  return params;
}
// 用于解析函数体
private _parseBlockStatement(): BlockStatement {
  const {
    start
  } = this._getCurrentToken();
  const blockStatement: BlockStatement = {
    type: NodeType.BlockStatement,
    body: [],
    start,
    end: Infinity,
  };
  // 消费 "{"
  this._goNext(TokenType.LeftCurly);
  while (!this._checkCurrentTokenType(TokenType.RightCurly)) {
    // 递归调用 _parseStatement 解析函数体中的语句(Statement)
    const node = this._parseStatement();
    blockStatement.body.push(node);
  }
  blockStatement.end = this._getCurrentToken()
    .end;
  // 消费 "}"
  this._goNext(TokenType.RightCurly);
  return blockStatement;
}

OK,一个简易的 Parser 现在就已经搭建出来了,你可以用如下的测试用例看看程序运行的效果,代码如下:

// src/__test__/parser.test.ts
describe("testParserFunction", () => {
  test("test example code", () => {
    const result = {
      type: "Program",
      body: [{
        type: "VariableDeclaration",
        kind: "let",
        declarations: [{
          type: "VariableDeclarator",
          id: {
            type: "Identifier",
            name: "a",
            start: 4,
            end: 5,
          },
          init: {
            type: "FunctionExpression",
            id: null,
            params: [],
            body: {
              type: "BlockStatement",
              body: [],
              start: 19,
              end: 21,
            },
            start: 8,
            end: 21,
          },
          start: 0,
          end: 21,
        }, ],
        start: 0,
        end: 21,
      }, ],
      start: 0,
      end: 21,
    };
    const code = `let a = function() {};`;
    const tokenizer = new Tokenizer(code);
    const parser = new Parser(tokenizer.tokenize());
    expect(parser.parse())
      .toEqual(result);
  });
});

总结

  • 我们要掌握 AST 解析器中 词法分析和 语法分析 的核心原理与实现细节
  • 虽然只是实现了一个比较简陋的 AST 解析器,但重点在于整个词法分析和语法分析代码框架的搭建
  • 当核心的流程已经实现之后,接下来的事情就是基于已有的代码框架不断地完善语法细节,整体的难度降低了很多
  • 当 AST 解析的功能被开发完成后,接下来要做的就是实现一个 Bundler 的功能了

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

liunx清理服务器内存和日志

1、查看服务器磁盘占用情况 # 查看磁盘占用大小 df -h 2、删除data文件夹下面的日志 3、查看每个服务下面的日志输出文件&#xff0c;过大就先停掉服务再删除out文件再重启服务 4、先进入想删除输入日志的服务文件夹下&#xff0c;查看服务进程&#xff0c;杀掉进程&#xff…

【算法】(C语言):二分查找

二分查找&#xff1a; 获取查找区域的中间位置。若中间位置的数据就是要找的值&#xff0c;则返回true。若要找的值 小于 中间位置的数据&#xff0c;则往左边查找。若要找的值 大于 中间位置的数据&#xff0c;则往右边查找。重复1和2&#xff0c;若没有要找的值&#xff0c;…

Mall,正在和年轻人重新对话

【潮汐商业评论/原创】 结束了一下午的苦闷培训&#xff0c;当Cindy赶到重庆十字大道时&#xff0c;才发现十字路口上的巨大“飞行棋”在前两天就已经撤展了。 “来了又错过&#xff0c;就会觉得遗憾&#xff0c;毕竟这样的路口不多&#xff0c;展陈又不可能会返场。” 飞行棋…

【机器学习】机器学习在AI Agent中的影响与作用

文章目录 &#x1f680;Al Agent是什么&#x1f4d5;Al Agent的工作原理与技术&#x1f4aa;Al Agent应用领域&#x1f680;智能家居应用&#x1f308;医疗健康领域⭐金融服务行业&#x1f302;交通运输管理&#x1f3ac;教育培训应用 &#x1f512;Al Agent优势与挑战✊Al Age…

苹果获得OpenAI董事会观察员职位、Runway最新估值40亿美元

ChatGPT狂飙160天&#xff0c;世界已经不是之前的样子。 更多资源欢迎关注 据知情人士透露&#xff0c;苹果应用商店&#xff08;App Store&#xff09;负责人、前营销主管Phil Schiller被选中担任这一职位。这位知情人士说&#xff0c;作为董事会观察员&#xff0c;他不会以正…

【综合能源】计及碳捕集电厂低碳特性及需求响应的综合能源系统多时间尺度调度模型

目录 1 主要内容 2 部分程序 3 实现效果 4 下载链接 1 主要内容 本程序是对《计及碳捕集电厂低碳特性的含风电电力系统源-荷多时间尺度调度方法》方法复现&#xff0c;非完全复现&#xff0c;只做了日前日内部分&#xff0c;并在上述基础上改进升级为电热综合电源微网系统&…

【uni-app】基础

一、官网 网址&#xff1a;https://zh.uniapp.dcloud.io/tutorial/其他辅助网页讲解&#xff1a;https://www.wenjiangs.com/doc/7y94pldun2插件下载free&#xff1a;https://ext.dcloud.net.cn/ 二、提示框 用uni.showToast提醒的次数超过7个字的时候就会导致文字显示不全&…

SSL证书遇到问题时的解决方案

当SSL证书遇到问题时&#xff0c;可能会影响到网站的安全性和用户体验&#xff0c;常见的问题包括证书过期、域名不匹配、证书链不完整、证书颁发机构不受信任、私钥丢失或损坏等。 一、证书过期 解决方法&#xff1a;更新或续订证书。这通常涉及联系你的SSL证书提供商&#…

ACL 2024 | CoCA:自注意力的缺陷与改进

近年来&#xff0c;在大语言模型&#xff08;LLM&#xff09;的反复刷屏过程中&#xff0c;作为其内核的 Transformer 始终是绝对的主角。然而&#xff0c;随着业务落地的诉求逐渐强烈&#xff0c;有些原本不被过多关注的特性&#xff0c;也开始成为焦点。例如&#xff1a;在 T…

Shopee Live的订单量在泰国猛增超40倍!然鹅,泰国站佣金费率上调,还有得做吗?

Shopee&#xff0c;作为东南亚地区电子商务领域的佼佼者&#xff0c;不仅在区域内树立了行业标杆&#xff0c;更在泰国这一充满活力的市场中占据了举足轻重的地位。其创新的商业模式与不断优化的服务体验&#xff0c;赢得了广大消费者的青睐与信赖。近日&#xff0c;Shopee官方…

合成孔径雷达原理与应用(四)

合成孔径雷达原理与应用&#xff08;四&#xff09; 2. 应用2.7. 沉降形变滑坡2.7.1. 地面沉降2.7.2. 铁路沉降2.7.3. 大坝形变2.7.4. 机场形变2.7.5. 桥梁形变2.7.6. 滑坡监测 2. 应用 2.7. 沉降形变滑坡 2.7.1. 地面沉降 由图2-17可知&#xff0c;从整体的区域分布上来看&a…

iPad手写笔哪款比较好?2024五款爆火iPad电容笔推荐!新手必看!

在iPad等触控设备日益普及的今天&#xff0c;手写笔作为提升生产力和创意表达的重要工具&#xff0c;正受到越来越多用户的青睐。然而&#xff0c;随着市场需求的激增&#xff0c;市面上电容笔品牌与型号繁多&#xff0c;跟风购买往往容易遭遇“踩雷”情况。因此&#xff0c;作…

认识不-物联网“六域模型”有哪些有什么作用

如下参考源于苏州稳联授权可见认知域-感知域-网络域-应用域-管理域-安全域-物联网六域模型 苏州稳联 (iotrouter.cn) 认识物联网“六域模型”&#xff1a;构成与作用 “六域模型”是一个有效的框架。这个模型通过将物联网划分为六个相互关联的域&#xff0c;帮助我们更好地理…

docker介绍与详细安装

1 docker 介绍 1.1 虚拟化 在计算机中&#xff0c;虚拟化&#xff08;英语&#xff1a;Virtualization&#xff09;是一种资源管理技术&#xff0c;是将计算机的各种实体资源&#xff0c;如服务器、网络、内存及存储等&#xff0c;予以抽象、转换后呈现出来&#xff0c;打破实…

关键帧功能怎么使用 关键帧控制视频特效怎么用 会声会影视频剪辑软件教程

一篇文章&#xff0c;轻松掌握关键帧的用法&#xff0c;小白也能将关键帧玩得出神入化。在专业级的视频剪辑软件中&#xff0c;滤镜、转场、调色、遮罩等功能都离不开关键帧。可以毫不夸张地说&#xff0c;学会了关键帧的用法&#xff0c;就等于掌握了视频特效的基本逻辑 一、…

Qualcomm QCA206x EasyMesh For Ubuntu

1. 引言 关于EasyMesh概念我们这里就不再过多的赘述&#xff0c;此篇文档的目的是&#xff0c;让广大初学者&#xff0c;有一个很方便的平台进行EasyMesh的学习和测试。 2. X86 Ubuntu平台 2.1 硬件环境准备 备注&#xff1a;QCA206x WiFi module推荐使用移远的FC64E/FC66E。…

详解yolov5的网络结构

转载自文章 网络结构图&#xff08;简易版和详细版&#xff09; 此图是博主的老师&#xff0c;杜老师的图 网络框架介绍 前言&#xff1a; YOLOv5是一种基于轻量级卷积神经网络&#xff08;CNN&#xff09;的目标检测算法&#xff0c;整体可以分为三个部分&#xff0c; ba…

大数据平台之CDC (Chanage Data Capture) 方案

Change Data Capture (CDC) 是一种用于跟踪和捕获数据库中数据变更的技术&#xff0c;它可以在数据发生变化时实时地将这些变更捕获并传递到下游系统。以下是一些常用的开源 CDC 方案&#xff1a; 1. Flink CDC Flink CDC 是基于 Apache Flink 的一个扩展&#xff0c;它通过集…

.NET下的开源OCR项目:解锁图片文字识别的新篇章

在数字化时代&#xff0c;从图片中高效准确地提取文字信息已成为众多应用场景的迫切需求。OCR&#xff08;Optical Character Recognition&#xff0c;光学字符识别&#xff09;技术正是满足这一需求的关键技术。对于.NET开发者而言&#xff0c;幸运的是&#xff0c;存在多个开…

Wireshark网络抓包工具入门指南

目录 引言 安装抓包工具 抓包基础概念 抓包步骤 流程 抓包工具头的分析 14.3 以太网的完整帧格式 粘包与拆包现象解析及解决方案 发生原因 解决方案 14.3.1以太网头 14.3.2 IP头 14.3.3 UDP头 14.3.4 TCP头 引言 Wireshark是一款功能强大的开源网络协议分析器&am…