欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

使用有限狀態(tài)機(jī)實(shí)現(xiàn)簡(jiǎn)版的html解析器

 更新時(shí)間:2023年11月29日 10:12:18   作者:咖啡教室  
FSM(Finite State Machines) 有限狀態(tài)機(jī),也叫有限狀態(tài)自動(dòng)機(jī),是為研究有限內(nèi)存的計(jì)算過(guò)程和某些語(yǔ)言類而抽象出的一種計(jì)算模型,本文將使用有限狀態(tài)機(jī)實(shí)現(xiàn)一個(gè)簡(jiǎn)版的html解析器,有需要的小伙伴可以參考下

FSM(Finite State Machines) 有限狀態(tài)機(jī),也叫有限狀態(tài)自動(dòng)機(jī),是為研究有限內(nèi)存的計(jì)算過(guò)程和某些語(yǔ)言類而抽象出的一種計(jì)算模型,它擁有有限個(gè)數(shù)量的狀態(tài),每個(gè)狀態(tài)可以遷移到零個(gè)或多個(gè)狀態(tài),輸入字串決定執(zhí)行哪個(gè)狀態(tài)的遷移。

有限狀態(tài)機(jī)有什么用

代碼編譯器在工作時(shí)就需要通過(guò)詞法分析、語(yǔ)法分析、語(yǔ)義分析來(lái)得到 AST(Abtract Syntaxt Tree) 抽象語(yǔ)法樹。需要先詞法分析拿到的所有 token 流,接著通過(guò)語(yǔ)法分析將 token 流進(jìn)行文法校驗(yàn)生成語(yǔ)法解析樹,這個(gè)過(guò)程一般有兩種:

  • 邊分詞邊生成 AST,像解析 HTML、CSS
  • 先分詞生成所有 token,再來(lái)進(jìn)行語(yǔ)法分析生成 AST,像 js

我們?cè)谇岸斯ぷ髦薪?jīng)常用到的:babel、typescript、eslint、postcss、prettier、uniapp、htmlparse、代碼編輯器的語(yǔ)法高亮...這些其實(shí)都是基于 AST 抽象語(yǔ)法樹來(lái)實(shí)現(xiàn)的,而為了得到 AST 我們需要先進(jìn)行分詞,而分詞一個(gè)比較好的方式就是通過(guò)有限狀態(tài)機(jī)來(lái)實(shí)現(xiàn)。

代碼的本質(zhì)就是字符串,分詞就是把代碼字符串變成一個(gè)個(gè)最小單元到不能再拆分的單詞,也叫 token(注意不是咱們平時(shí)用到的登錄態(tài) token),分詞器的英文 tokenizer。代碼其實(shí)跟我們一篇英文文章、一首中文古詩(shī)、一個(gè)數(shù)學(xué)運(yùn)算...都是一樣的,我們一樣可以用分詞技術(shù)來(lái)拆分這些元素。

有限狀態(tài)機(jī)是怎么工作的

為了理解有限狀態(tài)機(jī)到底是怎么工作的,我們先來(lái)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的減法運(yùn)算分詞。要求用狀態(tài)機(jī)把 500-250=250 這個(gè)減法運(yùn)算分詞成一個(gè)數(shù)組,首先定義一共有2種狀態(tài):number-數(shù)字、operator-運(yùn)算符,每一個(gè)最小的 token 只能是這兩個(gè)當(dāng)中的一個(gè),代碼如下

// 500-250=250
// [
//   { type: 'number', value: '500' },
//   { type: 'operator', value: '-' },
//   { type: 'number', value: '250' },
//   { type: 'operator', value: '=' },
//   { type: 'number', value: '250' }
// ]

function mathTokenizer(text) {
  // 匹配數(shù)字正則
  const numberReg = /[0-9]/
  // 匹配運(yùn)算符正則
  const operatorReg = /[-=]/
  // 存儲(chǔ)所有讀取到的 token 數(shù)組
  let tokens = []
  // 當(dāng)前正在讀取的 token 信息
  let currentToken = {}

  // 初始狀態(tài)
  function init(e) {
    if (numberReg.test(e)) {
      currentToken = { type: 'number', value: e }
      return onNumber
    } else if (operatorReg.test(e)) {
      currentToken = { type: 'operator', value: e }
      return onOperator
    }
  }

  // 讀取到數(shù)字
  function onNumber(e) {
    if (numberReg.test(e)) {
      currentToken.value += e
      return onNumber
    }
    if (operatorReg.test(e)) {
      pushToken(currentToken)
      currentToken = { type: 'operator', value: e }
      return onOperator
    }
  }

  // 讀取到運(yùn)算符
  function onOperator(e) {
    if (numberReg.test(e)) {
      pushToken(currentToken)
      currentToken = { type: 'number', value: e }
      return onNumber
    }
    if (operatorReg.test(e)) {
      pushToken(currentToken)
      currentToken = { type: 'operator', value: e }
      return onOperator
    }
  }

  // 每次讀取到完整的一個(gè) token 后存入到數(shù)組中
  function pushToken(token) {
    tokens.push(token)
    currentToken = {}
  }

  // 遍歷讀取數(shù)組
  function parse(str) {
    const len = str.length
    let stateMachine = init
    for (let i = 0; i < len; i++) {
      const char = str[i]
      stateMachine = stateMachine(char)

      // 最后一個(gè)字符匹配完了要自己 pushToken
      if (i === len - 1) {
        pushToken(currentToken)
      }
    }

    return tokens
  }

  return parse(text)
}

const arr = mathTokenizer('500-250=250')
console.log(arr)

簡(jiǎn)版的 html 解析器

詞法分析,生成 token 流

利用狀態(tài)機(jī)來(lái)生成 token 流,為了方便理解以下示例不考慮標(biāo)簽屬性節(jié)點(diǎn)、自閉合標(biāo)簽和一些異常情況。

我們先定義5個(gè)狀態(tài):標(biāo)簽開始、結(jié)束標(biāo)簽開始、標(biāo)簽名、標(biāo)簽結(jié)束、文本,每次讀取到的內(nèi)容會(huì)在這5個(gè)狀態(tài)之間切換,每次讀取時(shí)只要不是標(biāo)簽開始、結(jié)束標(biāo)簽開始、標(biāo)簽名、標(biāo)簽結(jié)束這4個(gè)狀態(tài)的我們都當(dāng)成文本來(lái)處理。

實(shí)際上我們只需要存儲(chǔ):開始標(biāo)簽、文本、結(jié)束標(biāo)簽這3個(gè)狀態(tài),所以定義的節(jié)點(diǎn) type 分別為:startTag、text、endTag。你要按前面定義的5個(gè)狀態(tài)來(lái)儲(chǔ)存其實(shí)也是可以的,在下面生成 AST 直接忽略掉我們不需要的標(biāo)簽開始、標(biāo)簽結(jié)束這些狀態(tài)信息就行了,只不過(guò)這里我們直接在分詞這一步提前就給過(guò)濾了。

這里我們可以把狀態(tài)機(jī)理解成一個(gè)函數(shù),每遍歷到一個(gè)字符我們都將這個(gè)字符傳到函數(shù)中,而函數(shù)中可以根據(jù)這個(gè)字符來(lái)判斷下一個(gè)狀態(tài)是什么,再返回出去下一個(gè)狀態(tài)函數(shù)就行了。

function htmlTokenizer(str){
  // 標(biāo)簽開始
  const tagStartReg = /</
  // 結(jié)束標(biāo)簽開始
  const closeTagReg = ///
  // 標(biāo)簽結(jié)束
  const tagEndReg = />/
  // 標(biāo)簽名
  const tagNameReg = /[a-zA-Z]/

    let tokens = []
    let currentToken = {}

  // 初始狀態(tài)
  function init(e) {
    if (tagStartReg.test(e)) {
      currentToken = { type: 'startTag', tagName: '' }
            return init
        }
    if (closeTagReg.test(e)) {
      currentToken = { type: 'endTag', tagName: '' }
            return onTagName
        }
    if (tagNameReg.test(e)) {
      currentToken.tagName += e
            return onTagName
        }

    // 不是上面3個(gè)狀態(tài)的都當(dāng)成文本節(jié)點(diǎn)處理
    currentToken = { type: 'text', text: e }
    return onText
  }

  // 讀取到標(biāo)簽名
  function onTagName(e) {
    if (tagEndReg.test(e)) {
      pushToken(currentToken)
            return init
        } else {
      currentToken.tagName = (currentToken.tagName || '') + e
            return onTagName
        }
  }

  // 讀取到文本
  function onText(e) {
    if (tagStartReg.test(e)) {
      pushToken(currentToken)
      currentToken = { type: 'startTag', tagName: '' }
            return init
        } else {
      currentToken.text = (currentToken.text || '') + e
            return onText
    }
  }

  // 每次讀取到完整的一個(gè) token 后存入到數(shù)組中
    function pushToken(e) {
        tokens.push(e)
        currentToken = {}
    }

  // 遍歷讀取數(shù)組
  function parse(chars){
    let stateMachine = init
        for (const char of chars) {
            stateMachine = stateMachine(char)
        }
        return tokens
    }

  return parse(str)
}

const tokenList = htmlTokenizer('<div>靜夜思<p>鋤禾日當(dāng)午</p>周小黑<p>粒粒皆辛苦</p>公元一七八八年</div>')
console.log(tokenList)

語(yǔ)法分析,生成 AST 抽象語(yǔ)法樹

這一步主要就怎么能把分詞得到的數(shù)組轉(zhuǎn)換成樹形 tree 數(shù)據(jù)結(jié)構(gòu),日常開發(fā)中我們 array 轉(zhuǎn) tree 一般都是需要根據(jù)父親 id 之類的來(lái)實(shí)現(xiàn)遍歷生成,但是這里咱拿到的數(shù)組是沒(méi)有這個(gè)父 id 的,那要怎么實(shí)現(xiàn)呢?

先觀察數(shù)據(jù)結(jié)構(gòu),雖然是一個(gè)數(shù)組,但是這個(gè)數(shù)組其實(shí)是個(gè)類似中心對(duì)稱結(jié)構(gòu)的,我們暫時(shí)先忽略掉數(shù)組里的 type 為 text 的文本內(nèi)容(因?yàn)檫@個(gè)其實(shí)我們是不能把它當(dāng)成一個(gè)父節(jié)點(diǎn)的,它只能是某個(gè)標(biāo)簽的子節(jié)點(diǎn)),過(guò)濾掉文本后數(shù)組第1個(gè)元素和最后1個(gè)元素正好是1對(duì),第2個(gè)元素和倒數(shù)第2個(gè)元素又是1對(duì),我們要實(shí)現(xiàn)的就是把內(nèi)層獲取到的一對(duì)對(duì)標(biāo)簽不斷掛載到它前面一對(duì)簽的 children 屬性上來(lái)實(shí)現(xiàn) tree 結(jié)構(gòu)。

那我們可以從數(shù)組第一項(xiàng)目開始遍歷,然后用一個(gè)數(shù)組來(lái)模擬 stack 棧存每次遍歷到的標(biāo)簽信息(棧的特點(diǎn)是先進(jìn)后出,類似我們往一個(gè)桶里放東西,放在最上面的可以最先拿出來(lái),規(guī)定數(shù)組只能使用 push 和 pop 就能模擬棧了)。

當(dāng)遇到開始標(biāo)簽的時(shí)候就說(shuō)明遇到一個(gè)新的標(biāo)簽了,這時(shí)就往棧里 push 進(jìn)去,當(dāng)遇到結(jié)束標(biāo)簽時(shí)就說(shuō)明當(dāng)前這個(gè)標(biāo)簽的所有信息都已經(jīng)讀取處理完了,那我們就可以將它從棧里彈出來(lái),然后現(xiàn)在棧里最上面的一個(gè)元素其實(shí)就是當(dāng)前彈出來(lái)的父標(biāo)簽了,直接掛載到 children 上就行了。整個(gè)過(guò)程其實(shí)主要就是理解下面2點(diǎn):

  • 用棧來(lái)緩存節(jié)點(diǎn):嵌套在內(nèi)部的節(jié)點(diǎn)就可以先出棧,根節(jié)點(diǎn)最后出棧
  • 用引用類型對(duì)象的特點(diǎn),來(lái)不斷掛載節(jié)點(diǎn)
function htmlAst(tokenList) {
    let stack = []

  for (let i = 0; i < tokenList.length; i++) {
    const node = tokenList[i]

    // 開始標(biāo)簽:入棧
        if (node.type === 'startTag'){
            stack.push(node)
        }

    // 結(jié)束標(biāo)簽:出棧
        if (node.type === 'endTag') {
            const currentNode = stack.pop()
            const parent = stack[stack.length - 1]

            if (parent) {
                if (!parent.children) parent.children = []
        parent.children.push(currentNode)
            } else {
        const root = { type: 'document', children: [currentNode] }
                return root
            }
        }

    // 文本:加到父標(biāo)簽的 children 上
        if (node.type === 'text') {
            const parent = stack[stack.length - 1]
      if (!parent.children) parent.children = []
      parent.children.push(node)
        }
  }
}

然后就能拿到我們需要的 AST 語(yǔ)法樹了,結(jié)構(gòu)如下:

{
  "type": "document",
  "children": [
    {
      "type": "startTag",
      "tagName": "div",
      "children": [
        {
          "type": "text",
          "text": "靜夜思"
        },
        {
          "type": "startTag",
          "tagName": "p",
          "children": [
            {
              "type": "text",
              "text": "鋤禾日當(dāng)午"
            }
          ]
        },
        {
          "type": "text",
          "text": "周小黑"
        },
        {
          "type": "startTag",
          "tagName": "p",
          "children": [
            {
              "type": "text",
              "text": "粒粒皆辛苦"
            }
          ]
        },
        {
          "type": "text",
          "text": "公元一七八八年"
        }
      ]
    }
  ]
}

理解了狀態(tài)機(jī)就如給你按上了一雙翅膀,不管給你任何一段字符任容,都可以通過(guò)狀態(tài)機(jī)來(lái)拆分成我們想要的結(jié)構(gòu),理解了上面這些再去看 vue 里的模板編譯,你就能知道它到底是怎么加進(jìn)去那些語(yǔ)法糖的了。還比如小程序中的富文本解析,特定平臺(tái)的小程序?qū)嶋H上是不能識(shí)別瀏覽器里的 html 的,那我們就需要先將 html 通過(guò)狀態(tài)機(jī)轉(zhuǎn)成 AST,然后再按照小程序的語(yǔ)法來(lái)進(jìn)行特定的轉(zhuǎn)換。

到此這篇關(guān)于使用有限狀態(tài)機(jī)實(shí)現(xiàn)簡(jiǎn)版的html解析器的文章就介紹到這了,更多相關(guān)有限狀態(tài)機(jī)實(shí)現(xiàn)html解析器內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評(píng)論