没有找到合适的产品?
联系客服协助选型:023-68661681
提供3000多款全球软件/控件产品
针对软件研发的各个阶段提供专业培训与技术咨询
根据客户需求提供定制化的软件开发服务
全球知名设计软件,显著提升设计质量
打造以经营为中心,实现生产过程透明化管理
帮助企业合理产能分配,提高资源利用率
快速打造数字化生产线,实现全流程追溯
生产过程精准追溯,满足企业合规要求
以六西格玛为理论基础,实现产品质量全数字化管理
通过大屏电子看板,实现车间透明化管理
对设备进行全生命周期管理,提高设备综合利用率
实现设备数据的实时采集与监控
利用数字化技术提升油气勘探的效率和成功率
钻井计划优化、实时监控和风险评估
提供业务洞察与决策支持实现数据驱动决策
转帖|其它|编辑:郝浩|2011-02-12 14:41:05.000|阅读 528 次
概述:众所周知,HTML是结构化文档(Structured Document),由诸多标签(
等)嵌套形成的著名的文档对象模型(DOM, Document Object Model),是显而易见的树形多层次结构。如果带着这种思路看待HTML、编写HTML解析器,无疑将导致问题复杂化。本文将告诉大家关于基本的HTML文本解析器的设计和实现的一些体会,希望对大家有帮助。
# 界面/图表报表/文档/IDE等千款热门软控件火热销售中 >>
众所周知,HTML是结构化文档(Structured Document),由诸多标签(<p>等)嵌套形成的著名的文档对象模型(DOM, Document Object Model),是显而易见的树形多层次结构。如果带着这种思路看待HTML、编写HTML解析器,无疑将导致问题复杂化。不妨从另一视角俯视HTML文本,视其为一维线状结构:诸多单一节点的顺序排列。仔细审视任何一段HTML文本,以左右尖括号(<和>)为边界,会发现HTML文本被天然地分割为:一个标签(Tag),接一段普通文字,再一个标签,再一段普通文字…… 如下图所示:
标签有两种,开始标签(如<p>)和结束标签(</p>),它们和普通文字一起,顺序排列,共同构成了HTML文本的全部。
为了再次简化编程模型,我(liigo)继续将“开始标签”“结束标签”“普通文字”三者统一抽象归纳为“节点”(HtmlNode),相应的,“节点”有三种类型,要么是开始标签,要么是结束标签,要么是普通文字。现在,HTML在我们眼里更加单纯了,它就是“节点”的线性顺序组合,是一维的“节点”数组。如下图所示:HTML文本 = 节点1 + 节点2 + 节点3 + ……
在正式编码之前,先确定好“节点”的数据结构。作为“普通文字”节点,需要记录一个文本(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);
//处理自封闭的结点, 如 <br/>, 删除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'=') //扩展支持这种格式: <tagName=value>, 等效于<tagName tagName=value>
{
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'/') //去掉最后可能会有的'/'字符, 如这种情况:
<img src="..." mce_src="..." />
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
}
下面是负责解析“开始标签”属性表文本(形如“key1=value1 key2=value2 key3”)的代码,parseNodeProps(),核心思路是按空格和等号字符进行分割属性名和属性值,由于想兼容HTML4.01及以前的不标准的属性表写法(如没有=号也没有属性值),颇费周折:
//[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();
}
//[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;
}
}
//[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;
}
请注意,上文负责解析属性表的parseNodeProps()函数,和负责识别标签名称的getHtmlTagTypeFromName()函数,都是虚函数(virtual method)。我(liigo)这么设计是有深意的,给使用者留下了很大的定制空间,可以自由发挥。例如,通过在子类中覆盖/覆写(override)parseNodeProps()方法,可以采用更好的解析算法,或者干脆不做任何处理以提高HTML解析效率——将来某一时间可以调用基类同名函数专门解析特定标签的属性表;例如,通过在子类中覆盖/覆写(override)getHtmlTagTypeFromName()方法,使用者可以选择识别跟多的标签名称(包括自定义标签),或者识别更少的标签名称,甚至不识别任何标签名称(以便提高解析效率)。以编写网络爬虫程序为实例,它多数情况下通常只需识别<A>标签及其属性就足够了,没必要浪费CPU运算去识别其它标签、解析其他标签属性。
至于HTML文本解析器的用途,我目前想到的有:用于HTML格式检查或规范化,用于重新排版HTML文本,用于编写网络爬虫程序/搜索引擎,用于基于HTML模板的动态网页生成,用于HTML网页渲染前的基础解析,等等。
本站文章除注明转载外,均为本站原创或翻译。欢迎任何形式的转载,但请务必注明出处、不得修改原文相关链接,如果存在内容上的异议请邮件反馈至chenjj@evget.com
文章转载自:liigo's blog面对“数字中国”建设和中国制造2025战略实施的机遇期,中车信息公司紧跟时代的步伐,以“集约化、专业化、标准化、精益化、一体化、平台化”为工作目标,大力推进信息服务、工业软件等核心产品及业务的发展。在慧都3D解决方案的实施下,清软英泰建成了多模型来源的综合轻量化显示平台、实现文件不失真的百倍压缩比、针对模型中的大模型文件,在展示平台上进行流畅展示,提升工作效率,优化了使用体验。
本站的模型资源均免费下载,登录后即可下载。模型仅供学习交流,勿做商业用途。
本站的模型资源均免费下载,登录后即可下载。模型仅供学习交流,勿做商业用途。
本站的模型资源均免费下载,登录后即可下载。模型仅供学习交流,勿做商业用途。
服务电话
重庆/ 023-68661681
华东/ 13452821722
华南/ 18100878085
华北/ 17347785263
客户支持
技术支持咨询服务
服务热线:400-700-1020
邮箱:sales@evget.com
关注我们
地址 : 重庆市九龙坡区火炬大道69号6幢
慧都科技 版权所有 Copyright 2003-
2025 渝ICP备12000582号-13 渝公网安备
50010702500608号