日本综合一区二区|亚洲中文天堂综合|日韩欧美自拍一区|男女精品天堂一区|欧美自拍第6页亚洲成人精品一区|亚洲黄色天堂一区二区成人|超碰91偷拍第一页|日韩av夜夜嗨中文字幕|久久蜜综合视频官网|精美人妻一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
AST解析基礎(chǔ):如何寫一個(gè)簡(jiǎn)單的html語法分析庫

前言

創(chuàng)新互聯(lián)建站專注于張店網(wǎng)站建設(shè)服務(wù)及定制,我們擁有豐富的企業(yè)做網(wǎng)站經(jīng)驗(yàn)。 熱誠為您提供張店?duì)I銷型網(wǎng)站建設(shè),張店網(wǎng)站制作、張店網(wǎng)頁設(shè)計(jì)、張店網(wǎng)站官網(wǎng)定制、微信小程序定制開發(fā)服務(wù),打造張店網(wǎng)絡(luò)公司原創(chuàng)品牌,更為您提供張店網(wǎng)站排名全網(wǎng)營銷落地服務(wù)。

虛擬語法樹(Abstract Syntax Tree, AST)是解釋器/編譯器進(jìn)行語法分析的基礎(chǔ), 也是眾多前端編譯工具的基礎(chǔ)工具, 比如webpack, postcss, less等. 對(duì)于ECMAScript, 由于前端輪子眾多, 人力過于充足, 早已經(jīng)被人們玩膩了. 光是語法分析器就有 uglify , acorn , bablyon , typescript , esprima 等等若干種. 并且也有了AST的社區(qū)標(biāo)準(zhǔn): ESTree。

這篇文章主要介紹如何去寫一個(gè)AST解析器, 但是并不是通過分析JavaScript, 而是通過分析 html5 的語法樹來介紹, 使用 html5 的原因有兩點(diǎn): 一個(gè)是其語法簡(jiǎn)單, 歸納起來只有兩種: Text 和 Tag , 其次是因?yàn)镴avaScript的語法分析器已經(jīng)有太多太多, 再造一個(gè)輪子毫無意義, 而對(duì)于 html5 , 雖然也有不少的AST分析器, 比如 htmlparser2 , parser5 等等, 但是沒有像 ESTree 那么標(biāo)準(zhǔn), 同時(shí), 這些分析器都有一個(gè)問題: 那就是定義的語法樹中無法對(duì)標(biāo)簽屬性進(jìn)行操作. 所以為了解決這個(gè)問題, 才寫了一個(gè)html的語法分析器, 同時(shí)定義了一個(gè)完善的AST結(jié)構(gòu), 然后再有的這篇文章。

AST定義

為了跟蹤每個(gè)節(jié)點(diǎn)的位置屬性, 首先定義一個(gè)基礎(chǔ)節(jié)點(diǎn), 所有的結(jié)點(diǎn)都繼承于此結(jié)點(diǎn):

 
 
 
 
  1. export interface IBaseNode {
  2.   start: number;  // 節(jié)點(diǎn)起始位置
  3.   end: number;    // 節(jié)點(diǎn)結(jié)束位置
  4. }

如前所述, html5的語法類型最終可以歸結(jié)為兩種: 一種是 Text , 另一種是 Tag , 這里用一個(gè)枚舉類型來標(biāo)志它們.

 
 
 
 
  1. export enum SyntaxKind {
  2.   Text = 'Text', // 文本類型
  3.   Tag  = 'Tag',  // 標(biāo)簽類型
  4. }

對(duì)于文本, 其屬性只有一個(gè)原始的字符串 value , 因此結(jié)構(gòu)如下:

 
 
 
 
  1. export interface IText extends IBaseNode {
  2.   type: SyntaxKind.Text; // 類型
  3.   value: string;         // 原始字符串
  4. }

而對(duì)于 Tag , 則應(yīng)該包括標(biāo)簽開始部分 open , 屬性列表 attributes , 標(biāo)簽名稱 name , 子標(biāo)簽/文本 body , 以及標(biāo)簽閉合部分 close :

 
 
 
 
  1. export interface ITag extends IBaseNode {
  2.   type: SyntaxKind.Tag;  // 類型
  3.   open: IText;           // 標(biāo)簽開始部分, 比如 
  4.   name: string;          // 標(biāo)簽名稱, 全部轉(zhuǎn)換為小寫
  5.   attributes: IAttribute[];  // 屬性列表
  6.   body: Array // 子節(jié)點(diǎn)列表, 如果是一個(gè)非自閉合的標(biāo)簽, 并且起始標(biāo)簽已結(jié)束, 則為一個(gè)數(shù)組
  7.     | void                  // 如果是一個(gè)自閉合的標(biāo)簽, 則為void 0
  8.     | null;                 // 如果起始標(biāo)簽未結(jié)束, 則為null
  9.   close: IText              // 關(guān)閉標(biāo)簽部分, 存在則為一個(gè)文本節(jié)點(diǎn)
  10.     | void                  // 自閉合的標(biāo)簽沒有關(guān)閉部分
  11.     | null;                 // 非自閉合標(biāo)簽, 但是沒有關(guān)閉標(biāo)簽部分
  12. }

標(biāo)簽的屬性是一個(gè)鍵值對(duì), 包含名稱 name 及值 value 部分, 定義結(jié)構(gòu)如下:

 
 
 
 
  1. export interface IAttribute extends IBaseNode {
  2.   name: IText;  // 名稱
  3.   value: IAttributeValue | void; // 值
  4. }

其中名稱是普通的文本節(jié)點(diǎn), 但是值比較特殊, 表現(xiàn)在其可能被單/雙引號(hào)包起來, 而引號(hào)是無意義的, 因此定義一個(gè)標(biāo)簽值結(jié)構(gòu):

 
 
 
 
  1. export interface IAttributeValue extends IBaseNode {
  2.   value: string; // 值, 不包含引號(hào)部分
  3.   quote: '\'' | '"' | void; // 引號(hào)類型, 可能是', ", 或者沒有
  4. }

Token解析

AST解析首先需要解析原始文本得到符號(hào)列表, 然后再通過上下文語境分析得到最終的語法樹.

相對(duì)于JSON, html雖然看起來簡(jiǎn)單, 但是上下文是必需的, 所以雖然JSON可以直接通過token分析得到最終的結(jié)果, 但是html卻不能, token分析是***步, 這是必需的. (JSON解析可以參考我的另一篇文章: 徒手寫一個(gè)JSON解析器(Golang) ).

token解析時(shí), 需要根據(jù)當(dāng)前的狀態(tài)來分析token的含義, 然后得出一個(gè)token列表.

首先定義token的結(jié)構(gòu):

 
 
 
 
  1. export interface IToken {
  2.   start: number;    // 起始位置
  3.   end: number;      // 結(jié)束位置
  4.   value: string;    // token
  5.   type: TokenKind;  // 類型
  6. }

Token類型一共有以下幾種:

 
 
 
 
  1. export enum TokenKind {
  2.   Literal     = 'Literal',      // 文本
  3.   OpenTag     = 'OpenTag',      // 標(biāo)簽名稱
  4.   OpenTagEnd  = 'OpenTagEnd',   // 開始標(biāo)簽結(jié)束符, 可能是 '/', 或者 '', '--'
  5.   CloseTag    = 'CloseTag',     // 關(guān)閉標(biāo)簽
  6.   Whitespace  = 'Whitespace',   // 開始標(biāo)簽類屬性值之間的空白
  7.   AttrValueEq = 'AttrValueEq',  // 屬性中的=
  8.   AttrValueNq = 'AttrValueNq',  // 屬性中沒有引號(hào)的值
  9.   AttrValueSq = 'AttrValueSq',  // 被單引號(hào)包起來的屬性值
  10.   AttrValueDq = 'AttrValueDq',  // 被雙引號(hào)包起來的屬性值
  11. }

Token分析時(shí)并沒有考慮屬性的鍵/值關(guān)系, 均統(tǒng)一視為屬性中的一個(gè)片段, 同時(shí), 視 = 為一個(gè)

特殊的獨(dú)立段片段, 然后交給上層的 parser 去分析鍵值關(guān)系. 這么做的原因是為了在token分析

時(shí)避免上下文處理, 并簡(jiǎn)化狀態(tài)機(jī)狀態(tài)表. 狀態(tài)列表如下:

 
 
 
 
  1. enum State {
  2.   Literal              = 'Literal',
  3.   BeforeOpenTag        = 'BeforeOpenTag',
  4.   OpeningTag           = 'OpeningTag',
  5.   AfterOpenTag         = 'AfterOpenTag',
  6.   InValueNq            = 'InValueNq',
  7.   InValueSq            = 'InValueSq',
  8.   InValueDq            = 'InValueDq',
  9.   ClosingOpenTag       = 'ClosingOpenTag',
  10.   OpeningSpecial       = 'OpeningSpecial',
  11.   OpeningDoctype       = 'OpeningDoctype',
  12.   OpeningNormalComment = 'OpeningNormalComment',
  13.   InNormalComment      = 'InNormalComment',
  14.   InShortComment       = 'InShortComment',
  15.   ClosingNormalComment = 'ClosingNormalComment',
  16.   ClosingTag           = 'ClosingTag',
  17. }

整個(gè)解析采用函數(shù)式編程, 沒有使用OO, 為了簡(jiǎn)化在函數(shù)間傳遞狀態(tài)參數(shù), 由于是一個(gè)同步操作,

這里利用了JavaScript的事件模型, 采用全局變量來保存狀態(tài). Token分析時(shí)所需要的全局變量列表如下:

 
 
 
 
  1. let state: State          // 當(dāng)前的狀態(tài)
  2. let buffer: string        // 輸入的字符串
  3. let bufSize: number       // 輸入字符串長(zhǎng)度
  4. let sectionStart: number  // 正在解析的Token的起始位置
  5. let index: number         // 當(dāng)前解析的字符的位置
  6. let tokens: IToken[]      // 已解析的token列表
  7. let char: number          // 當(dāng)前解析的位置的字符的UnicodePoint

在開始解析前, 需要初始化全局變量:

 
 
 
 
  1. function init(input: string) {
  2.   state        = State.Literal
  3.   buffer       = input
  4.   bufSize      = input.length
  5.   sectionStart = 0
  6.   index        = 0
  7.   tokens       = []
  8. }

然后開始解析, 解析時(shí)需要遍歷輸入字符串中的所有字符, 并根據(jù)當(dāng)前狀態(tài)進(jìn)行相應(yīng)的處理

(改變狀態(tài), 輸出token等), 解析完成后, 清空全局變量, 返回結(jié)束.

 
 
 
 
  1. export function tokenize(input: string): IToken[] {
  2.   init(input)
  3.   while (index < bufSize) {
  4.     char = buffer.charCodeAt(index)
  5.     switch (state) {
  6.     // ...根據(jù)不同的狀態(tài)進(jìn)行相應(yīng)的處理
  7.     // 文章忽略了對(duì)各個(gè)狀態(tài)的處理, 詳細(xì)了解可以查看源代碼
  8.     }
  9.     index++
  10.   }
  11.   const _nodes = nodes
  12.   // 清空狀態(tài)
  13.   init('')
  14.   return _nodes
  15. }

語法樹解析

在獲取到token列表之后, 需要根據(jù)上下文解析得到最終的節(jié)點(diǎn)樹, 方式與tokenize相似,均采用全局變量保存?zhèn)鬟f狀態(tài), 遍歷所有的token, 不同之處在于這里沒有一個(gè)全局的狀態(tài)機(jī)。

因?yàn)闋顟B(tài)完全可以通過正在解析的節(jié)點(diǎn)的類型來判斷。

 
 
 
 
  1. export function parse(input: string): INode[] {
  2.   init(input)
  3.   while (index < count) {
  4.     token = tokens[index]
  5.     switch (token.type) {
  6.       case TokenKind.Literal:
  7.         if (!node) {
  8.           node = createLiteral()
  9.           pushNode(node)
  10.         } else {
  11.           appendLiteral(node)
  12.         }
  13.         break
  14.       case TokenKind.OpenTag:
  15.         node = void 0
  16.         parseOpenTag()
  17.         break
  18.       case TokenKind.CloseTag:
  19.         node = void 0
  20.         parseCloseTag()
  21.         break
  22.       default:
  23.         unexpected()
  24.         break
  25.     }
  26.     index++
  27.   }
  28.   const _nodes = nodes
  29.   init()
  30.   return _nodes
  31. }

不太多解釋, 可以到GitHub查看源代碼.

結(jié)語

項(xiàng)目已開源, 名稱是 html5parser , 可以通過npm/yarn安裝:

 
 
 
 
  1. npm install html5parser -S 
  2. # OR 
  3. yarn add html5parser

或者到GitHub查看源代碼: acrazing/html5parser 。

目前對(duì)正常的HTML解析已完全通過測(cè)試, 已知的BUG包括對(duì)注釋的解析, 以及未正常結(jié)束的

輸入的解析處理(均在語法分析層面, token分析已通過測(cè)試).


名稱欄目:AST解析基礎(chǔ):如何寫一個(gè)簡(jiǎn)單的html語法分析庫
分享地址:http://www.dlmjj.cn/article/copgogs.html