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

})())

cd8dedf489c1?utm_campaign=maleskine

浏览器输出结果

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;

}

cd8dedf489c1?utm_campaign=maleskine

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;

};

cd8dedf489c1?utm_campaign=maleskine

HTMLtoDOM 测试结果,

其中settAttribute('@click',"clickItem(index)")会报错, 所以把@click临时改成了onclick

HTML.parser和正则解析,HTMLParser 的实现和使用相关推荐

  1. HTML.parser和正则解析,正则表达式+HTMLParser使用详.ppt

    ================================ getText:/body================================ getText: ============ ...

  2. 数据解析学习笔记(正则解析、bs4解析、xpath解析)

    聚焦爬虫:爬取页面中指定的页面内容. - 编码流程: - 指定url - 发起请求 - 获取响应数据 - 数据解析 - 持久化存储 数据解析分类: 正则 bs4 xpath(***) 数据解析原理概述 ...

  3. mysql binlog查看工具_【使用 Rust 写 Parser】4. 解析 binlog

    系列所有文章 PrivateRookie:[使用 Rust 写 Parser]1. 初识 nom​zhuanlan.zhihu.com PrivateRookie:[使用 Rust 写 Parser] ...

  4. php正则循环,PHP正则解析多重循环模板示例

    本文实例讲述了PHP正则解析多重循环模板.分享给大家供大家参考,具体如下: $str = "客户您好,为您推荐以下项目:(crm{项目2:项目名称} (crm{项目3:项目名称}crm) c ...

  5. 冰冻三尺,非一日之寒。数据解析——正则解析(1)

    正则解析爬取图片 因为讲的是正则解析,而不是正则表达式,所以我就默认大家会正则表达式了.最多在这里给大家看一下正则的语法. 这也是我从B站上截图截下来的.因为我自己都是学的半懂不懂的,实在没那脸说在这 ...

  6. python爬虫正则解析及xpath解析,lxml解析库

    正则解析模块re re模块使用流程 方法一 r_list=re.findall('正则表达式',html,re.S) 方法二 # 1.创建正则编译对象 pattern = re.compile('正则 ...

  7. html.parser python_python模块之HTMLParser

    HTMLParser是python用来解析html的模块.它可以分析出html里面的标签.数据等等,是一种处理html的简便途径. HTMLParser采用的是一种事件驱动的模式,当HTMLParse ...

  8. StarRocks Parser 源码解析

    导读:欢迎来到 StarRocks 源码解析系列文章,我们将为你全方位揭晓 StarRocks 背后的技术原理和实践细节,助你逐步了解这款明星开源数据库产品.本期将主要介绍 StarRocks Par ...

  9. java歌词解析器_JAVA 正则解析歌词

    //目录结构 //来看一下歌词内容 [00:00.00]领袖音乐分享平台 http://www.kugou.com [00:02.31]Pretty boy-M2M 英俊男孩 JY93653制作 [0 ...

最新文章

  1. linux自带的cd刻录,linux下刻录CDROM的命令
  2. Linux 守护进程的原理与实现
  3. Git clone时出现Please make sure you have the correct access rights and the repository exists.问题已解决。...
  4. 当我们不为了拿奖,而在做事情的时候
  5. Java nio Socket非阻塞模式
  6. 数据分析师的自我修养丨如何进阶为数据科学家
  7. MYQQ复活版 20220801
  8. 使用Retrofit和Rxjava实现外卖型二级列表购物车及进入地图显示位置信息
  9. 阴历阳历的相互转换(支持1900~2100年) 1
  10. Makefile使用总结
  11. 一群不想鸡娃的直男程序员,决定对一支笔下手了
  12. Cocos2d-x 2.0 百例精讲:如何让一个精灵跟随触点移动
  13. php+ajax上传文件
  14. 程序员找工作的个人经验及注意事项
  15. 简单脱壳教程笔记(2)---手脱UPX壳(1)
  16. 关于 未能加载文件或程序集“”或它的某一个依赖项。找到的程序集清单定义与程序集引用不匹配。 的解决办法
  17. μ1 - μ2的置信区间
  18. 计算机网络纸牌,wind纸牌合集
  19. hyper-v 无法移除问题
  20. 看C语言教学视频后第一次写博客

热门文章

  1. GUI设计禁忌 之一 ——使用了错误的控件
  2. 谷歌搜索留痕外推霸屏
  3. 雅虎将收购营销软件公司IndexTools
  4. java开发岗位招聘,涨姿势了!
  5. 计算机组装期末学生总结,期末教学总结
  6. wps如何保存最终状态_Word 2013文档设置最终状态和取消最终状态的方法
  7. MySql之length函数
  8. 如何解决git上传文件出错 [rejected] cjp -> cjp (fetch first)error: failed to push some refs to
  9. 全国大学生数学建模竞赛——大赛介绍与赛后总结
  10. java process.waitfor();,正确的调用系统命令——为Process.waitFor设置超时以及其他 | 学步园...