Лучший способ кодировать текстовые данные для XML

Я искал универсальный метод в.Net для кодирования строки для использования в элементе или атрибуте Xml, и был удивлен, когда не сразу нашел его. Итак, прежде чем я зайду слишком далеко, могу ли я просто пропустить встроенную функцию?

Предполагая на мгновение, что это действительно не существует, я собираю свой собственный общий EncodeForXml(string data) метод, и я думаю о лучшем способе сделать это.

Данные, которые я использую для запроса всего этого, могут содержать недопустимые символы, такие как &, <, "и т. Д. Иногда они могут также содержать правильно экранированные сущности: & amp;, & lt; и & quot;, что означает просто использование Раздел CDATA, возможно, не самая лучшая идея. В любом случае, это выглядит немного глупо, я бы предпочел получить хорошее строковое значение, которое можно использовать непосредственно в xml.

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

Итак, можно ли это оптимизировать дальше, не делая это слишком сложным, и я что-то упускаю?:

Function EncodeForXml(ByVal data As String) As String
    Static badAmpersand As new Regex("&(?![a-zA-Z]{2,6};|#[0-9]{2,4};)")

    data = badAmpersand.Replace(data, "&amp;")

    return data.Replace("<", "&lt;").Replace("""", "&quot;").Replace(">", "gt;")
End Function

Извините за все, что вы знаете только на C#. Мне все равно, какой язык я использую, но я хотел сделать Regex статическим, и вы не можете сделать это в C#, не объявив его вне метода, так что это будет VB.Сеть

Наконец, мы все еще на.Net 2.0, где я работаю, но если бы кто-то мог взять конечный продукт и превратить его в метод расширения для строкового класса, это тоже было бы неплохо.

Обновление Первые несколько ответов показывают, что.Net действительно имеет встроенные способы сделать это. Но теперь, когда я начал, я хочу закончить свой метод EncodeForXml() просто для удовольствия, поэтому я все еще ищу идеи для улучшения. В частности: более полный список символов, которые должны быть закодированы как сущности (возможно, сохранены в списке / карте), и что-то, что получает лучшую производительность, чем выполнение.Replace() для неизменяемых строк в последовательном соединении.

13 ответов

Решение

System.XML обрабатывает кодировку для вас, поэтому вам не нужен такой метод.

В зависимости от того, как много вы знаете о входных данных, вам, возможно, придется учитывать, что не все символы Unicode являются допустимыми символами XML.

Кажется, что и Server.HtmlEncode, и System.Security.SecurityElement.Escape игнорируют недопустимые символы XML, тогда как System.XML.XmlWriter.WriteString генерирует исключение ArgumentException при обнаружении недопустимых символов (если только вы не отключите эту проверку, в этом случае он их игнорирует). Обзор функций библиотеки доступен здесь.

Изменить 2011/8/14: видя, что по крайней мере несколько человек обращались к этому ответу за последние пару лет, я решил полностью переписать исходный код, в котором были многочисленные проблемы, включая ужасно неправильное обращение с UTF-16.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

/// <summary>
/// Encodes data so that it can be safely embedded as text in XML documents.
/// </summary>
public class XmlTextEncoder : TextReader {
    public static string Encode(string s) {
        using (var stream = new StringReader(s))
        using (var encoder = new XmlTextEncoder(stream)) {
            return encoder.ReadToEnd();
        }
    }

    /// <param name="source">The data to be encoded in UTF-16 format.</param>
    /// <param name="filterIllegalChars">It is illegal to encode certain
    /// characters in XML. If true, silently omit these characters from the
    /// output; if false, throw an error when encountered.</param>
    public XmlTextEncoder(TextReader source, bool filterIllegalChars=true) {
        _source = source;
        _filterIllegalChars = filterIllegalChars;
    }

    readonly Queue<char> _buf = new Queue<char>();
    readonly bool _filterIllegalChars;
    readonly TextReader _source;

    public override int Peek() {
        PopulateBuffer();
        if (_buf.Count == 0) return -1;
        return _buf.Peek();
    }

    public override int Read() {
        PopulateBuffer();
        if (_buf.Count == 0) return -1;
        return _buf.Dequeue();
    }

    void PopulateBuffer() {
        const int endSentinel = -1;
        while (_buf.Count == 0 && _source.Peek() != endSentinel) {
            // Strings in .NET are assumed to be UTF-16 encoded [1].
            var c = (char) _source.Read();
            if (Entities.ContainsKey(c)) {
                // Encode all entities defined in the XML spec [2].
                foreach (var i in Entities[c]) _buf.Enqueue(i);
            } else if (!(0x0 <= c && c <= 0x8) &&
                       !new[] { 0xB, 0xC }.Contains(c) &&
                       !(0xE <= c && c <= 0x1F) &&
                       !(0x7F <= c && c <= 0x84) &&
                       !(0x86 <= c && c <= 0x9F) &&
                       !(0xD800 <= c && c <= 0xDFFF) &&
                       !new[] { 0xFFFE, 0xFFFF }.Contains(c)) {
                // Allow if the Unicode codepoint is legal in XML [3].
                _buf.Enqueue(c);
            } else if (char.IsHighSurrogate(c) &&
                       _source.Peek() != endSentinel &&
                       char.IsLowSurrogate((char) _source.Peek())) {
                // Allow well-formed surrogate pairs [1].
                _buf.Enqueue(c);
                _buf.Enqueue((char) _source.Read());
            } else if (!_filterIllegalChars) {
                // Note that we cannot encode illegal characters as entity
                // references due to the "Legal Character" constraint of
                // XML [4]. Nor are they allowed in CDATA sections [5].
                throw new ArgumentException(
                    String.Format("Illegal character: '{0:X}'", (int) c));
            }
        }
    }

    static readonly Dictionary<char,string> Entities =
        new Dictionary<char,string> {
            { '"', "&quot;" }, { '&', "&amp;"}, { '\'', "&apos;" },
            { '<', "&lt;" }, { '>', "&gt;" },
        };

    // References:
    // [1] http://en.wikipedia.org/wiki/UTF-16/UCS-2
    // [2] http://www.w3.org/TR/xml11/#sec-predefined-ent
    // [3] http://www.w3.org/TR/xml11/#charsets
    // [4] http://www.w3.org/TR/xml11/#sec-references
    // [5] http://www.w3.org/TR/xml11/#sec-cdata-sect
}

Модульные тесты и полный код можно найти здесь.

SecurityElement.Escape

задокументировано здесь

В прошлом я использовал HttpUtility.HtmlEncode для кодирования текста для xml. Он выполняет ту же задачу, правда. Я еще не столкнулся с какими-либо проблемами, но это не значит, что я не буду в будущем. Как следует из названия, это было сделано для HTML, а не XML.

Вы, вероятно, уже читали это, но вот статья о кодировании и декодировании XML.

РЕДАКТИРОВАТЬ: Конечно, если вы используете xmlwriter или один из новых классов XElement, эта кодировка для вас. Фактически, вы можете просто взять текст, поместить его в новый экземпляр XElement, а затем вернуть строковую (.tostring) версию элемента. Я слышал, что SecurityElement.Escape будет выполнять ту же задачу, что и ваш служебный метод, но мало что о нем читал или использовал.

EDIT2: игнорировать мой комментарий о XElement, так как вы все еще на 2.0

Библиотека Microsoft AntiXss AntiXssEncoder Class в System.Web.dll имеет следующие методы:

AntiXss.XmlEncode(string s)
AntiXss.XmlAttributeEncode(string s)

он также имеет HTML:

AntiXss.HtmlEncode(string s)
AntiXss.HtmlAttributeEncode(string s)

В.net 3.5+

new XText("I <want> to & encode this for XML").ToString();

Дает тебе:

I &lt;want&gt; to &amp; encode this for XML

Оказывается, этот метод не кодирует некоторые вещи, которые он должен (например, кавычки).

SecurityElement.Escape ( ответ workmad3), кажется, справляется с этим лучше, и это включено в более ранние версии.net.

Если вы не возражаете против стороннего кода и хотите, чтобы в ваш XML-код не входили нелегальные символы, я бы порекомендовал ответ Майкла Кропата.

XmlTextWriter.WriteString() убегает

Если это приложение ASP.NET, почему бы не использовать Server.HtmlEncode()?

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

public override void WriteCData(string text)
    Member of System.Xml.XmlTextWriter

Summary:
Writes out a <![CDATA[...]]> block containing the specified text.

Parameters:
text: Text to place inside the CDATA block.

Простой пример будет выглядеть следующим образом:

writer.WriteStartElement("name");
writer.WriteCData("<unsafe characters>");
writer.WriteFullEndElement();

Результат выглядит так:

<name><![CDATA[<unsafe characters>]]></name>

При чтении значений узла XMLReader автоматически удаляет часть CData внутреннего текста, поэтому вам не нужно об этом беспокоиться. Единственный улов заключается в том, что вы должны хранить данные как значение innerText для узла XML. Другими словами, вы не можете вставить содержимое CData в значение атрибута.

Если вы серьезно относитесь к обработке всех недопустимых символов (не только нескольких "HTML"), и у вас есть доступ к System.XmlВот самый простой способ сделать правильную Xml-кодировку данных значения:

string theTextToEscape = "Something \x1d else \x1D <script>alert('123');</script>";
var x = new XmlDocument();
x.LoadXml("<r/>"); // simple, empty root element
x.DocumentElement.InnerText = theTextToEscape; // put in raw string
string escapedText = x.DocumentElement.InnerXml; // Returns:  Something &#x1D; else &#x1D; &lt;script&gt;alert('123');&lt;/script&gt;

// Repeat the last 2 lines to escape additional strings.

Важно знать, что XmlConvert.EncodeName() не подходит, потому что это для имен сущностей / тегов, а не значений. Использование этого было бы похоже на Url-кодирование, когда вам нужно Html-кодирование.

Вот решение с одной строкой, использующее XElements. Я использую это в очень маленьком инструменте. Мне это не нужно во второй раз, поэтому я продолжаю в том же духе. (Это странный Дуг)

StrVal = (<x a=<%= StrVal %>>END</x>).ToString().Replace("<x a=""", "").Replace(">END</x>", "")

Ох, и это работает только в VB, а не в C#

Brilliant! Это все, что я могу сказать.

Вот вариант VB обновленного кода (не в классе, просто функция), который будет очищать, а также дезинфицировать XML

Function cXML(ByVal _buf As String) As String
    Dim textOut As New StringBuilder
    Dim c As Char
    If _buf.Trim Is Nothing OrElse _buf = String.Empty Then Return String.Empty
    For i As Integer = 0 To _buf.Length - 1
        c = _buf(i)
        If Entities.ContainsKey(c) Then
            textOut.Append(Entities.Item(c))
        ElseIf (AscW(c) = &H9 OrElse AscW(c) = &HA OrElse AscW(c) = &HD) OrElse ((AscW(c) >= &H20) AndAlso (AscW(c) <= &HD7FF)) _
            OrElse ((AscW(c) >= &HE000) AndAlso (AscW(c) <= &HFFFD)) OrElse ((AscW(c) >= &H10000) AndAlso (AscW(c) <= &H10FFFF)) Then
            textOut.Append(c)
        End If
    Next
    Return textOut.ToString

End Function

Shared ReadOnly Entities As New Dictionary(Of Char, String)() From {{""""c, "&quot;"}, {"&"c, "&amp;"}, {"'"c, "&apos;"}, {"<"c, "&lt;"}, {">"c, "&gt;"}}

Вы можете использовать встроенный класс XAttribute, который автоматически обрабатывает кодировку:

using System.Xml.Linq;

XDocument doc = new XDocument();

List<XAttribute> attributes = new List<XAttribute>();
attributes.Add(new XAttribute("key1", "val1&val11"));
attributes.Add(new XAttribute("key2", "val2"));

XElement elem = new XElement("test", attributes.ToArray());

doc.Add(elem);

string xmlStr = doc.ToString();
Другие вопросы по тегам