C++ Html解析器-HtmlCxx用户手册和源代码解析

HtmlCxx用户手册

中科院计算所网络数据科学与工程研究中心

信息抽取小组

gengyun@sohu.com

1.1 简介

HtmlCxx是一款简洁的,非验证式的,用C++编写的css1和html解析器。和其他的几款Html解析器相比,它具有以下的几个特点:

使用由KasperPeeters编写的强大的tree.h库文件,可以实现类似STL的DOM树遍历和导航。

可以通过解析后生成的树,逐字节地重新生成原始文档。

打包好的Css解析器。

额外的属性解析功能

看似很像C++代码的C++代码(其实已不再是C++了)

原始文档中的tags/elements的偏移值都存储在DOM树的节点当中。

Htmlcxx的解析策略其实是尝试模仿mozilla firefox(http://www.mozilla.org)的模式。因此你应当尝试去解析那些由firefox所生成的文档。然而不同于firefox浏览器,htmlcxx并不会将一些原本不存在的东西加入到所生成的文档当中去。因此,在将生成树进行序列化的时候,能够完全地还原和原始Byte大小一样的HTML文档。

1.2 快速上手

下面我们通过一个简单的小例子来让大家对如何使用HtmlCxx进行开发有一个快速,直观地了解。

1.       安装环境

操作系统: Ubuntu 10.10 (32-bit Linux)

编译环境: GCC 4.4.4

HtmlCxx版本: 0.85

2.       源代码下载

各个版本的HtmlCxx可以从著名的开源代码网站SourceForge上下载。由于HtmlCxx已经于2005年停止更新,我们采用的是其最后更新的版本0.85。

除此之外,使用Ubuntu的用户还可以直接在命令行下使用以下命令很方便地进行安装:

 

sudo apt-get install htmlcxx

 

安装好后,可以到usr/include/htmlcxx目录下进行查看,其中包含了一些可以使用的功能的.h文件。

3.       编写小例子

在随意一个文件夹下新建一个文件,如Temp.cc,之后打开该文件,并输入如下代码:

 

#include <string>

#include <iostream>

#include <sstream>

#include <htmlcxx/html/ParserDom.h>

#include <stdlib.h>

#include <stdio.h>

#include <unistd.h>

using namespace std;

using namespace htmlcxx;

int main()

{

  //解析一段Html代码

string html ="<html><body>hey</body></html>";

HTML::ParserDom parser;

tree<HTML::Node> dom = parser.parseTree(html);

  //输出整棵DOM树

  cout<< dom << endl;

  //输出树中所有的超链接节点

tree<HTML::Node>::iterator it = dom.begin();

tree<HTML::Node>::iterator end = dom.end();

  for(; it != end; ++it)

  {

    if (strcasecmp(it->tagName().c_str(), "A") == 0)

    {

      it->parseAttributes();

       cout <<it->attribute("href").second << endl;

    }

  }

  //输出所有的文本节点

  it= dom.begin();

  end= dom.end();

  for(; it != end; ++it)

  {

   if ((!it->isTag()) && (!it->isComment()))

    {

     cout << it->text();

    }

  }

cout << endl;

}

 

 

将代码编写好保存后,打开控制台进入Temp.cc的目录下,输入以下的命令使用gcc将其编译: 

gcc –o Temp Temp.cc –Iusr/include/htmlcxx-lhtmlcxx

 

请注意上面这段代码中的第一个-I是大写的i,第二个-l是小写的L。

编译生成可执行文件Temp,直接输入./Temp运行,程序如果正常运行则应当出现如下的结果:

 

至此我们的htmlcxx已经可以成功地使用和运行了!

 

二.源代码分析

2.1 简介

         代码分为html和css两个部分,我们主要分析html部分。比较复杂的是tree.h文件,里面不但包含了对树的结构定义,而且包含了许多遍历算法。该文件的说明可以在http://tree.phi-sci.com/documentation.html上找到。在学习HtmlCxx的源代码的时候,要注意到的一点就是不仅应学习它的原理,更因为它引用了开源的树结构代码tree.h,优秀的编码风格贯穿始终,代码十分工整和简洁,广泛适用C++的template机制使其可扩展性非常强,为我们学习C++ 语言编程提供了良好的示范教材。

         由于其注释代码内部较少,下面这段源代码解析主要为方便大家理解。

2.2 ParserDom.h(.cc)

         我们首先来分析ParserDom.h这个文件。上面的例子中我们就用到了这个头文件来对Html文档进行解析。这里我们需要说明,一般的XML格式文档(包括Html文档)解析有两种方式,即DOM方式和SAX方式,下面简要介绍一下这两种方式的异同。

        

DOM方式:

         DOM(DocumentObject Model 文档对象模型)方式,从名字上来看就是将XML格式的文档读至内存,并为每个节点建立一个对象,之后我们可以通过操作对象的方式来对节点进行操作,即是将XML的节点映射到了内存中的一系列对象之上。

         该方法的优点是在建立好模型后的查询和修改速度较快。

该方法的缺点是在处理之前需要将整个XML文档全部读入内存进行解析,并根据XML树生成一个对象模型,当文档很大时,DOM方式就会突现出其肿大的特性,一个300KB的XML文档可以导致RAM或者虚拟内存中3000000KB的DOM树模型。

 

SAX方式:

SAX(Simple APIfor XML)是一种基于事件(Event)的XML处理模式,该模式和DOM相反,不需要将整个XML树读入内存后再进行处理,而是将XML文档作为一个输入“流”来处理,在处理该流的时候会触发很多事件,如SAX解析器在处理文档第一个字符的时候,发现是文档的开始,就会触发Start doucment事件,用户可以重载这个事件函数来对该事件进行相应处理。又如当遇到新的节点时,则会触发Start element事件,此时我们可以对该节点名进行一些判断,并作出相应的处理等。

该方法的优点是不用等待所有数据都被处理后再开始分析,且不需要将整个文档都装入内存中,这对大文档来说是个巨大的优点。一般来说SAX要比DOM方式快很多。

该方法的缺点是由于在处理过程中没有储存任何数据,因此在处理过程中不能够对数据流进行后移,或者回溯等操作。

 

在熟悉了这两种方式之后,我们可以看到在ParserDom.h文件中include了ParserSax.h文件,这是由于在HtmlCxx中,DOM方式是基于SAX方式之上的,关于这点我们之后再说。

首先我们来看一下ParserDom.h中ParserDom类的几个方法和数据成员的声明。

class ParserDom : public ParserSax

{

         public:

                   ParserDom(){}                            //构造方法

                   ~ParserDom(){}                 //析构方法

 

                   consttree<Node> &parseTree(const std::string &html);       //通过String字符串来解析树

                   consttree<Node> &getTree()                   //返回(解析好的)数据成员mHtmlTree

                   {return mHtmlTree; }

 

         protected:

                   //声明了一系列虚函数,用于之后的重载

                   virtualvoid beginParsing();      //开始解析

 

                   virtualvoid foundTag(Node node, bool isEnd);         //寻找指定标签

                   virtualvoid foundText(Node node); //寻找文本

                   virtualvoid foundComment(Node node);         //寻找注释文本

 

                   virtualvoid endParsing();         //结束解析

                                    

                   tree<Node>mHtmlTree;                   //数据成员,用于存放解析好的树

                   tree<Node>::iteratormCurrentState;  //用于遍历树的迭代器,具体声明可见tree.h文件

};

在ParseDom类之后我们还可以见到有如下代码:

 

std::ostream&operator<<(std::ostream &stream, const tree<HTML::Node>&tr);

该段代码重写了<<操作符,使得可以该操作符可以直接输出tree<HTML::Node>类型的变量。

 

parseTree(const std::string &html)

该方法主要是调用ParserSax.h中的parse()方法(并最终经过一系列调用,调用到ParserSax.tcc中的parse()方法)对输入的一段string字符串形式的html文档进行解析。

 

const tree<HTML::Node>&ParserDom::parseTree(const std::string &html)

{

         this->parse(html);                      //调用ParserSax.h中的parse方法对html字符串进行解析,并且将结果存入数据成员mHtmlTree

         returnthis->getTree();    //返回受保护的数据成员mHtmlTree

}

beginParsing()

该方法主要是为解析之前做一些初始化的工作,主要是在树的根节点之前插入一个新的节点作为新根节点。

 

void ParserDom::beginParsing()

{

         mHtmlTree.clear();           //首先清空mHtmlTree

         tree<HTML::Node>::iteratortop = mHtmlTree.begin();         //之后申请一个top游标,指向mHtmlTree树的开始节点

         HTML::Nodelambda_node;    //申请一个新节点

         lambda_node.offset(0);            //初始化moffset值为0,具体请查阅node.h和node.cc文件

         lambda_node.length(0);

         lambda_node.isTag(true);

         lambda_node.isComment(false);

         mCurrentState= mHtmlTree.insert(top,lambda_node);        //将这个节点插入到树中去成为根节点,注意是插入到top游标节点的之前,即previous sibling,具体请查阅tree.h中的insert方法。

}

 

endParsing()

该方法主要作用是停止当前的解析,并记录下已经解析的长度。

 

void ParserDom::endParsing()

{

         tree<HTML::Node>::iteratortop = mHtmlTree.begin();         //获取根节点

         top->length(mCurrentOffset);//将根节点的length数据成员设置为当年已解析的距离

}

 

foundComment()

该方法的主要作用是在当前正在解析的节点(即mCurrentState游标所指向的节点)下将节点作为注释内容插入进去。实际这个方法上和后面的foundText()方法完全一样,因为无论是Comment或Text都是作为Node添加进去的,对于程序来讲没有本质上的区别。

 

void ParserDom::foundComment(Node node)

{

         //Addchild content node, but do not update current state

         //在当前节点下添加一个新的节点node,但是不更新当前解析的进度,即不修改mCurrentState游标的位置

         mHtmlTree.append_child(mCurrentState,node);

}

 

foundText()

该方法的主要作用和前一个方法雷同,从函数名的字面解释来讲应当是添加文本。

 

void ParserDom::foundComment(Node node)

{

         //Addchild content node, but do not update current state

         //在当前节点下添加一个新的节点node,但是不更新当前解析的进度,即不修改mCurrentState游标的位置

         mHtmlTree.append_child(mCurrentState,node);

}

 

foundTag(Nodenode, bool isEnd)

该方法的主要作用是添加一个新的标签节点,并且需要根据isEnd变量进行判断是起始标签如<div>或<a>等,还是结束标签如</div>或</a>等。

 

void ParserDom::foundTag(Node node, boolisEnd)

{

         if(!isEnd)

         {

                   //appendto current tree node

                   tree<HTML::Node>::iteratornext_state;

                   next_state= mHtmlTree.append_child(mCurrentState, node);

                   mCurrentState= next_state;

         }

         else

         {

                   //Lookif there is a pending open tag with that same name upwards

                   //IfmCurrentState tag isn't matching tag, maybe a some of its parents

                   //matches

                   vector<tree<HTML::Node>::iterator > path;

                   tree<HTML::Node>::iteratori = mCurrentState;

                   boolfound_open = false;

                   while(i != mHtmlTree.begin())

                   {

#ifdef DEBUG

                            cerr<< "comparing " << node.tagName() << " with" << i->tagName()<<endl<<":";

                            if(!i->tagName().length()) cerr << "Tag with no name at"<<i->offset()<<";"<<i->offset()+i->length();

#endif

                            assert(i->isTag());

                            assert(i->tagName().length());

                            boolequal;

                            constchar *open = i->tagName().c_str();

                            constchar *close = node.tagName().c_str();

                            equal= !(strcasecmp(open,close));

                            if(equal)

                            {

                                     DEBUGP("Foundmatching tag %s\n", i->tagName().c_str());

                                     //Closingtag closes this tag

                                     //Setlength to full range between the opening tag and

                                     //closingtag

                                     i->length(node.offset()+ node.length() - i->offset());

                                     i->closingText(node.text());

 

                                     mCurrentState= mHtmlTree.parent(i);

                                     found_open= true;

                                     break;

                            }

                            else

                            {

                                     path.push_back(i);

                            }

 

                            i= mHtmlTree.parent(i);

                   }

 

                   if(found_open)

                   {

                            //Ifmatch was upper in the tree, so we need to invalidate child

                            //nodesthat were waiting for a close

                            for(unsigned int j = 0; j < path.size(); ++j)

                            {

//                                  path[j]->length(node.offset()- path[j]->offset());

                                     mHtmlTree.flatten(path[j]);

                            }

                   }

                   else

                   {

                            DEBUGP("Unmatchedtag %s\n", node.text().c_str());

 

                            //Treat as comment

                            node.isTag(false);

                            node.isComment(true);

                            mHtmlTree.append_child(mCurrentState,node);

                   }

         }

}

 

 

这段算法的思路很简单,首先根据isEnd判断要添加的节点是否是结束tag节点,如果不是则直接将该节点插入到当前节点下。否则的话则需要向前寻找其对应的起始tag节点,一直回溯找到树的根节点,若没有找到则报错。此时还需要考虑如下可能,即该起始tag节点和所插入的结束tag节点之间还可能有其他的tag节点,如插入的是</div>,而可能会有<div><a>…</a><div>…</div></div>,此时第一个<div>才是其所对应的起始tag节点。为了解决这个问题,本函数里采用了一个将树路径以vetor存储的方式进行查找匹配。

1.       将所插入节点与当前节点的tagName进行比较。如果相同或者当前节点就是根节点,则前往第三步。

2.       将当前节点重置为当前节点的父节点,并将当前节点按顺序添加至path。回到第1步。

3.       检查是否找到tagName相同的节点,如果找到,我们需要调用flatten()方法调整树的结构,从而消除掉原本正在等待结束tag的起始tag节点。

4.       否则即为未找到,报错。

为了方便大家理解,我们稍微介绍一下这里使用到的flatten()方法,这是一个很有意思的树操作方法,其效果如下图所示:

关于flatten()具体可以参看tree.h中的具体算法。在这个算法中,由于建树时的算法特性,需要在找到匹配的起始标签后,对path路径上的所有节点执行flatten算法,这样就可以将原本需要等待结束tag标签的起始标签放到合适的位置。具体原因可以去查看ParseSax.tcc中构建树节点的算法来理解。

本方法主要是提供给ParseSax.tcc中的解析方法所调用。

 

operator<<(ostream &stream, consttree<HTML::Node> &tr)

本方法主要重写了<<操作符,使其可以直接输出tree<HTML::Node>类型的对象。

 

ostream &HTML::operator<<(ostream&stream, const tree<HTML::Node> &tr)

{

         tree<HTML::Node>::pre_order_iteratorit = tr.begin();

         tree<HTML::Node>::pre_order_iteratorend = tr.end();

         introotdepth = tr.depth(it);

         stream<< "-----" << endl;

         unsignedint n = 0;

         while( it != end )

         {

                   intcur_depth = tr.depth(it);

                   for(inti=0; i < cur_depth - rootdepth; ++i) stream << "  ";

                   stream<< n << "@";

                   stream<< "[" << it->offset() << ";";

                   stream<< it->offset() + it->length() << ") ";

                   stream<< (string)(*it) << endl;

                   ++it,++n;

         }

         stream<< "-----" << endl;

         returnstream;

}

 

代码很简单,就是利用了前序的游标pre_order_iterator循环遍历整棵树并且进行一些格式化的输出。关于pre_order_iterator的定义可以参考tree.h部分代码。

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值