背景:需求需要把 html 字符串转成 DOM 对象树或者 js 对象树,然后进行一些处理/操作。htmlparser 这个库还行,但是对 attribute 上一些特殊属性值转换不行,同时看了看`开标签语法`(syntax-start-tag:whatwg)、`html-attribute 的支持规则`(attributes:whatwg) 和一些其他库的实现,在一些边界场景(特殊属性值和web component)处理还是缺少,算了... 自己撸了个 html parser 的函数么好了。
本文主要是记录下实现过程,做个技术沉淀,有相关需求的可以做个参考。

前期处理

首先,定义一些正则表达式,用以匹配希望找到的内容
const ltReg = /\</g
const gtReg = /\>/g
const sqReg = /'/g
const qReg = /"/g
const sqAttrReg = /(?<=\=')[^']*?(?=')/g
const qAttrReg = /(?<=\=")[^"]*?(?=")/g
const qRegBk = /"/g
const sqRegBk = /'/g
const ltRegBk = /</g
const gtRegBk = />/g
const attrReplaceReg = /[\:\w\d_-]*?=(["].*?["]|['].*?['])/g
const attrReg = /(?<=\s)([\:\w\d\-]+\=(["'].*?["']|[\w\d]+)|\w+)/g
const numReg = /^\d+$/
const clReg = /\n/g
const sReg = /\s/g
const spReg = /\s+/g
const tagReg = /\<[^\<\>]*?\>/
const startReg = /\<[^\/\!].*?\>/
const endReg = /\<\/.*?\>/
const commentReg = /(?<=\<\!\-\-).*?(?=\-\-\>)/
const tagCheckReg = /(?<=\<)[\w\-]+/

开始处理逻辑,拿个简单的 html 字符串做例子。
const str = `
<div id="container">
<div class="test" data-html="<p>hello 1</p>">
<p>hello 2</p>
<input type="text" value="hello 3" >
</div>
</div>
`

属性值转义

拿到字符串 str,取各个开标签,并将标签内的 attribute 里的特殊字符做转义字符替换,返回字符串 str1
const replaceAttribute = (html: string): string => {
return html.replace(attrReplaceReg, v => {
return v
.replace(ltReg, '<')
.replace(gtReg, '>')
.replace(sqAttrReg, v => {
return v.replace(qReg, '"')
})
.replace(qAttrReg, v => {
return v.replace(sqReg, ''')
})
})
}
结果如下:
;`<div id="container">
<div class="test" data-html="<p>hello 1</p>">
<p>hello 2</p>
<input type="text" value="hello 3" >
</div>
</div>`

形成内容数组

从上一步的字符串 str1 中截取出元素(元素是: 开标签、内容、闭合标签),放入新数组 arr。
const convertStringToArray = (html: string) => {
let privateHtml = html
let temporaryHtml = html
const arr = []
while (privateHtml.match(tagReg)) {
privateHtml = temporaryHtml.replace(tagReg, (v, i) => {
if (i > 0) {
const value = temporaryHtml.slice(0, i)
if (value.replace(sReg, '').length > 0) {
arr.push(value)
}
}
temporaryHtml = temporaryHtml.slice(i + v.length)
arr.push(v)
return ''
})
}
return arr
}
结果如下:
 ["<div id="container">", "<div class="test" data-html="<p>hello 1</p>">", "<p>", "hello 2", "</p>", "<input type="text" value="hello 3" >", "</div>", "</div>"]

生成对象树

循环上一步形成的 arr,处理成对象树
// 单标签集合
var singleTags = [
'img',
'input',
'br',
'hr',
'meta',
'link',
'param',
'base',
'basefont',
'area',
'source',
'track',
'embed'
]
// 其中 DomUtil 是根据 nodejs 还是 browser 环境生成 js 对象/ dom 对象的函数
var makeUpTree = function(arr) {
var root = DomUtil('container')
var deep = 0
var parentElements = [root]
arr.forEach(function(i) {
var parentElement = parentElements[parentElements.length - 1]
if (parentElement) {
var inlineI = toOneLine(i)
// 开标签处理,新增个开标签标记
if (startReg.test(inlineI)) {
deep++
var tagName = i.match(tagCheckReg)
if (!tagName) {
throw Error('标签规范错误')
}
var element_1 = DomUtil(tagName[0])
var attrs = matchAttr(i)
attrs.forEach(function(attr) {
if (element_1) {
element_1.setAttribute(attr[0], attr[1])
}
})
parentElement.appendChild(element_1)
// 单标签处理,deep--,完成一次闭合标记
if (
singleTags.indexOf(tagName[0]) > -1 ||
i.charAt(i.length - 2) === '/'
) {
deep--
} else {
parentElements.push(element_1)
}
}
// 闭合标签处理
else if (endReg.test(inlineI)) {
deep--
parentElements.pop()
} else if (commentReg.test(inlineI)) {
var matchValue = i.match(commentReg)
var comment = matchValue ? matchValue[0] : ''
deep++
var element = DomUtil('comment', comment)
parentElement.appendChild(element)
deep--
} else {
deep++
var textElement = DomUtil('text', i)
parentElement.appendChild(textElement)
deep--
}
}
})
if (deep < 0) {
throw Error('存在多余闭合标签')
} else if (deep > 0) {
throw Error('存在多余开标签')
}
return root.children
}
结果如下:
[
{
attrs: {
id: 'container'
},
parentElement: [DomElement],
children: [
{
attrs: {
class: 'test',
'data-html': '<p>hello 1</p>'
},
parentElement: [DomElement],
children: [
{
attrs: {},
parentElement: [DomElement],
children: [
{
attrs: {},
parentElement: [DomElement],
children: [],
tagName: 'text',
data: 'hello 2'
}
],
tagName: 'p'
},
{
attrs: {
type: 'text',
value: 'hello 3'
},
parentElement: [DomElement],
children: [],
tagName: 'input'
}
],
tagName: 'div'
}
],
tagName: 'div'
}
]

组合

组合以上的 3 个步骤
const Parser = (html: string) => {
const htmlAfterAttrsReplace = replaceAttribute(html)
const stringArray = convertStringToArray(htmlAfterAttrsReplace)
const domTree = makeUpTree(stringArray)
return domTree
}

测试

最后肯定的要测试一波。
把 tuya / taobao / baidu / jd / tx 的首页或者新闻页都拷贝了 html 试了一波,基本在 `100ms` 内执行完,并且 dom 数量大概在几千的样子,对比了一番, html 字符串上的标签属性和对象的 attrs 对象,都还对应的上。
 
emm... 还算行,先用着。

最后

写代码么...开心就好
如果您对我们团队感兴趣,欢迎加入,期待您的加入,可以投递我的邮箱 liaojc@tuya.com !
更多岗位可以查看 Tuya 招聘

HTML-Parser的更多相关文章

  1. [LeetCode] Mini Parser 迷你解析器

    Given a nested list of integers represented as a string, implement a parser to deserialize it. Each ...

  2. Log Parser 2.2 分析 IIS 日志

    1,安装Log Parser 2.2 https://www.microsoft.com/en-us/download/details.aspx?displaylang=en&id=24659 ...

  3. [译文]选择使用正确的 Markdown Parser

    以下客座文章由Ray Villalobos提供.在这篇文章中Ray将要去探索很多种不同的Markdown语法.所有的这些MarkDown变种均提供了不同的特性,都超越传统的Markdown语法,却又相 ...

  4. InnoDB全文索引:N-gram Parser【转】

    本文来自:http://mysqlserverteam.com/innodb%E5%85%A8%E6%96%87%E7%B4%A2%E5%BC%95%EF%BC%9An-gram-parser/ In ...

  5. Warning: simplexml_load_string(): Entity: line 432: parser error : EntityRef: expecting ';'

    Warning: simplexml_load_string(): Entity: line 432: parser error : EntityRef: expecting ';' characte ...

  6. Lex&Yacc Parser错误发生后再次parser之前恢复初始状态

    使用lex yacc 对文件进行parser时,如果文件内容有错,parser报错,然后你修改了文件,再次读入文件进行parser,如果你不是重启程序进行parser,那就需要对做些处理了. &quo ...

  7. 为sproto手写了一个python parser

    这是sproto系列文章的第三篇,可以参考前面的<为sproto添加python绑定>.<为python-sproto添加map支持>. sproto是云风设计的序列化协议,用 ...

  8. Python html.parser库学习小结

    分类路径:/Datazen/DataMining/Crawler/   前段时间,一朋友让我做个小脚本,抓一下某C2C商城上竞争对手的销售/价格数据,好让他可以实时调整自己的营销策略.自己之前也有过写 ...

  9. 用Log Parser Studio分析IIS日志

    发现一个强大的图形化IIS日志分析工具——Log Parser Studio,下面分享一个实际操作案例. 1. 安装Log Parser Studio a) 需要先安装Log Parser,下载地址: ...

  10. 云计算之路-阿里云上:借助IIS Log Parser Studio分析“黑色30秒”问题

    今天下午15:11-15:13间出现了类似“黑色30秒”的状况,我们用强大的IIS日志分析工具——Log Parser Studio进行了进一步的分析. 分析情况如下—— 先看一下Windows性能监 ...

随机推荐

  1. 第02组 Alpha冲刺(1/6)

    队名:無駄無駄 组长博客 作业博客 组员情况 张越洋 过去两天完成了哪些任务 如何进行团队代码的版本管理 如何使用微信云开发 如何使用管理微信开发团队 接下来的计划 沟通前后端成员,监督.提醒他们尽快 ...

  2. 基于web公交查询系统自我安排进度

    这周完成站点信息管理

  3. javaWeb项目配置自定义404错误页

        1.情景展示 为了隐藏tomcat版本信息以及显示更友好的错误信息提示,如何将404的错误跳转到指定页面? 2.解决方案 第一步:修改项目的web.xml 将如下代码添加到</web-a ...

  4. 本地VS调试服务器 IIS 程序

    由于读书的关系,毕业后选择在武汉,工作三年,至今年5月份挪窝到沿海某二线城市,换城市相当于裸辞,一切从头开始,新的城市,新的居住地,新的空气,新工作,新挑战.一直忙忙碌碌,孜孜不倦的汲取着,担心脱队, ...

  5. Solr7.x学习(3)-创建core并使用分词器

    1.创建core文件夹 ck /usr/local/solr-7.7.2/server/solr mkdir first_core cp -r configsets/_default/* first_ ...

  6. POJ-数据结构-优先队列模板

    优先队列模板 优先队列是用堆实现的,所以优先队列中的push().pop()操作的时间复杂度都是O(nlogn). 优先队列的初始化需要三个参数,元素类型.容器类型.比较算子. 需要熟悉的优先队列操作 ...

  7. 原生Ajax的怎么用?

    <script> function createXMLHttpRequest() { var xmlhttp; try { //先直接创建XMLHttpRequest xmlhttp = ...

  8. [转帖]AWR报告参数:DB TIME和DB CPU

    AWR报告参数:DB TIME和DB CPU http://blog.itpub.net/12679300/viewspace-1182396/ 一.前言:AWR报告是了解ORACLE运行的一个重要报 ...

  9. mysql 中 and和or 一起使用和之间的优先级

    SELECT address,job_title,education,SUM(recruiting) FROM commerce_jobs WHERE education = '大专' and ( j ...

  10. Authentication using SASL/Kerberos

    Prerequisites KerberosIf your organization is already using a Kerberos server (for example, by using ...