首页 > 其他分享 >正则表达式原理及其简单实现

正则表达式原理及其简单实现

时间:2023-06-02 09:12:58浏览次数:50  
标签:状态 NFA 正则表达式 状态机 简单 原理 DFA 输入

本文从文件依赖依赖这个需求切入, 详细阐述了文件依赖分析的实现过程, 对于其中所使用到的正则表达式进行原理上的分析, 说明了状态机的基本架构以及状态机的最小实现

文件依赖分析

如果我想获取某个文件的所有依赖(如下图中的紫色部分), 应该怎么做呢

image.png

  • 【方案1】 利用 webpack 打包后通过 babel 编译拿到 ast 中对应的引入部分
  • 【方案2】 直接使用正则进行提取

两种方案各有优劣, 由于本文的内容是跟正则相关的, 【方案1】最后拿到的 ast 也是通过正则匹配得到的, 所以直接一步到位, 阐述一下【方案2】实现过程以及其中的原理

假设只提取 require 相关模块

const reg = /require\((")([^\1]+?)\)/gm

image.png

提取所有模块

const reg = /(src|require|import|url|from).(['"`~(])([^(\2)]+?)(\)|\2)/gm

大括号是我临时加上去的, 之前没用 react 没考虑到大括号
image.png

可以看到, 上面的两张状态转移图已经接近于状态机了, 所以下文开始讲正则的原理以及状态机

正则表达式原理

正则表达式的定义

对于给定的字符集:Σ = {c1, c2, ..., cn},有
1. ε(空字符串)是正则表达式
2. 对于任意 c ∈ Σ,c 是正则表达式
3. 如果 M、N 是正则表达式,那么以下都是正则表达式
   - 选择:M | N == {M, N}
   - 连接:MN == {mn | m ∈ M, n ∈ N}
   - 闭包:M* == {ε, M, MM, MMM, ...}

以上为正则表达式的基本构造,但是如果用基本构造去写正则表达式,篇幅太长了,所以为了简化正则表达式,就有了正则表达式的语法糖:

1. [c1-cn] == c1|c2|...|cn
2. c+ == 一个或者多个 c
3. c? == 一个或者零个 c
4. "c*" == 就是 c* 本身,不是闭包,类似转义
5. c{i, j} == i 个到 j 个 c 的连接
6. 点 (.) == 除了 '\n' 之外的任意字符

如何去测试一个字符串是否通过了正则表达式的规则呢?

这就引出了状态机的定义

状态机的架构

输入的字符串 => 状态机 => yes/no

即,状态机 会告诉你,它能不能接受或者说识别你输入的字符串,可以回答 yes,否则回答 no。更加规范的描述是:

M = (Σ, S, q0, F, δ)

- Σ: 字母表
- S: 状态集
- q0: 初始状态 
- F: 终结状态集
- δ: 转移函数

举个例子:

对于这个图来说:

- Σ: {a, b}
- S: {0, 1, 2}
- q0: {0}
- F: {2}
- δ: {
  (q0, a) -> q1,
  (q0, b) -> q0,
  (q1, a) -> q2,
  (q1, b) -> q1,
  (q2, a) -> q2,
  (q2, b) -> q2,
}

接受的定义是,输入串消耗完毕之后,最后的状态一定要是终结状态,假如输入的字符串 S = abab,那显然是可以被接受的。

我们再详细地看它的转移函数,每个状态,遇到特定的一个输入,都只有一个路线可以走,那么这种状态机就叫做 DFA(确定状态有限状态自动机)

NFA 与 DFA

DFA (确定状态有限状态自动机)

所以有限状态机的工作过程,就是从开始状态,根据不同的输入,自动进行状态转换的过程。

  • 开始状态:圆圈表示状态,被一个“没有起点的箭头”指向的状态,是开始状态,上例中是 S1
  • 最终状态:也叫接受状态,图中用双圆圈表示,这个例子中也是 S1
  • 输入:在一个状态下,向状态机输入的符号/信号,不同输入导致状态机产生不同的状态改变
  • 转换:在一个状态下,根据特定输入,改变到特定状态的过程,就是转换

上图中的状态机的功能,是 检测二进制数是否含有偶数个 0 。从图上可以看出,输入只有 1 和 0 两种。从 S1 状态开始,只有输入 0 才会转换到 S2 状态,同样 S2 状态下只有输入 0 才会转换到 S1。所以,二进制数输入完毕,如果满足最终状态,也就是最后停在 S1 状态,那么输入的二进制数就含有偶数个 0。

NFA (非确定状态有限状态自动机)

DFA 最大的特点就是 「在一个状态下,输入一个符号,一定是转换到确定的状态,没有其他的可能性。」而 NFA,就不止一个可能性

举个例子,对于正则表达式 ab|ac,对应 NFA 可以是这样的:

可以看到,在状态 1 这里,如果输入 a,其实有两种可能,如果后面的符号是 b,那么可以匹配成功,后面符号是 c 也能匹配成功。所以状态机在执行过程中,可能要尝试所有的可能性。在尝试一种可能路径匹配失败后,还要回到之前的状态再尝试其他的路径,这就是 「回溯」

但是 DFA 消除了这种不确定性,所以可以想见,其执行性能应该要比 NFA 更好,因为不需要回溯。
但是,因为 DFA 引擎只包含有限的状态,所以它不能匹配具有反向引用的模式;并且因为它不构造显示扩展,所以它不可以捕获子表达式


DFANFA 的本质区别

DFA: 对于任意字符, 最多有一个状态可以转移

NFA: 对于任意字符, 有多于一个状态可以转移。


更多关于 NFA 与 DFA 的区别


如何将正则表达式转换成 NFA (Thompson算法)

正则表达式的定义

  1. ε,即空字符串
  2. c,即单个字符
  3. e1e2,连接
  4. e1|e2,选择
  5. e1*,闭包

Thompson 算法中使用最基本的两种转换:

正则表达式中的各种运算,可以通过组合上述两种转换实现:

  • 组合运算 RS

  • 选择运算 R|S

  • 闭包运算 R*

上面图中的 R、S 是有开始状态和结束状态的 NFA。

以正则表达式 ab|c 为例,包括两个运算:

  • ab 组合
  • ab 的结果,与 c 替换

这样我们把正则表达式视为一系列输入和运算,进行分解、组合,就可以得到最终的 NFA。

NFA的实现(dom的词法分析)

有这样一个dom, <p>content</p>,它的状态机如下所示。

状态机的实现如下所示

const str = `<p>text</p>`

const State = {
  initial: 1, // 初始状态
  tagOpen: 2, // 标签开始状态
  tagName: 3, // 标签名称状态
  text: 4, // 文本状态
  tagEnd: 5, // 标签结束状态
  tagEndName: 6 // 结束标签名称状态
}

function isAlpha(char) {
  return char >= 'a' && char <= 'z' || char >= 'A' && char <= 'Z'
}

function NFAForDom(str) {
  let currentState = State.initial
  const chars = []
  const tokens = []
  while(str) {
    const char = str[0]
    switch (currentState) {
      case State.initial:
        if (char === '<') {
          currentState = State.tagOpen
          str = str.slice(1)
        } else if (isAlpha(char)) {
          currentState = State.text
          chars.push(char)
          str = str.slice(1)
        }
        break
      case State.tagOpen:
        if (isAlpha(char)) {
          currentState = State.tagName
          chars.push(char)
          str = str.slice(1)
        } else if (char === '/') {
          currentState = State.tagEnd
          str = str.slice(1)
        }
        break
      case State.tagName:
        if (isAlpha(char)) {
          chars.push(char)
          str = str.slice(1)
        } else if (char === '>') {
          currentState = State.initial
          tokens.push({
            type: 'tag',
            name: chars.join('')
          })
          chars.length = 0
          str = str.slice(1)
        }
        break
      case State.text:
        if (isAlpha(char)) {
          chars.push(char)
          str = str.slice(1)
        } else if (char === '<') {
          currentState = State.tagOpen
          tokens.push({
            type: 'text',
            content: chars.join('')
          })
          chars.length = 0
          str = str.slice(1)
        }
        break
      case State.tagEnd:
        if (isAlpha(char)) {
          currentState = State.tagEndName
          chars.push(char)
          str = str.slice(1)
        }
        break
      case State.tagEndName:
        if (isAlpha(char)) {
          chars.push(char)
          str = str.slice(1)
        } else if (char === '>') {
          currentState = State.initial
          tokens.push({
            type: 'tagEnd',
            name: chars.join('')
          })
          chars.length = 0
          str = str.slice(1)
        }
        break
    }
  }

  return tokens
}

console.log(NFAForDom(str))

参考链接:
正则表达式引擎及其分类

markdown-it 词法分析经典入门

super-tiny-compiler

正则表达式测试

插图制作

插图来源1

插图来源2

标签:状态,NFA,正则表达式,状态机,简单,原理,DFA,输入
From: https://www.cnblogs.com/simple5960/p/17450806.html

相关文章

  • php数组排序原理
    以下是一个使用PHP中的sort函数对数组进行排序的示例代码:$fruits=array("apple","banana","orange");sort($fruits);print_r($fruits);在此示例中,我们使用sort函数对$fruits数组进行升序排序。结果输出为["apple","banana","orange"]。PHP中的数组排序函数通常使用......
  • vscode+linux+git:简单的代码版本管理工作流
    由于现有设备环境的限制,目前代码调试工作主要在远程服务器端进行,所以本文将记录基于linux+git场景下,vscode的可视化的代码管理。第一步,gitclone+代码仓库;第二步,在clone下的代码中修改代码;第三步,vscode图像化操作:(其实,发生修改时候,vscode时间线这里右击修改,可以备注修改原......
  • vue2响应式原理
    一、初识响应式原理如果我们在Vue实例中声明过的数据发生了改变,那么所有用到这份数据的视图都会更新重新渲染,我们称这些数据就是响应式数据。响应式概括来说就是数据驱动视图的自动更新。<divid="app">{{obj.message}}</div>letdata={obj:{message:'Hel......
  • 根据Servie接口 生成 Controller 代码-因业务需要简单应付勿喷
    附上垃圾代码(勿喷,只不过为了应付工作需求,百十来个service都要创建对应的controller的需求,复制实在吃不消,说明一下就是简单的字符串替换操作)ApplicationControllerimportjava.io.BufferedReader;importjava.io.File;importjava.io.FileReader;importjava.io.IOExcepti......
  • 服务器并发量的简单计算以及简单的分布式解决方案
      上课画的图,感觉不错......
  • BLE中SMP的配对原理分析
    蓝牙SMP层中的配对原理分析本文作为蓝牙SM协议的学习笔记,大部分内容取自于网上资料(密码学知识)和蓝牙核心规范。阅读需要有一定的蓝牙技术知识和密码学知识基础密码学基础基本的安全问题在通信中,安全问题至关重要,基本的安全入侵手段包括窃听、伪装和篡改。假设:Alice和Bob分别......
  • 简单图可平面图判定
    题目描述给出一个\(n\)个点,\(m\)条边的简单图,判断该图是否为可平面图。输入格式第一行输入两个正整数\(n\),\(m\)。下面\(m\)行每行输入两个正整数\(u\),\(v\)表示\(u\)到\(v\)有一条边。输出格式第一行输出是否为可平面图。是的话输出1,不是的话输出0。备注点......
  • Round-Robin轮询调度法及其实现原理
    轮询调度算法(Round-RobinScheduling)轮询调度算法的原理是每一次把来自用户的请求轮流分配给内部中的服务器,从1开始,直到N(内部服务器个数),然后重新开始循环。算法的优点是其简洁性,它无需记录当前所有连接的状态,所以它是一种无状态调度。轮询调度算法流程假设有......
  • 使用vue的简单的纯前端JS验证码实现
    使用vue的简单的纯前端JS验证码实现感觉人不能在SQL里面淹死,得看看别的东西了因为是上班摸鱼偷摸搞的,所以人比较懒,很多东西也懒得修修改改,直接放在一个html文件下了页面如下js的生成图形逻辑是21年毕业的时候百度CV的,出处是找不到了<!DOCTYPEhtml><htmllang="en"><head......
  • Flask 会话技术 cookies原理
    cookies#首页@blue.route('/')@blue.route('/home/')#装饰器可以用多个,这两个路由都能访问到home函数defhome()#4.获取cookieusername=request.cookies.get('user')returnrender_template('home.html',username=username)#......