前言

虚拟语法树(Abstract Syntax Tree, AST)是解释器/编译器进行语法分析的基础, 也是众多前端编译工具的基础工具, 比如webpack, postcss, less等. 对于ECMAScript, 由于前端轮子众多, 人力过于充足, 早已经被人们玩腻了. 光是语法分析器就有 uglify , acorn , bablyon , typescript , esprima 等等若干种. 并且也有了AST的社区标准: ESTree。

这篇文章主要介绍如何去写一个AST解析器, 但是并不是通过分析JavaScript, 而是通过分析 html5 的语法树来介绍, 使用 html5 的原因有两点: 一个是其语法简单, 归纳起来只有两种: Text 和 Tag , 其次是因为JavaScript的语法分析器已经有太多太多, 再造一个轮子毫无意义, 而对于 html5 , 虽然也有不少的AST分析器, 比如 htmlparser2 , parser5 等等, 但是没有像 ESTree 那么标准, 同时, 这些分析器都有一个问题: 那就是定义的语法树中无法对标签属性进行操作. 所以为了解决这个问题, 才写了一个html的语法分析器, 同时定义了一个完善的AST结构, 然后再有的这篇文章。

AST定义

为了跟踪每个节点的位置属性, 首先定义一个基础节点, 所有的结点都继承于此结点:

  1. export interface IBaseNode {
  2. start: number;  // 节点起始位置
  3. end: number;    // 节点结束位置
  4. }

如前所述, html5的语法类型最终可以归结为两种: 一种是 Text , 另一种是 Tag , 这里用一个枚举类型来标志它们.

  1. export enum SyntaxKind {
  2. Text = 'Text', // 文本类型
  3. Tag  = 'Tag',  // 标签类型
  4. }

对于文本, 其属性只有一个原始的字符串 value , 因此结构如下:

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

而对于 Tag , 则应该包括标签开始部分 open , 属性列表 attributes , 标签名称 name , 子标签/文本 body , 以及标签闭合部分 close :

  1. export interface ITag extends IBaseNode {
  2. type: SyntaxKind.Tag;  // 类型
  3. open: IText;           // 标签开始部分, 比如 <div id="1">
  4. name: string;          // 标签名称, 全部转换为小写
  5. attributes: IAttribute[];  // 属性列表
  6. body: Array<ITag | IText> // 子节点列表, 如果是一个非自闭合的标签, 并且起始标签已结束, 则为一个数组
  7. | void                  // 如果是一个自闭合的标签, 则为void 0
  8. | null;                 // 如果起始标签未结束, 则为null
  9. close: IText              // 关闭标签部分, 存在则为一个文本节点
  10. | void                  // 自闭合的标签没有关闭部分
  11. | null;                 // 非自闭合标签, 但是没有关闭标签部分
  12. }

标签的属性是一个键值对, 包含名称 name 及值 value 部分, 定义结构如下:

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

其中名称是普通的文本节点, 但是值比较特殊, 表现在其可能被单/双引号包起来, 而引号是无意义的, 因此定义一个标签值结构:

  1. export interface IAttributeValue extends IBaseNode {
  2. value: string; // 值, 不包含引号部分
  3. quote: '\'' | '"' | void; // 引号类型, 可能是', ", 或者没有
  4. }

Token解析

AST解析首先需要解析原始文本得到符号列表, 然后再通过上下文语境分析得到最终的语法树.

相对于JSON, html虽然看起来简单, 但是上下文是必需的, 所以虽然JSON可以直接通过token分析得到最终的结果, 但是html却不能, token分析是第一步, 这是必需的. (JSON解析可以参考我的另一篇文章: 徒手写一个JSON解析器(Golang) ).

token解析时, 需要根据当前的状态来分析token的含义, 然后得出一个token列表.

首先定义token的结构:

  1. export interface IToken {
  2. start: number;    // 起始位置
  3. end: number;      // 结束位置
  4. value: string;    // token
  5. type: TokenKind;  // 类型
  6. }

Token类型一共有以下几种:

  1. export enum TokenKind {
  2. Literal     = 'Literal',      // 文本
  3. OpenTag     = 'OpenTag',      // 标签名称
  4. OpenTagEnd  = 'OpenTagEnd',   // 开始标签结束符, 可能是 '/', 或者 '', '--'
  5. CloseTag    = 'CloseTag',     // 关闭标签
  6. Whitespace  = 'Whitespace',   // 开始标签类属性值之间的空白
  7. AttrValueEq = 'AttrValueEq',  // 属性中的=
  8. AttrValueNq = 'AttrValueNq',  // 属性中没有引号的值
  9. AttrValueSq = 'AttrValueSq',  // 被单引号包起来的属性值
  10. AttrValueDq = 'AttrValueDq',  // 被双引号包起来的属性值
  11. }

Token分析时并没有考虑属性的键/值关系, 均统一视为属性中的一个片段, 同时, 视 = 为一个

特殊的独立段片段, 然后交给上层的 parser 去分析键值关系. 这么做的原因是为了在token分析

时避免上下文处理, 并简化状态机状态表. 状态列表如下:

  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. }

整个解析采用函数式编程, 没有使用OO, 为了简化在函数间传递状态参数, 由于是一个同步操作,

这里利用了JavaScript的事件模型, 采用全局变量来保存状态. Token分析时所需要的全局变量列表如下:

  1. let state: State          // 当前的状态
  2. let buffer: string        // 输入的字符串
  3. let bufSize: number       // 输入字符串长度
  4. let sectionStart: number  // 正在解析的Token的起始位置
  5. let index: number         // 当前解析的字符的位置
  6. let tokens: IToken[]      // 已解析的token列表
  7. let char: number          // 当前解析的位置的字符的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. }

然后开始解析, 解析时需要遍历输入字符串中的所有字符, 并根据当前状态进行相应的处理

(改变状态, 输出token等), 解析完成后, 清空全局变量, 返回结束.

  1. export function tokenize(input: string): IToken[] {
  2. init(input)
  3. while (index < bufSize) {
  4. char = buffer.charCodeAt(index)
  5. switch (state) {
  6. // ...根据不同的状态进行相应的处理
  7. // 文章忽略了对各个状态的处理, 详细了解可以查看源代码
  8. }
  9. index++
  10. }
  11. const _nodes = nodes
  12. // 清空状态
  13. init('')
  14. return _nodes
  15. }

语法树解析

在获取到token列表之后, 需要根据上下文解析得到最终的节点树, 方式与tokenize相似,均采用全局变量保存传递状态, 遍历所有的token, 不同之处在于这里没有一个全局的状态机。

因为状态完全可以通过正在解析的节点的类型来判断。

  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查看源代码.

结语

项目已开源, 名称是 html5parser , 可以通过npm/yarn安装:

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

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

目前对正常的HTML解析已完全通过测试, 已知的BUG包括对注释的解析, 以及未正常结束的

输入的解析处理(均在语法分析层面, token分析已通过测试).

作者:佚名

来源:51CTO

AST解析基础: 如何写一个简单的html语法分析库相关推荐

  1. python解析器是什么_如何用python写一个简单的词法分析器

    编译原理老师要求写一个java的词法分析器,想了想决定用python写一个. 目标 能识别出变量,数字,运算符,界符和关键字,用excel表打印出来. 有了目标,想想要怎么实现词法分析器. 1.先进行 ...

  2. python可以做机器人吗_零基础如何用Python写一个简单的WeChat机器人?(内附代码)...

    (bing图片) python这两年热火朝天,依托其众多类库,基于python的应用层出不穷,也大大降低了非计算机专业人员的入门门槛,WeChat机器人自然不在话下!-- 聪明的瓦肯人 苦于有时候总是 ...

  3. 如何搭建python框架_从零开始:写一个简单的Python框架

    原标题:从零开始:写一个简单的Python框架 Python部落(python.freelycode.com)组织翻译,禁止转载,欢迎转发. 你为什么想搭建一个Web框架?我想有下面几个原因: 有一个 ...

  4. Jmeter使用基础笔记-写一个http请求

    前言 本篇文章主要讲述2个部分: 搭建一个简单的测试环境 用Jmeter发送一个简单的http请求 搭建测试环境 编写flask代码(我参考了开源项目HttpRunner的测试服务器),将如下的代码保 ...

  5. 用java写一个简单的区块链(下)

    用java写一个简单的区块链(下) 2018年03月29日 21:44:35 java派大星 阅读数:725 标签: 区块链java 更多 个人分类: 区块链 版权声明:本文为博主原创文章,转载请标明 ...

  6. 手写一个简单的IOC容器

    手写一个简单的IOC容器 原文 http://localhost:4000/2020/02/25/SSM/spring/%E6%89%8B%E5%86%99%E4%B8%80%E4%B8%AA%E5% ...

  7. 构建自己的购物搜索引擎一:写一个简单的

    记得2010年10月9号,淘宝全网搜索引擎一淘网上线,当时不怎么关注,只是在网站上看到过新闻而己,前两个月,觉得是时候走确定自己以后要走的方向了,于是决定以后加入到搜索的行列中,此时开始关注一淘网的技 ...

  8. 用Qt写一个简单的音乐播放器(六):显示歌词(正则表达式)

    一.前言 在用Qt写一个简单的音乐播放器(一):使用QMediaPlayer播放音乐中,我们已经知道如何去使用QMediaPlayer播放音乐. 在用Qt写一个简单的音乐播放器(二):增加界面(开始和 ...

  9. 用Qt写一个简单的音乐播放器(一):使用QMediaPlayer播放音乐

    一.前言 QMediaplayer可以用于解析音频文件和视频文件,继承自QMediaObject,涉及到的对象为QMediaContent.QMediaObject可以提供关于媒体内容的接入,通过UR ...

  10. 用python60行代码写一个简单的笔趣阁爬虫!三分一章?

    前言 利用python写一个简单的笔趣阁爬虫,根据输入的小说网址爬取整个小说并保存到txt文件.爬虫用到了BeautifulSoup库的select方法 结果如图所示: 本文只用于学习爬虫 一.网页解 ...

最新文章

  1. 论初次修改 Android framework 代码
  2. 双缓冲 android,Android 的 SurfaceView 双缓冲应用
  3. python基础代码的含义_Python基础学习篇
  4. vue-快速原型开发
  5. Codewar-008: Playing with passphrases 玩玩加密口令
  6. 【Java虚拟机】运行时数据区
  7. cart算法_ID3、C4.5、CART决策树算法
  8. table列最小宽度 vue_Vue组件设计 - 先别管view
  9. SPSS实现游程检验
  10. 惠普m128fn中文说明书_惠普M128fw使用说明书
  11. 查看电脑ip地址、查看手机ip地址、根据域名查看ip地址
  12. wamp5数据库密码修改
  13. SAR学习笔记-代码部分
  14. 微社区成为社交电商法宝的原因是什么?
  15. Strategy模式的具体实现
  16. gnome桌面无法显示的解决
  17. C# MD5 加密,解密
  18. tomcat 端口 8005 被 windows 系统服务占用导致启动闪退的问题
  19. 电脑上不显示WLAN,无法连接WIFI!
  20. 分布式-查询MySQL类算法-思路梳理

热门文章

  1. 《SEO实战密码》读后一点感受
  2. 深入C++“准”标准库,Boost你的力量
  3. 4.4 Spark SQL实现用户ip地址热度分析
  4. [C/C++]堆栈的概念与区别
  5. windows小工具
  6. twisted python_《Python网络爬虫与信息提取》笔记(10)
  7. pythonp2p网络_python Socket网络编程实现C/S模式和P2P
  8. python同步远程文件夹_利用python实现两个文件夹的同步
  9. python中random is not defined_Python random库使用方法及异常处理方案
  10. Ubuntu常用终端命令