




版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
1、基本的HTML文本解析器的设计和实现(C/C+源码),图文并茂作者:庄晓立(liigo)日期:2011-1-19原创链接: HYPERLINK /liigo/archive/2011/01/19/6153829.aspx /liigo/archive/2011/01/19/6153829.aspx转载请保持本文完整性,并注明出处: HYPERLINK /liigo /liigo关键字:HTML,解析器(Parser),节点(Node),标签(Tag)这是进入2011年以来,本人(liigo)“重复发明轮子系列博文中的最新一篇。本文主要探讨如何设计和实现一个基本的HTML文本解析器。众所周知,H
2、TML是结构化文档(StructuredDocument),由诸多标签(vp等)嵌套形成的著名的文档对象模型(DOM,DocumentObjectModel),是显而易见的树形多层次结构。如果带着这种思路看待HTML、编写HTML解析器,无疑将导致问题复杂化。不妨从另一视角俯视HTML文本,视其为一维线状结构:诸多单一节点的顺序排列。仔细审视任何一段HTML文本,以左右尖括号(v和)为边界,会发现HTML文本被天然地分割为:一个标签(Tag),接一段普通文字,再一个标签,再一段普通文字如下图所示:标签有两种,开始标签(如vp)和结束标签(v/p),它们和普通文字一起,顺序排列,共同构成了HTM
3、L文本的全部。为了再次简化编程模型,我(liigo)继续将开始标签”结束标签”普通文字三者统一抽象归纳为节点(HtmlNode),相应的,节点有三种类型,要么是开始标签,要么是结束标签,要么是普通文字。现在,HTML在我们眼里更加单纯了,它就是节点的线性顺序组合,是一维的节点数组。如下图所示:HTML文本=节点1+节点2+节点3+节点1节点2节点3节点4节点5节点6当我:span着的时候-亠i在正式编码之前,先确定好“节点”的数据结构。作为“普通文字”节点,需要记录一个文本(text);作为标签”节点,需要记录标签名称(tagName)、标签类型(tagType)、所有属性值(props);另
4、外还要有个类型(type)以便区分该节点是普通文字、开始标签还是结束标签。这其中固然有些冗余信息,比如对标签来说不需要记录文本,对普通文字来说又不需要记录标签名称、属性值等,不过无伤大雅,简洁的编程模型是最大的诱惑。用C/C+语言语法表示如下:viewplaincopytoclipboardprint?enumHtmlNodeTypeNODE_UNKNOWN=0,NODE_START_TAG,NODE_CLOSE_TAG,NODE_CONTENT,TOC o 1-5 h z;enumHtmlTagTypeTAG_UNKNOWN=0,TAG_A,TAG_DIV,TAG_FONT,TAG_IMG,
5、TAG_P,TAG_SPAN,TAG_BR,TAG_B,TAG_I,TAG_HR,;structHtmlNodePropWCHAR*szName;WCHAR*szValue;#defineMAX_HTML_TAG_LENGTH(15)structHtmlNodeHtmlNodeTypetype;HtmlTagTypetagType;WCHARtagNameMAX_HTML_TAG_LENGTH+1;WCHAR*text;intpropCount;HtmlNodeProp*props;具体到编写程序代码,要比想象中容易的多。编码的核心要点是,以左右尖括号(和)为边界自然分割标签和普通文字。左右尖
6、括号之间的当然是标签节点(开始标签或结束标签),左尖括号()之前(直到前一个右尖括号或开头)、右尖括号()之后(直到后一个左尖括号或结尾)的显然是普通文字节点。区分开始标签或结束标签的关键点是,看左尖括号()后面第一个非空白字符是否为/。对于开始标签,在标签名称后面,间隔至少一个空白字符,可能会有形式为“keyl=valuelkey2二value2key3的属性表,关于属性表,后文有专门的函数负责解析。此外有一点要注意,属性值一般有引号括住,引号内出现的左右尖括号应该不被视为边界分隔符。下面就是负责把HTML文本解析为一个个节点(HtmlNode)的核心代码(不足百行,够精简吧):viewpl
7、aincopytoclipboardprint?1.voidHtmlParser:ParseHtml(constWCHAR*szHtml)2.3.m_html=szHtml?szHtml:L;4.freeHtmlNodes();5.if(szHtml=NULL|*szHtml=L0)return6.WCHAR*p=(WCHAR*)szHtml;7.WCHAR*s=(WCHAR*)szHtml;8.HtmlNode*pNode=NULL;9.WCHARc;10.boolbInQuotes=false;11.while(c=*p)12.13.if(c=L)14.15.bInQuotes=!bInQ
8、uotes;16.p+;continue;17.18.if(bInQuotes)19.20.p+;continue;21.22.if(c=Ls)25.26./AddTextNode27.pNode=NewHtmlNode();28.pNode-type=NODE_CONTENT;2.63.64.pNode-text=duplicateStrUtill(s,L)if(ps)/AddHtmlTagNo
9、depNode=NewHtmlNode();while(isspace(*s)s+;pNode-type=(*s!=L/?NODE_START_TAG:NODE_CLOSE_TAG);if(*s=L/)s+;copyStrUtill(pNode-tagName,MAX_HTML_TAG_LENGTH,s,L,true);/处理自封闭的结点,如,删除tagName中可能会有的/字符/自封闭的结点的type设置为NODE_START_TAG应该可以接受(否则要引入新的NODE_STARTCLOSE_TAG)inttagNamelen=wcslen(pNode-tagName);if(pNode-t
10、agNametagNamelen-1=L/)pNode-tagNametagNamelen-1=L0;/处理结点属性for(inti=0;itagNamei=L/第一个空格后面跟的是属性列表|pNode-tagNamei=L=)/扩展支持这种格式:,等效于pNode-tagNamei=L0;WCHAR*props=(pNode-tagNamei=L?s+i+1:s);pNode-text=duplicateStrUtill(props,L,true);intnodeTextLen=wcslen(pNode-text);if(pNode-textnodeTextLen-1=L/)/去掉最后可能会
11、有的/字符,如这种情况:pNode-textnodeTextLen-1=L0;parseNodeProps(pNode);/parsepropsbreak;pNode-tagType=getHtmlTagTypeFromName(pNode-tagName);65.66.s=p+1;67.68.p+;69.70.if(ps)71.72./AddTextNode73.pNode=NewHtmlNode();74.pNode-type=NODE_CONTENT;75.pNode-text=duplicateStr(s,76.77.#ifdef_DEBUG78.dumpHtmlNodes();/ju
12、stfortest79.#endif80.-1);下面是负责解析开始标签”属性表文本(形如“key1=value1key2=value2key3”)的代码,parseNodeProps(),核心思路是按空格和等号字符进行分割属性名和属性值,由于想兼容HTML4.01及以前的不标准的属性表写法(如没有=号也没有属性值),颇费周折viewplaincopytoclipboardprint?/virtualvoidHtmlParser:parseNodeProps(HtmlNode*pNode)if(pNode=NULL|pNode-propCount0|pNode-text=NULL)return
13、;WCHAR*p=pNode-text;WCHAR*ps=NULL;CMemmem;boolinQuote1=false,inQuote2=false;WCHARc;while(c=*p)TOC o 1-5 h zif(c=L)inQuote1=!inQuote1;elseif(c=L)inQuote2=!inQuote2;7.if(!inQuote1&!inQuote2)&(c
14、=L|c=Lt|c=L=)if(ps)mem.AddPointer(duplicateStrAndUnquote(ps,p-ps);ps=NULL;if(c=L=)mem.AddPointer(NULL);elseif(ps=NULL)ps=p;p+;if(ps)mem.AddPointer(duplicateStrAndUnquote(ps,p-ps);mem.AddPointer(NULL);mem.AddPointer(NULL);WCHAR*pp=(WCHAR*)mem.GetPtr();CMemprops;for(inti=0,n=mem.GetSize()/sizeof(WCHAR
15、*)-2;ipropCount=props.GetSize()/sizeof(WCHAR*)/2;pNode-props=(HtmlNodeProp*)props.Detach();就非常直白了,查表,根据标签名称取标签类型的getHtmITagTypeFromName()方法,逐一识别:viewplaincopytoclipboardprint?/virtualHtmlTagTypeHtmlParser:getHtmlTagTypeFromName(constWCHAR*szTagName)/todo:useshashmapstructN2TconstWCHAR*name;HtmlTagTy
16、petype;staticN2Tn2tTable=7.8.LA,TAG_A,9.LFONT,TAG_FONT,10.LIMG,TAG_IMG,11.LP,TAG_P,12.LDIV,TAG_DIV,13.LSPAN,TAG_SPAN,14.LBR,TAG_BR,15.LB,TAG_B,16.LI,TAG_I,17.LHR,TAG_HR,18.;for(inti=0,count=sizeof(n2tTable)/sizeof(n2tTable0);iname,szTagName)=0)returnp-type;returnTAG_UNKNOWN;请注意,上文负责解析属性表的parseNodePr
17、ops()函数,和负责识别标签名称的getHtmITagTypeFromName()函数,都是虚函数(virtualmethod)。我(liigo)这么设计是有深意的,给使用者留下了很大的定制空间,可以自由发挥。例如,通过在子类中覆盖/覆写(override)parseNodeProps()方法,可以采用更好的解析算法,或者干脆不做任何处理以提高HTML解析效率将来某一时间可以调用基类同名函数专门解析特定标签的属性表;例如,通过在子类中覆盖/覆写(override)getHtmITagTypeFromName()方法,使用者可以选择识别跟多的标签名称(包括自定义标签),或者识别更少的标签名称,
18、甚至不识别任何标签名称(以便提高解析效率)。以编写网络爬虫程序为实例,它多数情况下通常只需识别vA标签及其属性就足够了,没必要浪费CPU运算去识别其它标签、解析其他标签属性。至于HTML文本解析器的用途,我目前想到的有:用于HTML格式检查或规范化,用于重新排版HTML文本,用于编写网络爬虫程序/搜索引擎,用于基于HTML模板的动态网页生成,用于HTML网页渲染前的基础解析,等等。面附上完整源码,仅供参考,欢迎指正。HtmlParser.h:viewplaincopytoclipboardprint?#includecommon.h/HtmlParser类,用于解析HTML文本/byliigo
19、,2010enumHtmlNodeTypeNODE_UNKNOWN=0,NODE_START_TAG,NODE_CLOSE_TAG,NODE_CONTENT,NODE_SOFT_LINE,;enumHtmlTagTypeTAG_UNKNOWN=0,TAG_A,TAG_DIV,TAG_FONT,TAG_IMG,TAG_P,TAG_SPAN,TAG_BR,TAG_B,TAG_I,TAG_HR,TAG_COLOR,TAG_BGCOLOR,/非标准HTML标签,可以这样使用:,等效于TOC o 1-5 h z;structHtmlNodePropWCHAR*szName;WCHAR*szValue;#
20、defineMAX_HTML_TAG_LENGTH(15)structHtmlNodeHtmlNodeTypetype;HtmlTagTypetagType;WCHARtagNameMAX_HTML_TAG_LENGTH+1;WCHAR*text;intpropCount;HtmlNodeProp*props;classHtmlParserfriendclassHTMLView;public:HtmlParser()public:/htmlvoidParseHtml(constWCHAR*szHtml);constWCHAR*GetHtml()constreturnm_html.GetText
21、();/nodesunsignedintgetHtmlNodeCount();HtmlNode*getHtmlNodes();/propsconstHtmlNodeProp*getNodeProp(constHtmlNode*pNode,constWCHAR*szPropName);constWCHAR*getNodePropStringValue(constHtmlNode*pNode,constWCHAR*szPropName,constWCHAR*szDefaultValue=NULL);intgetNodePropIntValue(constHtmlNode*pNode,constWC
22、HAR*szPropName,intdefaultValue=0);protected:允许子类覆盖,以便识别更多结点(提高解析质量),或者识别更少结点(提高解析速度)virtualHtmlTagTypegetHtmlTagTypeFromName(constWCHAR*szTagName);public:允许子类覆盖,以便更好的解析节点属性,或者干脆不解析节点属性(提高解析速度)virtualvoidparseNodeProps(HtmlNode*pNode);/todo:makeprotected,aftertestingprivate:HtmlNode*NewHtmlNode();voi
23、dfreeHtmlNodes();voiddumpHtmlNodes();private:CMemm_HtmlNodes;CMStringm_html;/一些文本处理函数WCHAR*duplicateStr(constWCHAR*pSrc,unsignedintnChar);voidfreeDuplicatedStr(WCHAR*p);unsignedintcopyStr(WCHAR*pDest,unsignedintnDest,constWCHAR*pSrc,unsignedintnChar);HtmlParser.cpp:viewplaincopytoclipboardprint?#inc
24、ludeHtmlParser.h/HtmlParser类,用于解析HTML文本/byliigo,20.4.45.46.constWCHAR*wcsnchr(constWCHAR*pStr,intlen,WCHARc)constWCHAR*p=pStr;while(1)if(*p=c)returnp;p+;if(p-pStr)=len)break;returnNULL
25、;constWCHAR*getFirstUnquotedChar(constWCHAR*pStr,WCHARendcahr)WCHARc;constWCHAR*p=pStr;boolinQuote1=false,inQuote2=false;/inQuote1,inQuote2while(c=*p)if(c=L)inQuote1=!inQuote1;elseif(c=L)inQuote2=!inQuote2;if(!inQuote1&!inQuote2)if(c=endcahr)returnp;p+;returnNULL;un/nDestandnCharcanby-1unsignedintco
26、pyStr(WCHAR*pDest,unsignedintnDest,constWCHAR*pSrc,signedintnChar)if(pDest=NULL|nDest=0)return0;if(pSrc=NULL)pDest0=L0;return0;0.51.if(nChar=(unsignedint)-1)nChar=wcslen(pSrc);if(nCharnDest)nChar=nDest;memcpy(pDest,pSrc,nChar*sizeof(WCHAR);pDestnChar=L0;returnnChar;intcopyStrUtill(WCHAR*pD
27、est,unsignedintnDest,constWCHAR*pSrc,WCHARendchar,boolignoreEndCharInQuoted)if(nDest=0)return0;pDest0=L0;constWCHAR*pSearched=(ignoreEndCharInQuoted?getFirstUnquotedChar(pSrc,endchar):wcschr(pSrc,endchar);if(pSearched=pSrc)return0;returncopyStr(pDest,nDest,pSrc,pSearched-pSrc);/nCharcanbe-1WCHAR*dup
28、licateStr(constWCHAR*pSrc,unsignedintnChar)if(nChar=(unsignedint)-1)nChar=wcslen(pSrc);WCHAR*pNew=(WCHAR*)malloc(nChar+1)*sizeof(WCHAR);copyStr(pNew,-1,pSrc,nChar);returnpNew;WCHAR*duplicateStrUtill(constWCHAR*pSrc,WCHARendchar,boolignoreEndCharInQuoted)constWCHAR*pSearched=(ignoreEndCharInQuoted?ge
29、tFirstUnquotedChar(pSrc,endchar):wcschr(pSrc,endchar);if(pSearched=pSrc)returnNULL;intn=pSearched-pSrc;returnduplicateStr(pSrc,n);TOC o 1-5 h zvoidfreeDuplicatedStr(WCHAR*p)if(p)free(p);HtmlNode*HtmlParser:NewHtmlNode()staticcharstaticHtmlNodeTemplatesizeof(HtmlNode)=0;6.
30、00101102103104105106107108109110111112113114115116117118119120121122123124125126127128129/*staticHtmlNodestaticHtmlNodeTemplate;/=0;staticHtmlNodeTemplate.type=NODE_UNKNOWN;staticHtmlNodeTemplate.tagName0=L0;staticHtmlNodeTemplate.text=NULL;*/m_HtmlNodes.Append(staticHtmlNodeTemplate,sizeo
31、f(HtmlNode);HtmlNode*pNode=(HtmlNode*)(m_HtmlNodes.GetPtr()+m_HtmlNodes.GetSize()-sizeof(HtmlNode);returnpNode;voidHtmlParser:ParseHtml(constWCHAR*szHtml)m_html=szHtml?szHtml:L;freeHtmlNodes();if(szHtml=NULL|*szHtml=L0)return;WCHAR*p=(WCHAR*)szHtml;WCHAR*s=(WCHAR*)szHtml;HtmlNode*pNode=NULL;WCHARc;b
32、oolbInQuotes=false;while(c=*p)if(c=L)bInQuotes=!bInQuotes;p+;continue;if(bInQuotes)p+;continue;if(c=Ls)/AddTextNodepNode=NewHtmlNode();pNode-type=NODE_CONTENT;pNode-text=duplicateStrUtill(s,L)36.if(ps)/AddHtmlTagNodepNode=NewHtmlNode();while(isspace(*s)s+;pNode-t
33、ype=(*s!=L/?NODE_START_TAG:NODE_CLOSE_TAG);137.138.if(*s=L/)s+;copyStrUtill(pNode-tagName,MAX_HTML_TAG_LENGTH,s,L,Ltrue);139.140.入新的NODE47.表/处理自封闭的结点,如,删除tagName中可能会有的/字符/自封闭的结点的type设置为NODE_START_TAG应该可以接受(否则要引STARTCLOSE_TAG)inttagNamelen=wcslen(pNode-tagName);if(pNode-tagNa
34、metagNamelen-1=L/)pNode-tagNametagNamelen-1=L0;/处理结点属性for(inti=0;itagNamei=L/第一个空格后面跟的是属性列L|pNode-tagNamei=L=)/扩展支持这种格148.式:,等效于149.150.151.pNode-tagNamei=L0;WCHAR*props=(pNode-tagNamei=L1:s);152.pNode-text=duplicateStrUtill(props,L,true);61.162.intnodeTextLen=wcslen(pNode-t
35、ext);if(pNode-textnodeTextLen-1=L/)/去掉最后可能会有的/字符,如这种情况:pNode-textnodeTextLen-1=L0;parseNodeProps(pNode);/parsepropsbreak;pNode-tagType=getHtmlTagTypeFromName(pNode-tagName);s=p+1;p+;92.193.19
36、0006.207.208.if(ps)/AddTextNodepNode=NewHtmlNode();pNode-type=NODE_CONTENT;pNode-text=duplicateStr(s,-1);#ifdef_DEBUGdumpHtmlNodes();/justfortest#endifunsignedintHtmlParser:getHtmlNodeCount()return(m_HtmlNodes.GetSize()/sizeof(HtmlNode);HtmlNode*HtmlParse
37、r:getHtmlNodes()return(HtmlNode*)m_HtmlNodes.GetPtr();voidHtmlParser:freeHtmlNodes()HtmlNode*pNodes=getHtmlNodes();for(inti=0,count=getHtmlNodeCount();itext)freeDuplicatedStr(pNode-text);if(pNode-props)MFreeMemory(pNode-props);/see:CMem:Allocm_HtmlNodes.Empty();/virtualHtmlTagTypeHtmlParser:getHtmlT
38、agTypeFromName(constWCHAR*szTagName)/todo:useshashmapstructN2TconstWCHAR*name;HtmlTagTypetype;staticN2Tn2tTable=LA,TAG_A,LFONT,TAG_FONT,LIMG,TAG_IMG,LP,TAG_P,LDIV,TAG_DIV,210.LSPAN,TAG_SPAN,211.LBR,TAG_BR,212.LB,TAG_B,213.LI,TAG_I,214.LHR,TAG_HR,215.LCOLOR,TAG_COLOR,216.LBGCOLOR,TAG_BGCOLOR217.;for(
39、inti=0,count=sizeof(n2tTable)/sizeof(n2tTable0);iname,szTagName)=0)returnp-type;returnTAG_UNKNOWN;voidskipSpaceChars(WCHAR*&p)(p)while(isspace(*p)p+;constWCHAR*nextUnqotedSpaceChar(constWCHAR*p)constWCHAR*r=getFirstUnquotedChar(p,L);if(!r)r=getFirstUnquotedChar(p,Lt);returnr;TOC o 1-5 h zconstWCHAR*
40、duplicateStrAndUnquote(constWCHAR*str,unsignedintnChar)if(nChar1&(str0=L&strnChar-1=L)|(str0=L&strnChar-1=L)str+;nChar-=2;returnduplicateStr(str,nChar);/virtualvoidHtmlParser:parseNodeProps(HtmlNode*pNode)if(pNode=NULL|pNode-propCount0|pNode-text=NULL)252253254255256257258259260261262263264265266267
41、268269270271272273274275276277278279280281282283284285286287288289290291292293294return;WCHAR*p=pNode-text;WCHAR*ps=NULL;CMemmem;boolinQuote1=false,inQuote2=false;WCHARc;while(c=*p)if(c=L)inQuote1=!inQuote1;elseif(c=L)inQuote2=!inQuote2;L=if(!inQuote1&!inQuote2)&(c=L|c=Lt|c=)if(ps)mem.AddPointer(dup
42、licateStrAndUnquote(ps,p-ps);ps=NULL;if(c=L=)mem.AddPointer(NULL);elseif(ps=NULL)ps=p;p+;if(ps)mem.AddPointer(duplicateStrAndUnquote(ps,p-ps);mem.AddPointer(NULL);mem.AddPointer(NULL);WCHAR*pp=(WCHAR*)mem.GetPtr();CMemprops;for(inti=0,n=mem.GetSize()/sizeof(WCHAR*)-2;ipropCount=props.GetSize()/sizeo
43、f(WCHAR*)/2;pNode-props=(HtmlNodeProp*)props.Detach();props.AddPointer(NULL);/propvlalueTOC o 1-5 h zconstHtmlNodeProp*HtmlParser:getNodeProp(constHtmlNode*pNode,constWCHAR*szPropName)if(pNode=NULL|pNode-propCount=0)returnNULL;for(inti=0;ipropCount;i+)HtmlNodeProp*prop=pNode-props+i;if(wcsicmp(prop-
44、szName,szPropName)=0)returnprop;returnNULL;constWCHAR*HtmlParser:getNodePropStringValue(constHtmlNode*pNode,constWCHAR*szPropName,constWCHAR*szDefaultValue/*=NULL*/)constHtmlNodeProp*pProp=getNodeProp(pNode,szPropName);if(pProp)returnpProp-szValue;elsereturnszDefaultValue;intHtmlParser:getNodePropIntValue(constHtmlNode*pNode,constWCHAR*szPropName,intdefaultValue/*=0*/)constHtmlNodeProp*pProp=getNodeProp(pNode,szPropName);if(pProp&pProp-szValue)return_wtoi(pProp-szValue);el
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 园艺师2024年考试需掌握的基础试题及答案
- 农艺师考试中的表现提升要素试题及答案
- 花艺师职业发展与自我提升策略试题及答案
- 产品分期合同样本
- 农艺师考试职业责任感试题及答案
- 家庭理财考试题及答案
- 2024年福建事业单位考试技能大赛的筹备和策划试题及答案
- 考生须知园艺师试题及答案
- 向2024年园艺师考试挑战的试题及答案
- 确保考前状态福建事业单位考试试题及答案
- T∕ZZB 2763-2022 汽车用底盘横向稳定杆
- 减速机生产工艺流程图
- 知识产权的国际保护完整版ppt全套教学教程课件(最新)
- 网络直播行业税收检查指引
- SAPERP_委外业务操作手册_v1.0
- 2022年上海公务员考试信息管理类专业真题
- 山东物业服务星级标准对照表x
- 喷塑车间员工培训课件
- 医疗废物管理工作督查记录表常用
- 主要安全设施一览表201603
- 成都社区居委会街道办信息一览表
评论
0/150
提交评论