С# Сбросить 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. Это автоматически обрабатывается с помощью
>
и т.д. Как один символ.
- Затем он сокращает текст "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´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-код!
Удачи:)