前端开发系列124-进阶篇之html-parser
因为 HTML 解析的过程相对麻烦和复杂,因此为了把这个过程讲清楚,我这里先从下面这段最简单的 HTML 标签开始入手。我们专注一个点,需要做的似乎就是封装一个解析函数来完成转换,把字符串模板(template)作为函数的输入,把Tree 结构对象作为函数的输出即可。
输入 字符串模板(template)
<!-- 举例: -->
<div id="app"></div>
输出 Tree 结构对象
{
tag: "div",
attrs:[{name:"id",value:"app"}],
}
观察上面的输入和输出,我们需要逐字的扫描HTML字符串模板,提取里面的标签名称作为最终对象的 Tag 属性值,提取里面的属性节点保存到 attrs 属性中,因为标签身上可能有多个属性节点,所以 attrs 使用对象数组结构。
在扫描<div id="app"></div>字符串的时候,我们需区分开始标签、属性节点、闭合标签等部分,又因为标签的类型可以有很多种(div、span等),而属性节点的 key 和 value我们也无法限定和预估,因此在具体操作的时候似乎还需要用到 正则表达式来进行匹配,下面给出需要用到的正则表达式,并试着给出解析上述 HTML 模板字符串的 JavaScript 实现代码。
/* 形如:abc-123 */
const nc_name = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;
/* 形如:<aaa:bbb> */
const q_nameCapture = `((?:${nc_name}\\:)?${nc_name})`;
/* 形如:<div 匹配开始标签的左半部分 */
const startTagOpen = new RegExp(`^<${q_nameCapture}`);
/* 匹配开始标签的右半部分(>) 形如`>`或者` >`前面允许存在 N(N>=0)个空格 */
const startTagClose = /^\s*(\/?)>/;
/* 匹配属性节点:形如 id="app" 或者 id='app' 或者 id=app 等形式的字符串 */
const att =/^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<div>`]+)))?/
let template = `<div id="app"></div>`;
function parser_html(html) {
/* 在字符串中搜索<字符并获取索引 */
let textStart = html.indexOf('<');
/* 标签的开头 */
if (textStart == 0) {
/* 匹配标签的开头 */
let start = html.match(startTagOpen);
/* start的结果为:["<div","div",...] */
if (start) {
const tagInfo = {
tag: start[1],
attrs: []
}
/* 删除已经匹配过的这部分标签 html->' id="app"></div>'*/
html = html.slice(start[0].length)
/* 匹配属性节点部分 */
/* 考虑到标签可能存在多个属性节点,因此这里使用循环 */
let attr, end;
/* 换言之:(如果 end 有值那么循环结束),即当匹配到关闭标签的时候结束循环 */
while (!(end = html.match(startTagClose)) && (attr = html.match(att))) {
tagInfo.attrs.push({
name: attr[1],
value: attr[3] || attr[4] || attr[5]
})
html = html.slice(attr[0].length)
}
/* html-> ' ></div>' */
if (end) {
/* 此处可能是' >'因此第一个参数不能直接写0 */
html = html.slice(end[0].length);
/* html-> '</div>' */
/* 此处,关闭标签并不影响整体结果,因此暂不处理 */
return tagInfo;
}
}
}
}
let tree = parser_html(template);
console.log(tree);
/*
打印结果:
{ tag: 'div',
attrs: [ { name: 'id', value: 'app' } ] }
*/
console.log(parser_html(`<span id="app" title="标题"></span>`));
/*
打印结果:
{ tag: 'span',
attrs:
[ { name: 'id', value: 'app' }, { name: 'title', value: '标题' } ] }
*/
在上面的代码中,多个地方都用到了字符串的match方法,该方法接收一个正则表达式作为参数,用于进行正则匹配,并返回匹配的结果。
这里以属性匹配为例,当我们对字符串' id="app"></div>'应用正则匹配att后,得到的结果是一个数组,而如果匹配不成功,那么得到的结果为 null。

上文中处理的HTML 字符串模板比较简单,是单标签的(只有一个标签),如果我们要处理的标签结构比较复杂,比如存在嵌套关系(既标签中又有一个或多个子标签,而子标签也有自己的属性节点、内容甚至是子节点)和文本内容等。
这里简单给出HTML 字符串模板编译的示例代码,基本上解决了标签嵌套的问题,能够最终得到一棵描述 标签结构的 "Tree"。
/* 形如:abc-123 */
const nc_name = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;
/* 形如:<aaa:bbb> */
const q_nameCapture = `((?:${nc_name}\\:)?${nc_name})`;
/* 形如:<div 匹配开始标签的左半部分 */
const startTagOpen = new RegExp(`^<${q_nameCapture}`);
/* 匹配开始标签的右半部分(>) 形如`>`或者` >`前面允许存在 N(N>=0)个空格 */
const startTagClose = /^\s*(\/?)>/;
/* 匹配闭合标签:形如 </div> */
const endTag = new RegExp(`^<\\/${q_nameCapture}[^>]*>`);
/* 匹配属性节点:形如 id="app" 或者 id='app' 或者 id=app 等形式的字符串 */
const att = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<div>`]+)))?/
// const template = `<div><span class="span-class">Hi 夏!</span></div>`;
const template = `<div id="app" title="标题"><p>hello</p><span>vito</span></div>`
/* 标记节点类型(文本节点) */
let NODE_TYPE_TEXT = 3;
/* 标记节点类型(元素节点) */
let NODE_TYPE_ELEMENT = 1;
let stack = []; /* 数组模拟栈结构 */
let root = null;
let currentParent;
function compiler(html) {
/* 推进函数:每处理完一部分模板就向前推进删除一段 */
function advance(n) {
html = html.substring(n);
}
/* 解析开始标签部分:主要提取标签名和属性节点 */
function parser_start_html() {
/* 00-正则匹配 <div id="app" title="标题">模板结构*/
let start = html.match(startTagOpen);
if (start) {
/* 01-提取标签名称 形如 div */
const tagInfo = {
tag: start[1],
attrs: []
};
/* 删除<div部分 */
advance(start[0].length);
/* 02-提取属性节点部分 形如:id="app" title="标题"*/
let attr, end;
while (!(end = html.match(startTagClose)) && (attr = html.match(att))) {
tagInfo.attrs.push({
name: attr[1],
value: attr[3] || attr[4] || attr[5]
});
advance(attr[0].length);
}
/* 03-处理开始标签 形如 >*/
if (end) {
advance(end[0].length);
return tagInfo;
}
}
}
while (html) {
let textTag = html.indexOf('<');
/* 如果以<开头 */
if (textTag == 0) {
/* (1) 可能是开始标签 形如:<div id="app"> */
let startTagMatch = parser_start_html();
if (startTagMatch) {
start(startTagMatch.tag, startTagMatch.attrs);
continue;
}
/* (2) 可能是结束标签 形如:</div>*/
let endTagMatch = html.match(endTag);
if (endTagMatch) {
advance(endTagMatch[0].length);
end(endTagMatch[1]);
continue;
}
}
/* 文本内容的处理 */
let text;
if (textTag >= 0) {
text = html.substring(0, textTag);
}
if (text) {
advance(text.length);
chars(text);
}
}
return root;
}
/* 文本处理函数:<span> hello <span> => text的值为 " hello "*/
function chars(text) {
/* 1.先处理文本字符串中所有的空格,全部替换为空 */
text = text.replace(/\s/g, '');
/* 2.把数据组织成{text:"hello",type:3}的形式保存为当前父节点的子元素 */
if (text) {
currentParent.children.push({
text,
type: NODE_TYPE_TEXT
})
}
}
function start(tag, attrs) {
let element = createASTElement(tag, attrs);
if (!root) {
root = element;
}
currentParent = element;
stack.push(element);
}
function end(tagName) {
let element = stack.pop();
currentParent = stack[stack.length - 1];
if (currentParent) {
element.parent = currentParent;
currentParent.children.push(element);
}
}
function createASTElement(tag, attrs) {
return {
tag,
attrs,
children: [],
parent: null,
nodeType: NODE_TYPE_ELEMENT
}
}
console.log(compiler(template));
执行上述代码,我们可以得到下面的显示结果。

前端开发系列124-进阶篇之html-parser的更多相关文章
- openlayers5-webpack 入门开发系列一初探篇(附源码下载)
前言 openlayers5-webpack 入门开发系列环境知识点了解: node 安装包下载webpack 打包管理工具需要依赖 node 环境,所以 node 安装包必须安装,上面链接是官网下载 ...
- leaflet-webpack 入门开发系列一初探篇(附源码下载)
前言 leaflet-webpack 入门开发系列环境知识点了解: node 安装包下载webpack 打包管理工具需要依赖 node 环境,所以 node 安装包必须安装,上面链接是官网下载地址 w ...
- 【Windows10 IoT开发系列】配置篇
原文:[Windows10 IoT开发系列]配置篇 Windows10 For IoT是Windows 10家族的一个新星,其针对不同平台拥有不同的版本.而其最重要的一个版本是运行在Raspberry ...
- ESP8266开发之旅 进阶篇② 闲聊Arduino IDE For ESP8266烧录配置
授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力.希望大家分享给你周边需要的朋友或者同学,说不定大神成长之路有博哥的奠基石... QQ技术互动交流群:ESP8266&3 ...
- 【webpack 系列】进阶篇
本文将继续引入更多的 webpack 配置,建议先阅读[webpack 系列]基础篇的内容.如果发现文中有任何错误,请在评论区指正.本文所有代码都可在 github 找到. 打包多页应用 之前我们配置 ...
- iOS开发系列--Swift进阶
概述 上一篇文章<iOS开发系列--Swift语言>中对Swift的语法特点以及它和C.ObjC等其他语言的用法区别进行了介绍.当然,这只是Swift的入门基础,但是仅仅了解这些对于使用S ...
- 旨在脱离后端环境的前端开发套件 - IDT Server篇
IDT,一个基于Nodejs的,旨在脱离后端环境的前端开发套件,目的就是能让前端开发完全脱离后端的环境,无论后端是什么模板引擎(主流),都能应付自如. IDT主要包括两大部分:Server + Bui ...
- 前端开发【第2篇:CSS】
鸡血 样式的属性多达几千个,但别担心,按照80-20原则,常用的也就几十个,你完全可以掌握它. Css初识 HTML的诞生 早期只有HTML的时候为了让HTML更美观一点,当时页面的开发者会把颜色写到 ...
- [置顶]【实用 .NET Core开发系列】- 导航篇
前言 此系列从出发点来看,是 上个系列的续篇, 上个系列因为后面工作的原因,后面几篇没有写完,后来.NET Core出来之后,注意力就转移到了.NET Core上,所以再也就没有继续下去,此是原因之一 ...
- openlayers4 入门开发系列之风场图篇
前言 openlayers4 官网的 api 文档介绍地址 openlayers4 api,里面详细的介绍 openlayers4 各个类的介绍,还有就是在线例子:openlayers4 官网在线例子 ...
随机推荐
- 使用Python和SymPy推导斯特林公式
引言 斯特林公式(Stirling's Approximation)是一种用来近似计算阶乘的数学公式.它是数学分析中非常重要的近似公式之一,尤其在概率论.统计学.物理学等领域中广泛应用.本文将使用 P ...
- 康谋分享 | aiSim5 物理相机传感器模型验证方法(一)
摘要: aiSim5可以实时模拟复杂的传感器配置,在多GPU分布式渲支持的支持下,aiSim可以渲染20多个摄像头.10多个雷达和10多个激光雷达在同一环境下运行.aiSim5独有的实时渲染引擎能够满 ...
- Tortoise-ORM与FastAPI集成:异步模型定义与实践
title: Tortoise-ORM与FastAPI集成:异步模型定义与实践 date: 2025/04/20 11:38:23 updated: 2025/04/20 11:38:23 autho ...
- 关于navicat导出和导入sql文件的方法
导出SQL文件 导入SQL文件 导出技巧 导出SQL文件 到处数据库的方法很简单,只需要在要到处的数据库上面右键,选择转储SQL文件,可以选结构和数据...或者结构... 导入SQL文件 导如SQL文 ...
- mysql8.0.12+hibernate5.4.1 的一些配置
目录 整体目录结构 第一步 创建数据库 第二步 创建java项目,导入相应的jar包 第三步 创建数据库对应的java类 第四步 创建hibernate映射文件 第五步 创建hibernate核心配置 ...
- 遇到的问题之"数据库编写SQL-》子查询中加入limit报错:This version of MySQL doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery'"
一.问题 > 1235 - This version of MySQL doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery' 二. ...
- latex常用符号及模板
\le \ge \in \mathbb{M} a \qquad b \ne \forall \exists \left \lfloor \right \rfloor \nmid \varnothing ...
- 【TensorRT 10 C++ inference example】最新版本TensorRT c++ api的推理部署教程
TensorRT是英伟达推出的部署框架,我的工作经常需要封装我的AI算法和模型给到桌面软件使用,那么tensorRT对我来说就是不二之选.TensorRT和cuda深度绑定,在c++版本的推理教程 ...
- C#线程池核心技术:从原理到高效调优的实用指南
1. 引言 在现代软件开发中,多线程编程是提升应用程序性能的关键手段.随着多核处理器的普及,合理利用并发能力已成为开发者的重要课题.然而,线程的创建和销毁是一个昂贵的过程,涉及系统资源的分配与回收,频 ...
- [原创]《C#高级GDI+实战:从零开发一个流程图》第02章:画一个矩形,能拖动!
一.前言 就像开发的教程都从"Hello World!"开篇一样,系列开始,我们也从一个最最简单的功能开始:画一个能拖动的矩形. 顺便说一下,另一篇教程:(原创)[C#] GDI+ ...