




已阅读5页,还剩14页未读, 继续免费阅读
版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
基本的HTML文本解析器的设计和实现(C/C+源码),图文并茂作者:庄晓立 (liigo)日期:2011-1-19原创链接:/liigo/archive/2011/01/19/6153829.aspx转载请保持本文完整性,并注明出处:/liigo关键字:HTML,解析器(Parser),节点(Node),标签(Tag)这是进入2011年以来,本人(liigo)“重复发明轮子”系列博文中的最新一篇。本文主要探讨如何设计和实现一个基本的HTML文本解析器。众所周知,HTML是结构化文档(Structured Document),由诸多标签(等)嵌套形成的著名的文档对象模型(DOM,Document Object Model),是显而易见的树形多层次结构。如果带着这种思路看待HTML、编写HTML解析器,无疑将导致问题复杂化。不妨从另一视角俯视HTML文本,视其为一维线状结构:诸多单一节点的顺序排列。仔细审视任何一段HTML文本,以左右尖括号()为边界,会发现HTML文本被天然地分割为:一个标签(Tag),接一段普通文字,再一个标签,再一段普通文字 如下图所示:标签有两种,开始标签(如)和结束标签(),它们和普通文字一起,顺序排列,共同构成了HTML文本的全部。为了再次简化编程模型,我(liigo)继续将“开始标签”“结束标签”“普通文字”三者统一抽象归纳为“节点”(HtmlNode),相应的,“节点”有三种类型,要么是开始标签,要么是结束标签,要么是普通文字。现在,HTML在我们眼里更加单纯了,它就是“节点”的线性顺序组合,是一维的“节点”数组。如下图所示:HTML文本 = 节点1 + 节点2 + 节点3 + 在正式编码之前,先确定好“节点”的数据结构。作为“普通文字”节点,需要记录一个文本(text);作为“标签”节点,需要记录标签名称(tagName)、标签类型(tagType)、所有属性值(props);另外还要有个类型(type)以便区分该节点是普通文字、开始标签还是结束标签。这其中固然有些冗余信息,比如对标签来说不需要记录文本,对普通文字来说又不需要记录标签名称、属性值等,不过无伤大雅,简洁的编程模型是最大的诱惑。用C/C+语言语法表示如下:view plaincopy to clipboardprint?1. enumHtmlNodeType2. 3. NODE_UNKNOWN=0,4. NODE_START_TAG,5. NODE_CLOSE_TAG,6. NODE_CONTENT,7. ;8. enumHtmlTagType9. 10. TAG_UNKNOWN=0,11. TAG_A,TAG_DIV,TAG_FONT,TAG_IMG,TAG_P,TAG_SPAN,TAG_BR,TAG_B,TAG_I,TAG_HR,12. ;13. structHtmlNodeProp14. 15. WCHAR*szName;16. WCHAR*szValue;17. ;18. #defineMAX_HTML_TAG_LENGTH(15)19. structHtmlNode20. 21. HtmlNodeTypetype;22. HtmlTagTypetagType;23. WCHARtagNameMAX_HTML_TAG_LENGTH+1;24. WCHAR*text;25. intpropCount;26. HtmlNodeProp*props;27. ;具体到编写程序代码,要比想象中容易的多。编码的核心要点是,以左右尖括号()为边界自然分割标签和普通文字。左右尖括号之间的当然是标签节点(开始标签或结束标签),左尖括号()之后(直到后一个左尖括号或结尾)的显然是普通文字节点。区分开始标签或结束标签的关键点是,看左尖括号()后面第一个非空白字符是否为/。对于开始标签,在标签名称后面,间隔至少一个空白字符,可能会有形式为“key1=value1 key2=value2 key3”的属性表,关于属性表,后文有专门的函数负责解析。此外有一点要注意,属性值一般有引号括住,引号内出现的左右尖括号应该不被视为边界分隔符。下面就是负责把HTML文本解析为一个个节点(HtmlNode)的核心代码(不足百行,够精简吧):view plaincopy to clipboardprint?1. voidHtmlParser:ParseHtml(constWCHAR*szHtml)2. 3. m_html=szHtml?szHtml:L;4. freeHtmlNodes();5. if(szHtml=NULL|*szHtml=L0)return;6. 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=!bInQuotes;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;29. pNode-text=duplicateStrUtill(s,L)34. 35. if(ps)36. 37. /AddHtmlTagNode38. pNode=NewHtmlNode();39. while(isspace(*s)s+;40. pNode-type=(*s!=L/?NODE_START_TAG:NODE_CLOSE_TAG);41. if(*s=L/)s+;42. copyStrUtill(pNode-tagName,MAX_HTML_TAG_LENGTH,s,L,true);43. /处理自封闭的结点,如,删除tagName中可能会有的/字符44. /自封闭的结点的type设置为NODE_START_TAG应该可以接受(否则要引入新的NODE_STARTCLOSE_TAG)45. inttagNamelen=wcslen(pNode-tagName);46. if(pNode-tagNametagNamelen-1=L/)47. pNode-tagNametagNamelen-1=L0;48. /处理结点属性49. for(inti=0;itagNamei=L/第一个空格后面跟的是属性列表52. |pNode-tagNamei=L=)/扩展支持这种格式:,等效于53. 54. pNode-tagNamei=L0;55. WCHAR*props=(pNode-tagNamei=L?s+i+1:s);56. pNode-text=duplicateStrUtill(props,L,true);57. intnodeTextLen=wcslen(pNode-text);58. if(pNode-textnodeTextLen-1=L/)/去掉最后可能会有的/字符,如这种情况:59. pNode-textnodeTextLen-1=L0;60. parseNodeProps(pNode);/parseprops61. break;62. 63. 64. 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,-1);76. 77. #ifdef_DEBUG78. dumpHtmlNodes();/justfortest79. #endif80. 下面是负责解析“开始标签”属性表文本(形如“key1=value1 key2=value2 key3”)的代码,parseNodeProps(),核心思路是按空格和等号字符进行分割属性名和属性值,由于想兼容HTML4.01及以前的不标准的属性表写法(如没有=号也没有属性值),颇费周折:view plaincopy to clipboardprint?1. /virtual2. voidHtmlParser:parseNodeProps(HtmlNode*pNode)3. 4. if(pNode=NULL|pNode-propCount0|pNode-text=NULL)5. return;6. WCHAR*p=pNode-text;7. WCHAR*ps=NULL;8. CMemmem;9. boolinQuote1=false,inQuote2=false;10. WCHARc;11. while(c=*p)12. 13. if(c=L)14. 15. inQuote1=!inQuote1;16. 17. elseif(c=L)18. 19. inQuote2=!inQuote2;20. 21. if(!inQuote1&!inQuote2)&(c=L|c=Lt|c=L=)22. 23. if(ps)24. 25. mem.AddPointer(duplicateStrAndUnquote(ps,p-ps);26. ps=NULL;27. 28. if(c=L=)29. mem.AddPointer(NULL);30. 31. else32. 33. if(ps=NULL)34. ps=p;35. 36. p+;37. 38. if(ps)39. mem.AddPointer(duplicateStrAndUnquote(ps,p-ps);40. mem.AddPointer(NULL);41. mem.AddPointer(NULL);42. WCHAR*pp=(WCHAR*)mem.GetPtr();43. CMemprops;44. for(inti=0,n=mem.GetSize()/sizeof(WCHAR*)-2;ipropCount=props.GetSize()/sizeof(WCHAR*)/2;56. pNode-props=(HtmlNodeProp*)props.Detach();57. 根据标签名称取标签类型的getHtmlTagTypeFromName()方法,就非常直白了,查表,逐一识别:view plaincopy to clipboardprint?1. /virtual2. HtmlTagTypeHtmlParser:getHtmlTagTypeFromName(constWCHAR*szTagName)3. 4. /todo:useshashmap5. structN2TconstWCHAR*name;HtmlTagTypetype;6. 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. ;19. for(inti=0,count=sizeof(n2tTable)/sizeof(n2tTable0);iname,szTagName)=0)23. returnp-type;24. 25. returnTAG_UNKNOWN;26. 请注意,上文负责解析属性表的parseNodeProps()函数,和负责识别标签名称的getHtmlTagTypeFromName()函数,都是虚函数(virtual method)。我(liigo)这么设计是有深意的,给使用者留下了很大的定制空间,可以自由发挥。例如,通过在子类中覆盖/覆写(override)parseNodeProps()方法,可以采用更好的解析算法,或者干脆不做任何处理以提高HTML解析效率将来某一时间可以调用基类同名函数专门解析特定标签的属性表;例如,通过在子类中覆盖/覆写(override)getHtmlTagTypeFromName()方法,使用者可以选择识别跟多的标签名称(包括自定义标签),或者识别更少的标签名称,甚至不识别任何标签名称(以便提高解析效率)。以编写网络爬虫程序为实例,它多数情况下通常只需识别标签及其属性就足够了,没必要浪费CPU运算去识别其它标签、解析其他标签属性。至于HTML文本解析器的用途,我目前想到的有:用于HTML格式检查或规范化,用于重新排版HTML文本,用于编写网络爬虫程序/搜索引擎,用于基于HTML模板的动态网页生成,用于HTML网页渲染前的基础解析,等等。下面附上完整源码,仅供参考,欢迎指正。HtmlParser.h:view plaincopy to clipboardprint?1. #includecommon.h2. /HtmlParser类,用于解析HTML文本3. /byliigo,20104. enumHtmlNodeType5. 6. NODE_UNKNOWN=0,7. NODE_START_TAG,8. NODE_CLOSE_TAG,9. NODE_CONTENT,10. NODE_SOFT_LINE,11. ;12. enumHtmlTagType13. 14. TAG_UNKNOWN=0,15. TAG_A,TAG_DIV,TAG_FONT,TAG_IMG,TAG_P,TAG_SPAN,TAG_BR,TAG_B,TAG_I,TAG_HR,16. TAG_COLOR,TAG_BGCOLOR,/非标准HTML标签,可以这样使用:,等效于17. ;18. structHtmlNodeProp19. 20. WCHAR*szName;21. WCHAR*szValue;22. ;23. #defineMAX_HTML_TAG_LENGTH(15)24. structHtmlNode25. 26. HtmlNodeTypetype;27. HtmlTagTypetagType;28. WCHARtagNameMAX_HTML_TAG_LENGTH+1;29. WCHAR*text;30. intpropCount;31. HtmlNodeProp*props;32. ;33. classHtmlParser34. 35. friendclassHTMLView;36. public:37. HtmlParser()38. public:39. /html40. voidParseHtml(constWCHAR*szHtml);41. constWCHAR*GetHtml()constreturnm_html.GetText();42. /nodes43. unsignedintgetHtmlNodeCount();44. HtmlNode*getHtmlNodes();45. /props46. constHtmlNodeProp*getNodeProp(constHtmlNode*pNode,constWCHAR*szPropName);47. constWCHAR*getNodePropStringValue(constHtmlNode*pNode,constWCHAR*szPropName,constWCHAR*szDefaultValue=NULL);48. intgetNodePropIntValue(constHtmlNode*pNode,constWCHAR*szPropName,intdefaultValue=0);49. protected:50. /允许子类覆盖,以便识别更多结点(提高解析质量),或者识别更少结点(提高解析速度)51. virtualHtmlTagTypegetHtmlTagTypeFromName(constWCHAR*szTagName);52. public:53. /允许子类覆盖,以便更好的解析节点属性,或者干脆不解析节点属性(提高解析速度)54. virtualvoidparseNodeProps(HtmlNode*pNode);/todo:makeprotected,aftertesting55. private:56. HtmlNode*NewHtmlNode();57. voidfreeHtmlNodes();58. voiddumpHtmlNodes();59. private:60. CMemm_HtmlNodes;61. CMStringm_html;62. ;63. /一些文本处理函数64. WCHAR*duplicateStr(constWCHAR*pSrc,unsignedintnChar);65. voidfreeDuplicatedStr(WCHAR*p);66. unsignedintcopyStr(WCHAR*pDest,unsignedintnDest,constWCHAR*pSrc,unsignedintnChar);HtmlParser.cpp:view plaincopy to clipboardprint?1. #includeHtmlParser.h2. /HtmlParser类,用于解析HTML文本3. /byliigo,20104. constWCHAR*wcsnchr(constWCHAR*pStr,intlen,WCHARc)5. 6. constWCHAR*p=pStr;7. while(1)8. 9. if(*p=c)returnp;10. p+;11. if(p-pStr)=len)break;12. 13. returnNULL;14. 15. constWCHAR*getFirstUnquotedChar(constWCHAR*pStr,WCHARendcahr)16. 17. WCHARc;18. constWCHAR*p=pStr;19. boolinQuote1=false,inQuote2=false;/inQuote1,inQuote220. while(c=*p)21. 22. if(c=L)23. 24. inQuote1=!inQuote1;25. 26. elseif(c=L)27. 28. inQuote2=!inQuote2;29. 30. if(!inQuote1&!inQuote2)31. 32. if(c=endcahr)returnp;33. 34. p+;35. 36. returnNULL;37. 38. /nDestandnCharcanby-139. unsignedintcopyStr(WCHAR*pDest,unsignedintnDest,constWCHAR*pSrc,unsignedintnChar)40. 41. if(pDest=NULL|nDest=0)42. return0;43. if(pSrc=NULL)44. 45. pDest0=L0;46. return0;47. 48. if(nChar=(unsignedint)-1)49. nChar=wcslen(pSrc);50. if(nCharnDest)51. nChar=nDest;52. memcpy(pDest,pSrc,nChar*sizeof(WCHAR);53. pDestnChar=L0;54. returnnChar;55. 56. intcopyStrUtill(WCHAR*pDest,unsignedintnDest,constWCHAR*pSrc,WCHARendchar,boolignoreEndCharInQuoted)57. 58. if(nDest=0)return0;59. pDest0=L0;60. constWCHAR*pSearched=(ignoreEndCharInQuoted?getFirstUnquotedChar(pSrc,endchar):wcschr(pSrc,endchar);61. if(pSearched=pSrc)return0;62. returncopyStr(pDest,nDest,pSrc,pSearched-pSrc);63. 64. /nCharcanbe-165. WCHAR*duplicateStr(constWCHAR*pSrc,unsignedintnChar)66. 67. if(nChar=(unsignedint)-1)68. nChar=wcslen(pSrc);69. WCHAR*pNew=(WCHAR*)malloc(nChar+1)*sizeof(WCHAR);70. copyStr(pNew,-1,pSrc,nChar);71. returnpNew;72. 73. WCHAR*duplicateStrUtill(constWCHAR*pSrc,WCHARendchar,boolignoreEndCharInQuoted)74. 75. constWCHAR*pSearched=(ignoreEndCharInQuoted?getFirstUnquotedChar(pSrc,endchar):wcschr(pSrc,endchar);76. if(pSearched=pSrc)returnNULL;77. intn=pSearched-pSrc;78. returnduplicateStr(pSrc,n);79. 80. voidfreeDuplicatedStr(WCHAR*p)81. 82. if(p)free(p);83. 84. HtmlNode*HtmlParser:NewHtmlNode()85. 86. staticcharstaticHtmlNodeTemplatesizeof(HtmlNode)=0;87. /*88. staticHtmlNodestaticHtmlNodeTemplate;/=0;89. staticHtmlNodeTemplate.type=NODE_UNKNOWN;90. staticHtmlNodeTemplate.tagName0=L0;91. staticHtmlNodeTemplate.text=NULL;92. */93. m_HtmlNodes.Append(staticHtmlNodeTemplate,sizeof(HtmlNode);94. HtmlNode*pNode=(HtmlNode*)(m_HtmlNodes.GetPtr()+m_HtmlNodes.GetSize()-sizeof(HtmlNode);95. returnpNode;96. 97. voidHtmlParser:ParseHtml(constWCHAR*szHtml)98. 99. m_html=szHtml?szHtml:L;100. freeHtmlNodes();101. if(szHtml=NULL|*szHtml=L0)return;102. WCHAR*p=(WCHAR*)szHtml;103. WCHAR*s=(WCHAR*)szHtml;104. HtmlNode*pNode=NULL;105. WCHARc;106. boolbInQuotes=false;107. while(c=*p)108. 109. if(c=L)110. 111. bInQuotes=!bInQuotes;112. p+;continue;113. 114. if(bInQuotes)115. 116. p+;continue;117. 118. if(c=Ls)121. 122. /AddTextNode123. pNode=NewHtmlNode();124. pNode-type=NODE_CONTENT;125. pNode-text=duplicateStrUtill(s,L)130. 131. if(ps)132. 133. /AddHtmlTagNode134. pNode=NewHtmlNode();135. while(isspace(*s)s+;136. pNode-type=(*s!=L/?NODE_START_TAG:NODE_CLOSE_TAG);137. if(*s=L/)s+;138. copyStrUtill(pNode-tagName,MAX_HTML_TAG_LENGTH,s,L,true);139. /处理自封闭的结点,如,删除tagName中可能会有的/字符140. /自封闭的结点的type设置为NODE_START_TAG应该可以接受(否则要引入新的NODE_STARTCLOSE_TAG)141. inttagNamelen=wcslen(pNode-tagName);142. if(pNode-tagNametagNamelen-1=L/)143. pNode-tagNametagNamelen-1=L0;144. /处理结点属性145. for(inti=0;itagNamei=L/第一个空格后面跟的是属性列表148. |pNode-tagNamei=L=)/扩展支持这种格式:,等效于149. 150. pNode-tagNamei=L0;151. WCHAR*props=(pNode-tagNamei=L?s+i+1:s);152. pNode-text=duplicateStrUtill(props,L,true);153. intnodeTextLen=wcslen(pNode-text);154. if(pNode-textnodeTextLen-1=L/)/去掉最后可能会有的/字符,如这种情况:155. pNode-textnodeTextLen-1=L0;156. parseNodeProps(pNode);/parseprops157. break;158. 159. 160. pNode-tagType=getHtmlTagTypeFromName(pNode-tagName);161. 162. s=p+1;163. 164. p+;165. 166. if(ps)167. 16
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 入职安全生产培训内容课件
- 重庆混凝土管理办法
- 集团相关方管理办法
- 拆迁安置补偿委托合同6篇
- 企业挂牌安全培训课件
- 纪检办案经费管理办法
- 社区私房占用管理办法
- 手术增强现实临床验证-洞察及研究
- 小学法律知识竞赛试题(附答案)
- 2025年应聘书、入职表可视为合同文件吗
- 海天注塑机全参数DOC资料全
- Test Plan Template:测试计划模板
- (完整版)三年级下册数学开学第一课ppt
- 苏教版四年级科学上册全册课件
- JJF1101-2019环境试验设备温度、湿度校准规范-(高清现行)
- 人教版新教材高中物理实验汇总及答案详解
- 中班语言《大狮子和小老鼠》课件
- TSG11-2020 锅炉安全技术规程
- 资助业务工作培训
- 《足球运动发展史》PPT课件
- IPQAM调制器操作说明书(共36页)
评论
0/150
提交评论