С# Сбросить HTML безопасно для сводки статьи

Есть ли у кого-то переменные С#?

Это значит, что я могу взять некоторый html и отобразить его, не нарушая его в виде сводной статьи в статье?

Обрезать текст, содержащий HTML, игнорируя теги

Спасите меня от переосмысления колеса!

Изменить

Извините, новый здесь, и ваше право, должно было сформулировать вопрос лучше, вот немного больше информации

Я хочу взять строку html и усечь ее до определенного количества слов (или даже длины char), чтобы затем показать начало ее как сводку (что затем приводит к основной статье). Я хочу сохранить html, чтобы я мог показывать ссылки и т.д. В превью.

Основной проблемой, которую я должен решить, является тот факт, что мы можем закончить с закрытыми тегами html, если мы обрезаем в середине 1 или более тегов!

Идея, которую я имею для решения, заключается в

  • сначала обрезать html до N слов (слова лучше, чем символы) (обязательно не останавливаться в середине тега и обрезать атрибут require)

  • работать через открытые теги html в этой усеченной строке (возможно, придерживаться их в стеке, когда я иду?)

  • то работайте с закрывающими тегами и убедитесь, что они совпадают с тегами в стеке, когда я их выталкиваю?

  • если после этого открываются теги, оставшиеся после стека, затем напишите их до конца усеченной строки, а html должен быть хорош, чтобы идти!!!!

Редактировать 12/11/2009

  • Вот то, что я объединил в качестве файла unittest в VS2008, это может помочь кому-то в будущем
  • Мои попытки взлома, основанные на январском коде, находятся наверху для версии char version + word (ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ: это грязный грубый код!! с моей стороны)
  • Я предполагаю работать с "хорошо сформированным" HTML во всех случаях (но не обязательно полный документ с корнем node в соответствии с XML-версией)
  • Версия Abels XML находится внизу, но еще не прошла раунд, чтобы полностью пройти тесты для этого (плюс нужно понять код)...
  • Я обновлю, когда у меня появится возможность уточнить
  • У вас возникли проблемы с отправкой кода? нет ли загрузочного объекта в стеке?

Спасибо за все комментарии:)

using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.XPath;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace PINET40TestProject
{
    [TestClass]
    public class UtilityUnitTest
    {
        public static string TruncateHTMLSafeishChar(string text, int charCount)
        {
            bool inTag = false;
            int cntr = 0;
            int cntrContent = 0;

            // loop through html, counting only viewable content
            foreach (Char c in text)
            {
                if (cntrContent == charCount) break;
                cntr++;
                if (c == '<')
                {
                    inTag = true;
                    continue;
                }

                if (c == '>')
                {
                    inTag = false;
                    continue;
                }
                if (!inTag) cntrContent++;
            }

            string substr = text.Substring(0, cntr);

            //search for nonclosed tags        
            MatchCollection openedTags = new Regex("<[^/](.|\n)*?>").Matches(substr);
            MatchCollection closedTags = new Regex("<[/](.|\n)*?>").Matches(substr);

            // create stack          
            Stack<string> opentagsStack = new Stack<string>();
            Stack<string> closedtagsStack = new Stack<string>();

            // to be honest, this seemed like a good idea then I got lost along the way 
            // so logic is probably hanging by a thread!! 
            foreach (Match tag in openedTags)
            {
                string openedtag = tag.Value.Substring(1, tag.Value.Length - 2);
                // strip any attributes, sure we can use regex for this!
                if (openedtag.IndexOf(" ") >= 0)
                {
                    openedtag = openedtag.Substring(0, openedtag.IndexOf(" "));
                }

                // ignore brs as self-closed
                if (openedtag.Trim() != "br")
                {
                    opentagsStack.Push(openedtag);
                }
            }

            foreach (Match tag in closedTags)
            {
                string closedtag = tag.Value.Substring(2, tag.Value.Length - 3);
                closedtagsStack.Push(closedtag);
            }

            if (closedtagsStack.Count < opentagsStack.Count)
            {
                while (opentagsStack.Count > 0)
                {
                    string tagstr = opentagsStack.Pop();

                    if (closedtagsStack.Count == 0 || tagstr != closedtagsStack.Peek())
                    {
                        substr += "</" + tagstr + ">";
                    }
                    else
                    {
                        closedtagsStack.Pop();
                    }
                }
            }

            return substr;
        }

        public static string TruncateHTMLSafeishWord(string text, int wordCount)
        {
            bool inTag = false;
            int cntr = 0;
            int cntrWords = 0;
            Char lastc = ' ';

            // loop through html, counting only viewable content
            foreach (Char c in text)
            {
                if (cntrWords == wordCount) break;
                cntr++;
                if (c == '<')
                {
                    inTag = true;
                    continue;
                }

                if (c == '>')
                {
                    inTag = false;
                    continue;
                }
                if (!inTag)
                {
                    // do not count double spaces, and a space not in a tag counts as a word
                    if (c == 32 && lastc != 32)
                        cntrWords++;
                }
            }

            string substr = text.Substring(0, cntr) + " ...";

            //search for nonclosed tags        
            MatchCollection openedTags = new Regex("<[^/](.|\n)*?>").Matches(substr);
            MatchCollection closedTags = new Regex("<[/](.|\n)*?>").Matches(substr);

            // create stack          
            Stack<string> opentagsStack = new Stack<string>();
            Stack<string> closedtagsStack = new Stack<string>();

            foreach (Match tag in openedTags)
            {
                string openedtag = tag.Value.Substring(1, tag.Value.Length - 2);
                // strip any attributes, sure we can use regex for this!
                if (openedtag.IndexOf(" ") >= 0)
                {
                    openedtag = openedtag.Substring(0, openedtag.IndexOf(" "));
                }

                // ignore brs as self-closed
                if (openedtag.Trim() != "br")
                {
                    opentagsStack.Push(openedtag);
                }
            }

            foreach (Match tag in closedTags)
            {
                string closedtag = tag.Value.Substring(2, tag.Value.Length - 3);
                closedtagsStack.Push(closedtag);
            }

            if (closedtagsStack.Count < opentagsStack.Count)
            {
                while (opentagsStack.Count > 0)
                {
                    string tagstr = opentagsStack.Pop();

                    if (closedtagsStack.Count == 0 || tagstr != closedtagsStack.Peek())
                    {
                        substr += "</" + tagstr + ">";
                    }
                    else
                    {
                        closedtagsStack.Pop();
                    }
                }
            }

            return substr;
        }

        public static string TruncateHTMLSafeishCharXML(string text, int charCount)
        {
            // your data, probably comes from somewhere, or as params to a methodint 
            XmlDocument xml = new XmlDocument();
            xml.LoadXml(text);
            // create a navigator, this is our primary tool
            XPathNavigator navigator = xml.CreateNavigator();
            XPathNavigator breakPoint = null;

            // find the text node we need:
            while (navigator.MoveToFollowing(XPathNodeType.Text))
            {
                string lastText = navigator.Value.Substring(0, Math.Min(charCount, navigator.Value.Length));
                charCount -= navigator.Value.Length;
                if (charCount <= 0)
                {
                    // truncate the last text. Here goes your "search word boundary" code:        
                    navigator.SetValue(lastText);
                    breakPoint = navigator.Clone();
                    break;
                }
            }

            // first remove text nodes, because Microsoft unfortunately merges them without asking
            while (navigator.MoveToFollowing(XPathNodeType.Text))
            {
                if (navigator.ComparePosition(breakPoint) == XmlNodeOrder.After)
                {
                    navigator.DeleteSelf();
                }
            }

            // moves to parent, then move the rest
            navigator.MoveTo(breakPoint);
            while (navigator.MoveToFollowing(XPathNodeType.Element))
            {
                if (navigator.ComparePosition(breakPoint) == XmlNodeOrder.After)
                {
                    navigator.DeleteSelf();
                }
            }

            // moves to parent
            // then remove *all* empty nodes to clean up (not necessary):
            // TODO, add empty elements like <br />, <img /> as exclusion
            navigator.MoveToRoot();
            while (navigator.MoveToFollowing(XPathNodeType.Element))
            {
                while (!navigator.HasChildren && (navigator.Value ?? "").Trim() == "")
                {
                    navigator.DeleteSelf();
                }
            }

            // moves to parent
            navigator.MoveToRoot();
            return navigator.InnerXml;
        }

        [TestMethod]
        public void TestTruncateHTMLSafeish()
        {
            // Case where we just make it to start of HREF (so effectively an empty link)

            // 'simple' nested none attributed tags
            Assert.AreEqual(@"<h1>1234</h1><b><i>56789</i>012</b>",
            TruncateHTMLSafeishChar(
                @"<h1>1234</h1><b><i>56789</i>012345</b>",
                12));

            // In middle of a!
            Assert.AreEqual(@"<h1>1234</h1><a href=""testurl""><b>567</b></a>",
            TruncateHTMLSafeishChar(
                @"<h1>1234</h1><a href=""testurl""><b>5678</b></a><i><strong>some italic nested in string</strong></i>",
                7));

            // more
            Assert.AreEqual(@"<div><b><i><strong>1</strong></i></b></div>",
            TruncateHTMLSafeishChar(
                @"<div><b><i><strong>12</strong></i></b></div>",
                1));

            // br
            Assert.AreEqual(@"<h1>1 3 5</h1><br />6",
            TruncateHTMLSafeishChar(
                @"<h1>1 3 5</h1><br />678<br />",
                6));
        }

        [TestMethod]
        public void TestTruncateHTMLSafeishWord()
        {
            // zero case
            Assert.AreEqual(@" ...",
                            TruncateHTMLSafeishWord(
                                @"",
                               5));

            // 'simple' nested none attributed tags
            Assert.AreEqual(@"<h1>one two <br /></h1><b><i>three  ...</i></b>",
            TruncateHTMLSafeishWord(
                @"<h1>one two <br /></h1><b><i>three </i>four</b>",
                3), "we have added ' ...' to end of summary");

            // In middle of a!
            Assert.AreEqual(@"<h1>one two three </h1><a href=""testurl""><b class=""mrclass"">four  ...</b></a>",
            TruncateHTMLSafeishWord(
                @"<h1>one two three </h1><a href=""testurl""><b class=""mrclass"">four five </b></a><i><strong>some italic nested in string</strong></i>",
                4));

            // start of h1
            Assert.AreEqual(@"<h1>one two three  ...</h1>",
            TruncateHTMLSafeishWord(
                @"<h1>one two three </h1><a href=""testurl""><b>four five </b></a><i><strong>some italic nested in string</strong></i>",
                3));

            // more than words available
            Assert.AreEqual(@"<h1>one two three </h1><a href=""testurl""><b>four five </b></a><i><strong>some italic nested in string</strong></i> ...",
            TruncateHTMLSafeishWord(
                @"<h1>one two three </h1><a href=""testurl""><b>four five </b></a><i><strong>some italic nested in string</strong></i>",
                99));
        }

        [TestMethod]
        public void TestTruncateHTMLSafeishWordXML()
        {
            // zero case
            Assert.AreEqual(@" ...",
                            TruncateHTMLSafeishWord(
                                @"",
                               5));

            // 'simple' nested none attributed tags
            string output = TruncateHTMLSafeishCharXML(
                @"<body><h1>one two </h1><b><i>three </i>four</b></body>",
                13);
            Assert.AreEqual(@"<body>\r\n  <h1>one two </h1>\r\n  <b>\r\n    <i>three</i>\r\n  </b>\r\n</body>", output,
             "XML version, no ... yet and addeds '\r\n  + spaces?' to format document");

            // In middle of a!
            Assert.AreEqual(@"<h1>one two three </h1><a href=""testurl""><b class=""mrclass"">four  ...</b></a>",
            TruncateHTMLSafeishCharXML(
                @"<body><h1>one two three </h1><a href=""testurl""><b class=""mrclass"">four five </b></a><i><strong>some italic nested in string</strong></i></body>",
                4));

            // start of h1
            Assert.AreEqual(@"<h1>one two three  ...</h1>",
            TruncateHTMLSafeishCharXML(
                @"<h1>one two three </h1><a href=""testurl""><b>four five </b></a><i><strong>some italic nested in string</strong></i>",
                3));

            // more than words available
            Assert.AreEqual(@"<h1>one two three </h1><a href=""testurl""><b>four five </b></a><i><strong>some italic nested in string</strong></i> ...",
            TruncateHTMLSafeishCharXML(
                @"<h1>one two three </h1><a href=""testurl""><b>four five </b></a><i><strong>some italic nested in string</strong></i>",
                99));
        }
    }
}

Ответы

Ответ 1

EDIT: см. ниже полное решение, эта первая попытка разбивает HTML, вторая не

Обобщите, что вы хотите:

  • В результате не получается HTML
  • Он должен принимать любые достоверные данные внутри <body>
  • Он имеет фиксированную максимальную длину

Если вы HTML-код XHTML, это становится тривиальным (и, хотя я не видел решения PHP, я очень сомневаюсь, что они используют подобный подход, но я считаю, что это понятно и довольно легко):

XmlDocument xml = new XmlDocument();

// replace the following line with the content of your full XHTML
xml.LoadXml(@"<body><p>some <i>text</i>here</p><div>that needs stripping</div></body>");

// Get all textnodes under <body> (twice "//" is on purpose)
XmlNodeList nodes = xml.SelectNodes("//body//text()");

// loop through the text nodes, replace this with whatever you like to do with the text
foreach (var node in nodes)
{
    Debug.WriteLine(((XmlCharacterData)node).Value);
}

Примечание: пробелы и т.д. будут сохранены. Обычно это хорошо.

Если у вас нет XHTML, вы можете использовать HTML Agility Pack, который позволит вам сделать то же самое для простого старого HTML ( он внутренне преобразует его в некоторый DOM). Я не пробовал, но он должен работать довольно гладко.


БОЛЬШОЙ РЕДАКТИРОВАНИЕ:

Фактическое решение

В небольшом комментарии я пообещал взять подход XHTML/XmlDocument и использовать его для метода типов для разделения HTML на основе длины текста, но сохраняя HTML-код. Я взял следующий HTML-код, код разбивает его правильно в середине needs, удаляет остальные, удаляет пустые узлы и автоматически закрывает любые открытые элементы.

Пример HTML:

<body>
    <p><tt>some<u><i>text</i>here</u></tt></p>
    <div>that <b><i>needs <span>str</span>ip</i></b><s>ping</s></div>
</body>

Код, протестированный и работающий с любым типом ввода (нормально, предоставлен, я просто сделал некоторые тесты, и код может содержать ошибки, дайте мне знать, если вы их найдете!).

// your data, probably comes from somewhere, or as params to a method
int lengthAvailable = 20;
XmlDocument xml = new XmlDocument();
xml.LoadXml(@"place-html-code-here-left-out-for-brevity");

// create a navigator, this is our primary tool
XPathNavigator navigator = xml.CreateNavigator();
XPathNavigator breakPoint = null;


string lastText = "";

// find the text node we need:
while (navigator.MoveToFollowing(XPathNodeType.Text))
{
    lastText = navigator.Value.Substring(0, Math.Min(lengthAvailable, navigator.Value.Length));
    lengthAvailable -= navigator.Value.Length;

    if (lengthAvailable <= 0)
    {
        // truncate the last text. Here goes your "search word boundary" code:
        navigator.SetValue(lastText);
        breakPoint = navigator.Clone();
        break;
    }
}

// first remove text nodes, because Microsoft unfortunately merges them without asking
while (navigator.MoveToFollowing(XPathNodeType.Text))
    if (navigator.ComparePosition(breakPoint) == XmlNodeOrder.After)
        navigator.DeleteSelf();   // moves to parent

// then move the rest
navigator.MoveTo(breakPoint);
while (navigator.MoveToFollowing(XPathNodeType.Element))
    if (navigator.ComparePosition(breakPoint) == XmlNodeOrder.After)
        navigator.DeleteSelf();   // moves to parent

// then remove *all* empty nodes to clean up (not necessary): 
// TODO, add empty elements like <br />, <img /> as exclusion
navigator.MoveToRoot();
while (navigator.MoveToFollowing(XPathNodeType.Element))
    while (!navigator.HasChildren && (navigator.Value ?? "").Trim() == "")
        navigator.DeleteSelf();  // moves to parent

navigator.MoveToRoot();
Debug.WriteLine(navigator.InnerXml);

Как работает код

Код выполняет следующие действия в следующем порядке:

  • Он проходит через все текстовые узлы, пока размер текста не расширится за допустимый предел, и в этом случае он обрезает это node. Это автоматически обрабатывается с помощью &gt; и т.д. Как один символ.
  • Затем он сокращает текст "break node" и сбрасывает его. Он клонирует XPathNavigator в этот момент, поскольку нам нужно запомнить эту "точку прерывания".
  • Чтобы обойти ошибку MS (древнюю, на самом деле), мы должны сначала удалить все оставшиеся текстовые узлы, которые следуют за точкой прерывания, в противном случае мы рискуем автоматическим слиянием текстовых узлов, когда они заканчиваются как братья и сестры друг друга, Примечание: DeleteSelf удобен, но перемещает позицию навигатора к его родительскому элементу, поэтому нам нужно проверить текущую позицию на позицию "точка разрыва", запомненную на предыдущем шаге.
  • Затем мы делаем то, что хотели сделать в первую очередь: удалите любой node после точки прерывания.
  • Не необходимый шаг: очистка кода и удаление любых пустых элементов. Это действие состоит только в том, чтобы очистить HTML и/или отфильтровать определенные (разрешенные) элементы. Его можно оставить без внимания.
  • Вернитесь к "root" и получите содержимое как строку с помощью InnerXml.

Это все, довольно просто, хотя на первый взгляд может показаться немного сложным.

PS: то же самое было бы проще читать и понимать, если бы вы использовали XSLT, который является идеальным инструментом для этого типа заданий.

Обновление: добавлен образец расширенного кода на основе отредактированного вопроса
Обновление: добавлено немного объяснений

Ответ 2

Если вы хотите сохранить теги html, вы можете использовать этот метод, который я недавно опубликовал. https://gist.github.com/2413598

Он использует XmlReader/XmlWriter. Это не готово к производству, т.е. Вы, вероятно, захотите SgmlReader или HtmlAgilityPack И вы хотите попробовать-уловы и выбрать резервный...

Ответ 3

Ok. Это должно работать (предупреждение о грязном коде):

        string blah = "hoi <strong>dit <em>is test bla meer tekst</em></strong>";
        int aantalChars = 10;


        bool inTag = false;
        int cntr = 0;
        int cntrContent = 0;
        foreach (Char c in blah)
        {
            if (cntrContent == aantalChars) break;



            cntr++;
            if (c == '<')
            {
                inTag = true;
                continue;
            }
            else if (c == '>')
            {
                inTag = false;
                continue;
            }

            if (!inTag) cntrContent++;
        }

        string substr = blah.Substring(0, cntr);

        //search for nonclosed tags
        MatchCollection openedTags = new Regex("<[^/](.|\n)*?>").Matches(substr);
        MatchCollection closedTags = new Regex("<[/](.|\n)*?>").Matches(substr);

        for (int i =openedTags.Count - closedTags.Count; i >= 1; i--)
        {
            string closingTag = "</" + openedTags[closedTags.Count + i - 1].Value.Substring(1);
            substr += closingTag;
        }

Ответ 4

Это сложно, и, насколько я вижу, ни одно из решений PHP не идеально. Что делать, если текст:

substr("Hello, my <strong>name is <em>Sam</em>. I&acute;m a 
  web developer.  And this text is very long and all the text 
  is inside the sam html tag..</strong>",0,26)."..."

На самом деле вам придется перебирать весь текст, чтобы найти конец стартового сильного тега.

Мой совет вам - удалить все html в сводке. Не забудьте использовать html-sanitizing, если вы показываете пользователям собственный html-код!

Удачи:)