如何截取html的子字符串作为内容摘要

    在一些web应用中,如新闻、日志等需要在其列表中提供摘要信息,有些CMS系统中提供了摘要字段,在新闻发布时手动填写,但更为便捷的方式是直接截取内容的前面一段作为摘要信息。如果内容为纯字符型,不带任何格式,那便好办,直接取其前n个字符即可,但如果内容为html代码,且不一定能保证html代码一定符合规范,那便如何是好?

    首先,不能直接截取,因为你可能会失去截取内容中已有标记的闭合标记,这样的摘要放在列表中会造成整个页面的html闭合失常

    其次,前N个字符有可能全部是html标记,并非实际内容

    再者,如果截断位置遇到img input等自闭合标签,需延长至其闭合的位置再截断

    总的来说,不能贸然行事,需要探查敌情,敌情就在于,普通截取字符串实际截取位置和实际截取长度为同一个值,但html内容的截取位置大于或等于截取长度,截取长度应只考虑纯文本内容,而截取位置应将容纳了截取长度内纯文本的html标签字符数也计算在内,于是,有了如下方案:

定义截取长度、截取的子字符串
在截取长度>0的情况下,循环遍历html字符:
    如果当前字符不处于标签中: #纯内容文本
        截取长度--
    截取的子字符串 += 当前字符 #拼积截取字符串

返回截取的子字符串

    如此,解决了不会在html标记中间截断的问题,但未能解决问题1:截取内容中的标记可能缺失闭合标记,所以上述方案需要能够同时自动闭合截取内容中未闭合的标签,可能有人会问,上述方案中如何判断当前字符是否在标签中?呵呵,后面会介绍,先卖个关子。

    关于html标签自动闭合,HtmlAgilityPack就可以整理和重新输出html内容,但是我得到了这样的结果:

<p>回来了,需要恶补越狱5集,看朋友博客的时候看到了阿拉丁的这篇,相当搞笑,收藏了~<br />
原帖地址:http://blog.mop.com/luthindadyp/2007/02/04/3279845.html<br /><br /></p>
<p />中央电视台准备引进《越狱》,现面向全国公布待选片名(整理版):<br />
神奇史沟飞<br />文身迷综<br />狐狸河的救赎 <br />大光头有智慧 <br />
小史快跑 <br />FQ总动员 <br />史沟飞和他的狱友 <br />拯救大哥林肯 <br />
阿史正传 <br />八仙打洞<br />拿什么来拯救我的老哥 <br />监狱豆腐渣工程带来的教
育意义——狐狸河监狱越狱事件全程系列跟踪报道<br />监狱挖墙party <br />
卧监藏洞 <br />小史和他的7个男人 <br />男人,你的名字叫屎沟飞 <br />
我的弟弟会越狱 <br />疯

  

  等等,<p />? HtmlAgilityPack的自动闭合太不靠谱,还是自己动手吧,html的标签闭合采取类似栈的先进后出原则,只要按序入栈再出栈,即可计算出需要补全的标记,请继续看题:

定义截取长度、截取的子字符串、当前字符是否在html标签中、左闭合标签数组、右闭合标签数组
在截取长度>0的情况下,循环遍历html字符:
    如果当前字符为<:
        当前字符进入html标签中
    如果当前字符为>:
        判断是开始标签还是闭合标签,标签名称分别加入左、右闭合标签中
        当前字符移出html标签中
    否则: 
        如果当前字符在html标签中: 
            当前标签名称 += 当前字符 #拼积当前标签名称
        否则: #纯内容文本
            截取长度--
    截取的子字符串 += 当前字符 #拼积截取字符串

对左右闭合标签数组进行比对,左标签数组中多出的标签按照逆序按照html闭合标签格式拼接至截取的子字符串末尾

返回截取的子字符串

    这个逻辑即可满足前述3个问题,但此时仍有一个问题,如果本身输入html内容的各种标签不符合规范,如何是好?这时候HtmlAgilityPack就有用武之地了,采用输入html内容构造一个HtmlDocument,然后再WriteTo输出,会发现全部标签已经小写化,虽然在不合html规范的时候有<p />这样的标签出现,但恐怕也是最好的解决办法了。

    文字描述中的各种判断条件,参考下述c#代码

        public static string HtmlSummary(string content, int length)
        {
            //如果输入字符串长度小于截取长度,直接返回
            if (content.Length <= length) return content;
            //如果输入字符串竟未包含html代码,那就直接截取
            if (!content.Contains("<")) return content.Substring(0, length);
            //如果输入字符串中包含手动截取标记,则返回标记前的内容
            if (content.Contains("<!-- summary end -->")) return Regex.Split(content, "<!-- summary end -->")[0];

            var IsTag = false; //当前字符是否在标签中
            var Summary = String.Empty; //截取的子字符串
            var TagName = String.Empty; //当前标签名称
            var TagArray = new { Left = new ArrayList(), Right = new ArrayList() }; //左标签数组、右标签数组

            content = FixHtml(content);

            foreach (var current in content)
            {
                if (length <= 0) break; //在截取长度>0的情况下,循环遍历html字符

                if (current == '<') //当前字符为<,进入html标签
                {
                    IsTag = true;
                }
                else if (current == '>') //当前字符为>,移出html标签
                {
                    //如果当前标签名称以/开头,则此标签为闭合标签,应加入右标签数组
                    if (TagName.Trim().StartsWith("/"))
                        TagArray.Right.Add(TagName);
                    //否则应加入左标签数组,但应考虑自闭合标签不加入闭合标签数组
                    else if (!TagName.Trim().EndsWith("/") && !"img|br|input".Split('|').Contains(TagName.ToLower()))
                        TagArray.Left.Add(TagName.Trim().ToLower().Split(' ')[0]);

                    IsTag = false; //标记已移出html标签
                    TagName = String.Empty; //完成标签加入标签数组后,应重置此变量
                }
                else
                {
                    if (IsTag) //如果在html标签中,则拼积当前标签名称
                        TagName += current;
                    else //否则为纯文本内容,计数器开始自减,直到减为0,循环退出
                        length--;
                }

                Summary += current; //拼积截取的子字符串
            }

            Summary = Summary.Trim();

            //从左标签数组中移除已闭合的标签
            //TagArray.Left.RemoveRange(0, TagArray.Right.Count);
            while (TagArray.Right.Count > 0)
            {
                TagArray.Left.RemoveAt(TagArray.Left.IndexOf(TagArray.Right[0].ToString().Replace("/","")));
                TagArray.Right.RemoveAt(0);
            }

                //剩余标签未闭合,逆序以html闭合标签格式拼接至截取的子字符串末尾
                if (TagArray.Left.Count > 0)
                    for (var i = TagArray.Left.Count - 1; i >= 0; i--)
                    {
                        Summary += String.Format("</{0}>", TagArray.Left[i].ToString());
                    }

            return Summary;
        }

  



==========2012-02-14 14:20===========

@Elgin Hou 在测试后发现第46行报错,经确认检查,发现是标签入栈时的判断有误已更改第29行,由

else if (!TagName.Trim().EndsWith("/") && !"img|br|input".Contains(TagName.ToLower()))
更正为
else if (!TagName.Trim().EndsWith("/") && !"img|br|input".Split('|').Contains(TagName.ToLower()))

在未改正前,将把‘p’、‘b’等标签也排除在外,导致左标签数组和右标签数组不匹配。

非常感谢@Elgin Hou,欢迎更多同仁使用和测试,共同完善。

==========2012-02-14 14:55===========

在我自己试用的过程中发现有未能正常闭合的情况出现,调试发现第46行,不可简单从左标签数组中直接移除前右标签数组长度个元素,因为不一定是这前几个元素未闭合,应严格按照右标签数组中标签出现的顺序,进行出栈操作

 

TagArray.Left.RemoveRange(0, TagArray.Right.Count);

更改为

while (TagArray.Right.Count > 0)

{

    TagArray.Left.RemoveAt(TagArray.Left.IndexOf(TagArray.Right[0].ToString().Replace("/","")));

    TagArray.Right.RemoveAt(0);

}


posted @ 2012-02-14 11:17  傲雪啸风  阅读(4463)  评论(15编辑  收藏  举报