老田的专栏

把我知道的奉献给你,你明白的也请告诉我

原创 解析Html生成标签树(一)收藏

新一篇: 解析Html生成标签树(二) | 旧一篇: 解析Html生成标签树(前言)

解析Html成标签树结构以后,我们不但可以很容易取得想要的元素,同时也很容易将Html转换成对应的XML文件。但是由于代码是在公司写的,所以没有粘贴出来的可能性,所以我只能给出大概的代码流程,具体细节描述,相信各位都很容易写出来,并且写的比我好,关键的是算法实现思想。算法的关键如下:

1.         Html中每个tag都是都将作为树中的一个节点存在的,每个tag都属于树中的某一层。
2.         辅助数据结构:栈(stack)、List、HashTable。其中HashTable[i](i属于int类型)是一个List,用于临时存储第i层子Tag。
3.         顺序扫描Html文本,当遇到”<A~Z”这样的标志,表示可能是一个Tag,调用GetTag()函数对此段代码进行解析,解析出Tag名,Tag属性等等。如果返回值不为空,那么将返回值入栈。并且记录次tag的开始位置。
4.         遇到</A~Z>这样的标志,表示可能是某个Tag的结束。解析出此结束标志的Tag名。如果在栈中找到与此结束标志名同名的元素(此元素属于栈中第iLevel层),那么表示找到匹配的Tag。则Tag出栈,将HashTable[iLevel+1]到HashTable[maxLevel]中的所有元素取出作为此Tag的子节点。放入第HashTable [iLevel]中。并记录Tag的结束位置。
5.         对于<Tag>xxx</Tag>之间的字符串xxx,将其作为特殊的HtmlTextTag处理。出栈,和入栈操作与普通Tag类似。
6.         当栈为空的时候表示最后一次出栈的Tag给根节点。   由于是在公司内部开发的东西,所以不可能把源代码拿出来粘贴,所以只能把大概的代码给出。
伪代码如下:
public void Parse()

{

    char ch = GetCurrentChar();  //取第一个字符   

    while (!Eof())

    {

        if (ch == '<')

        {

            ch = MoveNext();     //取下一个字符   

            if ((ch >= 'A') && (ch <= 'Z') || (ch == '!'))

            {

                iBeginPos = Index;       //记录开始位置

                //表示可能是一个标签 

                HtmlTag tag = GetTag();  //解析此Tag   

                if (tag != null)

                {

                    //首先判断是否有文本   

                    if (m_CurrentText.Lenght > 0)

                    {

                        //将文本作为一个普通Tag入栈   

                        Stack.Push(new HtmlTextTag(m_CurrentText));

                    }

                    tag.BeginPos = iBeginPos;   //记录此Tag的开始位置   

                    Stack.Push(tag);            //把Tag入栈   

                }

            }



            ch = GetCurrentChar();

            if (ch == '/')

            {

                //可能是结束标签   

                tagName = GetTagName();

                //从上到下查看Stack,如果Tag中存在   

                if (FindInStack(tagName))

                {

                    //在栈中找到名为tagName的元素,则把找到的元素出栈   

                    PopTag(tagName);

                }

            }

        }

        else

        {

            //对于<AAA>xxx</AAA>之间的文本xxx,这里将作为TextTag来处理  

            m_CurrentText.Append(GetCurrentChar());

        }

        //继续处理下一个字符   

        ch = MoveNext();

    }



    //解析完成以后,如果栈不空,那么把元素出栈,并把最后一次出栈的元素作为根   

    if (Stack.Count > 0)

    {

        HtmlTag tag = null;

        while (Stack.Count > 0)

        {

            tag = Stack.Pop();

            PopTag(tag);

        }



        //最后一个元素作为根元素   

        if (tag != null)

        {

            m_listRoot.Add(tag);

        }

    }

}



private void PopTag(HtmlTag tag)

{

    int iLevel = Stack.Count;



    //找到了元素,把iLevel到m_IMaxLevel中所有的元素按照全部作为tag的子元素   

    for (int i = iLevel + 1; i < m_iMaxLevel; i++)

    {

        for (j = 0; j < HashTable[i].Count; j++)

        {

            tag.Children.Add(HashTable[i][j]);

        }

    }



    //表示栈已经为空,那么最后一次出栈的tag将作为根   

    if (Stack.Count == 0)

    {

        m_listRoot.Add(tag);

    }

}



private void PopTag(string tagName)

{

    /*

     * 元素出栈的时候,首先需要把当前已经存在了的HtmlTextTag入栈

     * 比如:<A>文本段1<B>文本段2</B>文本段3</A>

     * 在Parse中,当解析出<B>入栈前,需要先把"文本段1"入栈

     * 在这里,解析出了</B>结束标志

     * 那么首先需要把"文本段2"入栈。

     * 解析出</A>则需要把"文本段3'入栈。

     * 这样才能够保证"文本段1"和"文本段3"成为<A>的子节点,而"文本段2"作为<B>的子节点

     */

    if (m_CurrentText.Lenght > 0)

    {

        //将文本作为一个普通Tag入栈   

        Stack.Push(new HtmlTextTag(m_CurrentText));

    }



    HtmlTag tag = Stack.Pop();  //元素出栈   

    int iLevel = Stack.Count;   //记录栈元素数   



    while (tag.Name != tagName)

    {

        //将tag放入第iLevel层的List中   

        HashTable[iLevel].Add(tag);

        tag = Stack.Pop();

        iLevel = Stack.Count;

    }



    //元素出栈后续处理   

    PopTag(tag);

}



private HtmlTag GetTag()

{

    if ("如果发现是 < !--开头的元素") //则表示是注释   

    {

        SkipComment();

    }



    HtmlTag tag = new HtmlTag();

    tag.Name = GetTagName();

    //这里的Attribute我将其作为HashTable类型,Hash[属性名]=属性值   

    tag.Attribute = GetTagAttribute();

    return tag;

} 

解析结束以后,通过访问m_listRoot就可以遍历出所有的节点了。上面仅仅是给出了大概的方法,不过我相信要将上面的方法转换成可运行代码,各位都是有这个能力的。。。

发表于 @ 2008年06月06日 17:38:00|评论(loading...)|收藏

新一篇: 解析Html生成标签树(二) | 旧一篇: 解析Html生成标签树(前言)

评论

#cooldonet 发表于2008-06-13 11:07:18  IP: 202.43.148.*
谢谢分享,研究一下先。
#RonoTian 发表于2008-06-13 15:15:58  IP: 61.141.157.*
这里的“伪”代码基本上把整个算法的核心思想表达出来了的。其中某些函数(比如.MoveNext() - 表示查看下一个字符,GetCurrentChar()表示取得当前字符),没有写出来。而SkipComment()函数则是当出现"<!--"的时候,直到找到"-->"。因为这种注释在Html中要求比较严格,所以只要出现"<!--"就一定存在"-->"否则浏览器是要报错的。。。
#RonoTian 发表于2008-06-13 15:18:29  IP: 61.141.157.*
另外,我目前实现的程序(我也命名为HtmlParser,觉得这个名字比较酷),解析http://www.Sina.com.cn这样的大型页面仅仅需要0.5秒,并且完全成功。所以相信是对各位有帮助的。
当然我会继续完善,也会继续想关注我的各位详细解释,我的想法就是把我知道的和大家进行分享,你乐,我乐!
#willko 发表于2008-06-14 10:19:36  IP: 121.15.119.*
謝謝,提供思維已經不錯了
謝謝還有代碼和注釋
我正需要
#mxlmwl 发表于2008-06-16 09:00:32  IP: 10.167.133.*
呵呵,把你这个加收藏了,接下来有时间会跟踪下。如果有可能,建议你考虑写个开源项目。另外,在测试之前,先用规范网页测试是第一步,接下来就是不规则网页的容错处理了。比如一些采集程序抓来的那种网页,很有可能就很不规则,接下来可以考虑对这种网页的解析情况。
#RonoTian 发表于2008-06-16 09:22:12  IP: 219.133.255.*
回复mxlmwl:这种通过Stack来进行控制的方法,对于不规则的网页是很容易的。。。比如:
1、<TD 阿道夫骄傲了沙发</TD>被解析成节点名为“td”而“阿道夫骄傲了沙发</TD”当作属性来处理。

2、对于“<td><tr></td> </tr>”被解析成父节点td,子节点“tr”。

3、开源,这个建议我一定考虑,目前主要是在公司内部开发的,所以不能把代码给出,不过我可以在空闲时间把代码写出来。希望继续关注。
#gxfnhm2003 发表于2008-06-16 20:04:42  IP: 121.15.210.*
不错 研究下!
#wonder 发表于2008-06-17 19:40:23  IP: 218.91.138.*
很好,谢谢!!!
#xl_0715 发表于2008-06-17 20:54:25  IP: 218.59.227.*
留个影子
#cochmu 发表于2008-06-23 10:40:46  IP: 125.33.195.*
像input,br 等不需要"</ "结束标记就可以使用的tag,不知道你打算怎么解决?
#diteric 发表于2008-06-23 17:47:47  IP: 194.0.226.*
C# 里有现成的结构没必要自己写啊。。。
#diteric 发表于2008-06-23 17:50:51  IP: 194.0.226.*
如果你有兴趣研究一下 mshtml.dll 这个package,你就发现你们公司这方面的开发白做了。。。
#diteric 发表于2008-06-23 17:51:29  IP: 194.0.226.*
我这么说不想你浪费时间而已,没有别的意思
#diteric 发表于2008-06-23 17:52:42  IP: 194.0.226.*
Java里面也有一个HTMLParser 不知道你看过没有http://htmlparser.sourceforge.net/
#sumtec 发表于2008-06-23 18:15:55  IP: 219.142.153.*
http://regex-lib.net/ViewRegex/simple-html-tag-plaintext-filter/

http://regex-lib.net/ViewRegex/simple-html-enhanced-filter/

这两个正则估计够你用的了。
#RonoTian 发表于2008-06-23 18:48:36  IP: 61.141.154.*
[回复diteric兄]
mshtml是需要结合WebBrowser控件进行使用的,所以这个在搜索引擎中是不适合的。
否则直接通过WebBrowser来进行Html的解析那当然是很好的了。
#ml_dark 发表于2008-06-24 17:10:43  IP: 220.194.27.*
...
象这种..
<div><div></div><div><div><div></div></div></div></div>
你看看你的解析结果....

再仔细点就好了
#RonoTian 发表于2008-06-24 20:49:23  IP: 121.15.18.*
[回复ml_dark兄]:
由于这里不能附加图片,所以我把图片放http://blog.csdn.net/RonoTian/archive/2008/06/13/2543921.aspx了
你可以看看,是否可行?
我这个东西是完全基于这个算法出来的。
#Navymk 发表于2008-06-24 22:54:24  IP: 61.51.98.*
现在做解析html的应用应基于xhtml标准。在此标准下br/hr标签也应封闭成
<hr />。所以如果有
形式的标签出项应作为非标准页面中处理。
不过现在这样的非标准实在很普遍,大家都不把w3c的标准当回事,这实在是很无奈的事情。
#Navymk 发表于2008-06-24 22:56:06  IP: 61.51.98.*
真晕,csdn的blog留言竟然不过滤半角的尖括号.实在是非标准....
#RonoTian 发表于2008-06-24 23:00:47  IP: 121.15.18.*
呵呵,估计现在要向页面设计人员谈w3c那是不可能的,除非浏览器做到了严格控制,非标准的都不能正常显示。
#sdsuper 发表于2008-06-25 00:09:22  IP: 59.57.143.*
去看看tidy应该有很大帮助
#冬冬 发表于2008-06-26 14:50:21  IP: 149.20.54.*
我的看法是:用于搜索引擎的Html解析分成两种,一种使用于抓取的,也就是只需要提取超链接;另一种适用于索引的,又叫正文提取。

提取超链接的算法,记得在网上看到的说法是,大的搜索引擎都用的正则,我写的爬虫也是这么用的:
"href=[\\\"\\\'](http:\\/\\/|\\.\\/|\\/)?\\w+(\\.\\w+)*(\\/\\w+(\\.\\w+)?)*(\\/|\\?\\w*=\\w*(&\\w*=\\w*)*)?[\\\"\\\']";

我们实验室的爬虫使用纯C写的,Linux环境下运行。用RegComp编译正则,用RegExec匹配。

感觉这一块效率不是很重要,毕竟整个搜索引擎的瓶颈不在这。而如何保证所有的Url全部提取出来更重要,特别是现在Ajax、RIA满天飞的情况下……

我也在写爬虫的系列文件,可以一起讨论:yuandong.cnblogs.com
#RonoTian 发表于2008-06-26 15:02:46  IP: 219.133.255.*
呵呵,太好了,找到一个正在弄这东西的,一定去好好研究
#RonoTian 发表于2008-06-28 10:47:38  IP: 116.25.53.*
希望您能够在这里发表您的看法和意见,我们一起来进行解决。这样给后面近来的一些参考意见。

也有助于更多的想法和意见的总结整理。
#wz 发表于2008-07-01 12:35:20  IP: 221.239.0.*
http://www.codeproject.com/KB/dotnet/apmilhtml.aspx
开源的HTML解析程序
#zzzzz 发表于2008-07-03 11:36:36  IP: 218.65.129.*
lucene不是自带爬虫了吗?
#NO.7 发表于2008-07-04 14:40:30  IP: 59.40.203.*
造锤子,现成的类一大把
#RonoTian 发表于2008-07-04 15:19:06  IP: 219.134.53.*
“造锤子”不知道是什么意思哦。。。
不过现成的类可没有啊,除了HtmlDocument之类需要借助WebBrowser才能使用的东西。。。
#NO.7 发表于2008-07-04 16:47:03  IP: 59.40.203.*
http://www.codeplex.com/htmlagilitypack/
要写,就像人家这样专业。
#RonoTian 发表于2008-07-04 20:52:10  IP: 116.25.53.*
谢谢NO.7的提醒,不过专业不专业我真不知道体现在什么地方。
我已经下载了你提到的这个包,经过测试,我所做的不比这个差。
看来我真有必要把这东西开源出来。。。。。
#thistangyong2008 发表于2008-08-02 14:21:16  IP: 121.232.228.*
还是编不出来……
现在已经得到一个html文件了,接着如何解析呢……
这个程序给出了个大概啊……
但是我这种菜鸟级别的还是不会啊
#RonoTian 发表于2008-08-04 08:27:25  IP: 219.134.26.*
回复thistangyong2008 兄,你不要考虑我的代码,你只要明白了这种解析算法,然后按照你自己的理解来做就可以了的。。
发表评论  


登录
Csdn Blog version 3.1a
Copyright © 老田