编程杂谈

注:有好久没更新了,也不是说没有学习,主要Js逆向,不适合发在网站上本文借鉴自:https://juejin.cn/post/6844903811601924103在此篇文章上进行了一点优化:处理标签多个属性值,增加属性值的兼容性说明JsinnerHTML方法是把文本型的HTML标签转换成DOM树,实现过程与解释器差不多,也算了解一下解释器解释器步骤词法分析->语法分析->解释执行词法分析词法分析的具体任务就是:把字符流变成token流。词法分析通常有两种方案:一种是状态机,一种是正则表达式。我们这里选择状态机。状态机的原理将整个HTML字符串进行遍历,每次读取一个字符,进行一次决策(决定出下一个字符处于哪个状态),当一个状态决策完成的token就会被存入到tokens里。<pclass="yy"id="xx">测试元素</p>对于上述节点信息,我们可以拆分出如下token开始标签:<p属性标签:class="yy"属性标签:id="xx"文本节点:测试元素结束标签:</p>词法分析函数说明封装开头函数functionHTMLLexicalParser(htmlString,tokenHandler){this.token=[];//存储已经分析完成的一个个tokenthis.tokens=[];//标签属性词法分析结束标志为处理标签多个属性添加this.attrFlag=0;//待处理的字符串this.htmlString=htmlString//处理函数tokens转换成树结构函数this.tokenHandler=tokenHandler}start函数start处理的比较简单,如果是<字符,表示开始标签或结束标签,因此我们需要下一个字符信息才能确定到底是哪一类token,所以返回tagState函数去进行再判断,否则认为是文本节点,返回文本状态函数HTMLLexicalParser.prototype.start=function(c){if(c==='<'){//表示开始标签或结束标签,所以需要进一步确认this.token.push(c)//记录tokenreturnthis.tagState}else{returnthis.textState(c)}}tagState、textState函数tagState根据下一个字符,判断进入开始标签状态还是结束标签状态,如果是/表示是结束标签,否则是开始标签textState用来处理每一个文本节点字符,遇到<表示得到一个完整的文本节点token。HTMLLexicalParser.prototype.tagState=function(c){this.token.push(c)if(c==='/'){//表示结束状态,返回结束处理函数returnthis.endTagState}else{//表示开始处理标签状态,接下来会有字母(开始)、空格(属性)、>(标签结束)returnthis.startTagState}}HTMLLexicalParser.prototype.textState=function(c){if(c==='<'){//表示文本状态处理完成,把此窗台存入tokens中this.emitToken('text',this.token.join(''));this.token=[]//置空token准备处理下一状态returnthis.start(c)}else{//还处理文本状态this.token.push(c)returnthis.textState}}emitToken、startTagState、endTagState函数emitToken用来将产生的完整token存储在tokens中,参数是token类型和值。startTagState用来处理开始标签,这里有三种情况:接下来的字符是字母,则认定依旧处于开始标签状态遇到空格,则认定开始标签态结束,接下来是处理属性遇到>同样认定为开始标签态结束,但接下来是处理新的节点信息endTagState用来处理结束标签,结束标签没有属性,因此只有两种情况:如果接下来的字符是字母,则认定依旧处于结束标签态遇到>同样认定为结束标签态结束,但接下来是处理新的节点信息HTMLLexicalParser.prototype.emitToken=function(type,value){varres={type,value}this.tokens.push(res)//流式处理this.tokenHandler&&this.tokenHandler(res)//存在则执行该函数}HTMLLexicalParser.prototype.startTagState=function(c){if(c.match(/[a-zA-Z]/)){//处理标签名状态this.token.push(c.toLowerCase())returnthis.startTagState}if(c===''){//标签名状态结束进入标签属性状态this.emitToken('startTag',this.token.join(''))this.token=[]returnthis.attrState}if(c==='>'){//标签结束状态进入开始分析状态this.emitToken('startTag',this.token.join(''))this.token=[]returnthis.start}}HTMLLexicalParser.prototype.endTagState=function(c){if(c.match(/[a-zA-Z]/)){//双标签结束时状态this.token.push(c.toLowerCase())returnthis.endTagState}if(c==='>'){//双标签结束时状态进入开始分析状态this.token.push(c)this.emitToken('endTag',this.token.join(''))this.token=[]returnthis.start}}attrStateattrState处理属性标签,也处理三种情形如果是字母、数字、等号、下划线、空格、中划线、冒号、分号,则认定为依旧处于属性标签态如果遇到引号,则表示遇到标签属性值,第二次遇到才表示一个标签属性结束(不代表标签状态结束),继续处理标签状态如果遇到>则认定为属性标签状态结束,接下来开始新的节点信息HTMLLexicalParser.prototype.attrState=function(c){if(c.match(/[a-zA-Z0-9=_\-\:;]/)){this.token.push(c)returnthis.attrState}if(c.match(/['"]/)){this.attrFlag=this.attrFlag+1;if(this.attrFlag==2){this.token.push(c)this.emitToken('attr',this.token.join(''))this.token=[]this.attrFlag=0;returnthis.attrState}this.token.push(c)returnthis.attrState}if(c==='>'){returnthis.start}}parse、getOutPutparse解析函数HTMLLexicalParser.prototype.parse=function(){varstate=this.start;for(varcofthis.htmlString.split('')){state=state.bind(this)(c)}}HTMLLexicalParser.prototype.getOutPut=function(){returnthis.tokens}测试词法分析varp=newHTMLLexicalParser('<divclass="xxyy"data="hh">测试并列元素的</div><pclass="pp"data="kk"style="display:none;">测试并列元素的</p>')p.parse()p.getOutPut()语法分析语法分析:就是把上一步分析的结果,处理成有层次的树结构定义树结构//语法分析functionElement(tagName){this.tagName=tagNamethis.attr={}this.childNodes=[]}functionText(value){this.value=value||''}处理词法分析结果思路通过上图分析结果很容易看出层次结构startTagtoken,push一个新节点elementendTagtoken,则表示当前节点处理完成,此时出栈一个节点,同时将该节点归入栈顶元素节点的childNodes属性,这里需要做个判断,如果出栈之后栈空了,表示整个节点处理完成,考虑到可能有平行元素,将元素push到stacks。attrtoken,直接写入栈顶元素的attr属性texttoken,由于文本节点的特殊性,不存在有子节点、属性等,就认定为处理完成。这里需要做个判断,因为文本节点可能是根级别的,判断是否存在栈顶元素,如果存在直接压入栈顶元素的childNodes属性,不存在push到stacks。functionHTMLSyntacticalParser(){this.stack=[]this.stacks=[]}HTMLSyntacticalParser.prototype.getOutPut=function(){returnthis.stacks}//一开始搞复杂了,合理利用基本数据结构真是一件很酷炫的事HTMLSyntacticalParser.prototype.receiveInput=function(token){varstack=this.stackconsole.log('token',token)if(token.type==='startTag'){stack.push(newElement(token.value.substring(1)))}elseif(token.type==='attr'){vart=token.value.split('=');//console.log('t',t);varkey=t[0].replace(/^\s*|\s*$/g,""),value=t[1].replace(/'|"/g,'')stack[stack.length-1].attr[key]=value}elseif(token.type==='text'){if(stack.length){stack[stack.length-1].childNodes.push(newText(token.value))}else{this.stacks.push(newText(token.value))}}elseif(token.type==='endTag'){varparsedTag=stack.pop()if(stack.length){stack[stack.length-1].childNodes.push(parsedTag)}else{this.stacks.push(parsedTag)}}console.log(stack);}测试语法分析结果varhtml='<divclass="xxyy"data="hh"><pclass="ss"data="ff"style="display:none;">嵌套</p></div><pclass="pp"data="kk"style="display:none;">并列</p>'varsyntacticalParser=newHTMLSyntacticalParser()varlexicalParser=newHTMLLexicalParser(html,syntacticalParser.receiveInput.bind(syntacticalParser))lexicalParser.parse()syntacticalParser.getOutPut()解释执行就是把上面的树结构,使用递归映射成真实的dom结构functionvdomToDom(array){varres=[]for(letitemofarray){res.push(handleDom(item))}returnres}functionhandleDom(item){if(iteminstanceofElement){varelement=document.createElement(item.tagName)for(letkeyinitem.attr){element.setAttribute(key,item.attr[key])}if(item.childNodes.length){for(leti=0;i<item.childNodes.length;i++){element.appendChild(handleDom(item.childNodes[i]))}}returnelement}elseif(iteminstanceofText){returndocument.createTextNode(item.value)}}封装函数functionhtml(element,htmlString){varsyntacticalParser=newHTMLSyntacticalParser()varlexicalParser=newHTMLLexicalParser(htmlString,syntacticalParser.receiveInput.bind(syntacticalParser))lexicalParser.parse()vardom=vdomToDom(syntacticalParser.getOutPut())varfragment=document.createDocumentFragment()dom.forEach(item=>{fragment.appendChild(item)})element.appendChild(fragment)}我修改过的代码地址:https://pan.bigdataboy.cn/s/MzmIB