HTML.parser和正则解析,HTMLParser 的实现和使用
1. 概览
HTMLParser 在很多地方都有它施展拳脚的地方, 例如在 Vue 中, Vue 把 template 模板字符串编译成 render 函数的过程就用到了 HTMLParser.
要注意的一点是, HTMLParser 做的工作是解析字符串并把参数传递给回调函数, 后续要执行什么操作全部依赖于传入的options中的start end comment chars等钩子函数
1.1 一般的简单 parser 逻辑
初始时若发现第一个 tagName 是 script 或 style, 则先做特殊处理. 原则上这种情况不应该出现
while(html) 循环判断当前处理的字符串是否为空
若当前字符串以< !--开头, 则进入注释处理逻辑, 调用 comment 钩子
若当前字符以< /开头, 则进入endTag处理逻辑, 进入内部的 parseEndTag 函数
检查 stack && 关闭那些特殊闭合方式的 tagName , 例如 和
若 tagName 不是自闭和节点则入栈
把 attrs 即id="app"的字符串replace 为{ name:'id', value:'app', escaped }的数据
调用 start(tagName, attrs, unary) 钩子
若当前字符以
parseStartTag 实际上做的工作就是: 找到合适的 tagName 出栈, 调用 end 钩子
其它情况则调用 chars 钩子
收尾工作再次调用 parseEndTag() 来检查标签是否闭合
1.2 Vue 额外增加的内容
大量的正确性检查&&验证
根节点只能有一个
根节点不能是等
tagName 要闭合(通过 stack 来判断)
对v-for v-if v-pre v-once等特殊属性的处理(增加到构建的 AST 的属性中)
对{{text}}这类模板解析字符串的处理
2. 一个160行的 HTMLParser
该项目地址在Github blowsie, 约200个Star , 代码中正则+HTMLParser 函数体约160行, 只看函数体的话约130行
2.1 所用到的正则表达式
//匹配 tag 的开头(包含 attrs), 例如 '
var startTag = /^\s]+))?)*)\s*(\/?)>/
//匹配 tag 的结尾, 例如'
'
var endTag = /^]*>/
//匹配 attr 内容, 可以有等号, 等号后面可以是双引号或单引号, 例如 :class="c" :msg='msg'
//ps:该简单 parser 不支持es6的``字符串, 而 vue 的正则是支持的
var attr = /([a-zA-Z_:@][-a-zA-Z0-9_:.]*)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g;
//HTML5中的可以不写 endTag 的那些元素的集合, 例如
var empty = makeMap("area,base,basefont,br,col,frame,hr,img,input,link,meta,param,embed,command,keygen,source,track,wbr");
//普通块状 HTML5标签 tagName 的集合, 例如
var block = makeMap("a,address,article,applet,aside,audio,blockquote,button,canvas,center,dd,del,dir,div,dl,dt,fieldset,figcaption,figure,footer,form,frameset,h1,h2,h3,h4,h5,h6,header,hgroup,hr,iframe,ins,isindex,li,map,menu,noframes,noscript,object,ol,output,p,pre,section,script,table,tbody,td,tfoot,th,thead,tr,ul,video");
// Inline Elements - HTML 5
var inline = makeMap("abbr,acronym,applet,b,basefont,bdo,big,br,button,cite,code,del,dfn,em,font,i,iframe,img,input,ins,kbd,label,map,object,q,s,samp,script,select,small,span,strike,strong,sub,sup,textarea,tt,u,var");
// Elements that you can, intentionally, leave open
// (and which close themselves)
var closeSelf = makeMap("colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr");
// Attributes that have their values filled in disabled="disabled"
var fillAttrs = makeMap("checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected");
// Special Elements (can contain anything)
var special = makeMap("script,style");
//makeMap是为了方便取 key-value 对. 实际上数组的 indexOf 也不慢
function makeMap(str) {
var obj = {}, items = str.split(",");
for (var i = 0; i < items.length; i++)
obj[items[i]] = true;
return obj;
}
2.2 函数体
var HTMLParser = this.HTMLParser = function (html, handler) {
var index, chars, match, stack = [], last = html;
stack.last = function () {
return this[this.length - 1];
};
while (html) {
chars = true;
// Make sure we're not in a script or style element
if (!stack.last() || !special[stack.last()]) {
// Comment
if (html.indexOf("");
if (index >= 0) {
if (handler.comment)
handler.comment(html.substring(4, index));
html = html.substring(index + 3);
chars = false;
}
// end tag
} else if (html.indexOf("") == 0) {
match = html.match(endTag);
if (match) {
html = html.substring(match[0].length);
match[0].replace(endTag, parseEndTag);
chars = false;
}
// start tag
} else if (html.indexOf("
match = html.match(startTag);
if (match) {
html = html.substring(match[0].length);
match[0].replace(startTag, parseStartTag);
chars = false;
}
}
if (chars) {
index = html.indexOf("
var text = index < 0 ? html : html.substring(0, index);
html = index < 0 ? "" : html.substring(index);
if (handler.chars)
handler.chars(text);
}
} else {
html = html.replace(new RegExp("([\\s\\S]*?)]*>"), function (all, text) {
text = text.replace(/|/g, "$1$2");
if (handler.chars)
handler.chars(text);
return "";
});
parseEndTag("", stack.last());
}
if (html == last)
throw "Parse Error: " + html;
last = html;
}
// Clean up any remaining tags
parseEndTag();
function parseStartTag(tag, tagName, rest, unary) {
tagName = tagName.toLowerCase();
if (block[tagName]) {
while (stack.last() && inline[stack.last()]) {
parseEndTag("", stack.last());
}
}
if (closeSelf[tagName] && stack.last() == tagName) {
parseEndTag("", tagName);
}
unary = empty[tagName] || !!unary;
if (!unary)
stack.push(tagName);
if (handler.start) {
var attrs = [];
rest.replace(attr, function (match, name) {
var value = arguments[2] ? arguments[2] :
arguments[3] ? arguments[3] :
arguments[4] ? arguments[4] :
fillAttrs[name] ? name : "";
attrs.push({
name: name,
value: value,
escaped: value.replace(/(^|[^\\])"/g, '$1\\\"') //"
});
});
if (handler.start)
handler.start(tagName, attrs, unary);
}
}
function parseEndTag(tag, tagName) {
// If no tag name is provided, clean shop
if (!tagName)
var pos = 0;
// Find the closest opened tag of the same type
else
for (var pos = stack.length - 1; pos >= 0; pos--)
if (stack[pos] == tagName)
break;
if (pos >= 0) {
// Close all the open elements, up the stack
for (var i = stack.length - 1; i >= pos; i--)
if (handler.end)
handler.end(stack[i]);
// Remove the open elements from the stack
stack.length = pos;
}
}
};
2.3 查看函数执行结果
可用如下代码进行测试钩子函数的返回值. ps, 原生的 HTMLParser 是不能识别 vue 的@符号的, 简单修改attr和starTag的正则表达式即可
- {{item}}:{{index}}
window.HTMLParser(document.querySelector('#app').outerHTML,(function(){
var obj = {};
['start','end','comment','chars'].forEach(x=>{
obj[x] = function (...args){
console.log(x,args)
}
})
return obj
})())
浏览器输出结果
3. 使用 HTMLParser 来构建目标数据结构
3.1 以构建 XML 为例来展示 options 配置
function HTMLtoXML(html) {
var results = "";
HTMLParser(html, {
start: function (tag, attrs, unary) {
results += "
for (var i = 0; i < attrs.length; i++)
results += " " + attrs[i].name + '="' + attrs[i].escaped + '"';
results += ">";
},
end: tag => results += "" + tag + ">",
chars: text => results += text,
comment: text => results += ""
});
return results;
}
HTMLtoXML测试结果
3.2 使用 HTMLParser 构建 AST
最简单的版本, 不考虑标签不闭合带来的错误, 同时保留空白节点
function HTMLtoAST(html){
var root = null
var stack = []
HTMLParser(html,{
start(tag,attrs,unary){
var element = { tag, attrs, children:[], type:1 }
if(!root){
root = element
}
if(stack.length){
var currentParent = stack[stack.length-1]
currentParent.children.push(element)
element.parent = currentParent
}
if(!unary){
stack.push(element)
}
},
end(){
stack.pop()
},
chars(text){
var currentParent = stack[stack.length - 1]
currentParent.children.push({ type: 2, text })
}
})
return root
}
如果要考虑不闭合标签的错误恢复, 例如
=>
实际上, 这件事已经在前面的160行的 HTMLParser 的parseStartTag函数中处理过了.
也可以参考 vue 的parseStartTag和parseEndTag函数
function handleStartTag (match) {
const tagName = match.tagName
const unarySlash = match.unarySlash
if (expectHTML) {
if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
parseEndTag(lastTag)
}
if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
parseEndTag(tagName)
}
}
//...
}
关于什么是 isNonPhrasingTag , 即不允许藏在
3.3 构建浏览器 DOM
function HTMLtoDOM(html) {
var doc = window.document
var stack = []
var curParentNode,root
HTMLParser(html, {
start: function (tagName, attrs, unary) {
var elem = doc.createElement(tagName);
for (var attr in attrs)
elem.setAttribute(attrs[attr].name, attrs[attr].value);
if (curParentNode && curParentNode.appendChild)
curParentNode.appendChild(elem);
if (!root) root = elem
if (!unary) {
stack.push(elem);
curParentNode = elem;
}
},
end: function (tag) {
curParentNode = stack.pop();
},
chars: function (text) {
curParentNode.appendChild(doc.createTextNode(text));
},
comment: function (text) {
// create comment node
}
});
return root;
};
HTMLtoDOM 测试结果,
其中settAttribute('@click',"clickItem(index)")会报错, 所以把@click临时改成了onclick
HTML.parser和正则解析,HTMLParser 的实现和使用相关推荐
- HTML.parser和正则解析,正则表达式+HTMLParser使用详.ppt
================================ getText:/body================================ getText: ============ ...
- 数据解析学习笔记(正则解析、bs4解析、xpath解析)
聚焦爬虫:爬取页面中指定的页面内容. - 编码流程: - 指定url - 发起请求 - 获取响应数据 - 数据解析 - 持久化存储 数据解析分类: 正则 bs4 xpath(***) 数据解析原理概述 ...
- mysql binlog查看工具_【使用 Rust 写 Parser】4. 解析 binlog
系列所有文章 PrivateRookie:[使用 Rust 写 Parser]1. 初识 nomzhuanlan.zhihu.com PrivateRookie:[使用 Rust 写 Parser] ...
- php正则循环,PHP正则解析多重循环模板示例
本文实例讲述了PHP正则解析多重循环模板.分享给大家供大家参考,具体如下: $str = "客户您好,为您推荐以下项目:(crm{项目2:项目名称} (crm{项目3:项目名称}crm) c ...
- 冰冻三尺,非一日之寒。数据解析——正则解析(1)
正则解析爬取图片 因为讲的是正则解析,而不是正则表达式,所以我就默认大家会正则表达式了.最多在这里给大家看一下正则的语法. 这也是我从B站上截图截下来的.因为我自己都是学的半懂不懂的,实在没那脸说在这 ...
- python爬虫正则解析及xpath解析,lxml解析库
正则解析模块re re模块使用流程 方法一 r_list=re.findall('正则表达式',html,re.S) 方法二 # 1.创建正则编译对象 pattern = re.compile('正则 ...
- html.parser python_python模块之HTMLParser
HTMLParser是python用来解析html的模块.它可以分析出html里面的标签.数据等等,是一种处理html的简便途径. HTMLParser采用的是一种事件驱动的模式,当HTMLParse ...
- StarRocks Parser 源码解析
导读:欢迎来到 StarRocks 源码解析系列文章,我们将为你全方位揭晓 StarRocks 背后的技术原理和实践细节,助你逐步了解这款明星开源数据库产品.本期将主要介绍 StarRocks Par ...
- java歌词解析器_JAVA 正则解析歌词
//目录结构 //来看一下歌词内容 [00:00.00]领袖音乐分享平台 http://www.kugou.com [00:02.31]Pretty boy-M2M 英俊男孩 JY93653制作 [0 ...
最新文章
- linux自带的cd刻录,linux下刻录CDROM的命令
- Linux 守护进程的原理与实现
- Git clone时出现Please make sure you have the correct access rights and the repository exists.问题已解决。...
- 当我们不为了拿奖,而在做事情的时候
- Java nio Socket非阻塞模式
- 数据分析师的自我修养丨如何进阶为数据科学家
- MYQQ复活版 20220801
- 使用Retrofit和Rxjava实现外卖型二级列表购物车及进入地图显示位置信息
- 阴历阳历的相互转换(支持1900~2100年) 1
- Makefile使用总结
- 一群不想鸡娃的直男程序员,决定对一支笔下手了
- Cocos2d-x 2.0 百例精讲:如何让一个精灵跟随触点移动
- php+ajax上传文件
- 程序员找工作的个人经验及注意事项
- 简单脱壳教程笔记(2)---手脱UPX壳(1)
- 关于 未能加载文件或程序集“”或它的某一个依赖项。找到的程序集清单定义与程序集引用不匹配。 的解决办法
- μ1 - μ2的置信区间
- 计算机网络纸牌,wind纸牌合集
- hyper-v 无法移除问题
- 看C语言教学视频后第一次写博客
热门文章
- GUI设计禁忌 之一 ——使用了错误的控件
- 谷歌搜索留痕外推霸屏
- 雅虎将收购营销软件公司IndexTools
- java开发岗位招聘,涨姿势了!
- 计算机组装期末学生总结,期末教学总结
- wps如何保存最终状态_Word 2013文档设置最终状态和取消最终状态的方法
- MySql之length函数
- 如何解决git上传文件出错 [rejected] cjp -> cjp (fetch first)error: failed to push some refs to
- 全国大学生数学建模竞赛——大赛介绍与赛后总结
- java process.waitfor();,正确的调用系统命令——为Process.waitFor设置超时以及其他 | 学步园...