- Елементарна обрізка за допомогою регулярних виразів
- Обрізка за допомогою Html Agility Pack
- Просунута обрізка за допомогою регулярних виразів
- висновок
Виконати обрізку HTML тегів за допомогою C # можна багатьма способами, але кожен має свої премущества і недоліки, особливості реалізації та додаткові нюанси. У цій статті я хочу доступно пояснити ті підходи до вирішення цього завдання, які допомогли мені, і показати які нюанси можуть виникнути при їх використанні.
Для початку потрібно уточнити що ж таке обрізання HTML тегів і навіщо вона потрібна. Під обрізанням тегів я розумію повне або часткове видалення HTML тегів з рядка. Говорячи про часткове видалення, я маю на увазі залишення тих тегів і їх атрибутів, які знаходяться в білому списку, тобто в списку дозволених тегів. Знадобитися така операція вам може, наприклад, якщо при залишенні коментаря на вашому сайті ви хочете дозволити користувачу вводити тільки деякий обмежений набір HTML тегів. Або ж ви могли Спарс HTML контент з будь то джерела, і при вставці його на свій сайт також хочете залишити тільки деякі теги (<p>, <br />, <strong>, ...).
Для тестів будемо використовувати наступний рядок:
<Div> <p style = 'text-align: center;'> Some <font size = '3' color = 'red'> text in font </ font> - another <br /> <customAttr> text in custom attribute </ customAttr> <br> More Text </ p> <div>Елементарна обрізка за допомогою регулярних виразів
Якщо треба повністю обрізати всі без винятку HTML теги з рядка, то можна скористатися одним простим і елегантним рішенням на основі нескладного регулярного виразу:
static string StripHtmlTagsUsingRegex (string inputString) {return Regex.Replace (inputString, @ "<[^>] *>", String.Empty); }Для розглянутої нами вхідного рядка цей метод поверне такий результат:
Some text in font - another text in custom attribute More TextДане просте регулярний вираз можна трохи вдосконалити, що б мати можливість залишити деякі теги:
static string StripHtmlTagsUsingRegex (string inputString) {return Regex.Replace (inputString, @ "<(?! br | p | \ / p) [^>] *>", String.Empty); }Тоді отримаємо такий результат, де будуть збережені теги <p> і <br/>:
<P style = 'text-align: center;'> Some text in font - another <br /> text in custom attribute <br> More Text </ p> Але як бачимо, залишаючи теги, ми залишаємо також всі їх атрибути. Можна звичайно продовжити розширення нашого простого регулярного виразу, але тоді воно буде вже не дуже простим.
Якщо вже вам потрібна якась більш витончена обрізка тегів, то краще не експериментувати, а використовувати вже готові відтестовані рішення.
Далі я представлю кілька варіантів, які я знайшов в мережі.
Обрізка за допомогою Html Agility Pack
Якщо пошукати рішення задачі яка є темою цієї статті, щось не Складно помітити, що на багатьох сайтах пропонують використовувати Html Agility Pack (HAP). Ця бібліотека дозволяє в зручній формі працювати з DOM деревом HTML документа, і її використання цілком доцільно.
Ось цікавий приклад взятий звідси :
public static class HtmlUtility {// Original list courtesy of Robert Beal: // http://www.robertbeal.com/37/sanitising-html private static readonly Dictionary <string, string []> ValidHtmlTags = new Dictionary <string, string []> {{ "p", new string [] { "style", "class", "align"}}, { "div", new string [] { "style", "class", "align"} }, { "span", new string [] { "style", "class"}}, { "br", new string [] { "style", "class"}}, { "hr", new string [ ] { "style", "class"}}, { "label", new string [] { "style", "class"}}, { "h1", new string [] { "style", "class"} }, { "h2", new string [] { "style", "class"}}, { "h3", new string [] { "style", "class"}}, { "h4", new string [ ] { "style", "class"}}, { "h5", new string [] { "style", "class"}}, { "h6", new string [] { "style", "class"} }, { "font", new string [] { "style", "class", "color", "face", "size"}}, { "strong", new string [] { "style", "class "}}, {" b ", new string [] {" style "," class "}}, {" em ", new string [] {" style "," class "}}, {" i ", new string [] { "style", "class"}}, { "u", new string [] { "s tyle "," class "}}, {" strike ", new string [] {" style "," class "}}, {" ol ", new string [] {" style "," class "}}, { "ul", new string [] { "style", "class"}}, { "li", new string [] { "style", "class"}}, { "blockquote", new string [] { " style "," class "}}, {" code ", new string [] {" style "," class "}}, {" a ", new string [] {" style "," class "," href " , "title"}}, { "img", new string [] { "style", "class", "src", "height", "width", "alt", "title", "hspace", " vspace "," border "}}, {" table ", new string [] {" style "," class "}}, {" thead ", new string [] {" style "," class "}}, { "tbody", new string [] { "style", "class"}}, { "tfoot", new string [] { "style", "class"}}, { "th", new string [] { " style "," class "," scope "}}, {" tr ", new string [] {" style "," class "}}, {" td ", new string [] {" style "," class " , "colspan"}}, { "q", new string [] { "style", "class", "cite"}}, { "cite", new string [] { "style", "class"}} , { "abbr", new string [] { "style", "class"}}, { "acronym", new string [] { "style", "class"}}, { "del", new string [] { "style", "class"}}, { "ins", ne w string [] { "style", "class"}}}; /// <summary> /// Takes raw HTML input and cleans against a whitelist /// </ summary> /// <param name = "source"> Html source </ param> /// <returns> Clean output </ returns> public static string SanitizeHtml (string source) {HtmlDocument html = GetHtml (source); if (html == null) return String.Empty; // All the nodes HtmlNode allNodes = html.DocumentNode; // Select whitelist tag names string [] whitelist = (from kv in ValidHtmlTags select kv.Key) .ToArray (); // Scrub tags not in whitelist CleanNodes (allNodes, whitelist); // Filter the attributes of the remaining foreach (KeyValuePair <string, string []> tag in ValidHtmlTags) {IEnumerable <HtmlNode> nodes = (from n in allNodes.DescendantsAndSelf () where n.Name == tag.Key select n) ; if (nodes == null) continue; foreach (var n in nodes) {if (! n.HasAttributes) continue; // Get all the allowed attributes for this tag HtmlAttribute [] attr = n.Attributes.ToArray (); foreach (HtmlAttribute a in attr) {if (! tag.Value.Contains (a.Name)) {a.Remove (); // Was not in the list} else {// AntiXss a.Value = Microsoft.Security.Application.Encoder.UrlPathEncode (a.Value); }}}} Return allNodes.InnerHtml; } /// <summary> /// Takes a raw source and removes all HTML tags /// </ summary> /// <param name = "source"> </ param> /// <returns> </ returns > public static string StripHtml (string source) {source = SanitizeHtml (source); // No need to continue if we have no clean Html if (String.IsNullOrEmpty (source)) return String.Empty; HtmlDocument html = GetHtml (source); StringBuilder result = new StringBuilder (); // For each node, extract only the innerText foreach (HtmlNode node in html.DocumentNode.ChildNodes) result.Append (node.InnerText); return result.ToString (); } /// <summary> /// Recursively delete nodes not in the whitelist /// </ summary> private static void CleanNodes (HtmlNode node, string [] whitelist) {if (node.NodeType == HtmlNodeType.Element) { if (! whitelist.Contains (node.Name)) {node.ParentNode.RemoveChild (node); return; // We're done}} if (node.HasChildNodes) CleanChildren (node, whitelist); } /// <summary> /// Apply CleanNodes to each of the child nodes /// </ summary> private static void CleanChildren (HtmlNode parent, string [] whitelist) {for (int i = parent.ChildNodes.Count - 1; i> = 0; i--) CleanNodes (parent.ChildNodes [i], whitelist); } /// <summary> /// Helper function that returns an HTML document from text /// </ summary> private static HtmlDocument GetHtml (string source) {HtmlDocument html = new HtmlDocument (); html.OptionFixNestedTags = true; html.OptionAutoCloseOnEnd = true; html.OptionDefaultStreamEncoding = Encoding.UTF8; html.LoadHtml (source); return html; }}Цей статичний клас має два публічних методу - StripHtml () для видалення всіх тегів і SanitizeHtml () для «дезинфікування» HTML, тобто видалення всіх тегів крім білого списку. Використовувати його можна в такий спосіб:
static string StripHtmlTagsUsingHtmlAgilityPack (string inputString) {return HtmlUtility.SanitizeHtml (inputString); }Не буду вдаватися в подробиці реалізації самого класу HtmlUtility, а тільки зверну вашу увагу на цікавий нюанс.
Використання методу SanitizeHtml () для нашої тестової рядки дасть такий результат:
<Div> <p style = 'text-align: center;'> Some <font size = '3' color = 'red'> text in font </ font> - another <br> <br> More Text </ p > <div> </ div> </ div> Як бачите, тег <customAttr> був урізаний разом з усім його вмістом. Хоча ми могли очікувати, що ви забираєте тег, а текст залишиться. Більш того, якщо прибрати з білого списку тег <div>, то взагалі отримаємо порожній рядок.
Така поведінка пов'язана з особливістю Html Agility Pack - робота з HTML в ньому зводиться до роботи з нодамі, і автор не передбачив збереження внутрішніх елементів Ноди в разі її відсутності в білому списку - він просто їх видаляє разом із самою нодою.
У коментарях до наведеної статті хтось запропонував рішення цієї проблеми. Але провівши кілька тестів я прийшов до висновку що воно не ідеально і в ньому явно присутні баги.
Після невдалих спроб підправити це рішення я вирішив кинути цю справу, і знайшов ще одну альтернативу, про яку розповім далі.
Просунута обрізка за допомогою регулярних виразів
На допомогу прийшло рішення, представлене тут .
Автор пропонує рішення у вигляді методу розширення, реалізованого за допомогою регулярних виразів:
/// <summary> /// Filters HTML to the valid html tags set (with only the attributes specified) /// /// Thanks to http://refactormycode.com/codes/333-sanitize-html for the original /// </ summary> public static class HtmlSanitizeExtension {private const string HTML_TAG_PATTERN = @ "(? 'tag_start' </?) (? 'tag' \ w +) ((\ s + (? 'attr' (? 'attr_name' \ w +) (\ s * = \ s * (?: "". *? "" | '. *?' | [^ ' ""> \ s] +)))?) + \ s * | \ s *) (? 'tag_end' /?>) "; /// <summary> /// A dictionary of allowed tags and their respectived allowed attributes. If no /// attributes are provided, all attributes will be stripped from the allowed tag /// </ summary> public static Dictionary <string, List <string >> ValidHtmlTags = new Dictionary <string, List <string >> {{ "p", new List <string> ()}, { "br", new List <string> ()}, { "strong", new List <string> ()}, { "ul", new List <string > ()}, { "li", new List <string> ()}, { "a", new List <string> { "href", "target"}}}; /// <summary> /// Extension filters your HTML to the whitelist specified in the ValidHtmlTags dictionary /// </ summary> public static string FilterHtmlToWhitelist (this string text) {Regex htmlTagExpression = new Regex (HTML_TAG_PATTERN, RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.Compiled); return htmlTagExpression.Replace (text, m => {if (! ValidHtmlTags.ContainsKey (m.Groups [ "tag"]. Value)) return String.Empty; StringBuilder generatedTag = new StringBuilder (m.Length); Group tagStart = m .Groups [ "tag_start"]; Group tagEnd = m.Groups [ "tag_end"]; Group tag = m.Groups [ "tag"]; Group tagAttributes = m.Groups [ "attr"]; generatedTag.Append (tagStart. Success? tagStart.Value: "<"); generatedTag.Append (tag.Value); foreach (Capture attr in tagAttributes.Captures) {int indexOfEquals = attr.Value.IndexOf ( '='); // do not proceed any futurer if there is no equal sign or just an equal sign if (indexOfEquals <1) continue; string attrName = attr.Value.Substring (0, indexOfEquals); // check to see if the attribute name is allowed and write attribute if it is if (ValidHtmlTags [tag.Value] .Contains (attrName)) {generatedTag.Append ( ''); generatedTag.Append (attr.Value);}} generatedTag.Append (tagEnd.Success? tagEnd.Value: "> "); return generatedTag.ToString ();}); }}Зверніть увагу що як в класі HtmlUtility так і в HtmlSanitizeExtension є можливість задати не тільки білий список дозволених тегів, але також для кожного з них можна вказати дозволені атрибути.
Використання цього методу продемонстровано нижче:
static string StripHtmlTagsUsingHtmlSanitizeExtension (string inputString) {return inputString.FilterHtmlToWhitelist (); }Отримуємо такий результат:
<P> Some text in font - another <br/> text in custom attribute <br> More Text </ p>Це саме те чого я добивався - залишити тільки елементарні теги без аттрибутов, при цьому не видаляючи ніякої текст.
Але ніщо не ідеально, і я все таки знайшов один баг в цьому рішенні. Якщо в рядку буде присутній тег з простором імен, наприклад <customAttr: namespace> то він не буде видалений.
Цю проблемку я усунув трохи підправив константу HTML_TAG_PATTERN:
Якщо ви виявите ще якісь проблеми з наведеними прикладами, буду вдячний якщо напишете про них в коментарях.
висновок
Як бачимо підходів до обрізку HTML тегів в C # багато. Вибір конкретного способу залишається за вами. Якщо потрібно просто видалити всі теги, то можна використовувати перший спосіб з регулярним виразом. Якщо ж є потреба в білому списку дозволених тегів, то краще вибрати якийсь нибуть з готових рішень.
Після обрізки тегів, краще видалити всі зайві прогалини і переноси рядків. Зробити це можна за допомогою такого рядка коду: strippedHtml = Regex.Replace (strippedHtml, @ "[\ s \ r \ n] +", "") .Trim ();Tag_end' /?Success?
Success?
Tag_end '/ ?