c # Обрезать HTML безопасно для резюме статьи - PullRequest
13 голосов
/ 11 ноября 2009

У кого-нибудь есть вариант c # этого?

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

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

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

Редактировать

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

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

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

У меня есть идея для решения

  1. сначала обрежьте слова html до N (слова лучше, но символы в порядке) (не останавливайтесь на середине тега и не обрезайте атрибут require)

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

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

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

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

  • Вот то, что я собрал вместе с файлом unittest в VS2008, это может кому-то помочь в будущем
  • Мои попытки взлома, основанные на коде Яна, на вершине для версии char + word version (ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ: это грязный грубый код !! с моей стороны)
  • Я предполагаю, что работаю с «правильно сформированным» HTML во всех случаях (но не обязательно с полным документом с корневым узлом согласно версии 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));
        }
    }
}

Ответы [ 4 ]

11 голосов
/ 11 ноября 2009

РЕДАКТИРОВАТЬ: полное решение см. Ниже, эта первая попытка лишает 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);

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

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

  1. Он проходит через все текстовые узлы, пока размер текста не выходит за допустимые пределы, и в этом случае он усекает этот узел. Это автоматически правильно обрабатывает &gt; и т. Д. Как один символ.
  2. Затем он сокращает текст «ломающего узла» и сбрасывает его. На этом этапе он клонирует XPathNavigator, так как нам нужно помнить эту «точку разрыва».
  3. Чтобы обойти ошибку MS (на самом деле, древнюю), мы должны сначала удалить все оставшиеся текстовые узлы, чтобы следовал за точку разрыва, в противном случае мы рискуем автоматически объединить текстовые узлы, когда они закончатся как братья и сестры друг друга. Примечание: DeleteSelf удобен, но перемещает позицию навигатора к его родителю, поэтому нам нужно сравнить текущую позицию с позицией «точки разрыва», запомненной на предыдущем шаге.
  4. Затем мы делаем то, что хотели в первую очередь: удалить любой узел после точки разрыва.
  5. Не обязательный шаг: очистка кода и удаление любых пустых элементов. Это действие просто для очистки HTML и / или фильтрации определенных (запрещенных) разрешенных элементов. Это можно не указывать.
  6. Вернитесь в "root" и получите содержимое в виде строки с InnerXml.

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

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

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

4 голосов
/ 18 апреля 2012

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

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

2 голосов
/ 11 ноября 2009

Хорошо. Это должно работать (оповещение о грязном коде):

        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;
        }
0 голосов
/ 11 ноября 2009

Это сложно, и, насколько я понимаю, ни одно из решений 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)."..."

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

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

Удачи:)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...