Як видалити недійсні шістнадцяткові символи з джерела даних на основі XML перед створенням XmlReader або XPathDocument, який використовує дані?


76

Чи існує якийсь простий / загальний спосіб очищення джерела даних на основі XML перед використанням у XmlReader, щоб я міг витончено споживати дані XML, які не відповідають обмеженням шістнадцяткових символів, накладених на XML?

Примітка:

  • Рішення повинно обробляти джерела даних XML, які використовують кодування символів, крім UTF-8, наприклад, вказавши кодування символів у декларації документа XML. Неправильне кодування символів джерела під час видалення недійсних шістнадцяткових символів стало основною проблемою.
  • Видалення недійсних шістнадцяткових символів повинно видаляти лише шістнадцяткові закодовані значення, оскільки часто можна знайти значення href у даних, які випадково містять рядок, який би відповідав рядку для шістнадцяткового символу.

Передумови:

Мені потрібно споживати джерело даних на основі XML, яке відповідає певному формату (думаю, що Atom або RSS-канали), але хочу мати можливість використовувати джерела даних, які були опубліковані та містять недійсні шістнадцяткові символи відповідно до специфікації XML.

У .NET, якщо у вас є потік, який представляє джерело даних XML, а потім намагається проаналізувати його за допомогою XmlReader та / або XPathDocument, виникає виняток через включення недійсних шістнадцяткових символів до даних XML. Моєю поточною спробою вирішити цю проблему є аналіз потоку як рядка та використання регулярного виразу для видалення та / або заміни недійсних шістнадцяткових символів, але я шукаю більш ефективного рішення.

Відповіді:


76

Це може бути не ідеально (підкреслено, оскільки люди втрачають цей застереження), але те, що я зробив у цьому випадку, знаходиться нижче. Ви можете налаштувати для використання з потоком.

/// <summary>
/// Removes control characters and other non-UTF-8 characters
/// </summary>
/// <param name="inString">The string to process</param>
/// <returns>A string with no control characters or entities above 0x00FD</returns>
public static string RemoveTroublesomeCharacters(string inString)
{
    if (inString == null) return null;

    StringBuilder newString = new StringBuilder();
    char ch;

    for (int i = 0; i < inString.Length; i++)
    {

        ch = inString[i];
        // remove any characters outside the valid UTF-8 range as well as all control characters
        // except tabs and new lines
        //if ((ch < 0x00FD && ch > 0x001F) || ch == '\t' || ch == '\n' || ch == '\r')
        //if using .NET version prior to 4, use above logic
        if (XmlConvert.IsXmlChar(ch)) //this method is new in .NET 4
        {
            newString.Append(ch);
        }
    }
    return newString.ToString();

}

1
спробуйте рішення dnewcome нижче.
Eugene Katz,

2
-1 ця відповідь вводить в оману, оскільки вона видаляє символи, які є дійсними в XML, які не є контрольними символами та є дійсними UTF-8.
Даніель Кассіді

2
Якщо ви хочете оновити відповідь кращим набором фільтрів, сміливо робіть це. Як зазначено у моїй відповіді, це може бути не ідеально, але це задовольнило мої потреби.
Eugene Katz,

3
Я використав XmlConvert.IsXmlChar (ch) для свого фільтра.
Brad J

1
@BradJ, дуже хороший момент. Здається, метод був доданий у .NET 4, тому переключено код, щоб просто використовувати його у прикладі. Дякую!
Євген Кац

60

Мені подобається концепція білого списку Євгена. Мені потрібно було зробити подібне, як оригінальний плакат, але мені потрібно було підтримувати всі символи Unicode, а не лише до 0x00FD. Специфікація XML:

Char = # x9 | #xA | #xD | [# x20- # xD7FF] | [# xE000- # xFFFD] | [# x10000- # x10FFFF]

У .NET внутрішнє представлення символів Unicode становить лише 16 біт, тому ми не можемо явно `дозволити '0x10000-0x10FFFF. Специфікація XML явно забороняє появу сурогатних кодових точок, починаючи з 0xD800. Однак можливо, що якщо ми дозволимо ці сурогатні кодові точки в нашому білому списку, utf-8, що кодує наш рядок, може в кінцевому підсумку створити дійсний XML, доки правильне кодування utf-8 було створено із сурогатних пар символів utf-16 у .NET рядок. Хоча я цього не вивчав, тому погодився зробити більш безпечну ставку і не дозволив сурогатам у своєму білому списку.

Коментарі до рішення Євгена оманливі, однак проблема полягає в тому, що символи, яких ми виключаємо, не є дійсними в XML ... вони є цілком дійсними кодовими точками Unicode. Ми не видаляємо `символи, не пов'язані з utf-8 '. Ми видаляємо символи utf-8, які можуть не відображатися у добре сформованих документах XML.

public static string XmlCharacterWhitelist( string in_string ) {
    if( in_string == null ) return null;

    StringBuilder sbOutput = new StringBuilder();
    char ch;

    for( int i = 0; i < in_string.Length; i++ ) {
        ch = in_string[i];
        if( ( ch >= 0x0020 && ch <= 0xD7FF ) || 
            ( ch >= 0xE000 && ch <= 0xFFFD ) ||
            ch == 0x0009 ||
            ch == 0x000A || 
            ch == 0x000D ) {
            sbOutput.Append( ch );
        }
    }
    return sbOutput.ToString();
}

він буде додавати & і це викликає doc = XDocument.Load(@strXMLPath);дати виключення
CODError

1
привіт, ти вважаєш, що XmlConvert.IsXmlChar () буде точнішим? Відповідь Євгена змінилася після вашого останнього коментаря. подяка
DaFi4

30

Як спосіб видалення недійсних символів XML я пропоную вам використовувати метод XmlConvert.IsXmlChar . Він був доданий з .NET Framework 4 і також представлений у Silverlight. Ось невеличка вибірка:

void Main() {
    string content = "\v\f\0";
    Console.WriteLine(IsValidXmlString(content)); // False

    content = RemoveInvalidXmlChars(content);
    Console.WriteLine(IsValidXmlString(content)); // True
}

static string RemoveInvalidXmlChars(string text) {
    char[] validXmlChars = text.Where(ch => XmlConvert.IsXmlChar(ch)).ToArray();
    return new string(validXmlChars);
}

static bool IsValidXmlString(string text) {
    try {
        XmlConvert.VerifyXmlChars(text);
        return true;
    } catch {
        return false;
    }
}

13

СУХА реалізація рішення цієї відповіді (за допомогою іншого конструктора - сміливо використовуйте той, який вам потрібен у вашому додатку):

public class InvalidXmlCharacterReplacingStreamReader : StreamReader
{
    private readonly char _replacementCharacter;

    public InvalidXmlCharacterReplacingStreamReader(string fileName, char replacementCharacter) : base(fileName)
    {
        this._replacementCharacter = replacementCharacter;
    }

    public override int Peek()
    {
        int ch = base.Peek();
        if (ch != -1 && IsInvalidChar(ch))
        {
            return this._replacementCharacter;
        }
        return ch;
    }

    public override int Read()
    {
        int ch = base.Read();
        if (ch != -1 && IsInvalidChar(ch))
        {
            return this._replacementCharacter;
        }
        return ch;
    }

    public override int Read(char[] buffer, int index, int count)
    {
        int readCount = base.Read(buffer, index, count);
        for (int i = index; i < readCount + index; i++)
        {
            char ch = buffer[i];
            if (IsInvalidChar(ch))
            {
                buffer[i] = this._replacementCharacter;
            }
        }
        return readCount;
    }

    private static bool IsInvalidChar(int ch)
    {
        return (ch < 0x0020 || ch > 0xD7FF) &&
               (ch < 0xE000 || ch > 0xFFFD) &&
                ch != 0x0009 &&
                ch != 0x000A &&
                ch != 0x000D;
    }
}

можливо, краще використовувати XmlConvert.IsXmlChar () під час перевірки діапазону ch? як ти гадаєш?
DaFi4

@montewhizdoh: IsXmlChar є новим у .NET 4. Якщо це доступно для вас, сміливо використовуйте. Це рішення .NET 2.0+.
Неоліск

1
Той самий підхід, який я застосував для себе, але я успадкував його від Stream, що було не дуже вдалою ідеєю, оскільки Stream.Read () працював із масивом байтів, а не символами, і перевіряти символи було не так елегантно. Ваше рішення шляхом успадкування від StreamReader краще, дякую!
Mar

1
+1 Оскільки це дозволяє читати ДІЙСНО великі файли XML (успішно протестовано з файлами розміром 100 МБ). Рішення, які завантажували все в рядок перед фільтрацією невдалих символів, не вдавалися з винятками OutOfMemory.
Бред Естріхер

9

Модернізуючи відповідь dnewcombe , ви можете застосувати дещо простіший підхід

public static string RemoveInvalidXmlChars(string input)
{
    var isValid = new Predicate<char>(value =>
        (value >= 0x0020 && value <= 0xD7FF) ||
        (value >= 0xE000 && value <= 0xFFFD) ||
        value == 0x0009 ||
        value == 0x000A ||
        value == 0x000D);

    return new string(Array.FindAll(input.ToCharArray(), isValid));
}

або, з Linq

public static string RemoveInvalidXmlChars(string input)
{
    return new string(input.Where(value =>
        (value >= 0x0020 && value <= 0xD7FF) ||
        (value >= 0xE000 && value <= 0xFFFD) ||
        value == 0x0009 ||
        value == 0x000A ||
        value == 0x000D).ToArray());
}

Мені було б цікаво дізнатись, як порівнюється ефективність цих методів і як вони всі порівнюються із підходом до чорного списку Buffer.BlockCopy.


У мене виникла проблема з методом Linq, кидаючи System.OutOfMemoryException, коли рядок XML на більші файли XML.
Brad J

@BradJ, мабуть, рядок, переданий дуже довгий у цих випадках?
Джодрелл

@BradJ, зрештою, було б кращим якесь перетворення потоку, ви можете передати це безпосередньо, XmlReader.Createзамість того, щоб завантажувати весь файл у рядок у пам'яті.
Джодрелл

2
щойно провів тест швидкості порівняно з відповіддю dnewcombe, і обидва ваші рішення приблизно в 3-4 рази швидші, а версія Linq лише трохи повільніша за вашу версію non linq. Я не очікував такої різниці. використовував довгі рядки та 100k ітерацій із секундоміром для опрацювання таймінгу.
Провидця

@Seer Я використовую потоки символів довжиною ~ 60 тис., І це рішення працює трохи повільніше, ніж метод StringBuilder, не впевнений, що я зробив інакше.
adotout

5

Ось відповідь dnewcome у власному StreamReader. Він просто обгортає справжній зчитувач потоків і замінює символи під час їх читання.

Я застосував лише кілька методів, щоб заощадити час. Я використовував це разом із XDocument.Load та потоком файлів, і був викликаний лише метод Read (char [] буфер, індекс int, int count), тому він працював так. Можливо, вам доведеться застосувати додаткові методи, щоб це працювало для вашої програми. Я використав цей підхід, оскільки він видається ефективнішим за інші відповіді. Я також реалізував лише один з конструкторів, ви, очевидно, можете реалізувати будь-який з потрібних вам конструкторів StreamReader, оскільки це лише прохід.

Я вирішив замінити символи, а не видаляти їх, оскільки це значно спрощує рішення. Таким чином довжина тексту залишається незмінною, тому немає необхідності відстежувати окремий покажчик.

public class InvalidXmlCharacterReplacingStreamReader : TextReader
{
    private StreamReader implementingStreamReader;
    private char replacementCharacter;

    public InvalidXmlCharacterReplacingStreamReader(Stream stream, char replacementCharacter)
    {
        implementingStreamReader = new StreamReader(stream);
        this.replacementCharacter = replacementCharacter;
    }

    public override void Close()
    {
        implementingStreamReader.Close();
    }

    public override ObjRef CreateObjRef(Type requestedType)
    {
        return implementingStreamReader.CreateObjRef(requestedType);
    }

    public void Dispose()
    {
        implementingStreamReader.Dispose();
    }

    public override bool Equals(object obj)
    {
        return implementingStreamReader.Equals(obj);
    }

    public override int GetHashCode()
    {
        return implementingStreamReader.GetHashCode();
    }

    public override object InitializeLifetimeService()
    {
        return implementingStreamReader.InitializeLifetimeService();
    }

    public override int Peek()
    {
        int ch = implementingStreamReader.Peek();
        if (ch != -1)
        {
            if (
                (ch < 0x0020 || ch > 0xD7FF) &&
                (ch < 0xE000 || ch > 0xFFFD) &&
                ch != 0x0009 &&
                ch != 0x000A &&
                ch != 0x000D
                )
            {
                return replacementCharacter;
            }
        }
        return ch;
    }

    public override int Read()
    {
        int ch = implementingStreamReader.Read();
        if (ch != -1)
        {
            if (
                (ch < 0x0020 || ch > 0xD7FF) &&
                (ch < 0xE000 || ch > 0xFFFD) &&
                ch != 0x0009 &&
                ch != 0x000A &&
                ch != 0x000D
                )
            {
                return replacementCharacter;
            }
        }
        return ch;
    }

    public override int Read(char[] buffer, int index, int count)
    {
        int readCount = implementingStreamReader.Read(buffer, index, count);
        for (int i = index; i < readCount+index; i++)
        {
            char ch = buffer[i];
            if (
                (ch < 0x0020 || ch > 0xD7FF) &&
                (ch < 0xE000 || ch > 0xFFFD) &&
                ch != 0x0009 &&
                ch != 0x000A &&
                ch != 0x000D
                )
            {
                buffer[i] = replacementCharacter;
            }
        }
        return readCount;
    }

    public override Task<int> ReadAsync(char[] buffer, int index, int count)
    {
        throw new NotImplementedException();
    }

    public override int ReadBlock(char[] buffer, int index, int count)
    {
        throw new NotImplementedException();
    }

    public override Task<int> ReadBlockAsync(char[] buffer, int index, int count)
    {
        throw new NotImplementedException();
    }

    public override string ReadLine()
    {
        throw new NotImplementedException();
    }

    public override Task<string> ReadLineAsync()
    {
        throw new NotImplementedException();
    }

    public override string ReadToEnd()
    {
        throw new NotImplementedException();
    }

    public override Task<string> ReadToEndAsync()
    {
        throw new NotImplementedException();
    }

    public override string ToString()
    {
        return implementingStreamReader.ToString();
    }
}

Зрештою, це правильна ідея, але ваша реалізація може бути DRYer.
Джодрелл,

@Jodrell: Додав сюди СУХУ версію .
Неоліск

1
@Neolisk: Дякую! Ймовірно, я мав би це очистити, перш ніж публікувати :)
Райан Адамс,

4

Підхід на основі регулярних виразів

public static string StripInvalidXmlCharacters(string str)
{
    var invalidXmlCharactersRegex = new Regex("[^\u0009\u000a\u000d\u0020-\ud7ff\ue000-\ufffd]|([\ud800-\udbff](?![\udc00-\udfff]))|((?<![\ud800-\udbff])[\udc00-\udfff])");
    return invalidXmlCharactersRegex.Replace(str, "");

}

Дивіться мій BlogPost для отримання більш докладної інформації


1
Це ~ 50 разів повільніше, ніж рішення dnewcome на моїй машині.
adotout

2

Вищезазначені рішення, здається, призначені для видалення недійсних символів перед перетворенням у XML.

Використовуйте цей код, щоб видалити недійсні символи XML із рядка XML. напр. & x1A;

    public static string CleanInvalidXmlChars( string Xml, string XMLVersion )
    {
        string pattern = String.Empty;
        switch( XMLVersion )
        {
            case "1.0":
                pattern = @"&#x((10?|[2-F])FFF[EF]|FDD[0-9A-F]|7F|8[0-46-9A-F]9[0-9A-F]);";
                break;
            case "1.1":
                pattern = @"&#x((10?|[2-F])FFF[EF]|FDD[0-9A-F]|[19][0-9A-F]|7F|8[0-46-9A-F]|0?[1-8BCEF]);";
                break;
            default:
                throw new Exception( "Error: Invalid XML Version!" );
        }

        Regex regex = new Regex( pattern, RegexOptions.IgnoreCase );
        if( regex.IsMatch( Xml ) )
            Xml = regex.Replace( Xml, String.Empty );
        return Xml;
    }

http://balajiramesh.wordpress.com/2008/05/30/strip-illegal-xml-characters-based-on-w3c-standard/


1
-1 Ця відповідь не стосується поставленого питання, і в будь-якому випадку є неправильною та оманливою, оскільки вона лише видаляє недійсні посилання на сутність символів XML, але не недійсні символи XML.
Даніель Кассіді

1

Модифікована відповідь або оригінальна відповідь Neolisk вище .
Зміни: передано символ \ 0, виконується видалення, а не заміна. також, використав метод XmlConvert.IsXmlChar (char)

    /// <summary>
    /// Replaces invalid Xml characters from input file, NOTE: if replacement character is \0, then invalid Xml character is removed, instead of 1-for-1 replacement
    /// </summary>
    public class InvalidXmlCharacterReplacingStreamReader : StreamReader
    {
        private readonly char _replacementCharacter;

        public InvalidXmlCharacterReplacingStreamReader(string fileName, char replacementCharacter)
            : base(fileName)
        {
            _replacementCharacter = replacementCharacter;
        }

        public override int Peek()
        {
            int ch = base.Peek();
            if (ch != -1 && IsInvalidChar(ch))
            {
                if ('\0' == _replacementCharacter)
                    return Peek(); // peek at the next one

                return _replacementCharacter;
            }
            return ch;
        }

        public override int Read()
        {
            int ch = base.Read();
            if (ch != -1 && IsInvalidChar(ch))
            {
                if ('\0' == _replacementCharacter)
                    return Read(); // read next one

                return _replacementCharacter;
            }
            return ch;
        }

        public override int Read(char[] buffer, int index, int count)
        {
            int readCount= 0, ch;

            for (int i = 0; i < count && (ch = Read()) != -1; i++)
            {
                readCount++;
                buffer[index + i] = (char)ch;
            }

            return readCount;
        }


        private static bool IsInvalidChar(int ch)
        {
            return !XmlConvert.IsXmlChar((char)ch);
        }
    }

1

Я створив трохи оновлену версію відповіді @ Neolisk , яка підтримує *Asyncфункції та використовує функцію .Net 4.0 XmlConvert.IsXmlChar.

public class InvalidXmlCharacterReplacingStreamReader : StreamReader
{
    private readonly char _replacementCharacter;

    public InvalidXmlCharacterReplacingStreamReader(string fileName, char replacementCharacter) : base(fileName)
    {
        _replacementCharacter = replacementCharacter;
    }

    public InvalidXmlCharacterReplacingStreamReader(Stream stream, char replacementCharacter) : base(stream)
    {
        _replacementCharacter = replacementCharacter;
    }

    public override int Peek()
    {
        var ch = base.Peek();
        if (ch != -1 && IsInvalidChar(ch))
        {
            return _replacementCharacter;
        }
        return ch;
    }

    public override int Read()
    {
        var ch = base.Read();
        if (ch != -1 && IsInvalidChar(ch))
        {
            return _replacementCharacter;
        }
        return ch;
    }

    public override int Read(char[] buffer, int index, int count)
    {
        var readCount = base.Read(buffer, index, count);
        ReplaceInBuffer(buffer, index, readCount);
        return readCount;
    }

    public override async Task<int> ReadAsync(char[] buffer, int index, int count)
    {
        var readCount = await base.ReadAsync(buffer, index, count).ConfigureAwait(false);
        ReplaceInBuffer(buffer, index, readCount);
        return readCount;
    }

    private void ReplaceInBuffer(char[] buffer, int index, int readCount)
    {
        for (var i = index; i < readCount + index; i++)
        {
            var ch = buffer[i];
            if (IsInvalidChar(ch))
            {
                buffer[i] = _replacementCharacter;
            }
        }
    }

    private static bool IsInvalidChar(int ch)
    {
        return IsInvalidChar((char)ch);
    }

    private static bool IsInvalidChar(char ch)
    {
        return !XmlConvert.IsXmlChar(ch);
    }
}

0

Використовуйте цю функцію для видалення недійсних символів xml.

public static string CleanInvalidXmlChars(string text)   
{   
       string re = @"[^\x09\x0A\x0D\x20-\xD7FF\xE000-\xFFFD\x10000-x10FFFF]";   
       return Regex.Replace(text, re, "");   
} 

-1
private static String removeNonUtf8CompliantCharacters( final String inString ) {
    if (null == inString ) return null;
    byte[] byteArr = inString.getBytes();
    for ( int i=0; i < byteArr.length; i++ ) {
        byte ch= byteArr[i]; 
        // remove any characters outside the valid UTF-8 range as well as all control characters
        // except tabs and new lines
        if ( !( (ch > 31 && ch < 253 ) || ch == '\t' || ch == '\n' || ch == '\r') ) {
            byteArr[i]=' ';
        }
    }
    return new String( byteArr );
}

1
-1 Ця відповідь вводить в оману та помилково, оскільки вона видаляє символи, які є дійсними як в Unicode, так і в XML.
Даніель Кассіді

-1

Ви можете передавати символи, що не належать до UTF, із наступним:

string sFinalString  = "";
string hex = "";
foreach (char ch in UTFCHAR)
{
    int tmp = ch;
   if ((ch < 0x00FD && ch > 0x001F) || ch == '\t' || ch == '\n' || ch == '\r')
    {
    sFinalString  += ch;
    }
    else
    {  
      sFinalString  += "&#" + tmp+";";
    }
}

1
-1 Ця відповідь неправильна, оскільки вона генерує недійсні посилання на сутність символів XML (наприклад &#1;, не є дійсним посиланням на сутність символу XML). Крім того, це вводить в оману, оскільки видаляє символи, які є дійсними як в Unicode, так і в XML.
Даніель Кассіді

ya thats true, але вище рішення, якщо ви хочете передати недійсний xml у файлі xml, це спрацює, або ви не можете передати недійсний символ xml у документі xml
Murari Kumar

Ви не можете передавати недійсні символи XML у XML-документі, незалежно від того, що ви робите. Наприклад, символ U+0001 START OF HEADINGне має дозволу у добре сформованому XML-документі, і навіть якщо ви намагаєтесь уникнути його як &#1;, це все одно заборонено у добре сформованому XML-документі.
Даніель Кассіді,

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.