Я очищаю строку HTML, чтобы закодировать знак «меньше» (<) в объект HTML (<
), но оставив HTML-теги такими, какие они есть. Примером является преобразование "<div>Total < 500</div>"
к "<div>Total < 500</div>"
.
Есть ряд сообщений, посвященных этому, в том числе:
https://stackoverflow.com/questions/5464686/html-entity-encode-text-only-not-html-tag, https://stackoverflow.com/questions/2245066/encode-html-in-asp-net-c-sharp-but-leave-tags-intact, https://stackoverflow.com/questions/28301206/how-to-encode-special-characters-in-html-but-exclude-tags
Каждый пост указывает на использование Html Agility Pack и, в частности, HtmlEntity.Entitize(html)
метод. Это не работает для меня, потому что на самом деле игнорируются знаки <и &, и, используя приведенный выше пример, вывод был таким же, как и ввод! Возможно, я не понимаю, как использовать этот метод, но код был просто таким:
public static string EntitizeHtml( this string html )
{
return HtmlEntity.Entitize( html );
}
Я решил написать свой собственный метод, чтобы найти символы меньше чем и преобразовать их в эквивалент сущности HTML. Это работает, но кажется очень неуклюжим (цикл внутри цикла и множество манипуляций со строками). Я избегал Regex, чтобы он был быстрее и удобнее для чтения. Я полностью понимаю, что манипулирование строками HTML часто сопряжено с проблемами из-за различных версий HTML и искаженного HTML, но для моей цели этот метод решает мою проблему на основе дискретного набора тегов HTML. Следующий метод преобразует "<div>Total < 500</div>"
к "<div>Total < 500</div>"
как и ожидалось.
Если кто-то сможет повысить эффективность этого метода, я был бы очень признателен.
public static string EncodeLessThanEntities( this string html )
{
if( !html.Contains( "<" ) )
{
return html;
}
// get the full html length
int length = html.Length;
// set a limit on the tag string to compare to reduce the
// string size (i.e. the longest tags are <center> and <strong>)
int tagLength = 6;
// gather all the included html tags to check
string s = "div|span|p|br|ol|ul|li|center|font|strong|em|sub|sup";
string[] tags = s.Split( '|' ).ToArray( );
// find all the indices of the less than entity or tag
var indices = AllIndexesOf( html, "<" );
// initiate a list of indices to be replaced
var replaceable = new List<int>( );
// loop through the indices
foreach( var index in indices )
{
// store the potential tag (up to the tag length)
if( length - ( index + 1 ) < tagLength ) tagLength = length - ( index + 1 );
string possibleTag = html.Substring( index + 1, tagLength );
// automatically ignore any closing tags
if( possibleTag.Substring( 0, 1 ) == "/" )
{
continue;
}
bool match = false;
// loop through each html tag to find a match
foreach( var tag in tags )
{
if( possibleTag.StartsWith( tag ) )
{
match = true;
break;
}
}
if( !match )
{
// if there is no match to a html tag, store the index
replaceable.Add( index );
}
}
if( replaceable?.Any( ) ?? false )
{
// loop through each index and replace the '<' with '<'
foreach( var index in Enumerable.Reverse( replaceable ) )
{
html = html.ReplaceAt( index, 1, "<" );
}
}
return html;
}
public static List<int> AllIndexesOf( this string input, string value )
{
List<int> indexes = new List<int>( );
if( string.IsNullOrEmpty( value ) )
{
return indexes;
}
for( int index = 0; ; index += value.Length )
{
index = input.IndexOf( value, index );
if( index == -1 )
{
return indexes;
}
indexes.Add( index );
}
}
public static string ReplaceAt( this string str, int index, int length, string replace )
{
return str.Remove( index, Math.Min( length, str.Length - index ) ).Insert( index, replace );
}
Я надеюсь, что есть способ сделать описанный выше метод более эффективным. Или, может быть, это так хорошо, как есть? Я знаю, что есть много очень умных парней с гораздо большим опытом, и я не включаю себя в эту группу!
1 ответ
Каждый раз, когда ты звонишь
Substring
,Remove
,Replace
или любой другой метод, изменяющийstring
, вы создаете новыйstring
экземпляр с новым выделением памяти. Потому чтоstring
неизменен. Всеstring
операции (относительные к числовым) медленные / дорогие. Но ничего страшного, если иметь в виду этот вопрос и соглашаться с ним.html.Contains
,AllIndexesOf
,foreach(var index in indices)
— 2,5 сканирования одной и той же строки.possibleTag.Substring(0, 1) == "/"
, почему нетpossibleTag[0] == "https://codereview.stackexchange.com/"
? Быстрее бы.if (replaceable?.Any() ?? false)
1)replaceable
не может бытьnull
, потомif (replaceable.Any())
почти нормально 2) но это просто проверитьList
содержит любые элементы, тоif (replaceable.Length > 0)
это лучше. 3) ноforeach
не будет обрабатывать коллекцию, если она пуста, дажеEnumerable.Reverse
вызов подходит для пустой коллекции. Таким образом вы можете стеретьif
заявление полностью.Самый быстрый способ построить строку из данных в .NET Framework — это
StringBuilder
. (.NET Core и более новые версии .NET имеютSpan
-основанный методstring.Create
что в некоторых случаях быстрее)
И напоследок мой вариант реализации
// required array can be created once per application start
private static readonly string[] tags = "div span p br ol ul li center font strong em sub sup".Split();
public static string EncodeLessThanEntities(this string html)
{
if (html.Length < 8) // the shortest valid html is <p></p>: 7 chars but there's no room for other chars
return html;
StringBuilder sb = new StringBuilder(html.Length); // spawn StringBuilder with initial capacity, this will reduce amount of memory allocations
int i;
for (i = 0; i < html.Length - 2; i++)
{
if (html[i] == '<' && !tags.Any(tag => html.StartsWithAt(i + (html[i + 1] == "https://codereview.stackexchange.com/" ? 2 : 1), tag)))
sb.Append("<");
else
sb.Append(html[i]);
}
// 'i' has value 'html.Length - 2' here, append two last chars without changes
return sb.Append(html[i]).Append(html[i + 1]).ToString();
}
// same as `String.StartsWith` but accepts a start index
public static bool StartsWithAt(this string text, int startIndex, string value)
{
if (text.Length - startIndex < value.Length)
return false;
for (int i = 0; i < value.Length; i++)
{
if (text[startIndex + i] != value[i])
return false;
}
return true;
}
Я не тестировал это много, но вы можете.