众所周知,HTML是结构化文档(Structured Document),由诸多标签(
下面就是负责把HTML文本解析为一个个节点(HtmlNode)的核心代码(不足百行,够精简吧):
还是直接上源码,代码格式很清晰。
点击下载:Html
等)嵌套形成的著名的文档对象模型(DOM, Document Object Model),是显而易见的树形多层次结构。如果带着这种思路看待HTML、编写HTML解析器,无疑将导致问题复杂化。不妨从另一视角俯视HTML文本,视其为一维线状结构:诸多单一节点的顺序排列。仔细审视任何一段HTML文本,以左右尖括号(<和>)为边界,会发现HTML文本被天然地分割为:一个标签(Tag),接一段普通文字,再一个标签,再一段普通文字……
在正式编码之前,先确定好“节点”的数据结构。作为“普通文字”节点,需要记录一个文本(text);作为“标签”节点,需要记录标签名称(tagName)、标签类型(tagType)、所有属性值(props);另外还要有个类型(type)以便区分该节点是普通文字、开始标签还是结束标签。这其中固然有些冗余信息,比如对标签来说不需要记录文本,对普通文字来说又不需要记录标签名称、属性值等,不过无伤大雅,简洁的编程模型是最大的诱惑。用C/C++语言语法表示如下:
enum HtmlNodeType { NODE_UNKNOWN = 0, NODE_START_TAG, NODE_CLOSE_TAG, NODE_CONTENT, }; enum HtmlTagType { TAG_UNKNOWN = 0, TAG_A, TAG_DIV, TAG_FONT, TAG_IMG, TAG_P, TAG_SPAN, TAG_BR, TAG_B, TAG_I, TAG_HR, }; struct HtmlNodeProp { WCHAR* szName; WCHAR* szValue; }; #define MAX_HTML_TAG_LENGTH (15) struct HtmlNode { HtmlNodeType type; HtmlTagType tagType; WCHAR tagName[MAX_HTML_TAG_LENGTH+1]; WCHAR* text; int propCount; HtmlNodeProp* props; };具体到编写程序代码,要比想象中容易的多。编码的核心要点是,以左右尖括号(<和>)为边界自然分割标签和普通文字。左右尖括号之间的当然是标签节点(开始标签或结束标签),左尖括号(<)之前(直到前一个右尖括号或开头)、右尖括号(>)之后(直到后一个左尖括号或结尾)的显然是普通文字节点。区分开始标签或结束标签的关键点是,看左尖括号(<)后面第一个非空白字符是否为'/'。对于开始标签,在标签名称后面,间隔至少一个空白字符,可能会有形式为“key1=value1 key2=value2 key3”的属性表,关于属性表,后文有专门的函数负责解析。此外有一点要注意,属性值一般有引号括住,引号内出现的左右尖括号应该不被视为边界分隔符。
下面就是负责把HTML文本解析为一个个节点(HtmlNode)的核心代码(不足百行,够精简吧):
void HtmlParser::ParseHtml(const WCHAR* szHtml) { m_html = szHtml ? szHtml : L""; freeHtmlNodes(); if(szHtml == NULL || *szHtml == L'/0') return; WCHAR* p = (WCHAR*) szHtml; WCHAR* s = (WCHAR*) szHtml; HtmlNode* pNode = NULL; WCHAR c; bool bInQuotes = false; while( c = *p ) { if(c == L'/"') { bInQuotes = !bInQuotes; p++; continue; } if(bInQuotes) { p++; continue; } if(c == L'<') { if(p > s) { //Add Text Node pNode = NewHtmlNode(); pNode->type = NODE_CONTENT; pNode->text = duplicateStrUtill(s, L'<', true); } s = p + 1; } else if(c == L'>') { if(p > s) { //Add HtmlTag Node pNode = 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) int tagNamelen = wcslen(pNode->tagName); if(pNode->tagName[tagNamelen-1] == L'/') pNode->tagName[tagNamelen-1] = L'/0'; //处理结点属性 for(int i = 0; i < tagNamelen; i++) { if(pNode->tagName[i] == L' ' //第一个空格后面跟的是属性列表 || pNode->tagName[i] == L'=') //扩展支持这种格式:下面是负责解析“开始标签”属性表文本(形如“key1=value1 key2=value2 key3”)的代码,parseNodeProps(),核心思路是按空格和等号字符进行分割属性名和属性值,由于想兼容HTML4.01及以前的不标准的属性表写法(如没有=号也没有属性值),颇费周折:, 等效于 { WCHAR* props = (pNode->tagName[i] == L' ' ? s + i + 1 : s); pNode->text = duplicateStrUtill(props, L'>', true); int nodeTextLen = wcslen(pNode->text); if(pNode->text[nodeTextLen-1] == L'/') //去掉最后可能会有的'/'字符, 如这种情况: pNode->text[nodeTextLen-1] = L'/0'; pNode->tagName[i] = L'/0'; parseNodeProps(pNode); //parse props break; } } pNode->tagType = getHtmlTagTypeFromName(pNode->tagName); } s = p + 1; } p++; } if(p > s) { //Add Text Node pNode = NewHtmlNode(); pNode->type = NODE_CONTENT; pNode->text = duplicateStr(s, -1); } #ifdef _DEBUG dumpHtmlNodes(); //just for test #endif }
//[virtual] void HtmlParser::parseNodeProps(HtmlNode* pNode) { if(pNode == NULL || pNode->propCount > 0 || pNode->text == NULL) return; WCHAR* p = pNode->text; WCHAR *ps = NULL; CMem mem; bool inQuote1 = false, inQuote2 = false; WCHAR c; while(c = *p) { if(c == L'/"') { inQuote1 = !inQuote1; } else if(c == L'/'') { inQuote2 = !inQuote2; } if((!inQuote1 && !inQuote2) && (c == L' ' || c == L'/t' || c == L'=')) { if(ps) { mem.AddPointer(duplicateStrAndUnquote(ps, p - ps)); ps = NULL; } if(c == L'=') mem.AddPointer(NULL); } else { if(ps == NULL) ps = p; } p++; } if(ps) mem.AddPointer(duplicateStrAndUnquote(ps, p - ps)); mem.AddPointer(NULL); mem.AddPointer(NULL); WCHAR** pp = (WCHAR**) mem.GetPtr(); CMem props; for(int i = 0, n = mem.GetSize() / sizeof(WCHAR*) - 2; i < n; i++) { props.AddPointer(pp[i]); //prop name if(pp[i+1] == NULL) { props.AddPointer(pp[i+2]); //prop value i += 2; } else props.AddPointer(NULL); //prop vlalue } pNode->propCount = props.GetSize() / sizeof(WCHAR*) / 2; pNode->props = (HtmlNodeProp*) props.Detach(); }根据标签名称取标签类型的getHtmlTagTypeFromName()方法,就非常直白了,查表,逐一识别:
//[virtual] HtmlTagType HtmlParser::getHtmlTagTypeFromName(const WCHAR* szTagName) { //todo: uses hashmap struct N2T { const WCHAR* name; HtmlTagType type; }; static N2T n2tTable[] = { { L"A", TAG_A }, { L"FONT", TAG_FONT }, { L"IMG", TAG_IMG }, { L"P", TAG_P }, { L"DIV", TAG_DIV }, { L"SPAN", TAG_SPAN }, { L"BR", TAG_BR }, { L"B", TAG_B }, { L"I", TAG_I }, { L"HR", TAG_HR }, }; for(int i = 0, count = sizeof(n2tTable)/sizeof(n2tTable[0]); i < count; i++) { N2T* p = &n2tTable[i]; if(wcsicmp(p->name, szTagName) == 0) return p->type; } return TAG_UNKNOWN; }使用也非常之简单:
void CHtmlDlg::OnBnClickedButton1() { // TODO: 在此添加控件通知处理程序代码 char* pStr = ""; //属性值双引号和单引号嵌套 HtmlParser htmlParser; htmlParser.parseHtml(pStr); int nCount = htmlParser.getHtmlNodeCount(); for (int i=0;i运行结果:aurl="abc'def'" x='hello "liigo"'tagName); OutputDebugString(htmlParser.getHtmlNode(i)->text); } }
还是直接上源码,代码格式很清晰。
点击下载:Html
收藏的用户(0) X
正在加载信息~
推荐阅读
最新回复 (0)
站点信息
- 文章2299
- 用户1336
- 访客10618509
每日一句
You leave, or I go with you.
你留下,或者我跟你走。——《海角七号》
你留下,或者我跟你走。——《海角七号》
新会员